import { stat } from 'node:fs/promises'; import { FileSecretStore } from './file-secret-store.js'; import { MacOSKeychainSecretStore } from './keychain-secret-store.js'; import type { SecretStore } from './secret-store.js'; export type SecretBackend = 'macos-keychain' | 'secret-file'; const BACKENDS: readonly SecretBackend[] = ['macos-keychain', 'secret-file']; export class SecretBackendError extends Error { constructor(message: string) { super(message); this.name = 'SecretBackendError'; } } /** * Resolves the secret backend: explicit MULTI_SIMADMIN_SECRET_BACKEND override, * otherwise the macOS Keychain on darwin and the 0600 file store elsewhere. */ export function resolveSecretBackend( env: Readonly> = process.env, platform: NodeJS.Platform = process.platform, ): SecretBackend { const raw = env.MULTI_SIMADMIN_SECRET_BACKEND; if (raw !== undefined) { if (!BACKENDS.includes(raw as SecretBackend)) throw new SecretBackendError( 'MULTI_SIMADMIN_SECRET_BACKEND must be one of: macos-keychain, secret-file', ); return raw as SecretBackend; } return platform === 'darwin' ? 'macos-keychain' : 'secret-file'; } export interface CreateDefaultSecretStoreOptions { readonly env?: Readonly>; /** Absolute path of the file store map; required for the secret-file backend. */ readonly secretFilePath: string; readonly platform?: NodeJS.Platform; } export function createDefaultSecretStore(options: CreateDefaultSecretStoreOptions): SecretStore { const platform = options.platform ?? process.platform; const backend = resolveSecretBackend(options.env, platform); if (backend === 'secret-file') return new FileSecretStore(options.secretFilePath); if (platform !== 'darwin') throw new SecretBackendError( 'The macos-keychain backend requires /usr/bin/security (macOS only)', ); return new MacOSKeychainSecretStore(); } interface SecretFileMetadata { isFile(): boolean; readonly mode: number; } /** * Readiness metadata probe for the file backend. Availability only — this must * never read secret material. */ export async function defaultSecretFileMetadataCheck( filePath: string, platform: NodeJS.Platform = process.platform, statFn: (path: string) => Promise = stat, ): Promise { try { const info = await statFn(filePath); if (!info.isFile()) return false; // Windows keeps no POSIX mode bits; NTFS ACLs govern access there. if (platform !== 'win32' && (info.mode & 0o777) !== 0o600) return false; return true; } catch (error) { // A missing map is healthy: secrets are added lazily by instance setup. return (error as NodeJS.ErrnoException).code === 'ENOENT'; } }