import { existsSync, readFileSync } from 'node:fs'; import { chmod, mkdir, rename, writeFile } from 'node:fs/promises'; import { dirname } from 'node:path'; import { parseSecretsFile, serializeSecrets } from '@dorfteich/shared'; /** * File I/O for the env-backed secret store (security.md §Secrets, issue #80). * The pure format/merge helpers (`parseSecretsFile`, `serializeSecrets`, * `overlayEnv`) moved to `@dorfteich/shared` in issue #83 so the backup * sidecar can read the same store; they are re-exported here to keep the * api-internal import paths stable. */ export { overlayEnv, parseSecretsFile, serializeSecrets } from '@dorfteich/shared'; /** Reads the store file; a missing file is an empty store, not an error. */ export function readSecretsFile(path: string): Record { if (!existsSync(path)) return {}; return parseSecretsFile(readFileSync(path, 'utf8')); } /** * Merges entries into the store file atomically (staging file + rename, so a * crash mid-write never leaves a torn file) and keeps it owner-only readable. */ export async function writeSecretsFile( path: string, entries: Record, ): Promise { const merged = { ...readSecretsFile(path), ...entries }; await mkdir(dirname(path), { recursive: true }); const staging = `${path}.tmp-${process.pid}`; await writeFile(staging, serializeSecrets(merged), { mode: 0o600 }); await rename(staging, path); // rename preserves the staging file's mode, but be explicit in case a // pre-existing file with looser permissions was replaced on some platforms. await chmod(path, 0o600); }