208 lines
6.5 KiB
TypeScript
208 lines
6.5 KiB
TypeScript
|
|
import * as fs from 'node:fs';
|
||
|
|
import * as os from 'node:os';
|
||
|
|
import * as path from 'node:path';
|
||
|
|
import { resolvePackageRegistry } from './config-store.js';
|
||
|
|
|
||
|
|
type JsonRecord = Record<string, unknown>;
|
||
|
|
|
||
|
|
export interface IRegistryCredential {
|
||
|
|
readonly token: string;
|
||
|
|
readonly username: string;
|
||
|
|
readonly email?: string;
|
||
|
|
readonly expiresAt?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ICredentialFile {
|
||
|
|
readonly registries: Record<string, IRegistryCredential>;
|
||
|
|
}
|
||
|
|
|
||
|
|
function asRecord(value: unknown): JsonRecord | null {
|
||
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return value as JsonRecord;
|
||
|
|
}
|
||
|
|
|
||
|
|
function matrixHome(cwd: string): string {
|
||
|
|
return path.resolve(
|
||
|
|
cwd,
|
||
|
|
process.env.MATRIX_HOME
|
||
|
|
?? path.join(cwd, '.matrix'),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function resolveCredentialPath(cwd: string, credentialsPath?: string): string {
|
||
|
|
return path.resolve(
|
||
|
|
cwd,
|
||
|
|
credentialsPath ?? path.join(matrixHome(cwd), 'credentials', 'registry.json'),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function readCredentialFile(filePath: string): ICredentialFile {
|
||
|
|
if (!fs.existsSync(filePath)) {
|
||
|
|
return { registries: {} };
|
||
|
|
}
|
||
|
|
if (fs.statSync(filePath).isDirectory()) {
|
||
|
|
throw new Error(`MX_AUTH_CREDENTIALS_PATH_IS_DIRECTORY: ${filePath}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
|
||
|
|
const record = asRecord(parsed);
|
||
|
|
const registries = asRecord(record?.registries) ?? {};
|
||
|
|
const normalized: Record<string, IRegistryCredential> = {};
|
||
|
|
|
||
|
|
for (const [registry, value] of Object.entries(registries)) {
|
||
|
|
const entry = asRecord(value);
|
||
|
|
if (!entry) continue;
|
||
|
|
if (typeof entry.token !== 'string' || typeof entry.username !== 'string') continue;
|
||
|
|
normalized[registry] = {
|
||
|
|
token: entry.token,
|
||
|
|
username: entry.username,
|
||
|
|
email: typeof entry.email === 'string' ? entry.email : undefined,
|
||
|
|
expiresAt: typeof entry.expiresAt === 'string' ? entry.expiresAt : undefined,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return { registries: normalized };
|
||
|
|
}
|
||
|
|
|
||
|
|
function writeCredentialFile(filePath: string, payload: ICredentialFile): void {
|
||
|
|
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
|
||
|
|
throw new Error(`MX_AUTH_CREDENTIALS_PATH_IS_DIRECTORY: ${filePath}`);
|
||
|
|
}
|
||
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||
|
|
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
|
||
|
|
}
|
||
|
|
|
||
|
|
function readNpmTokenFromEnvironment(): string | null {
|
||
|
|
const token = process.env.NODE_AUTH_TOKEN ?? process.env.NPM_TOKEN;
|
||
|
|
if (typeof token !== 'string') {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const trimmed = token.trim();
|
||
|
|
return trimmed.length > 0 ? trimmed : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function configuredNpmUserConfigPath(): string | undefined {
|
||
|
|
const value = process.env.NPM_CONFIG_USERCONFIG ?? process.env.npm_config_userconfig;
|
||
|
|
if (typeof value !== 'string') {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
const trimmed = value.trim();
|
||
|
|
return trimmed.length > 0 ? trimmed : undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
function npmrcSearchPaths(cwd: string): readonly string[] {
|
||
|
|
const configuredUserConfig = configuredNpmUserConfigPath();
|
||
|
|
if (configuredUserConfig) {
|
||
|
|
return [configuredUserConfig];
|
||
|
|
}
|
||
|
|
return [
|
||
|
|
path.join(cwd, '.npmrc'),
|
||
|
|
path.join(os.homedir(), '.npmrc'),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
function readNpmTokenFromNpmrc(filePath: string, registry: string): string | null {
|
||
|
|
if (!fs.existsSync(filePath)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const lines = fs.readFileSync(filePath, 'utf8')
|
||
|
|
.split(/\r?\n/)
|
||
|
|
.map((line) => line.trim())
|
||
|
|
.filter((line) => line.length > 0 && !line.startsWith('#') && !line.startsWith(';'));
|
||
|
|
const registryKeys = npmRegistryAuthKeys(registry);
|
||
|
|
for (const line of lines) {
|
||
|
|
for (const key of registryKeys) {
|
||
|
|
const prefix = `${key}:_authToken=`;
|
||
|
|
if (line.startsWith(prefix)) {
|
||
|
|
const token = line.slice(prefix.length).trim();
|
||
|
|
return token.length > 0 ? token : null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function npmRegistryAuthKeys(registry: string): readonly string[] {
|
||
|
|
const parsed = new URL(registry);
|
||
|
|
const host = parsed.host;
|
||
|
|
const pathname = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
|
||
|
|
const segments = pathname.split('/').filter((segment) => segment.length > 0);
|
||
|
|
const keys = new Set<string>();
|
||
|
|
for (let length = segments.length; length >= 0; length -= 1) {
|
||
|
|
const partial = length === 0 ? '/' : `/${segments.slice(0, length).join('/')}/`;
|
||
|
|
keys.add(`//${host}${partial}`);
|
||
|
|
}
|
||
|
|
return [...keys];
|
||
|
|
}
|
||
|
|
|
||
|
|
function readNpmCredential(cwd: string, registry: string): IRegistryCredential | null {
|
||
|
|
const envToken = readNpmTokenFromEnvironment();
|
||
|
|
if (envToken) {
|
||
|
|
return { username: 'npm-token', token: envToken };
|
||
|
|
}
|
||
|
|
for (const filePath of npmrcSearchPaths(cwd)) {
|
||
|
|
const token = readNpmTokenFromNpmrc(filePath, registry);
|
||
|
|
if (token) {
|
||
|
|
return { username: 'npm-token', token };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function saveRegistryCredential(
|
||
|
|
cwd: string,
|
||
|
|
credential: IRegistryCredential,
|
||
|
|
options: {
|
||
|
|
readonly registry?: string;
|
||
|
|
readonly credentialsPath?: string;
|
||
|
|
} = {},
|
||
|
|
): { readonly registry: string; readonly credentialsPath: string } {
|
||
|
|
const registry = resolvePackageRegistry(options.registry, { cwd }).registry;
|
||
|
|
const filePath = resolveCredentialPath(cwd, options.credentialsPath);
|
||
|
|
const payload = readCredentialFile(filePath);
|
||
|
|
payload.registries[registry] = credential;
|
||
|
|
writeCredentialFile(filePath, payload);
|
||
|
|
return { registry, credentialsPath: filePath };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function removeRegistryCredential(
|
||
|
|
cwd: string,
|
||
|
|
options: {
|
||
|
|
readonly registry?: string;
|
||
|
|
readonly credentialsPath?: string;
|
||
|
|
} = {},
|
||
|
|
): { readonly registry: string; readonly removed: boolean; readonly credentialsPath: string } {
|
||
|
|
const registry = resolvePackageRegistry(options.registry, { cwd }).registry;
|
||
|
|
const filePath = resolveCredentialPath(cwd, options.credentialsPath);
|
||
|
|
const payload = readCredentialFile(filePath);
|
||
|
|
const removed = Boolean(payload.registries[registry]);
|
||
|
|
if (removed) {
|
||
|
|
delete payload.registries[registry];
|
||
|
|
writeCredentialFile(filePath, payload);
|
||
|
|
}
|
||
|
|
return { registry, removed, credentialsPath: filePath };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function readRegistryCredential(
|
||
|
|
cwd: string,
|
||
|
|
options: {
|
||
|
|
readonly registry?: string;
|
||
|
|
readonly credentialsPath?: string;
|
||
|
|
} = {},
|
||
|
|
): {
|
||
|
|
readonly registry: string;
|
||
|
|
readonly credentialsPath: string;
|
||
|
|
readonly credential: IRegistryCredential | null;
|
||
|
|
} {
|
||
|
|
const registry = resolvePackageRegistry(options.registry, { cwd }).registry;
|
||
|
|
const filePath = resolveCredentialPath(cwd, options.credentialsPath);
|
||
|
|
const payload = readCredentialFile(filePath);
|
||
|
|
return {
|
||
|
|
registry,
|
||
|
|
credentialsPath: filePath,
|
||
|
|
credential: payload.registries[registry] ?? readNpmCredential(cwd, registry),
|
||
|
|
};
|
||
|
|
}
|