import { discoverOmp, piAuthPath, readHarness, readJson, type StoreLookup } from "./stores.js"; import { homedir } from "node:os"; import { join } from "node:path"; import { balanceToneFromRemaining, hashAccountKey, unavailable, type UsageAccount, toneFromUsedPct, windowFromUsedPct, type UsageReport, type UsageWindow, } from "@getpaseo/plugin/server/usage"; import { z } from "zod"; import type { CodexUsageInput } from "../shared/input.js"; const authSchema = z.object({ tokens: z .object({ access_token: z.string().optional(), account_id: z.string().optional(), id_token: z.string().optional(), }) .optional(), }); const number = z.coerce.number().finite(); const windowSchema = z.object({ used_percent: number.optional(), reset_at: number.optional() }); const responseSchema = z.object({ plan_type: z.string().optional(), email: z.string().optional(), rate_limit: z .object({ primary_window: windowSchema.nullish(), secondary_window: windowSchema.nullish() }) .nullish(), code_review_rate_limit: z.object({ primary_window: windowSchema.nullish() }).nullish(), credits: z.object({ balance: number.optional() }).nullish(), }); interface Auth { token: string; accountId?: string; idToken?: string; expires?: number; } export async function discover(lookup: StoreLookup = {}): Promise { const env = lookup.env ?? process.env; const home = lookup.home ?? homedir(); const paths = [ ...(env.CODEX_HOME ? [join(env.CODEX_HOME, "auth.json")] : []), join(home, ".codex", "auth.json"), ]; const candidates: CodexUsageInput[] = [...new Set(paths)].map((path) => ({ route: { store: "codex", path }, })); candidates.push( { route: { store: "opencode", path: join(env.XDG_DATA_HOME || join(home, ".local", "share"), "opencode", "auth.json"), }, }, { route: { store: "pi", path: piAuthPath(lookup) } }, ...discoverOmp(lookup).map((route) => ({ route })), ); const present: UsageAccount[] = []; for (const input of candidates) { const auth = await readAuth(input, lookup); if (auth) present.push({ ...accountIdentity(auth, input), input }); } return present; } export async function readAuth( input: CodexUsageInput, lookup: StoreLookup = {}, ): Promise { const route = input.route; if (route.store !== "codex") { const oauth = await readHarness(route, lookup); return oauth ? { token: oauth.access, accountId: oauth.accountId, expires: oauth.expires } : null; } const auth = authSchema.safeParse(await readJson(route.path)); if (!auth.success || !auth.data.tokens?.access_token) return null; return { token: auth.data.tokens.access_token, accountId: auth.data.tokens.account_id, idToken: auth.data.tokens.id_token, }; } function usageWindow( spec: { id: string; label: string; shortLabel: string; summary?: boolean }, value: z.infer | null | undefined, ): UsageWindow | null { if (!value) return null; const usedPct = value.used_percent ?? 0; return windowFromUsedPct({ ...spec, utilizationPct: usedPct, resetsAt: value.reset_at != null ? new Date(value.reset_at * 1000).toISOString() : null, tone: toneFromUsedPct(usedPct), }); } export async function fetchUsage( input: CodexUsageInput, fetchApi: typeof fetch = fetch, lookup: StoreLookup = {}, ): Promise { const auth = await readAuth(input, lookup); if (!auth) throw new Error("Codex login store no longer exists"); const refreshedBy = input.route.store; if (auth.expires !== undefined && auth.expires <= (lookup.now ?? Date.now)()) return unavailable({ kind: "expired", expiresAt: new Date(auth.expires).toISOString(), refreshedBy, }); const headers: Record = { Authorization: `Bearer ${auth.token}`, Accept: "application/json", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", }; if (auth.accountId) headers["ChatGPT-Account-Id"] = auth.accountId; const response = await fetchApi("https://chatgpt.com/backend-api/wham/usage", { headers, signal: AbortSignal.timeout(15_000), }); if (response.status === 401 || response.status === 403) return unavailable({ kind: "rejected", status: response.status, refreshedBy }); if (!response.ok) throw new Error(`Codex usage API returned ${response.status}`); const text = await response.text(); if (text.trim().startsWith("<")) throw new Error("Codex usage API returned HTML"); const usage = responseSchema.parse(JSON.parse(text)); const windows = [ usageWindow( { id: "session", label: "Session", shortLabel: "5h", summary: true }, usage.rate_limit?.primary_window, ), usageWindow( { id: "weekly", label: "Weekly", shortLabel: "wk", summary: true }, usage.rate_limit?.secondary_window, ), usageWindow( { id: "code_review", label: "Code review", shortLabel: "review" }, usage.code_review_rate_limit?.primary_window, ), ].filter((window): window is UsageWindow => window !== null); const balance = usage.credits?.balance; return { status: "available", planLabel: usage.plan_type, windows, balances: balance === undefined ? [] : [ { id: "credits", label: "Credits", remaining: balance, unit: "credits", tone: balanceToneFromRemaining(balance), }, ], details: [], }; } /** JWT claims are decoded locally; no token or email becomes an account key. */ function jwtClaims(token: string | undefined): Record | null { try { const payload = token?.split(".")[1]; return payload ? (JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as Record) : null; } catch { return null; } } function claimObject( claims: Record | null, name: string, ): Record | null { const value = claims?.[name]; return value && typeof value === "object" ? (value as Record) : null; } function claimString(value: unknown): string | null { return typeof value === "string" && value.length > 0 ? value : null; } function accountIdentity(auth: Auth, input: CodexUsageInput): { key: string; label?: string } { const access = jwtClaims(auth.token); const id = jwtClaims(auth.idToken); const accessAuth = claimObject(access, "https://api.openai.com/auth"); const idAuth = claimObject(id, "https://api.openai.com/auth"); const key = auth.accountId ?? claimString(accessAuth?.["chatgpt_account_id"]) ?? claimString(access?.["chatgpt_account_id"]) ?? claimString(idAuth?.["chatgpt_account_id"]); const label = claimString(claimObject(access, "https://api.openai.com/profile")?.["email"]) ?? claimString(access?.["email"]) ?? claimString(claimObject(id, "https://api.openai.com/profile")?.["email"]) ?? claimString(id?.["email"]); return { key: key ?? hashAccountKey(JSON.stringify(input.route)), ...(label ? { label } : {}) }; }