import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; import { appendCleanupFailure } from "./error-diagnostics.ts"; export interface AliasDefinition { source: string; } export type AliasConfig = Record; const ALIAS_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; export function validateAliasName(name: string): void { if (!name.trim()) { throw new Error("Alias name must be non-empty."); } if (!ALIAS_NAME_PATTERN.test(name)) { throw new Error("Alias names may contain only letters, numbers, '.', '_' and '-'."); } } function parseAliasConfig(value: unknown, path: string): AliasConfig { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error(`Invalid alias config in ${path}: expected an object.`); } const aliases: AliasConfig = {}; for (const [alias, definition] of Object.entries(value)) { validateAliasName(alias); if (!definition || typeof definition !== "object" || Array.isArray(definition)) { throw new Error(`Invalid alias config in ${path}: ${alias} must be an object.`); } const source = (definition as { source?: unknown }).source; if (typeof source !== "string" || !source.trim()) { throw new Error(`Invalid alias config in ${path}: ${alias}.source must be a string.`); } aliases[alias] = { source }; } return aliases; } export async function loadAliasConfig(path: string): Promise { try { const contents = await readFile(path, "utf8"); return parseAliasConfig(JSON.parse(contents) as unknown, path); } catch (error) { if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") { return {}; } throw error; } } interface AliasConfigFileOperations { mkdir: typeof mkdir; rename: typeof rename; rm: typeof rm; writeFile: typeof writeFile; } const defaultFileOperations: AliasConfigFileOperations = { mkdir, rename, rm, writeFile }; export async function saveAliasConfig( path: string, config: AliasConfig, fileOperations: AliasConfigFileOperations = defaultFileOperations, ): Promise { for (const [alias, definition] of Object.entries(config)) { validateAliasName(alias); if (!definition.source.trim()) { throw new Error(`Alias ${alias} must have a source provider.`); } } await fileOperations.mkdir(dirname(path), { recursive: true, mode: 0o700 }); const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`; try { await fileOperations.writeFile(temporaryPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600, }); await fileOperations.rename(temporaryPath, path); } catch (primaryError) { try { await fileOperations.rm(temporaryPath, { force: true }); } catch (cleanupError) { throw appendCleanupFailure(primaryError, "remove temporary config file", cleanupError); } throw primaryError; } }