74 lines
3.1 KiB
TypeScript
74 lines
3.1 KiB
TypeScript
import { execFile as execFileCallback } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
import { promisify } from "node:util";
|
|
|
|
const execFile = promisify(execFileCallback);
|
|
|
|
// The single identity-and-freshness predicate. Every credential writer that must
|
|
// answer "should the caller replace `destination` with `source`?" runs this one
|
|
// predicate: the inbound restore, the outbound copy-back, the per-identity cache
|
|
// slot, and the device-login promotion. The predicate lives in
|
|
// `codex-auth-merge-decision.cjs`. It reads only the two files, and it exits with
|
|
// a code; it never prints token bytes. Argument order sets source and
|
|
// destination (first = source, second = destination), so the caller frames the
|
|
// direction. Exit 10 = use source; exit 20 = keep destination. The leading
|
|
// `--seed-if-dest-absent` flag adds one behavior: fill an absent destination slot
|
|
// from a usable subscription source. This module gives every caller one shared
|
|
// entry point, so the predicate contract can never drift between callers.
|
|
|
|
const DECISION_SCRIPT_PATH = fileURLToPath(
|
|
new URL("./codex-auth-merge-decision.cjs", import.meta.url),
|
|
);
|
|
const SEED_IF_DEST_ABSENT_FLAG = "--seed-if-dest-absent";
|
|
|
|
/** Exit code: install the source credential over the destination. */
|
|
export const USE_SOURCE_EXIT = 10;
|
|
/** Exit code: keep the destination credential. */
|
|
export const KEEP_DESTINATION_EXIT = 20;
|
|
|
|
export interface DecideCodexAuthMergeOptions {
|
|
/** Opt in to the cache-slot seed mode: fill an absent destination from a
|
|
* usable subscription source. */
|
|
seedIfDestAbsent?: boolean;
|
|
/** The caller name that prefixes a predicate error, for example
|
|
* `codex auth cache`. */
|
|
errorLabel: string;
|
|
}
|
|
|
|
/**
|
|
* Runs the shared decision predicate and returns its exit code (10 or 20). A
|
|
* non-10/20 exit, or a failure to run `node`, is a hard failure: this throws so a
|
|
* broken predicate is never mistaken for a "keep destination" decision.
|
|
*/
|
|
export async function decideCodexAuthMerge(
|
|
sourcePath: string,
|
|
destinationPath: string,
|
|
options: DecideCodexAuthMergeOptions,
|
|
): Promise<number> {
|
|
const args = options.seedIfDestAbsent
|
|
? [DECISION_SCRIPT_PATH, SEED_IF_DEST_ABSENT_FLAG, sourcePath, destinationPath]
|
|
: [DECISION_SCRIPT_PATH, sourcePath, destinationPath];
|
|
try {
|
|
await execFile("node", args);
|
|
} catch (error) {
|
|
const code = (error as { code?: unknown }).code;
|
|
if (code === USE_SOURCE_EXIT || code === KEEP_DESTINATION_EXIT) {
|
|
return code;
|
|
}
|
|
const detail =
|
|
typeof code === "string"
|
|
? `node could not be executed (${code})`
|
|
: typeof code === "number"
|
|
? `unexpected predicate exit code ${code}`
|
|
: error instanceof Error
|
|
? error.message
|
|
: String(error);
|
|
throw new Error(`${options.errorLabel} decision predicate failed: ${detail}`);
|
|
}
|
|
// `execFile` resolved, so the predicate exited 0. The predicate always exits 10
|
|
// or 20, so a clean exit 0 is unexpected; fail loud.
|
|
throw new Error(
|
|
`${options.errorLabel} decision predicate exited 0 (expected 10 or 20)`,
|
|
);
|
|
}
|