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),
};
}