96 lines
2.9 KiB
TypeScript
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;
|
|
}
|
|
}
|