mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-16 18:05:31 +02:00
* feat(cso): add verified audits and replayable repair bundles * fix(cso): harden qualification and setup boundaries * fix(cso): assemble security canaries at runtime * fix(cso): bound release proof and maintenance work Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): require complete evaluation reports Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): replay expired snapshots from supplied source Co-Authored-By: OpenAI Codex <noreply@openai.com> * test(cso): synchronize DNS cancellation assertion Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore(ship): exempt repository owner from liveness proof Co-Authored-By: OpenAI Codex <noreply@openai.com> * test(cso): make recheck retention overlap deterministic Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore: bump version and changelog (v1.85.0.0) Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): pass native release gates Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore: move release to v1.86.0.0 Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): resolve rechecks by finding Co-Authored-By: OpenAI Codex <noreply@openai.com> * chore: move release to v1.87.0.0 Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): pass macOS and Windows release gates Normalize BSD wc output, compare Windows paths by filesystem identity, preserve portable snapshot race coverage, and narrow POSIX-only Windows fixtures. Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix(cso): harden native verification gates * fix(cso): refine Windows native diagnostics * test(cso): isolate Windows Git startup failure * test(cso): stabilize Windows native diagnostics * fix(cso): support hardened Git on Windows * fix(cso): close final verification gaps * test(cso): bound cold Docker fixture setup * fix(cso): restore cross-platform free-suite gates --------- Co-authored-by: OpenAI Codex <noreply@openai.com>
84 lines
2.9 KiB
TypeScript
84 lines
2.9 KiB
TypeScript
/**
|
|
* Atomic file writes — the ONE implementation of tmp-write-then-rename.
|
|
*
|
|
* Before this module, the pattern was reimplemented ~20 times across lib/,
|
|
* bin/, and browse/src with three different tmp-suffix conventions — one of
|
|
* which (a bare `.tmp`) carries a real collision race that browse's
|
|
* server.ts documented after hitting it in production: two writers (batch
|
|
* subcommands, /tunnel/start handlers, or any combination) collide on the
|
|
* rename when the tmp filename is deterministic. The suffix here includes
|
|
* pid AND a random component so concurrent writers in the SAME process
|
|
* (async interleavings) can't collide either.
|
|
*
|
|
* Contract:
|
|
* - atomicWriteSync ALWAYS throws on failure, after best-effort tmp cleanup.
|
|
* Callers own the error. Use it everywhere except shutdown paths.
|
|
* - atomicWriteQuiet swallows everything (returns false on failure). ONLY
|
|
* for shutdown/emergency-cleanup paths where a throw would abort the rest
|
|
* of cleanup — same philosophy as browse's safeUnlinkQuiet.
|
|
* - `mode` applies to the tmp file at creation (0600 for sensitive state),
|
|
* so the final file never exists with looser permissions.
|
|
* - The tmp file is created in the target's directory (same filesystem, so
|
|
* rename stays atomic). Parent dirs are NOT created — callers that need
|
|
* mkdir own that decision (and its mode).
|
|
*/
|
|
import * as fs from 'fs';
|
|
import * as crypto from 'crypto';
|
|
|
|
export interface AtomicWriteOpts {
|
|
/** File mode for the tmp file at creation (e.g. 0o600). Default: umask. */
|
|
mode?: number;
|
|
/** Publish only when the target does not already exist. */
|
|
noReplace?: boolean;
|
|
}
|
|
|
|
function tmpPathFor(target: string): string {
|
|
return `${target}.tmp.${process.pid}.${crypto.randomBytes(4).toString('hex')}`;
|
|
}
|
|
|
|
/** Atomic write. Throws on failure (after best-effort tmp cleanup). */
|
|
export function atomicWriteSync(
|
|
target: string,
|
|
data: string | NodeJS.ArrayBufferView,
|
|
opts: AtomicWriteOpts = {},
|
|
): void {
|
|
const tmp = tmpPathFor(target);
|
|
try {
|
|
if (opts.mode !== undefined) {
|
|
fs.writeFileSync(tmp, data, { mode: opts.mode });
|
|
} else {
|
|
fs.writeFileSync(tmp, data);
|
|
}
|
|
if (opts.noReplace) {
|
|
// Publishing the complete temp inode with link(2) gives atomic
|
|
// no-replace semantics; unlink only removes the temporary name.
|
|
fs.linkSync(tmp,target);
|
|
fs.unlinkSync(tmp);
|
|
} else fs.renameSync(tmp, target);
|
|
} catch (err) {
|
|
try {
|
|
fs.unlinkSync(tmp);
|
|
} catch {
|
|
// Best-effort cleanup; the original error is the one that matters.
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Atomic write that swallows all errors. Returns true on success.
|
|
* ONLY for shutdown/emergency paths — a throw there aborts remaining cleanup.
|
|
*/
|
|
export function atomicWriteQuiet(
|
|
target: string,
|
|
data: string | NodeJS.ArrayBufferView,
|
|
opts: AtomicWriteOpts = {},
|
|
): boolean {
|
|
try {
|
|
atomicWriteSync(target, data, opts);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|