@opengeni/codex 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2ESTGQ57.js +54 -0
- package/dist/chunk-2ESTGQ57.js.map +1 -0
- package/dist/constants.d.ts +24 -0
- package/dist/constants.js +49 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +267 -0
- package/dist/index.js +879 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/src/api-client.ts +54 -0
- package/src/billing.ts +15 -0
- package/src/constants.ts +57 -0
- package/src/device-code.ts +111 -0
- package/src/fetch.ts +337 -0
- package/src/index.ts +10 -0
- package/src/mcp-sanitize.ts +290 -0
- package/src/normalize.ts +132 -0
- package/src/refresh.ts +124 -0
- package/src/request-context.ts +50 -0
- package/src/usage-normalize.ts +263 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/billing.ts","../src/device-code.ts","../src/refresh.ts","../src/normalize.ts","../src/usage-normalize.ts","../src/api-client.ts","../src/request-context.ts","../src/fetch.ts","../src/mcp-sanitize.ts"],"sourcesContent":["import { CODEX_MODEL_ID_PREFIX } from \"./constants\";\n\n/**\n * Pure NECESSARY condition for a Codex-billed turn: the model id is namespaced\n * for the Codex subscription provider (`codex/<slug>`).\n *\n * This is NOT sufficient to bypass billing — an active, connected workspace\n * credential and the deployment flag (`settings.codexSubscriptionEnabled`) are\n * ALSO required (see `isCodexBilledTurn`/`workspaceCodexSubscriptionActive` in\n * `@opengeni/db`). Used only as a cheap, synchronous short-circuit so the common\n * non-codex path never issues a credential read.\n */\nexport function isCodexBilledModel(model: string | null | undefined): boolean {\n return typeof model === \"string\" && model.startsWith(CODEX_MODEL_ID_PREFIX);\n}\n","// Device-code (headless) login flow for a ChatGPT/Codex subscription.\n// Grounded in codex-rs device_code_auth.rs:67-145 + server.rs:732-766 (spec §1.1).\n// Every call takes an injectable fetch so tests can supply a fake.\n\nimport {\n CODEX_AUTH_BASE,\n CODEX_CLIENT_ID,\n CODEX_DEVICE_REDIRECT_URI,\n CODEX_DEVICE_VERIFICATION_URL,\n CODEX_TOKEN_URL,\n} from \"./constants\";\n\nexport type CodexFetch = (input: string | URL, init?: RequestInit) => Promise<Response>;\n\nexport type CodexDeviceStart = {\n deviceAuthId: string;\n userCode: string;\n verificationUri: string;\n intervalSeconds: number;\n};\n\nexport type CodexTokens = { idToken: string; accessToken: string; refreshToken: string };\n\nexport type CodexPollResult =\n | { status: \"pending\" }\n | { status: \"expired\" }\n | { status: \"authorized\"; authorizationCode: string; codeVerifier: string };\n\nexport class CodexDeviceError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CodexDeviceError\";\n }\n}\n\n/** Step 1: POST {auth}/deviceauth/usercode {client_id}. device_code_auth.rs:67-95 */\nexport async function startDeviceCode(fetchImpl: CodexFetch = fetch): Promise<CodexDeviceStart> {\n const res = await fetchImpl(`${CODEX_AUTH_BASE}/deviceauth/usercode`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ client_id: CODEX_CLIENT_ID }),\n });\n if (res.status === 404) {\n throw new CodexDeviceError(\"device code login is not enabled for this Codex server\");\n }\n if (!res.ok) {\n throw new CodexDeviceError(`device code request failed with status ${res.status}`);\n }\n const body = (await res.json()) as {\n device_auth_id: string;\n user_code?: string;\n usercode?: string;\n interval?: string | number;\n };\n return {\n deviceAuthId: body.device_auth_id,\n userCode: body.user_code ?? body.usercode ?? \"\",\n verificationUri: CODEX_DEVICE_VERIFICATION_URL,\n intervalSeconds: normalizeInterval(body.interval),\n };\n}\n\n/** Clamp the poll interval to a sane minimum: a missing/0/NaN value must not become a 0-delay poll loop. */\nfunction normalizeInterval(raw: string | number | undefined): number {\n const n = typeof raw === \"string\" ? Number.parseInt(raw.trim(), 10) : raw;\n return typeof n === \"number\" && Number.isFinite(n) && n >= 1 ? n : 5;\n}\n\n/** Step 3 (single, non-blocking): POST {auth}/deviceauth/token. 403/404 => pending. device_code_auth.rs:106-145 */\nexport async function pollDeviceCode(\n input: { deviceAuthId: string; userCode: string },\n fetchImpl: CodexFetch = fetch,\n): Promise<CodexPollResult> {\n const res = await fetchImpl(`${CODEX_AUTH_BASE}/deviceauth/token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ device_auth_id: input.deviceAuthId, user_code: input.userCode }),\n });\n if (res.ok) {\n const body = (await res.json()) as { authorization_code: string; code_verifier: string };\n return { status: \"authorized\", authorizationCode: body.authorization_code, codeVerifier: body.code_verifier };\n }\n if (res.status === 403 || res.status === 404) {\n return { status: \"pending\" };\n }\n throw new CodexDeviceError(`device auth failed with status ${res.status}`);\n}\n\n/** Step 4: POST {issuer}/oauth/token form-encoded grant_type=authorization_code. server.rs:732-766 */\nexport async function exchangeDeviceCode(\n input: { authorizationCode: string; codeVerifier: string },\n fetchImpl: CodexFetch = fetch,\n): Promise<CodexTokens> {\n const form = new URLSearchParams({\n grant_type: \"authorization_code\",\n code: input.authorizationCode,\n redirect_uri: CODEX_DEVICE_REDIRECT_URI,\n client_id: CODEX_CLIENT_ID,\n code_verifier: input.codeVerifier,\n });\n const res = await fetchImpl(CODEX_TOKEN_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: form.toString(),\n });\n if (!res.ok) {\n throw new CodexDeviceError(`device code exchange failed with status ${res.status}`);\n }\n const body = (await res.json()) as { id_token: string; access_token: string; refresh_token: string };\n return { idToken: body.id_token, accessToken: body.access_token, refreshToken: body.refresh_token };\n}\n","// Token refresh + JWT helpers + permanent-failure classification.\n// Refresh is JSON-bodied (exchange is form-encoded — spec §1.1 contrasts these).\n// Classification mirrors codex-rs manager.rs:180-184.\n\nimport { CODEX_CLIENT_ID, CODEX_ID_TOKEN_AUTH_CLAIM, CODEX_TOKEN_URL } from \"./constants\";\nimport type { CodexFetch } from \"./device-code\";\n\n/** Permanent — the workspace must reconnect (status => needs_relogin). */\nexport class CodexReloginRequired extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CodexReloginRequired\";\n }\n}\n\n/** Transient — safe to retry later. */\nexport class CodexRefreshTransient extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CodexRefreshTransient\";\n }\n}\n\n/** Only present fields are returned (the server may rotate any subset). */\nexport type CodexRefreshTokens = {\n idToken?: string | undefined;\n accessToken?: string | undefined;\n refreshToken?: string | undefined;\n};\n\n/** POST {issuer}/oauth/token JSON {client_id, grant_type:\"refresh_token\", refresh_token}. manager.rs:1336-1340 */\nexport async function refreshCodexToken(\n refreshToken: string,\n fetchImpl: CodexFetch = fetch,\n): Promise<CodexRefreshTokens> {\n const res = await fetchImpl(CODEX_TOKEN_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ client_id: CODEX_CLIENT_ID, grant_type: \"refresh_token\", refresh_token: refreshToken }),\n });\n const text = await res.text();\n if (!res.ok) {\n const code = extractRefreshErrorCode(text);\n const msg = code ? PERMANENT_REFRESH_FAILURES[code] : undefined;\n if (msg) {\n throw new CodexReloginRequired(msg);\n }\n if (res.status === 401) {\n throw new CodexReloginRequired(\"Your Codex session could not be refreshed. Please disconnect and sign in again.\");\n }\n throw new CodexRefreshTransient(`Failed to refresh Codex token: ${res.status}`);\n }\n const body = JSON.parse(text) as { id_token?: string; access_token?: string; refresh_token?: string };\n return { idToken: body.id_token, accessToken: body.access_token, refreshToken: body.refresh_token };\n}\n\n// Codes that mean the refresh token is permanently dead -> reconnect required.\n// Includes the standard OAuth `invalid_grant` alongside the Codex-specific codes.\nconst PERMANENT_REFRESH_FAILURES: Record<string, string> = {\n refresh_token_expired: \"Your Codex refresh token has expired. Please disconnect and sign in again.\",\n refresh_token_reused: \"Your Codex refresh token was already used. Please disconnect and sign in again.\",\n refresh_token_invalidated: \"Your Codex refresh token was revoked. Please disconnect and sign in again.\",\n invalid_grant: \"Your Codex session is no longer valid. Please disconnect and sign in again.\",\n};\n\n/** Pull an error code from any of the shapes the auth server may return. */\nfunction extractRefreshErrorCode(text: string): string | undefined {\n try {\n const o = JSON.parse(text) as Record<string, unknown>;\n const err = o.error;\n if (typeof err === \"string\") {\n return err; // { \"error\": \"invalid_grant\" }\n }\n if (err && typeof err === \"object\") {\n const e = err as Record<string, unknown>;\n if (typeof e.code === \"string\") return e.code; // { \"error\": { \"code\": \"...\" } }\n if (typeof e.type === \"string\") return e.type;\n }\n if (typeof o.code === \"string\") return o.code; // { \"code\": \"...\" }\n if (typeof o.type === \"string\") return o.type;\n } catch {\n /* not JSON */\n }\n return undefined;\n}\n\n/** Decode a JWT payload (base64url, no signature check). */\nexport function decodeJwtPayload(jwt: string): Record<string, unknown> | null {\n const part = jwt.split(\".\")[1];\n if (!part) {\n return null;\n }\n try {\n const json = Buffer.from(part.replace(/-/g, \"+\").replace(/_/g, \"/\"), \"base64\").toString(\"utf8\");\n return JSON.parse(json) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n/** access-token `exp` claim -> Date | null. token_data.rs:101-105 */\nexport function accessTokenExpiry(accessToken: string): Date | null {\n const payload = decodeJwtPayload(accessToken);\n return typeof payload?.exp === \"number\" ? new Date(payload.exp * 1000) : null;\n}\n\n/** id_token -> {chatgptAccountId, planType, isFedramp}. server.rs:827-832; token_data.rs:71-99 */\nexport function parseIdToken(idToken: string): {\n chatgptAccountId: string | null;\n planType: string | null;\n isFedramp: boolean;\n email: string | null;\n} {\n const payload = decodeJwtPayload(idToken);\n const auth = (payload?.[CODEX_ID_TOKEN_AUTH_CLAIM] ?? {}) as Record<string, unknown>;\n return {\n chatgptAccountId: typeof auth.chatgpt_account_id === \"string\" ? auth.chatgpt_account_id : null,\n planType: typeof auth.chatgpt_plan_type === \"string\" ? auth.chatgpt_plan_type : null,\n isFedramp: auth.chatgpt_account_is_fedramp === true,\n // The user's own email (standard OIDC `email` claim on the id_token); a\n // non-secret display field for the accounts UI. Null when absent.\n email: typeof payload?.email === \"string\" ? payload.email : null,\n };\n}\n","// Pure request-body + model-slug transforms for the ChatGPT/Codex backend.\n//\n// Per the verified NORMALIZATION VERDICT (CODEX-IMPL-PACKET §0), against our\n// @openai/agents stack we do EXACTLY this and no more:\n// - force store:false\n// - union include with reasoning.encrypted_content\n// - strip max_output_tokens / max_completion_tokens\n// - reasoning effort minimal -> low\n// - normalize the model slug (longest-prefix against the live catalog)\n// - strip every item `id` but PRESERVE `call_id`\n// We do NOT filter item_reference (the SDK never emits it) and do NOT convert\n// orphaned tool outputs (the SDK's runner already prunes by call_id).\n\nconst MINIMAL = \"minimal\";\n\n// The ChatGPT/Codex backend is a STRICT ALLOWLIST: it 400s on ANY top-level field\n// the Codex CLI itself does not send (confirmed live against the backend —\n// \"Unsupported parameter: temperature / top_p / metadata / previous_response_id /\n// logprobs / service_tier / user / safety_identifier / truncation / max_tool_calls /\n// background / conversation\", and \"Unsupported tool type: mcp\"). Our @openai/agents\n// stack adds several of these, so after our transforms we keep ONLY the codex\n// Responses payload fields (CODEX-SUBSCRIPTION-SPEC §1 field table).\nconst CODEX_ALLOWED_TOP_LEVEL_KEYS = new Set<string>([\n \"model\",\n \"instructions\",\n \"input\",\n \"tools\",\n \"tool_choice\",\n \"parallel_tool_calls\",\n \"reasoning\",\n \"store\",\n \"stream\",\n \"include\",\n \"prompt_cache_key\",\n \"text\",\n]);\n\n/** Mutates a parsed Responses request body in place and returns it. Pure + synchronous + unit-testable. */\nexport function normalizeCodexRequestBody(\n body: Record<string, unknown>,\n resolveModel: (slug: string) => string,\n): Record<string, unknown> {\n body.store = false; // ChatGPT backend REQUIRES store=false (spec §1.3)\n body.stream = true; // ChatGPT backend REQUIRES stream=true (confirmed live: 400 \"Stream must be set to true\").\n\n // include MUST contain reasoning.encrypted_content (stateless continuity, spec §1.6)\n const include = Array.isArray(body.include) ? (body.include as unknown[]).filter((v): v is string => typeof v === \"string\") : [];\n if (!include.includes(\"reasoning.encrypted_content\")) {\n include.push(\"reasoning.encrypted_content\");\n }\n body.include = include;\n\n // reasoning effort: minimal -> low (backend rejects minimal). spec §1.5\n const reasoning = body.reasoning as { effort?: string } | null | undefined;\n if (reasoning && reasoning.effort === MINIMAL) {\n reasoning.effort = \"low\";\n }\n\n // model slug: longest-prefix against the live catalog. spec §1.4\n if (typeof body.model === \"string\") {\n body.model = resolveModel(body.model);\n }\n\n // strip every item id; PRESERVE call_id. spec §1.6 / verdict §0(b)\n // (This also covers tool_search items: the backend accepts an id-less\n // tool_search_call/output pair correlated by call_id — verified live — and\n // stripping the account-bound `tsc_…` id here sanitizes BOTH replay paths.)\n if (Array.isArray(body.input)) {\n for (const item of body.input as unknown[]) {\n if (!item || typeof item !== \"object\") {\n continue;\n }\n const record = item as Record<string, unknown>;\n if (\"id\" in record) {\n delete record.id;\n }\n // A replayed tool_search_call must carry `arguments` as an OBJECT — the\n // backend 400s a string (\"Invalid type for 'input[N].arguments': expected\n // an object\", verified live). The live wire emits an object (the SDK's\n // protocol schema is z.unknown() and round-trips it), so this only fires\n // for a defensively-stringified row; unparseable strings fall back to {}.\n if (record.type === \"tool_search_call\" && typeof record.arguments === \"string\") {\n try {\n const parsed = JSON.parse(record.arguments) as unknown;\n record.arguments = parsed && typeof parsed === \"object\" ? parsed : {};\n } catch {\n record.arguments = {};\n }\n }\n }\n }\n\n // Drop hosted-MCP tool entries: the backend rejects them (\"Unsupported tool\n // type: mcp\"). OpenGeni's MCP servers are client-connected, so their tools\n // already arrive as `function` tools — this only sheds a stray `mcp` entry.\n if (Array.isArray(body.tools)) {\n body.tools = (body.tools as unknown[]).filter(\n (t) => !(t && typeof t === \"object\" && (t as Record<string, unknown>).type === \"mcp\"),\n );\n }\n\n // Final allowlist: shed every other top-level field our @openai/agents stack\n // may have added (temperature, top_p, metadata, previous_response_id,\n // max_output_tokens, truncation, …) so the strict backend does not 400.\n for (const key of Object.keys(body)) {\n if (!CODEX_ALLOWED_TOP_LEVEL_KEYS.has(key)) {\n delete body[key];\n }\n }\n return body;\n}\n\n/**\n * Build a longest-prefix model resolver. Catalog slugs come from GET /models\n * (api-client.ts). One leading `namespace/` segment is stripped first; an\n * unknown slug returns the fallback (caller should log — spec §1.4 step 4).\n */\nexport function buildModelResolver(\n liveSlugs: readonly string[],\n fallbackSlug: string,\n): (slug: string) => string {\n return (requested: string): string => {\n const stripped = requested.includes(\"/\") ? requested.slice(requested.indexOf(\"/\") + 1) : requested;\n let best = \"\";\n for (const slug of liveSlugs) {\n if (stripped.startsWith(slug) && slug.length > best.length) {\n best = slug;\n }\n }\n return best || fallbackSlug;\n };\n}\n","// Normalizer for GET /wham/usage (P2). The live body exposes `used_percent` +\n// reset timing per window and NO raw used/limit/remaining integer counts (the only\n// raw counts live under `credits.approx_*_messages`). So the brief's\n// used/limit/remaining/percent/resetAt shape is SYNTHESIZED off `used_percent`,\n// with `percent` authoritative and used/limit/remaining carried on a normalized\n// 0–100 scale (limit = 100). `remaining = 100 - percent` is the P3 rotation key\n// (rotationStrategy:\"most_remaining\" ranks by max(min(fiveHour, weekly).remaining)).\n//\n// Windows are identified by `limit_window_seconds` (18000 ⇒ 5h, 604800 ⇒ weekly),\n// NEVER by position. A 200 may carry `limit_reached:true`; a 404 carries a\n// limit-reached body. The parser is zod over rate_limit.{primary,secondary}_window.\n\nimport * as z from \"zod/v4\";\n\n/** The 5-hour (primary) window's `limit_window_seconds`. */\nexport const CODEX_FIVE_HOUR_WINDOW_SECONDS = 18000;\n/** The weekly (secondary) window's `limit_window_seconds`. */\nexport const CODEX_WEEKLY_WINDOW_SECONDS = 604800;\n\n/** One normalized usage window (applied to BOTH primary_window and secondary_window). */\nexport type CodexUsageWindow = {\n used: number; // = percent (0–100 scale, limit = 100)\n limit: number; // = 100 (normalized; the provider gives no raw cap)\n remaining: number; // = 100 - percent ← P3 rotation key\n percent: number; // = used_percent (authoritative)\n resetAt: string | null; // ISO 8601, from reset_at*1000 (absolute), or derived from reset_after_seconds\n resetAfterSeconds: number | null; // from reset_after_seconds (skew-free countdown)\n limitWindowSeconds: number; // 18000 | 604800 — identify the window, never positional\n};\n\n/** One additional (per-feature) limit (forward-compat; P2 renders nothing from it). */\nexport type CodexAdditionalLimit = {\n limitName: string;\n meteredFeature: string;\n fiveHour: CodexUsageWindow | null;\n weekly: CodexUsageWindow | null;\n};\n\nexport type CodexUsageStatus = \"ok\" | \"limit_reached\" | \"error\" | \"no-data\";\n\n/** The normalized usage payload — the P2/P3 contract. */\nexport type CodexUsagePayload = {\n status: CodexUsageStatus;\n planType: string | null; // \"pro\" | \"plus\" | ... (rate row label)\n fiveHour: CodexUsageWindow | null; // ← rate_limit.primary_window (limitWindowSeconds === 18000)\n weekly: CodexUsageWindow | null; // ← rate_limit.secondary_window (604800)\n limitReached: boolean; // rate_limit.limit_reached || !rate_limit.allowed\n fetchedAt: string; // ISO; server stamp\n /** Present only on a refresh/auth failure path; carries the precise reason. */\n reason?: \"needs_relogin\" | undefined;\n // forward-compat, populated but unused in P2:\n additionalLimits?: CodexAdditionalLimit[] | undefined;\n credits?: { hasCredits: boolean; unlimited: boolean; overageLimitReached: boolean; balance: string } | undefined;\n};\n\n/**\n * Build a normalized window from the PERSISTED cache columns (used_percent +\n * absolute reset timestamp). The same 0–100 synthesis as the live path, with the\n * skew-free countdown derived from `resetAt − now` at read time. Returns null when\n * there is no cached percent yet. `limitWindowSeconds` is the constant that\n * identifies the window (18000 ⇒ 5h, 604800 ⇒ weekly).\n */\nexport function buildCodexUsageWindowFromCache(\n usedPercent: number | null | undefined,\n resetAt: Date | string | null | undefined,\n limitWindowSeconds: number,\n): CodexUsageWindow | null {\n if (typeof usedPercent !== \"number\") {\n return null;\n }\n const percent = clampPercent(usedPercent);\n const resetDate = resetAt ? new Date(resetAt) : null;\n const resetIso = resetDate && !Number.isNaN(resetDate.getTime()) ? resetDate.toISOString() : null;\n const resetAfterSeconds = resetDate && !Number.isNaN(resetDate.getTime())\n ? Math.max(0, Math.round((resetDate.getTime() - Date.now()) / 1000))\n : null;\n return {\n used: percent,\n limit: 100,\n remaining: 100 - percent,\n percent,\n resetAt: resetIso,\n resetAfterSeconds,\n limitWindowSeconds,\n };\n}\n\nconst windowSchema = z\n .object({\n used_percent: z.number().optional(),\n reset_after_seconds: z.number().optional(),\n reset_at: z.number().optional(),\n limit_window_seconds: z.number().optional(),\n })\n .nullish();\n\nconst rateLimitSchema = z\n .object({\n allowed: z.boolean().optional(),\n limit_reached: z.boolean().optional(),\n primary_window: windowSchema,\n secondary_window: windowSchema,\n })\n .nullish();\n\nconst additionalLimitSchema = z.object({\n limit_name: z.string().optional(),\n metered_feature: z.string().optional(),\n primary_window: windowSchema,\n secondary_window: windowSchema,\n});\n\nconst creditsSchema = z\n .object({\n has_credits: z.boolean().optional(),\n unlimited: z.boolean().optional(),\n overage_limit_reached: z.boolean().optional(),\n balance: z.union([z.string(), z.number()]).optional(),\n })\n .nullish();\n\nconst usageBodySchema = z.object({\n plan_type: z.string().nullish(),\n rate_limit: rateLimitSchema,\n additional_limits: z.array(additionalLimitSchema).nullish(),\n credits: creditsSchema,\n});\n\ntype RawWindow = z.infer<typeof windowSchema>;\n\nfunction clampPercent(value: number): number {\n if (!Number.isFinite(value)) return 0;\n return Math.min(100, Math.max(0, Math.round(value)));\n}\n\n/** Build a normalized window from a raw provider window, or null when it carries no percent. */\nfunction normalizeWindow(w: RawWindow): CodexUsageWindow | null {\n if (!w || typeof w.used_percent !== \"number\") {\n return null;\n }\n const percent = clampPercent(w.used_percent);\n const resetAfterSeconds = typeof w.reset_after_seconds === \"number\" ? Math.max(0, Math.round(w.reset_after_seconds)) : null;\n let resetAt: string | null = null;\n if (typeof w.reset_at === \"number\") {\n resetAt = new Date(w.reset_at * 1000).toISOString(); // epoch SECONDS → ms\n } else if (resetAfterSeconds != null) {\n resetAt = new Date(Date.now() + resetAfterSeconds * 1000).toISOString();\n }\n return {\n used: percent,\n limit: 100,\n remaining: 100 - percent,\n percent,\n resetAt,\n resetAfterSeconds,\n limitWindowSeconds: typeof w.limit_window_seconds === \"number\" ? w.limit_window_seconds : 0,\n };\n}\n\n/**\n * Map the two named windows to fiveHour/weekly by `limit_window_seconds`\n * (18000 vs 604800), NEVER by position; fall back to position (primary ⇒ 5h,\n * secondary ⇒ weekly) only for a window whose limit_window_seconds is absent.\n */\nfunction pickWindows(primary: RawWindow, secondary: RawWindow): { fiveHour: CodexUsageWindow | null; weekly: CodexUsageWindow | null } {\n let fiveHour: CodexUsageWindow | null = null;\n let weekly: CodexUsageWindow | null = null;\n // Track each unplaced window with the slot it came from, so the positional\n // fallback can place it (re-normalizing produces a fresh object that would\n // never match by reference — the bug this replaces).\n const unplaced: Array<{ slot: \"primary\" | \"secondary\"; window: CodexUsageWindow }> = [];\n for (const [slot, raw] of [[\"primary\", primary], [\"secondary\", secondary]] as const) {\n const nw = normalizeWindow(raw);\n if (!nw) continue;\n if (nw.limitWindowSeconds === CODEX_WEEKLY_WINDOW_SECONDS) {\n weekly = nw;\n } else if (nw.limitWindowSeconds === CODEX_FIVE_HOUR_WINDOW_SECONDS) {\n fiveHour = nw;\n } else {\n unplaced.push({ slot, window: nw });\n }\n }\n // Positional fallback for windows whose limit_window_seconds was absent/unknown\n // (primary ⇒ 5h, secondary ⇒ weekly).\n for (const { slot, window } of unplaced) {\n if (slot === \"primary\" && !fiveHour) fiveHour = window;\n else if (slot === \"secondary\" && !weekly) weekly = window;\n }\n return { fiveHour, weekly };\n}\n\n/**\n * Normalize a /wham/usage fetch result into the P2/P3 contract.\n *\n * @param httpStatus the HTTP status from fetchCodexUsage (404 ⇒ a limit body)\n * @param rawPayload the parsed JSON body (or null when the body was unreadable)\n */\nexport function normalizeCodexUsage(httpStatus: number, rawPayload: unknown): CodexUsagePayload {\n const fetchedAt = new Date().toISOString();\n const parsed = usageBodySchema.safeParse(rawPayload);\n const body = parsed.success ? parsed.data : null;\n\n const base: CodexUsagePayload = {\n status: \"no-data\",\n planType: body?.plan_type ?? null,\n fiveHour: null,\n weekly: null,\n limitReached: false,\n fetchedAt,\n };\n\n // A non-404 HTTP error, or a body we could not parse at all, is an error state.\n if ((httpStatus >= 400 && httpStatus !== 404) || body == null) {\n return { ...base, status: \"error\" };\n }\n\n const rate = body.rate_limit ?? null;\n const { fiveHour, weekly } = pickWindows(rate?.primary_window ?? null, rate?.secondary_window ?? null);\n const limitReached =\n !!(rate?.limit_reached || rate?.allowed === false) || (fiveHour?.percent ?? 0) >= 100 || (weekly?.percent ?? 0) >= 100;\n\n const additionalLimits: CodexAdditionalLimit[] | undefined = body.additional_limits\n ? body.additional_limits.map((al) => {\n const windows = pickWindows(al.primary_window ?? null, al.secondary_window ?? null);\n return {\n limitName: al.limit_name ?? \"\",\n meteredFeature: al.metered_feature ?? \"\",\n fiveHour: windows.fiveHour,\n weekly: windows.weekly,\n };\n })\n : undefined;\n\n const credits = body.credits\n ? {\n hasCredits: body.credits.has_credits ?? false,\n unlimited: body.credits.unlimited ?? false,\n overageLimitReached: body.credits.overage_limit_reached ?? false,\n balance: body.credits.balance != null ? String(body.credits.balance) : \"0\",\n }\n : undefined;\n\n // Status derivation: 404 ⇒ limit_reached; a 200 may still carry limit_reached;\n // succeeded-but-no-windows ⇒ no-data; otherwise ok.\n let status: CodexUsageStatus;\n if (httpStatus === 404 || limitReached) {\n status = \"limit_reached\";\n } else if (!fiveHour && !weekly) {\n status = \"no-data\";\n } else {\n status = \"ok\";\n }\n\n return {\n ...base,\n status,\n fiveHour,\n weekly,\n limitReached,\n ...(additionalLimits ? { additionalLimits } : {}),\n ...(credits ? { credits } : {}),\n };\n}\n","// Thin ChatGPT/Codex API client used outside the streamed turn: the login-check\n// (GET /codex/models) and the usage/limits readback (GET /wham/usage). spec §1.4, §1.8, §F.\n\nimport { CODEX_ORIGINATOR, CODEX_RESPONSES_BASE, CODEX_WHAM_BASE } from \"./constants\";\nimport type { CodexFetch } from \"./device-code\";\n\nexport type CodexAuthHeaders = {\n accessToken: string;\n chatgptAccountId: string | null;\n isFedramp: boolean;\n clientVersion: string;\n};\n\nfunction subscriptionHeaders(a: CodexAuthHeaders): Record<string, string> {\n return {\n Authorization: `Bearer ${a.accessToken}`,\n ...(a.chatgptAccountId ? { \"ChatGPT-Account-ID\": a.chatgptAccountId } : {}),\n originator: CODEX_ORIGINATOR,\n \"User-Agent\": `${CODEX_ORIGINATOR}/${a.clientVersion}`,\n version: a.clientVersion,\n ...(a.isFedramp ? { \"X-OpenAI-Fedramp\": \"true\" } : {}),\n };\n}\n\n/** GET /codex/models — login-check + live catalog. A 200 means the token is accepted. spec §1.4/§F */\nexport async function fetchCodexModels(\n a: CodexAuthHeaders,\n fetchImpl: CodexFetch = fetch,\n): Promise<{ ok: boolean; status: number; slugs: string[] }> {\n const res = await fetchImpl(`${CODEX_RESPONSES_BASE}/models?client_version=${encodeURIComponent(a.clientVersion)}`, {\n method: \"GET\",\n headers: subscriptionHeaders(a),\n });\n if (!res.ok) {\n return { ok: false, status: res.status, slugs: [] };\n }\n const body = (await res.json()) as { models?: Array<{ slug?: string }> };\n const slugs = (body.models ?? []).map((m) => m.slug).filter((s): s is string => typeof s === \"string\");\n return { ok: true, status: res.status, slugs };\n}\n\n/** GET /wham/usage — authoritative limits. NB the WHAM base is /backend-api, NOT /codex (spec §1.8a). */\nexport async function fetchCodexUsage(\n a: CodexAuthHeaders,\n fetchImpl: CodexFetch = fetch,\n): Promise<{ status: number; payload: unknown }> {\n const res = await fetchImpl(`${CODEX_WHAM_BASE}/wham/usage`, {\n method: \"GET\",\n headers: subscriptionHeaders(a),\n });\n // A 404 may carry a usage-limit body; the route layer normalizes it to a limits state (spec §1.8c).\n const payload = res.ok || res.status === 404 ? await res.json().catch(() => null) : null;\n return { status: res.status, payload };\n}\n","// Per-request Codex context, carried via AsyncLocalStorage.\n//\n// The runtime caches one OpenAI client per provider id (process-wide), so the\n// per-workspace token must NOT be baked into the client. Instead the worker sets\n// this context around the model run, and codexSubscriptionFetch reads it at call\n// time — one cached client, correct per-workspace token, no cross-tenant leak.\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nexport type CodexTokenSnapshot = {\n accessToken: string;\n chatgptAccountId: string | null;\n isFedramp: boolean;\n};\n\n/**\n * Multi-account P4 (Part A): a full usage snapshot scraped FOR FREE from the\n * `x-codex-primary-*` / `x-codex-secondary-*` response headers the codex backend\n * stamps on every `/codex/responses` turn (success AND 429 hard-cap). Integer-\n * identical to GET /wham/usage but with zero extra round-trip. parseCodexUsageHeaders\n * returns this only when BOTH windows parse, so a write is always a full 5-column\n * snapshot (no partial-window clobber). Shape mirrors db's CodexAccountUsageSnapshot\n * (non-null here: a partial read is filtered to null upstream, never half-written).\n */\nexport type CodexUsageHeaderSnapshot = {\n primaryUsedPercent: number;\n primaryResetAt: Date;\n secondaryUsedPercent: number;\n secondaryResetAt: Date;\n checkedAt: Date;\n};\n\nexport type CodexRequestContext = {\n clientVersion: string;\n /** Worker-supplied: proactive refresh + single-flight + db persist. */\n getToken: () => Promise<CodexTokenSnapshot>;\n /** Forced refresh used for the 401 retry. */\n refresh: () => Promise<CodexTokenSnapshot>;\n /** Model-slug resolver (longest-prefix against the live catalog). */\n resolveModel: (slug: string) => string;\n /**\n * Multi-account P4 (Part A): fire-and-forget usage-header sink. Called by\n * codexSubscriptionFetch on EVERY response (sync, non-throwing, never awaited)\n * with the parsed full-window snapshot. The worker records the latest into the\n * P2 usage cache once per turn in its `finally` — packages/codex stays db-free.\n */\n onUsageHeaders?: (snapshot: CodexUsageHeaderSnapshot) => void;\n};\n\nexport const codexRequestStorage = new AsyncLocalStorage<CodexRequestContext>();\n","// codexSubscriptionFetch — the transport installed on the OpenAI client for the\n// \"codex-subscription\" provider. Mirrors the runtime's computerCallNormalizingFetch\n// pattern: wraps a base fetch and returns a (input, init) => Promise<Response>.\n//\n// It reads the per-request Codex context from AsyncLocalStorage at CALL time, so a\n// single process-cached client serves every workspace with the correct token. It:\n// - rewrites /responses -> /codex/responses\n// - injects the subscription auth headers (omits OpenAI-Beta on SSE; spec §1.2)\n// - normalizes the request body (spec §0 verdict)\n// - retries once on 401 after a forced token refresh (spec §1.9)\n// Stream parsing is delegated to the SDK (SSE passthrough; spec §0(d)).\n\nimport { CODEX_ORIGINATOR } from \"./constants\";\nimport { normalizeCodexRequestBody } from \"./normalize\";\nimport { codexRequestStorage, type CodexTokenSnapshot, type CodexUsageHeaderSnapshot } from \"./request-context\";\n\nexport type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\n\n/** Parse an integer header value; null when absent or not a finite integer. */\nfunction parseIntHeader(value: string | null): number | null {\n if (value === null) {\n return null;\n }\n const n = Number.parseInt(value.trim(), 10);\n return Number.isFinite(n) ? n : null;\n}\n\n/**\n * Resolve a window reset instant from the response headers: prefer the absolute\n * `*-reset-at` (epoch SECONDS → ms, mirroring codex-token-resolver's usage parse),\n * else the relative `*-reset-after-seconds` from now, else now (a missing reset\n * reads as \"already cleared\" — availableAt treats an elapsed reset as a bounded\n * default cooldown, so the ranker never strands on it).\n */\nfunction resolveResetAt(headers: Headers, atKey: string, afterKey: string, nowMs: number): Date {\n const at = parseIntHeader(headers.get(atKey));\n if (at !== null) {\n return new Date(at * 1000);\n }\n const after = parseIntHeader(headers.get(afterKey));\n if (after !== null) {\n return new Date(nowMs + after * 1000);\n }\n return new Date(nowMs);\n}\n\n/**\n * Multi-account P4 (Part A): scrape the full usage snapshot the codex backend\n * stamps on every `/codex/responses` response in `x-codex-primary-*` /\n * `x-codex-secondary-*` headers (integer-identical to GET /wham/usage, for free).\n *\n * CRITICAL clobber-fix: return null unless BOTH windows expose a valid used-percent\n * integer. recordCodexAccountUsage writes all five columns unconditionally, so a\n * primary-only snapshot would null the weekly column. Both windows are always\n * emitted together on `/codex/responses`; gating on both makes every write a full\n * 5-column snapshot byte-identical to the poll path, and a malformed/absent header\n * set simply no-ops (the /wham/usage poll fallback still covers it).\n */\nexport function parseCodexUsageHeaders(headers: Headers): CodexUsageHeaderSnapshot | null {\n const primaryUsedPercent = parseIntHeader(headers.get(\"x-codex-primary-used-percent\"));\n const secondaryUsedPercent = parseIntHeader(headers.get(\"x-codex-secondary-used-percent\"));\n if (primaryUsedPercent === null || secondaryUsedPercent === null) {\n return null; // not a full both-windows snapshot — no-op (never a partial clobber)\n }\n const nowMs = Date.now();\n return {\n primaryUsedPercent,\n primaryResetAt: resolveResetAt(headers, \"x-codex-primary-reset-at\", \"x-codex-primary-reset-after-seconds\", nowMs),\n secondaryUsedPercent,\n secondaryResetAt: resolveResetAt(headers, \"x-codex-secondary-reset-at\", \"x-codex-secondary-reset-after-seconds\", nowMs),\n checkedAt: new Date(nowMs),\n };\n}\n\nexport function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): FetchLike {\n return async (input, init) => {\n const ctx = codexRequestStorage.getStore();\n if (!ctx) {\n return base(input, init); // not a codex turn — passthrough, untouched\n }\n\n const rawUrl =\n typeof input === \"string\" ? input : input instanceof URL ? input.toString() : input.url;\n // /responses -> /codex/responses, idempotent: the negative lookbehind skips\n // URLs whose base already includes /codex (avoids /codex/codex/responses).\n const rewritten = rawUrl.replace(/(?<!\\/codex)\\/responses(\\b|$)/, \"/codex/responses$1\");\n\n const attempt = async (auth: CodexTokenSnapshot): Promise<Response> => {\n const headers = new Headers(init?.headers);\n headers.set(\"Authorization\", `Bearer ${auth.accessToken}`);\n if (auth.chatgptAccountId) {\n headers.set(\"ChatGPT-Account-ID\", auth.chatgptAccountId);\n }\n headers.set(\"originator\", CODEX_ORIGINATOR);\n headers.set(\"User-Agent\", `${CODEX_ORIGINATOR}/${ctx.clientVersion}`);\n headers.set(\"version\", ctx.clientVersion);\n headers.set(\"accept\", \"text/event-stream\");\n headers.set(\"content-type\", \"application/json\");\n if (auth.isFedramp) {\n headers.set(\"X-OpenAI-Fedramp\", \"true\");\n }\n headers.delete(\"OpenAI-Beta\"); // omit on SSE (spec §1.2); fallback: \"responses=experimental\" if backend 400s\n headers.delete(\"x-api-key\");\n\n // The backend is streaming-only; force stream=true on the wire but remember\n // the caller's intent so a non-streaming caller (e.g. the compaction\n // summarizer) still gets a single JSON Response back.\n let callerWantsStream = true;\n const nextInit: RequestInit = { ...init, headers };\n if (typeof init?.body === \"string\") {\n try {\n const parsed = JSON.parse(init.body) as Record<string, unknown>;\n callerWantsStream = parsed.stream === true;\n nextInit.body = JSON.stringify(normalizeCodexRequestBody(parsed, ctx.resolveModel));\n } catch {\n /* leave unparseable bodies untouched (already copied from init) */\n }\n }\n if (process.env.CODEX_DEBUG) {\n const keys = typeof nextInit.body === \"string\" ? Object.keys(JSON.parse(nextInit.body) as Record<string, unknown>) : [];\n console.error(`[codex-debug] POST ${rewritten} stream=${callerWantsStream} bodyKeys=[${keys.join(\",\")}]`);\n }\n const res = await base(rewritten, nextInit);\n // Multi-account P4 (Part A): scrape the usage headers ONCE, before the\n // OK/!res.ok branch, so the same fire-and-forget read also covers the 429\n // hard-cap path (an exhausted serving account stamps its own fresh\n // used_percent with no extra fetch). Sync + non-throwing + never awaited;\n // `if (usage)` makes an absent/malformed header set a safe no-op. We read\n // res.headers only — the SSE body is never touched here.\n const usage = parseCodexUsageHeaders(res.headers);\n if (usage) {\n ctx.onUsageHeaders?.(usage);\n }\n if (process.env.CODEX_DEBUG && !res.ok) {\n console.error(`[codex-debug] <- ${res.status} ${await res.clone().text()}`);\n }\n // The codex backend leaves the terminal event's response.output empty and\n // delivers the assistant items via output_item.done events instead. The\n // @openai/agents parser (streaming AND non-streaming) reads response.output,\n // so we must reconstruct it: collapse to one JSON Response for a non-streaming\n // caller, or repair the live stream's terminal event for a streaming caller.\n if (!res.ok) {\n // Buffer the error body once and re-emit it as a concrete JSON Response.\n // A streaming responses request whose error body is left as the raw\n // (possibly SSE / already-streamed) Response makes the SDK throw\n // \"<status> status code (no body)\" — the JSON error (type/message/\n // resets_in_seconds) is lost, so a 429 usage cap surfaces as a generic,\n // wrongly-retryable rate-limit. Re-emitting a clean application/json\n // Response lets the SDK reconstruct error.error for EVERY codex error\n // (401/400/5xx too). For a hard usage cap we also pin x-should-retry:false\n // so the SDK does not burn its retry budget on a limit that won't lift.\n return await bufferCodexErrorResponse(res);\n }\n return callerWantsStream ? repairCodexStream(res) : await sseToJsonResponse(res);\n };\n\n let res = await attempt(await ctx.getToken());\n if (res.status === 401) {\n res = await attempt(await ctx.refresh()); // single refresh-on-401 retry (spec §1.9)\n }\n return res;\n };\n}\n\n/** The codex backend's hard-cap error type (ChatGPT/Codex usage limit reached). */\nexport const CODEX_USAGE_LIMIT_ERROR_TYPE = \"usage_limit_reached\";\n\nexport type CodexUsageLimitInfo = {\n /** Seconds until the usage cap resets, when the backend reported it. */\n resetsInSeconds: number | null;\n};\n\n/**\n * Classify a thrown error as a ChatGPT/Codex usage-cap (429 usage_limit_reached)\n * and extract the reset window. The SDK surfaces the codex backend's 429 as an\n * OpenAI APIError whose `.type` (and `.error.type`) is `usage_limit_reached` and\n * whose `.error.resets_in_seconds` carries the cap reset. Walks the cause chain\n * and tolerates the message-only shape so it survives any SDK re-wrapping.\n * Returns null for anything that is not a usage cap.\n */\nexport function classifyCodexUsageLimitError(error: unknown): CodexUsageLimitInfo | null {\n let cur: unknown = error;\n for (let depth = 0; depth < 6 && cur && typeof cur === \"object\"; depth++) {\n const e = cur as Record<string, unknown>;\n const body = (e.error && typeof e.error === \"object\" ? e.error : undefined) as Record<string, unknown> | undefined;\n const type = (typeof e.type === \"string\" ? e.type : undefined) ?? (typeof body?.type === \"string\" ? body.type : undefined);\n const message = typeof e.message === \"string\" ? e.message : \"\";\n const status = Number(e.status);\n if (\n type === CODEX_USAGE_LIMIT_ERROR_TYPE ||\n message.includes(CODEX_USAGE_LIMIT_ERROR_TYPE) ||\n (status === 429 && /usage limit/i.test(message))\n ) {\n const resets =\n (typeof body?.resets_in_seconds === \"number\" ? body.resets_in_seconds : undefined) ??\n (typeof e.resets_in_seconds === \"number\" ? (e.resets_in_seconds as number) : undefined) ??\n null;\n return { resetsInSeconds: resets };\n }\n cur = e.cause;\n }\n return null;\n}\n\n/**\n * Buffer a non-OK codex Response and re-emit it as a clean `application/json`\n * Response so the SDK can reconstruct `error.error` from the body. A 429 usage\n * cap (`error.type === \"usage_limit_reached\"`) is a HARD limit, not transient\n * backpressure, so we pin `x-should-retry: false` to stop the SDK retrying it.\n * Reading the body here also drains the socket of a discarded 401 (no leak).\n */\nasync function bufferCodexErrorResponse(res: Response): Promise<Response> {\n const bodyText = await res.text().catch(() => \"\");\n const headers = new Headers(res.headers);\n headers.set(\"content-type\", \"application/json\");\n headers.delete(\"content-length\"); // body re-serialized\n headers.delete(\"content-encoding\"); // text() already decoded any gzip\n let errorType: string | undefined;\n try {\n const parsed = JSON.parse(bodyText) as { error?: { type?: unknown } };\n errorType = typeof parsed.error?.type === \"string\" ? parsed.error.type : undefined;\n } catch {\n /* non-JSON error body — leave as-is, no retry-header override */\n }\n if (errorType === CODEX_USAGE_LIMIT_ERROR_TYPE) {\n headers.set(\"x-should-retry\", \"false\");\n }\n return new Response(bodyText, { status: res.status, statusText: res.statusText, headers });\n}\n\n/**\n * Collapse a Responses SSE stream into the single JSON Response object a\n * non-streaming `responses.create` caller expects: the terminal response.*\n * event carries the full `response` payload.\n */\nasync function sseToJsonResponse(res: Response): Promise<Response> {\n const text = await res.text();\n let final: Record<string, unknown> | null = null;\n const items: unknown[] = []; // assembled from output_item.done (the codex backend\n // leaves response.completed.response.output empty and emits the items separately).\n for (const block of text.split(\"\\n\\n\")) {\n const data = block\n .split(\"\\n\")\n .filter((l) => l.startsWith(\"data:\"))\n .map((l) => l.slice(5).trim())\n .join(\"\\n\");\n if (!data || data === \"[DONE]\") {\n continue;\n }\n try {\n const ev = JSON.parse(data) as { type?: string; response?: Record<string, unknown>; item?: unknown };\n if (ev.type === \"response.output_item.done\" && ev.item !== undefined) {\n items.push(ev.item);\n } else if (ev.type === \"response.completed\" || ev.type === \"response.done\" || ev.type === \"response.incomplete\") {\n final = ev.response ?? null;\n }\n } catch {\n /* ignore non-JSON keepalive lines */\n }\n }\n if (final && items.length > 0) {\n final = { ...final, output: items }; // prefer the assembled items over an empty output array\n }\n if (process.env.CODEX_DEBUG) {\n console.error(`[codex-debug] sse->json items=${items.length} outputLen=${Array.isArray(final?.output) ? (final.output as unknown[]).length : \"?\"}`);\n }\n const headers = new Headers(res.headers);\n headers.set(\"content-type\", \"application/json\");\n headers.delete(\"content-length\");\n return new Response(JSON.stringify(final ?? {}), { status: 200, headers });\n}\n\n/**\n * Repair a live Responses SSE stream for the @openai/agents streaming parser: pass\n * every event through unchanged, collect the output_item.done items, and inject\n * them into the terminal event's empty `output` so the parser sees the message.\n */\nfunction repairCodexStream(res: Response): Response {\n if (!res.body) {\n return res;\n }\n const items: unknown[] = [];\n const decoder = new TextDecoder();\n const encoder = new TextEncoder();\n let buffer = \"\";\n const transform = new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n buffer += decoder.decode(chunk, { stream: true });\n let idx = buffer.indexOf(\"\\n\\n\");\n while (idx !== -1) {\n const block = buffer.slice(0, idx);\n buffer = buffer.slice(idx + 2);\n controller.enqueue(encoder.encode(`${patchSseBlock(block, items)}\\n\\n`));\n idx = buffer.indexOf(\"\\n\\n\");\n }\n },\n flush(controller) {\n if (buffer.length > 0) {\n controller.enqueue(encoder.encode(patchSseBlock(buffer, items)));\n }\n },\n });\n const headers = new Headers(res.headers);\n headers.delete(\"content-length\");\n return new Response(res.body.pipeThrough(transform), { status: res.status, headers });\n}\n\n/** Collect output_item.done items (mutating `items`); rewrite the terminal event's empty output. */\nfunction patchSseBlock(block: string, items: unknown[]): string {\n const lines = block.split(\"\\n\");\n const dataStr = lines.filter((l) => l.startsWith(\"data:\")).map((l) => l.slice(5).trim()).join(\"\\n\");\n if (!dataStr || dataStr === \"[DONE]\") {\n return block;\n }\n let ev: { type?: string; item?: unknown; response?: Record<string, unknown> };\n try {\n ev = JSON.parse(dataStr);\n } catch {\n return block;\n }\n if (ev.type === \"response.output_item.done\" && ev.item !== undefined) {\n items.push(ev.item);\n return block;\n }\n if (\n (ev.type === \"response.completed\" || ev.type === \"response.done\" || ev.type === \"response.incomplete\") &&\n ev.response\n ) {\n const out = ev.response.output;\n if ((!Array.isArray(out) || out.length === 0) && items.length > 0) {\n ev.response = { ...ev.response, output: items };\n const nonData = lines.filter((l) => !l.startsWith(\"data:\"));\n return [...nonData, `data: ${JSON.stringify(ev)}`].join(\"\\n\");\n }\n }\n return block;\n}\n","// The codex_apps connector MCP is incompatible with the Responses API tool\n// contract in two ways that each fail the whole turn:\n//\n// 1. NAMES. Connector tools are named like \"vercel.deploy_to_vercel\" (dots).\n// The Responses API requires every function-tool name to match\n// ^[A-Za-z0-9_-]+$, so the request 400s (\"Invalid 'tools[0].name': string\n// does not match pattern\"). We cannot just rename them in tools/list — the\n// model would then call a name the MCP server does not know. So we remap\n// BIDIRECTIONALLY at the transport: sanitize the name (and remember the\n// mapping) on the tools/list RESPONSE, and reverse it back to the original on\n// the tools/call REQUEST.\n//\n// 2. OUTPUT SCHEMAS. 122 of 217 tools return an empty `outputSchema: {}` (no\n// `type`). @modelcontextprotocol/sdk validates every tool's outputSchema as a\n// strict `{ type: \"object\", ... }` and ZodErrors the WHOLE tools/list. Since\n// codex_apps runs with cacheToolsList:false it re-lists per turn, so that\n// error (thrown during tool enumeration, outside the best-effort connect\n// wrapper) fails the turn. We drop any non-object outputSchema before the\n// validator sees it — safe, as outputSchema is an advisory hint only.\n\nimport { CODEX_APPS_MCP_SERVER_ID } from \"./constants\";\nimport type { FetchLike } from \"./fetch\";\n\nconst VALID_TOOL_NAME = /^[a-zA-Z0-9_-]+$/;\n\n// The Responses API rejects a function-tool name longer than 64 chars (it 400s\n// the WHOLE turn). Some namespaced connector tool names exceed this, and the\n// collision-disambiguation suffix only lengthens names, so the mapper must cap\n// length too — not just charset.\nconst MAX_TOOL_NAME_LEN = 64;\n\n// CRITICAL: this sanitizer runs on the codex_apps tools/list wire BEFORE OpenGeni's\n// PrefixedMcpServer (packages/runtime) prepends `<serverId>__` to every tool name\n// (prefixedMcpToolName). The 64-char limit applies to that FINAL prefixed name the\n// model sees, so a name we cap at 64 here becomes 64 + 12 = 76 after prefixing and\n// 400s the whole turn. Reserve the runtime prefix so `codex_apps__<sanitized>` is\n// always <= 64. The sanitizer owns the server id, so the reservation is exact and\n// stays self-contained (no runtime import). The reverse mapping is unaffected: the\n// mapper is keyed on the pre-prefix sanitized name, which is what tools/call carries\n// back after PrefixedMcpServer strips its prefix.\nconst RUNTIME_TOOL_NAME_PREFIX_LEN = CODEX_APPS_MCP_SERVER_ID.length + \"__\".length; // `codex_apps__` = 12\nconst EFFECTIVE_MAX_TOOL_NAME_LEN = MAX_TOOL_NAME_LEN - RUNTIME_TOOL_NAME_PREFIX_LEN; // 52\n\n/** Short, stable, charset-legal hash of a string (djb2 → base36). Deterministic. */\nfunction shortHash(input: string): string {\n let h = 5381;\n for (let i = 0; i < input.length; i++) {\n h = ((h << 5) + h + input.charCodeAt(i)) >>> 0; // h * 33 + c, kept unsigned\n }\n return h.toString(36);\n}\n\n/** Truncate to <= EFFECTIVE_MAX_TOOL_NAME_LEN (reserving the runtime prefix), appending `_<hash(original)>` so the result stays unique + deterministic. */\nfunction capLength(candidate: string, original: string): string {\n if (candidate.length <= EFFECTIVE_MAX_TOOL_NAME_LEN) {\n return candidate;\n }\n const suffix = `_${shortHash(original)}`;\n return candidate.slice(0, Math.max(0, EFFECTIVE_MAX_TOOL_NAME_LEN - suffix.length)) + suffix;\n}\n\n/**\n * Maps connector tool names to a Responses-API-legal charset and back. One\n * instance per codex_apps transport (i.e. per turn): tools/list populates it,\n * tools/call reads it. Idempotent across repeat listings.\n */\nexport class ToolNameMapper {\n private readonly sanitizedToOriginal = new Map<string, string>();\n private readonly used = new Set<string>();\n\n /** Return a legal, unique name (<= EFFECTIVE_MAX_TOOL_NAME_LEN, so `<prefix>__name` <= 64) for `original`, recording the reverse mapping. */\n sanitize(original: string): string {\n let candidate = VALID_TOOL_NAME.test(original)\n ? original\n : original.replace(/[^a-zA-Z0-9_-]/g, \"_\") || \"tool\";\n // Enforce the Responses-API 64-char cap (stable hash suffix keyed on the\n // ORIGINAL → deterministic across repeat listings, distinct originals don't\n // collide after truncation).\n candidate = capLength(candidate, original);\n // Disambiguate a genuine collision with a DIFFERENT original (never with\n // the same original — that keeps repeat listings stable/idempotent). Re-cap\n // after each suffix so disambiguation never re-breaches the effective limit.\n if (this.used.has(candidate) && this.sanitizedToOriginal.get(candidate) !== original) {\n const base = candidate;\n let n = 2;\n do {\n const suffix = `_${n++}`;\n candidate = (base.length + suffix.length > EFFECTIVE_MAX_TOOL_NAME_LEN\n ? base.slice(0, EFFECTIVE_MAX_TOOL_NAME_LEN - suffix.length)\n : base) + suffix;\n } while (this.used.has(candidate));\n }\n this.used.add(candidate);\n this.sanitizedToOriginal.set(candidate, original);\n return candidate;\n }\n\n /** Reverse a sanitized name back to the MCP server's original, if known. */\n toOriginal(sanitized: string): string | undefined {\n return this.sanitizedToOriginal.get(sanitized);\n }\n}\n\n/**\n * Drop bad outputSchemas + sanitize tool names on a JSON-RPC tools/list result, in place.\n *\n * P4 (Part B.1): when `namespaceSink` is provided, accumulate each tool's ORIGINAL\n * connector namespace (the segment BEFORE the first dot, e.g. `github` from\n * `github.create_issue`) into it — captured HERE because this pass sees the original\n * dotted name BEFORE mapper.sanitize rewrites the dot away. Only dotted names carry a\n * connector namespace; un-dotted (already-legal) names are not connectors and are skipped.\n */\nfunction sanitizeToolsInRpcMessage(message: unknown, mapper: ToolNameMapper, namespaceSink?: Set<string>): void {\n if (!message || typeof message !== \"object\") {\n return;\n }\n const tools = (message as { result?: { tools?: unknown } }).result?.tools;\n if (!Array.isArray(tools)) {\n return;\n }\n for (const tool of tools) {\n if (!tool || typeof tool !== \"object\") {\n continue;\n }\n const record = tool as Record<string, unknown>;\n if (\"outputSchema\" in record) {\n // Drop EVERY outputSchema, not just malformed/empty ones. The MCP SDK client\n // caches a validator for any tool that declares an outputSchema and validates\n // each tool CALL's `structuredContent` against it — and the codex_apps\n // connectors return results that do NOT match their own declared schemas\n // (e.g. the schema requires a `result` property the response omits), so the\n // SDK throws `McpError -32602: Structured content does not match the tool's\n // output schema` and EVERY such connector tool call fails (observed live:\n // gmail_search_emails / gmail_get_profile / gmail_list_labels all -32602ed).\n // outputSchema is advisory — the agent reads the text `content` regardless —\n // so dropping it makes the connector tools usable. This also subsumes the\n // empty-`{}` case the strict Tool schema rejected at tools/list time.\n delete record.outputSchema;\n }\n if (typeof record.name === \"string\") {\n if (namespaceSink && record.name.includes(\".\")) {\n const namespace = record.name.slice(0, record.name.indexOf(\".\"));\n if (namespace) {\n namespaceSink.add(namespace);\n }\n }\n record.name = mapper.sanitize(record.name);\n }\n }\n}\n\n/**\n * Surface a tool CALL's `structuredContent` to the model by inlining it as a text\n * `content` block, in place.\n *\n * WHY. The @openai/agents MCP bridge forwards ONLY `result.content` to the model\n * (agents-core shims/mcp-server: `const result = parsed.content`) and DISCARDS\n * `result.structuredContent`. The codex_apps connectors return the real payload in\n * `structuredContent` and a bare `\"Action completed.\"` placeholder in `content`\n * (verified live: `gmail.get_profile` → content=[{text:\"Action completed.\"}],\n * structuredContent={id,name,email,…}). Without this the agent's tool call\n * \"succeeds\" but carries NO data — the model sees only the placeholder. Appending\n * the structured payload as a text block makes the data reach the model while\n * leaving the original content untouched.\n *\n * No-op when there is no `structuredContent` — so a tools/list response (or any\n * result without it) passes through unchanged. Runs alongside (after) the empty\n * outputSchema drop, which is what stops the MCP SDK -32602ing this same result.\n */\nfunction inlineStructuredContentInRpcMessage(message: unknown): void {\n if (!message || typeof message !== \"object\") {\n return;\n }\n const result = (message as { result?: unknown }).result;\n if (!result || typeof result !== \"object\") {\n return;\n }\n const record = result as Record<string, unknown>;\n if (!(\"structuredContent\" in record)) {\n return;\n }\n const structured = record.structuredContent;\n if (structured === undefined || structured === null) {\n return;\n }\n const text = typeof structured === \"string\" ? structured : JSON.stringify(structured);\n const content = Array.isArray(record.content) ? [...record.content] : [];\n content.push({ type: \"text\", text });\n record.content = content;\n}\n\n/** Sanitize a single JSON body (application/json MCP response). */\nexport function sanitizeMcpJsonBody(text: string, mapper: ToolNameMapper = new ToolNameMapper(), namespaceSink?: Set<string>): string {\n try {\n const parsed = JSON.parse(text);\n sanitizeToolsInRpcMessage(parsed, mapper, namespaceSink);\n inlineStructuredContentInRpcMessage(parsed);\n return JSON.stringify(parsed);\n } catch {\n return text; // not JSON we understand — leave untouched\n }\n}\n\n/** Sanitize an SSE body: each JSON-RPC message rides on a `data:` line. */\nexport function sanitizeMcpSseBody(text: string, mapper: ToolNameMapper = new ToolNameMapper(), namespaceSink?: Set<string>): string {\n return text\n .split(\"\\n\")\n .map((line) => {\n if (!line.startsWith(\"data:\")) {\n return line;\n }\n const payload = line.slice(\"data:\".length).trimStart();\n try {\n const parsed = JSON.parse(payload);\n sanitizeToolsInRpcMessage(parsed, mapper, namespaceSink);\n inlineStructuredContentInRpcMessage(parsed);\n return `data: ${JSON.stringify(parsed)}`;\n } catch {\n return line;\n }\n })\n .join(\"\\n\");\n}\n\n/** Reverse a sanitized tools/call name back to the original; returns null if no rewrite is needed. */\nexport function remapToolCallRequestBody(body: string, mapper: ToolNameMapper): string | null {\n try {\n const message = JSON.parse(body) as { method?: unknown; params?: { name?: unknown } };\n if (message.method !== \"tools/call\") {\n return null;\n }\n const name = message.params?.name;\n if (typeof name !== \"string\") {\n return null;\n }\n const original = mapper.toOriginal(name);\n if (original === undefined || original === name) {\n return null;\n }\n message.params!.name = original;\n return JSON.stringify(message);\n } catch {\n return null;\n }\n}\n\n/**\n * Wrap a base fetch so the codex_apps MCP transport is Responses-API-compatible:\n * tools/list responses get their names sanitized + bad outputSchemas dropped (and\n * the name mapping recorded), and tools/call requests get their name reversed back\n * to the MCP server's original. Only the POST request/response is buffered; the\n * long-lived GET notification SSE stream is passed through untouched.\n *\n * P4 (Part B.1): an optional `namespaceSink` Set accumulates the ORIGINAL-dotted\n * connector namespaces seen across every tools/list this turn (captured before the\n * dot is sanitized away). The worker reads the (live, by-reference) Set after the\n * turn to cache the serving account's connector set — packages/codex stays db-free.\n */\nexport function codexAppsSanitizingFetch(base: FetchLike = globalThis.fetch, namespaceSink?: Set<string>): FetchLike {\n const mapper = new ToolNameMapper();\n return async (input, init) => {\n // Outgoing: reverse a sanitized tools/call name to the server's original.\n let nextInit = init;\n if (init && typeof init.body === \"string\" && (init.method ?? \"GET\").toUpperCase() === \"POST\") {\n const remapped = remapToolCallRequestBody(init.body, mapper);\n if (remapped !== null) {\n nextInit = { ...init, body: remapped };\n }\n }\n const res = await base(input, nextInit);\n const method = (init?.method ?? (input instanceof Request ? input.method : \"GET\")).toUpperCase();\n if (method !== \"POST\" || !res.ok || !res.body) {\n return res;\n }\n const contentType = res.headers.get(\"content-type\") ?? \"\";\n const isJson = contentType.includes(\"application/json\");\n const isSse = contentType.includes(\"text/event-stream\");\n if (!isJson && !isSse) {\n return res;\n }\n const originalBody = await res.text();\n const sanitized = isJson\n ? sanitizeMcpJsonBody(originalBody, mapper, namespaceSink)\n : sanitizeMcpSseBody(originalBody, mapper, namespaceSink);\n const headers = new Headers(res.headers);\n headers.delete(\"content-length\"); // body length changed\n headers.delete(\"content-encoding\");\n return new Response(sanitized, { status: res.status, statusText: res.statusText, headers });\n };\n}\n"],"mappings":"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","names":["res"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opengeni/codex",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "ChatGPT/Codex subscription auth + transport: device-code login, token refresh, and the Responses-backend fetch. Pure HTTP + transforms; no database dependency.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./constants": {
|
|
17
|
+
"types": "./dist/constants.d.ts",
|
|
18
|
+
"import": "./dist/constants.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"provenance": true
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"build": "tsup"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"zod": "^4.2.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"tsup": "^8.5.0",
|
|
41
|
+
"typescript": "^6.0.3"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/Cloudgeni-ai/opengeni.git",
|
|
46
|
+
"directory": "packages/codex"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Thin ChatGPT/Codex API client used outside the streamed turn: the login-check
|
|
2
|
+
// (GET /codex/models) and the usage/limits readback (GET /wham/usage). spec §1.4, §1.8, §F.
|
|
3
|
+
|
|
4
|
+
import { CODEX_ORIGINATOR, CODEX_RESPONSES_BASE, CODEX_WHAM_BASE } from "./constants";
|
|
5
|
+
import type { CodexFetch } from "./device-code";
|
|
6
|
+
|
|
7
|
+
export type CodexAuthHeaders = {
|
|
8
|
+
accessToken: string;
|
|
9
|
+
chatgptAccountId: string | null;
|
|
10
|
+
isFedramp: boolean;
|
|
11
|
+
clientVersion: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function subscriptionHeaders(a: CodexAuthHeaders): Record<string, string> {
|
|
15
|
+
return {
|
|
16
|
+
Authorization: `Bearer ${a.accessToken}`,
|
|
17
|
+
...(a.chatgptAccountId ? { "ChatGPT-Account-ID": a.chatgptAccountId } : {}),
|
|
18
|
+
originator: CODEX_ORIGINATOR,
|
|
19
|
+
"User-Agent": `${CODEX_ORIGINATOR}/${a.clientVersion}`,
|
|
20
|
+
version: a.clientVersion,
|
|
21
|
+
...(a.isFedramp ? { "X-OpenAI-Fedramp": "true" } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** GET /codex/models — login-check + live catalog. A 200 means the token is accepted. spec §1.4/§F */
|
|
26
|
+
export async function fetchCodexModels(
|
|
27
|
+
a: CodexAuthHeaders,
|
|
28
|
+
fetchImpl: CodexFetch = fetch,
|
|
29
|
+
): Promise<{ ok: boolean; status: number; slugs: string[] }> {
|
|
30
|
+
const res = await fetchImpl(`${CODEX_RESPONSES_BASE}/models?client_version=${encodeURIComponent(a.clientVersion)}`, {
|
|
31
|
+
method: "GET",
|
|
32
|
+
headers: subscriptionHeaders(a),
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
return { ok: false, status: res.status, slugs: [] };
|
|
36
|
+
}
|
|
37
|
+
const body = (await res.json()) as { models?: Array<{ slug?: string }> };
|
|
38
|
+
const slugs = (body.models ?? []).map((m) => m.slug).filter((s): s is string => typeof s === "string");
|
|
39
|
+
return { ok: true, status: res.status, slugs };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** GET /wham/usage — authoritative limits. NB the WHAM base is /backend-api, NOT /codex (spec §1.8a). */
|
|
43
|
+
export async function fetchCodexUsage(
|
|
44
|
+
a: CodexAuthHeaders,
|
|
45
|
+
fetchImpl: CodexFetch = fetch,
|
|
46
|
+
): Promise<{ status: number; payload: unknown }> {
|
|
47
|
+
const res = await fetchImpl(`${CODEX_WHAM_BASE}/wham/usage`, {
|
|
48
|
+
method: "GET",
|
|
49
|
+
headers: subscriptionHeaders(a),
|
|
50
|
+
});
|
|
51
|
+
// A 404 may carry a usage-limit body; the route layer normalizes it to a limits state (spec §1.8c).
|
|
52
|
+
const payload = res.ok || res.status === 404 ? await res.json().catch(() => null) : null;
|
|
53
|
+
return { status: res.status, payload };
|
|
54
|
+
}
|
package/src/billing.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CODEX_MODEL_ID_PREFIX } from "./constants";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure NECESSARY condition for a Codex-billed turn: the model id is namespaced
|
|
5
|
+
* for the Codex subscription provider (`codex/<slug>`).
|
|
6
|
+
*
|
|
7
|
+
* This is NOT sufficient to bypass billing — an active, connected workspace
|
|
8
|
+
* credential and the deployment flag (`settings.codexSubscriptionEnabled`) are
|
|
9
|
+
* ALSO required (see `isCodexBilledTurn`/`workspaceCodexSubscriptionActive` in
|
|
10
|
+
* `@opengeni/db`). Used only as a cheap, synchronous short-circuit so the common
|
|
11
|
+
* non-codex path never issues a credential read.
|
|
12
|
+
*/
|
|
13
|
+
export function isCodexBilledModel(model: string | null | undefined): boolean {
|
|
14
|
+
return typeof model === "string" && model.startsWith(CODEX_MODEL_ID_PREFIX);
|
|
15
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Wire constants for the ChatGPT/Codex subscription backend.
|
|
2
|
+
// Source: CODEX-SUBSCRIPTION-SPEC.md (verified against openai/codex codex-rs).
|
|
3
|
+
|
|
4
|
+
export const CODEX_ISSUER = "https://auth.openai.com";
|
|
5
|
+
export const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"; // spec §1.1 (manager.rs:1444)
|
|
6
|
+
export const CODEX_AUTH_BASE = `${CODEX_ISSUER}/api/accounts`; // device endpoints (device_code_auth.rs:164)
|
|
7
|
+
export const CODEX_TOKEN_URL = `${CODEX_ISSUER}/oauth/token`; // exchange (form) + refresh (json)
|
|
8
|
+
export const CODEX_DEVICE_VERIFICATION_URL = `${CODEX_ISSUER}/codex/device`;
|
|
9
|
+
export const CODEX_DEVICE_REDIRECT_URI = `${CODEX_ISSUER}/deviceauth/callback`;
|
|
10
|
+
|
|
11
|
+
// Model requests: base already includes /codex; client appends /responses, /models.
|
|
12
|
+
export const CODEX_RESPONSES_BASE = "https://chatgpt.com/backend-api/codex";
|
|
13
|
+
// Usage lives on the WHAM base — NOT under /codex (verified, spec §1.8a).
|
|
14
|
+
export const CODEX_WHAM_BASE = "https://chatgpt.com/backend-api";
|
|
15
|
+
|
|
16
|
+
export const CODEX_ORIGINATOR = "codex_cli_rs"; // whitelisted originator (spec §1.2)
|
|
17
|
+
export const CODEX_ID_TOKEN_AUTH_CLAIM = "https://api.openai.com/auth";
|
|
18
|
+
|
|
19
|
+
// Synthetic registry-provider identity. The provider's baseURL is the bare
|
|
20
|
+
// /backend-api (NOT /codex) — codexSubscriptionFetch rewrites /responses ->
|
|
21
|
+
// /codex/responses. Codex model ids are namespaced `codex/<slug>` so they never
|
|
22
|
+
// collide with the built-in OpenAI provider's model ids; the fetch's resolveModel
|
|
23
|
+
// strips the prefix before the slug reaches the backend.
|
|
24
|
+
export const CODEX_PROVIDER_ID = "codex-subscription";
|
|
25
|
+
export const CODEX_PROVIDER_BASE_URL = "https://chatgpt.com/backend-api";
|
|
26
|
+
export const CODEX_MODEL_ID_PREFIX = "codex/";
|
|
27
|
+
|
|
28
|
+
// Offline fallback catalog. The live catalog from GET /models supersedes this
|
|
29
|
+
// when wired; until then these are the selectable codex models and the
|
|
30
|
+
// longest-prefix resolution targets. Confirmed live (2026-06) against a pro plan:
|
|
31
|
+
// GET /codex/models returned exactly these (plus the internal codex-auto-review).
|
|
32
|
+
export const CODEX_FALLBACK_MODEL_SLUGS = [
|
|
33
|
+
"gpt-5.5",
|
|
34
|
+
"gpt-5.4",
|
|
35
|
+
"gpt-5.4-mini",
|
|
36
|
+
"gpt-5.3-codex-spark",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
// Sent as the `version` header and inside the User-Agent. Confirmed live: the
|
|
40
|
+
// backend accepts the current codex CLI version; an older value risks /models
|
|
41
|
+
// min_client_version filtering. Keep in step with the codex CLI releases.
|
|
42
|
+
export const CODEX_CLIENT_VERSION = "0.142.4";
|
|
43
|
+
|
|
44
|
+
export const CODEX_REFRESH_WINDOW_MS = 5 * 60 * 1000; // proactive refresh when within 5 min of exp (spec §1.1)
|
|
45
|
+
export const CODEX_REFRESH_FALLBACK_MS = 8 * 24 * 60 * 60 * 1000; // 8 days when exp is unparseable
|
|
46
|
+
|
|
47
|
+
// ── Apps / connectors MCP (spec §1.10, §E) ───────────────────────────────────
|
|
48
|
+
// One server-side MCP exposes ALL the user's ChatGPT/Codex connectors
|
|
49
|
+
// (gmail/github/linear/slack/sentry/drive/calendar/…). Streamable HTTP, always.
|
|
50
|
+
export const CODEX_APPS_MCP_SERVER_ID = "codex_apps"; // tools surface as mcp__codex_apps__<tool>
|
|
51
|
+
export const CODEX_APPS_MCP_SERVER_NAME = "codex_apps"; // MCP `name` — MUST equal the id so the SDK namespaces tools as mcp__codex_apps__*
|
|
52
|
+
export const CODEX_APPS_MCP_URL = "https://chatgpt.com/backend-api/ps/mcp"; // live URL (NOT /codex, NOT the legacy /wham/apps)
|
|
53
|
+
export const CODEX_APPS_STARTUP_TIMEOUT_MS = 30_000; // startup_timeout 30s (spec §1.10) — maps to timeoutMs on this server only
|
|
54
|
+
// Connector scopes that the apps MCP requires. Present ONLY when granted at
|
|
55
|
+
// browser-authorize time; the device-code path CANNOT be confirmed to grant
|
|
56
|
+
// them, so treat connector availability as runtime-discovered (spec §1.10 / §E).
|
|
57
|
+
export const CODEX_APPS_REQUIRED_SCOPES = ["api.connectors.read", "api.connectors.invoke"] as const;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Device-code (headless) login flow for a ChatGPT/Codex subscription.
|
|
2
|
+
// Grounded in codex-rs device_code_auth.rs:67-145 + server.rs:732-766 (spec §1.1).
|
|
3
|
+
// Every call takes an injectable fetch so tests can supply a fake.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
CODEX_AUTH_BASE,
|
|
7
|
+
CODEX_CLIENT_ID,
|
|
8
|
+
CODEX_DEVICE_REDIRECT_URI,
|
|
9
|
+
CODEX_DEVICE_VERIFICATION_URL,
|
|
10
|
+
CODEX_TOKEN_URL,
|
|
11
|
+
} from "./constants";
|
|
12
|
+
|
|
13
|
+
export type CodexFetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
14
|
+
|
|
15
|
+
export type CodexDeviceStart = {
|
|
16
|
+
deviceAuthId: string;
|
|
17
|
+
userCode: string;
|
|
18
|
+
verificationUri: string;
|
|
19
|
+
intervalSeconds: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type CodexTokens = { idToken: string; accessToken: string; refreshToken: string };
|
|
23
|
+
|
|
24
|
+
export type CodexPollResult =
|
|
25
|
+
| { status: "pending" }
|
|
26
|
+
| { status: "expired" }
|
|
27
|
+
| { status: "authorized"; authorizationCode: string; codeVerifier: string };
|
|
28
|
+
|
|
29
|
+
export class CodexDeviceError extends Error {
|
|
30
|
+
constructor(message: string) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "CodexDeviceError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Step 1: POST {auth}/deviceauth/usercode {client_id}. device_code_auth.rs:67-95 */
|
|
37
|
+
export async function startDeviceCode(fetchImpl: CodexFetch = fetch): Promise<CodexDeviceStart> {
|
|
38
|
+
const res = await fetchImpl(`${CODEX_AUTH_BASE}/deviceauth/usercode`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "Content-Type": "application/json" },
|
|
41
|
+
body: JSON.stringify({ client_id: CODEX_CLIENT_ID }),
|
|
42
|
+
});
|
|
43
|
+
if (res.status === 404) {
|
|
44
|
+
throw new CodexDeviceError("device code login is not enabled for this Codex server");
|
|
45
|
+
}
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
throw new CodexDeviceError(`device code request failed with status ${res.status}`);
|
|
48
|
+
}
|
|
49
|
+
const body = (await res.json()) as {
|
|
50
|
+
device_auth_id: string;
|
|
51
|
+
user_code?: string;
|
|
52
|
+
usercode?: string;
|
|
53
|
+
interval?: string | number;
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
deviceAuthId: body.device_auth_id,
|
|
57
|
+
userCode: body.user_code ?? body.usercode ?? "",
|
|
58
|
+
verificationUri: CODEX_DEVICE_VERIFICATION_URL,
|
|
59
|
+
intervalSeconds: normalizeInterval(body.interval),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Clamp the poll interval to a sane minimum: a missing/0/NaN value must not become a 0-delay poll loop. */
|
|
64
|
+
function normalizeInterval(raw: string | number | undefined): number {
|
|
65
|
+
const n = typeof raw === "string" ? Number.parseInt(raw.trim(), 10) : raw;
|
|
66
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 1 ? n : 5;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Step 3 (single, non-blocking): POST {auth}/deviceauth/token. 403/404 => pending. device_code_auth.rs:106-145 */
|
|
70
|
+
export async function pollDeviceCode(
|
|
71
|
+
input: { deviceAuthId: string; userCode: string },
|
|
72
|
+
fetchImpl: CodexFetch = fetch,
|
|
73
|
+
): Promise<CodexPollResult> {
|
|
74
|
+
const res = await fetchImpl(`${CODEX_AUTH_BASE}/deviceauth/token`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "Content-Type": "application/json" },
|
|
77
|
+
body: JSON.stringify({ device_auth_id: input.deviceAuthId, user_code: input.userCode }),
|
|
78
|
+
});
|
|
79
|
+
if (res.ok) {
|
|
80
|
+
const body = (await res.json()) as { authorization_code: string; code_verifier: string };
|
|
81
|
+
return { status: "authorized", authorizationCode: body.authorization_code, codeVerifier: body.code_verifier };
|
|
82
|
+
}
|
|
83
|
+
if (res.status === 403 || res.status === 404) {
|
|
84
|
+
return { status: "pending" };
|
|
85
|
+
}
|
|
86
|
+
throw new CodexDeviceError(`device auth failed with status ${res.status}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Step 4: POST {issuer}/oauth/token form-encoded grant_type=authorization_code. server.rs:732-766 */
|
|
90
|
+
export async function exchangeDeviceCode(
|
|
91
|
+
input: { authorizationCode: string; codeVerifier: string },
|
|
92
|
+
fetchImpl: CodexFetch = fetch,
|
|
93
|
+
): Promise<CodexTokens> {
|
|
94
|
+
const form = new URLSearchParams({
|
|
95
|
+
grant_type: "authorization_code",
|
|
96
|
+
code: input.authorizationCode,
|
|
97
|
+
redirect_uri: CODEX_DEVICE_REDIRECT_URI,
|
|
98
|
+
client_id: CODEX_CLIENT_ID,
|
|
99
|
+
code_verifier: input.codeVerifier,
|
|
100
|
+
});
|
|
101
|
+
const res = await fetchImpl(CODEX_TOKEN_URL, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
104
|
+
body: form.toString(),
|
|
105
|
+
});
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
throw new CodexDeviceError(`device code exchange failed with status ${res.status}`);
|
|
108
|
+
}
|
|
109
|
+
const body = (await res.json()) as { id_token: string; access_token: string; refresh_token: string };
|
|
110
|
+
return { idToken: body.id_token, accessToken: body.access_token, refreshToken: body.refresh_token };
|
|
111
|
+
}
|