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; export interface IRegistryCredential { readonly token: string; readonly username: string; readonly email?: string; readonly expiresAt?: string; } interface ICredentialFile { readonly registries: Record; } 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 = {}; 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(); 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), }; }