Files
bot-agents/auth.ts
T
Danny Kim 80f3d922d1
Check / deno (pull_request) Failing after 42s
Centralize encrypted bot profiles
2026-09-24 04:56:11 +09:00

96 lines
2.9 KiB
TypeScript

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 };
}
export async function retryInvalidToken<
T extends { status: { success: boolean }; error: string },
>(attempt: () => Promise<T>, relogin: () => Promise<void>): Promise<T> {
let result = await attempt();
if (
!result.status.success && result.error.includes("invalid_refresh_token")
) {
await relogin();
result = await attempt();
}
return result;
}
async function readCodexAuth(path: string): Promise<string> {
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<string, unknown> | null;
const tokens = record?.tokens as Record<string, unknown> | 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<string> {
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<void> {
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;
}
}