/** * Env-file writer and atomic private-file write for `hikoutei setup`. * * The secure, atomic `.env` output-write machinery of the setup flow, * extracted verbatim: update only the two managed env keys while preserving * unrelated lines, refuse to read or write through any symlink/hardlink * alias of a reserved path, and always land the result via a unique private * temp file plus rename (never pathname-chmodded). Also carries * `revalidateSetupPaths`, the fail-closed re-run of the reserved-path * collision check performed immediately before every checkpoint and `.env` * write, so an alias planted mid-run can never redirect those writes. */ import { type SetupStateWriteFs } from "./setupPaths.js"; import { type SetupErrorResult } from "./flowResult.js"; /** The .env keys the setup CLI manages. */ export declare const SETUP_ENV_KEYS: { readonly CREDENTIALS: "GOOGLE_APPLICATION_CREDENTIALS"; readonly SPREADSHEET_URL: "HIKOUTEI_SYNC_SPREADSHEET_URL"; /** Comma-separated credential pool for multi-SA runs (written only when ≥2 paths). */ readonly CREDENTIAL_POOL: "HIKOUTEI_SYNC_CREDENTIALS"; }; /** Result of writing the .env output file. */ export interface EnvFileWriteResult { /** True when the file did not exist before this write. */ readonly created: boolean; /** True when the file content changed (created or keys added/updated). */ readonly modified: boolean; } /** * Writes or updates the .env output file securely and atomically. * * Updates only the managed env keys (the single-SA credentials path, the * spreadsheet URL, and — for multi-SA runs with at least two pool paths — * the comma-separated credential pool) while preserving unrelated lines. * * An existing output must be a regular file at the lstat boundary (a * symlink is rejected outright, and directories/FIFOs/devices/sockets are * refused before any open so a FIFO can never block), the file is opened * WITHOUT following symlinks (`O_NOFOLLOW` where supported) and * non-blocking (`O_NONBLOCK` where supported), the descriptor is * fstat-verified as the SAME file the lstat observed (device/inode * identity, covering a same-type replacement as well as a non-regular one) * BEFORE a single byte is read, and any * alias of the credentials file or other reserved paths (hardlink/symlink * alias — the key contents are never read through it) is refused before a * single byte is read. The preserved env content is built in memory and written to a unique private sibling temp * file (`O_CREAT|O_EXCL|O_WRONLY` plus `O_NOFOLLOW`, mode 0600, fsync + * close), then atomically renamed over the output: rename replaces the * directory entry and never follows a symlink or hardlink planted after * validation, so it cannot overwrite the key inode. Cleanup removes only * the temp inode this invocation created. Missing file is created; a file * whose content is unchanged is not rewritten. An existing file whose * owner bits are not exactly 0600 counts as modified and is atomically * replaced by a fresh verified-0600 file — never pathname-chmodded — so * hardlinks sharing the old inode keep their own mode and content. Throws on filesystem * failure or an unsafe existing entry; the caller preserves carrier codes * (`SetupPathSafetyError`) and falls back to `output_write_failed` for other errors. */ export declare function writeSetupEnvFile(outputPath: string, credentialsPath: string, spreadsheetUrl: string, reservedPaths?: readonly string[], /** * Full credential pool (entry 1 first). The pool line is written only * when at least two paths are present; single-SA runs never contain it * (a stale pool line is removed as a managed key). Paths are joined * with commas and no spaces, matching the runtime pool parser. */ poolPaths?: readonly string[], /** * Filesystem used for the existing-output read; injectable so tests can * prove the descriptor-identity check without a racy real swap. The * atomic write below always uses the default filesystem. */ loadFs?: EnvFileLoadFs): EnvFileWriteResult; /** * Filesystem operations the existing-output read uses; injectable for * tests (same-type replacement-descriptor coverage without a racy real * swap). * * Both `lstatSync` and `fstatSync` expose the device/inode so the read can * BIND the opened descriptor to the entry the lstat verified: a file * replaced between the type check and the open is refused before a single * byte is read. Mirrors `SetupStateLoadFs` in checkpoint.ts. */ export interface EnvFileLoadFs { lstatSync(path: string): { isSymbolicLink(): boolean; isFile(): boolean; readonly dev: number; readonly ino: number; }; openSync(path: string, flags: number): number; fstatSync(fd: number): { isFile(): boolean; readonly dev: number; readonly ino: number; readonly mode: number; }; readFileSync(fd: number, encoding: "utf8"): string; closeSync(fd: number): void; } /** * Writes content to the output path atomically via a unique private temp. * * The temp file (PID + UUID sibling name) is created with exclusive * no-follow flags and mode 0600, owner-only mode is applied and verified * THROUGH the still-open descriptor (`fchmod` + `fstat`) before the content * is written and fsynced, the path is re-verified against the created * inode, and the file is renamed over the output. No pathname `chmod` is * ever performed. Rename replaces the directory entry rather than * following a symlink/hardlink planted after validation. Cleanup removes * only the temp inode this invocation created. Throws on failure. `fs` is * injectable so tests can prove the descriptor-mode behavior. * * Exported for the injected descriptor-mode regression; the flow calls it * through `writeSetupEnvFile` with the default filesystem. */ export declare function atomicWritePrivateFile(outputPath: string, content: string, fs?: SetupStateWriteFs): void; /** * `O_NOFOLLOW`/`O_NONBLOCK` where the platform defines each, else 0. * * Compat re-export: these flags moved to the dependency-free `setupPaths.ts` * leaf; the envFileWriter module path stays stable for existing importers. */ export { noFollowFlag, nonBlockFlag } from "./setupPaths.js"; /** * Re-runs the reserved-path collision check and fails closed when aliases * changed after the initial preflight. * * The reserved paths (key, output, checkpoint, temp, lock, and any extra * pool-key paths) are re-resolved immediately before every checkpoint and * `.env` write: aliases planted after preflight must never redirect a write. * Returns an error result on collision, `null` when safe. */ export declare function revalidateSetupPaths(options: { readonly keyPath: string; readonly outputPath: string; readonly statePath: string; }, extraReserved?: readonly string[]): SetupErrorResult | null; //# sourceMappingURL=envFileWriter.d.ts.map