import { dirname, join } from "jsr:@std/path@1"; export type CodexAuthentication = | { kind: "api-key"; token: string } | { kind: "persistent" }; export function codexAuthentication(token?: string): CodexAuthentication { return token === undefined ? { kind: "persistent" } : { kind: "api-key", token }; } // Codex logs revoked, expired, or reused ChatGPT sign-ins and rejected API keys // on its own ERROR lines; tool output and the echoed prompt must not count. const CODEX_AUTH_FAILURE = /^(?:\S+\s+)?ERROR\b.*(?:\b401\b|unauthorized|invalid_refresh_token|refresh_token_(?:expired|reused|invalidated)|token_(?:revoked|expired)|invalid_api_key|(?:log|sign) in again)/im; export function isCodexAuthFailure(error: string): boolean { return CODEX_AUTH_FAILURE.test(error); } // Claude Code reports a rejected CLAUDE_CODE_OAUTH_TOKEN as an is_error // stream-json result, e.g. "Failed to authenticate. API Error: 401 OAuth // access token is invalid." with api_error_status 401. export function isClaudeAuthFailure( result: { api_error_status?: number | null; result?: string }, ): boolean { return result.api_error_status === 401 || /failed to authenticate|oauth (?:access )?token (?:is invalid|has expired)|invalid api key|authentication_error|please run \/login|not logged in/i .test(result.result ?? ""); } export async function retryInvalidToken< T extends { status: { success: boolean }; error: string }, >(attempt: () => Promise, relogin: () => Promise): Promise { let result = await attempt(); if (!result.status.success && isCodexAuthFailure(result.error)) { await relogin(); result = await attempt(); } return result; } async function readCodexAuth(path: string): Promise { const stat = await Deno.lstat(path); if (!stat.isFile || stat.isSymlink) { throw new Error("Codex login must be a regular file"); } if ((stat.mode! & 0o077) !== 0) { throw new Error("Codex login must be private"); } const text = await Deno.readTextFile(path); let value: unknown; try { value = JSON.parse(text); } catch { throw new Error("invalid Codex login"); } const record = value as Record | null; const tokens = record?.tokens as Record | null; if ( record === null || typeof record !== "object" || typeof record.auth_mode !== "string" || record.auth_mode === "" || tokens === null || typeof tokens !== "object" || !["access_token", "refresh_token", "id_token", "account_id"].every( (name) => typeof tokens[name] === "string" && tokens[name] !== "", ) ) { throw new Error("invalid Codex login"); } return text; } export async function stageCodexAuth( canonicalPath: string, profileHome: string, uid: number, gid: number, ): Promise { const codexHome = join(profileHome, ".codex"); await Deno.mkdir(codexHome, { recursive: true, mode: 0o700 }); await Deno.chmod(codexHome, 0o700); await Deno.chown(codexHome, uid, gid); const stagedPath = join(codexHome, "auth.json"); try { await Deno.writeTextFile(stagedPath, await readCodexAuth(canonicalPath), { mode: 0o600, }); await Deno.chmod(stagedPath, 0o600); await Deno.chown(stagedPath, uid, gid); } catch (error) { if (!(error instanceof Deno.errors.NotFound)) throw error; } return stagedPath; } export async function persistCodexAuth( stagedPath: string, canonicalPath: string, ): Promise { const text = await readCodexAuth(stagedPath); const directory = dirname(canonicalPath); await Deno.mkdir(directory, { recursive: true, mode: 0o700 }); const temporary = await Deno.makeTempFile({ dir: directory }); try { await Deno.writeTextFile(temporary, text, { mode: 0o600 }); await Deno.chmod(temporary, 0o600); await Deno.rename(temporary, canonicalPath); } catch (error) { await Deno.remove(temporary).catch(() => undefined); throw error; } }