Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 166 additions & 13 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { execFileSync } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { chmodSync, constants as fsConstants, copyFileSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, truncateSync, unlinkSync, writeFileSync } from "node:fs";
import { chmodSync, constants as fsConstants, copyFileSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmdirSync, truncateSync, unlinkSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { Database } from "bun:sqlite";
Expand Down Expand Up @@ -382,14 +382,35 @@ export class OpenAiTierBackupCollisionError extends Error {
}

export class OpenAiTierRollbackPreserveError extends Error {
readonly code?: "missing" | "not-rollback" | "mismatch" | "exhausted";
readonly code?: "missing" | "not-rollback" | "mismatch" | "exhausted" | "changed";
constructor(message: string, options?: ErrorOptions & { code?: OpenAiTierRollbackPreserveError["code"] }) {
super(message, options);
this.name = "OpenAiTierRollbackPreserveError";
this.code = options?.code;
}
}

export class OpenAiTierRollbackPreserveSecretResidualError extends Error {
constructor(readonly preservedPath: string, options?: ErrorOptions) {
super("OpenAI tier rollback preserve could not scrub or remove an unverified snapshot", options);
this.name = "OpenAiTierRollbackPreserveSecretResidualError";
}
}

export class OpenAiTierRollbackPreserveCleanupError extends Error {
constructor(readonly preservedPath: string, readonly restricted = false, options?: ErrorOptions) {
super("OpenAI tier rollback preserve could not remove a scrubbed unverified snapshot", options);
this.name = "OpenAiTierRollbackPreserveCleanupError";
}
}

export class OpenAiTierRollbackPreserveClaimError extends Error {
constructor(readonly claimedPath: string, options?: ErrorOptions) {
super("OpenAI tier rollback backup was replaced during preserve; the claimed snapshot was kept", options);
this.name = "OpenAiTierRollbackPreserveClaimError";
}
}

export class OpenAiTierBackupSecretResidualError extends Error {
constructor(readonly tempPath: string, options?: ErrorOptions) {
super("OpenAI tier backup could not scrub or remove a secret-bearing temporary file", options);
Expand Down Expand Up @@ -570,7 +591,14 @@ export interface OpenAiTierRollbackPreserveIO {
exists(path: string): boolean;
read(path: string): Uint8Array;
copyExclusive(source: string, destination: string): void;
harden(path: string): void;
truncate(path: string): void;
write(path: string, bytes: Uint8Array): void;
unlink(path: string): void;
mkdirExclusive(path: string): void;
claimExclusive(source: string, destination: string): void;
linkExclusive(source: string, destination: string): void;
rmdir(path: string): void;
}

const DEFAULT_ROLLBACK_PRESERVE_IO: OpenAiTierRollbackPreserveIO = {
Expand All @@ -579,17 +607,38 @@ const DEFAULT_ROLLBACK_PRESERVE_IO: OpenAiTierRollbackPreserveIO = {
copyExclusive: (source, destination) => {
copyFileSync(source, destination, fsConstants.COPYFILE_EXCL);
},
harden: target => {
// Fail closed: chmod errors propagate. Windows ACL uses required:true so a
// failed icacls cannot continue into source unlink. The v2 migration backup
// path keeps required:false; this callback is only for preserved rollback
// snapshots. CopyFile does not copy the source DACL.
chmodSync(target, 0o600);
if (process.platform === "win32") hardenSecretPath(target, { required: true });
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
truncate: target => truncateSync(target, 0),
write: (target, bytes) => writeFileSync(target, bytes),
unlink: unlinkSync,
mkdirExclusive: target => { mkdirSync(target, { mode: 0o700 }); },
claimExclusive: (source, destination) => { renameSync(source, destination); },
linkExclusive: (source, destination) => { linkSync(source, destination); },
rmdir: target => { rmdirSync(target); },
};

const OPENAI_TIER_ROLLBACK_PRESERVE_ATTEMPTS = 16;

/**
* Copy a rollback-classified `.pre-openai-tiers-v2.bak` to a unique
* `.pre-openai-tiers-v1-rollback.<timestamp>[suffix].bak` path, then unlink the
* blocking v2 name. The original bytes are copied with no-replace publication;
* the v2 path is removed only after the copy is verified. Shared by startup
* migration recovery and `ocx init` cleanup so the two paths cannot drift.
* `.pre-openai-tiers-v1-rollback.<timestamp>[suffix].bak` path, then atomically
* claim the blocking v2 name into a private directory. Order is copy → verify
* bytes → harden preserved → mkdir claim dir → rename source onto the unique
* claim path → read the claimed inode → unlink only the claim. The original
* backup path is never unlinked, so a replacement that appears after the claim
* stays. After a successful claim, a claimed-read or claimed-byte failure never
* unlinks the claimed path: it independently hardens that leftover, independently
* restores the original directory entry when vacant (EEXIST keeps a replacement),
* and throws `OpenAiTierRollbackPreserveClaimError` with `claimedPath`. Pre-harden
* failures scrub the unverified destination and never claim the source. Shared by
* startup migration recovery and `ocx init` cleanup.
*/
export function preserveOpenAiTierRollbackSnapshot(
configPath = getConfigPath(),
Expand All @@ -603,6 +652,57 @@ export function preserveOpenAiTierRollbackSnapshot(
if (classifyOpenAiTierBackup(original) !== "rollback") {
throw new OpenAiTierRollbackPreserveError("OpenAI tier backup is not a rollback snapshot", { code: "not-rollback" });
}

const failUnverifiedCopy = (preserved: string, cause: unknown): never => {
let scrubbed = false;
try {
io.truncate(preserved);
scrubbed = true;
} catch (error) {
if (isMissingPathError(error)) scrubbed = true;
else {
try { io.write(preserved, new Uint8Array()); scrubbed = true; } catch { /* removal may still succeed */ }
}
}
let removed = false;
try {
io.unlink(preserved);
removed = true;
} catch (error) {
if (isMissingPathError(error)) removed = true;
else {
try { io.unlink(preserved); removed = true; }
catch (retryError) { if (isMissingPathError(retryError)) removed = true; }
}
}
let restricted = false;
if (!removed) {
try { io.harden(preserved); restricted = true; } catch { /* leftover restriction is best-effort */ }
}
if (!removed && !scrubbed) {
throw new OpenAiTierRollbackPreserveSecretResidualError(preserved, { cause });
}
if (!removed) {
throw new OpenAiTierRollbackPreserveCleanupError(preserved, restricted, { cause });
}
throw cause;
};

const restoreClaimedIfVacant = (claimedPath: string): void => {
try {
io.linkExclusive(claimedPath, backup);
} catch (error) {
if (isAlreadyExistsError(error)) return;
throw error;
}
};

const failClaimedSnapshot = (claimedPath: string, cause?: unknown): never => {
try { io.harden(claimedPath); } catch { /* claimed leftover must remain inspectable; restore still runs */ }
try { restoreClaimedIfVacant(claimedPath); } catch { /* EEXIST keeps B; other restore failures must not hide claimedPath */ }
throw new OpenAiTierRollbackPreserveClaimError(claimedPath, cause === undefined ? undefined : { cause });
};

for (let attempt = 0; attempt < OPENAI_TIER_ROLLBACK_PRESERVE_ATTEMPTS; attempt++) {
const preserved = `${configPath}.pre-openai-tiers-v1-rollback.${Date.now()}${attempt ? `-${attempt}` : ""}.bak`;
try {
Expand All @@ -611,17 +711,70 @@ export function preserveOpenAiTierRollbackSnapshot(
if (isAlreadyExistsError(error)) continue;
throw error;
}
let copied: Uint8Array;
const copied = (() => {
try {
return io.read(preserved);
} catch (error) {
return failUnverifiedCopy(preserved, new OpenAiTierRollbackPreserveError("Failed to read preserved rollback snapshot", { cause: error, code: "mismatch" }));
}
})();
if (!sameBytes(original, copied)) {
failUnverifiedCopy(preserved, new OpenAiTierRollbackPreserveError("Preserved rollback snapshot does not match source bytes", { code: "mismatch" }));
}
try {
copied = io.read(preserved);
io.harden(preserved);
} catch (error) {
throw new OpenAiTierRollbackPreserveError("Failed to read preserved rollback snapshot", { cause: error, code: "mismatch" });
failUnverifiedCopy(preserved, error);
}
if (!sameBytes(original, copied)) {
try { io.unlink(preserved); } catch { /* keep the original backup; incomplete copy is best-effort */ }
throw new OpenAiTierRollbackPreserveError("Preserved rollback snapshot does not match source bytes", { code: "mismatch" });

let claimedDir = "";
let claimedPath = "";
for (let claimAttempt = 0; claimAttempt < OPENAI_TIER_ROLLBACK_PRESERVE_ATTEMPTS; claimAttempt++) {
const candidateDir = `${configPath}.pre-openai-tiers-v2-claim.${Date.now()}${claimAttempt ? `-${claimAttempt}` : ""}`;
try {
io.mkdirExclusive(candidateDir);
} catch (error) {
if (isAlreadyExistsError(error)) continue;
throw error;
}
const candidatePath = join(candidateDir, "claimed.bak");
try {
io.claimExclusive(backup, candidatePath);
} catch (error) {
try { io.rmdir(candidateDir); } catch { /* empty claim dir leftover is not secret-bearing */ }
throw error;
}
claimedDir = candidateDir;
claimedPath = candidatePath;
break;
}
if (!claimedPath) {
throw new OpenAiTierRollbackPreserveError("Unable to find a unique rollback claim path", { code: "exhausted" });
}

const claimedBytes = (() => {
try {
return io.read(claimedPath);
} catch (error) {
return failClaimedSnapshot(claimedPath, error);
}
})();
if (!sameBytes(copied, claimedBytes)) {
failClaimedSnapshot(claimedPath);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

try {
io.unlink(claimedPath);
} catch (error) {
if (!isMissingPathError(error)) {
throw new OpenAiTierRollbackPreserveCleanupError(claimedPath, false, { cause: error });
}
}
try {
io.rmdir(claimedDir);
} catch (error) {
throw new OpenAiTierRollbackPreserveCleanupError(claimedDir, true, { cause: error });
}
io.unlink(backup);
return preserved;
}
throw new OpenAiTierRollbackPreserveError("Unable to find a unique rollback snapshot path", { code: "exhausted" });
Expand Down
Loading
Loading