Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 76 additions & 15 deletions src/codex/auth-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ import type { CodexAccount, CodexAccountCredentials, OcxConfig } from "../types"
import type { CatalogDisposition } from "./convergence-types";
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
import { providerCodexAccountMode } from "../providers/registry";
import { readBoundedResponseBody } from "../lib/bounded-body";
import { BOUNDED_BODY_MAX_BYTES, readBoundedResponseBody } from "../lib/bounded-body";
import { cancelBodyOnAbort, signalWithTimeout } from "../lib/abort";
import {
oauthAccountHealthFields,
projectCodexAccountHealth,
Expand Down Expand Up @@ -337,6 +338,43 @@ function safeResetCreditConsumeDto(input: unknown): { code: string } {
return { code: typeof obj.code === "string" ? obj.code : "unknown" };
}

type ResetCreditJsonRead =
| { ok: true; value: unknown }
| { ok: false };

function cancelResponseBodyWithoutWaiting(body: ReadableStream<Uint8Array> | null): void {
if (!body) return;
try {
void body.cancel().catch(() => undefined);
} catch {
// Some stream implementations throw synchronously from cancel().
}
}

async function readResetCreditJson(
response: Response,
signal: AbortSignal,
): Promise<ResetCreditJsonRead> {
const declaredLength = Number(response.headers.get("content-length"));
if (Number.isSafeInteger(declaredLength)
&& declaredLength >= 0
&& declaredLength > BOUNDED_BODY_MAX_BYTES) {
cancelResponseBodyWithoutWaiting(response.body);
return { ok: false };
}
try {
const body = await readBoundedResponseBody(response, {
signal,
maxBytes: BOUNDED_BODY_MAX_BYTES,
fatalUtf8: true,
});
if (!body.displaySafe || body.truncated || !body.text.trim()) return { ok: false };
return { ok: true, value: JSON.parse(body.text) as unknown };
} catch {
return { ok: false };
}
}

export function isUnverifiedCodexImportEnabled(): boolean {
return process.env[MANUAL_IMPORT_ENV] === "1";
}
Expand Down Expand Up @@ -1682,21 +1720,44 @@ export async function handleCodexAuthAPI(

try {
const result = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
const resp = await fetch(
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
{
headers: {
Authorization: `Bearer ${auth.accessToken}`,
"ChatGPT-Account-Id": auth.chatgptAccountId,
},
signal: AbortSignal.timeout(8000),
},
);
if (!resp.ok) {
await resp.body?.cancel().catch(() => {});
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
const linkedSignal = signalWithTimeout(8000, req.signal);
let detachBodyAbort = () => {};
try {
let resp: Response;
try {
resp = await fetch(
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
{
headers: {
Authorization: `Bearer ${auth.accessToken}`,
"ChatGPT-Account-Id": auth.chatgptAccountId,
},
signal: linkedSignal.signal,
},
);
} catch (error) {
if (linkedSignal.signal.aborted) {
return jsonResponse({ error: "Invalid upstream reset-credit response" }, 502);
}
throw error;
}
// Own the response body before the bounded reader attaches. If the client
// disconnects in that narrow window, Bun otherwise tears down the native
// body off the awaited path and can report an unhandled rejection.
detachBodyAbort = cancelBodyOnAbort(resp.body, linkedSignal.signal);
if (!resp.ok) {
await resp.body?.cancel().catch(() => {});
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
}
const parsed = await readResetCreditJson(resp, linkedSignal.signal);
if (!parsed.ok) {
return jsonResponse({ error: "Invalid upstream reset-credit response" }, 502);
}
return jsonResponse(safeResetCreditsDto(parsed.value));
} finally {
detachBodyAbort();
linkedSignal.cleanup();
}
Comment thread
luvs01 marked this conversation as resolved.
return jsonResponse(safeResetCreditsDto(await resp.json()));
});
return result.ok ? result.value : result.response;
} catch (e) {
Expand Down
171 changes: 171 additions & 0 deletions tests/codex-auth-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
listOpenAiForwardSidecarCandidates,
resolveFirstUsableOpenAiSidecar,
} from "../src/providers/openai-sidecar";
import { BOUNDED_BODY_MAX_BYTES } from "../src/lib/bounded-body";

const TEST_DIR = join(import.meta.dir, ".tmp-codex-auth-api-test");
const TEST_CODEX_HOME = join(TEST_DIR, "codex");
Expand Down Expand Up @@ -1958,6 +1959,176 @@ describe("codex-auth API", () => {
}
});

test("reset-credit lookup rejects a declared oversized response before reading it", async () => {
const config = makeConfig();
seedPoolAccount(config, { id: "credit-declared-cap", email: "declared@example.test" });
let pulls = 0;
let markCancelled!: () => void;
const cancelled = new Promise<void>(resolve => { markCancelled = resolve; });
globalThis.fetch = (async () => new Response(new ReadableStream<Uint8Array>({
pull() { pulls += 1; },
cancel() { markCancelled(); },
}, { highWaterMark: 0 }), {
headers: { "content-length": String(BOUNDED_BODY_MAX_BYTES + 1) },
})) as typeof fetch;

const req = new Request("http://localhost/api/codex-auth/reset-credits?accountId=credit-declared-cap");
const resp = await handleCodexAuthAPI(req, new URL(req.url), config);

expect(resp?.status).toBe(502);
expect(await resp?.json()).toEqual({ error: "Invalid upstream reset-credit response" });
await cancelled;
expect(pulls).toBe(0);
});

test("reset-credit lookup cancels an undeclared response that crosses the byte cap", async () => {
const config = makeConfig();
seedPoolAccount(config, { id: "credit-stream-cap", email: "stream@example.test" });
let sent = false;
let markCancelled!: () => void;
const cancelled = new Promise<void>(resolve => { markCancelled = resolve; });
globalThis.fetch = (async () => new Response(new ReadableStream<Uint8Array>({
pull(controller) {
if (!sent) {
sent = true;
controller.enqueue(new Uint8Array(BOUNDED_BODY_MAX_BYTES));
return;
}
controller.enqueue(new Uint8Array([0x61]));
},
cancel() { markCancelled(); },
}))) as typeof fetch;

const req = new Request("http://localhost/api/codex-auth/reset-credits?accountId=credit-stream-cap");
const resp = await handleCodexAuthAPI(req, new URL(req.url), config);

expect(resp?.status).toBe(502);
expect(await resp?.json()).toEqual({ error: "Invalid upstream reset-credit response" });
await cancelled;
});

test("reset-credit lookup binds client cancellation to the upstream body", async () => {
const config = makeConfig();
seedPoolAccount(config, { id: "credit-cancel", email: "cancel@example.test" });
let started!: () => void;
const bodyStarted = new Promise<void>(resolve => { started = resolve; });
let markCancelled!: () => void;
const cancelled = new Promise<void>(resolve => { markCancelled = resolve; });
globalThis.fetch = (async () => new Response(new ReadableStream<Uint8Array>({
pull() { started(); },
cancel() { markCancelled(); },
}))) as typeof fetch;
const controller = new AbortController();
const req = new Request("http://localhost/api/codex-auth/reset-credits?accountId=credit-cancel", {
signal: controller.signal,
});

const pending = handleCodexAuthAPI(req, new URL(req.url), config);
await bodyStarted;
controller.abort(new DOMException("client disconnected", "AbortError"));
const resp = await pending;

expect(resp?.status).toBe(502);
expect(await resp?.json()).toEqual({ error: "Invalid upstream reset-credit response" });
await cancelled;
});

test("reset-credit lookup cancels a response when the client aborts before the reader attaches", async () => {
const config = makeConfig();
seedPoolAccount(config, { id: "credit-pre-reader-cancel", email: "pre-reader@example.test" });
const controller = new AbortController();
let pulls = 0;
let markCancelled!: () => void;
const cancelled = new Promise<void>(resolve => { markCancelled = resolve; });
globalThis.fetch = (async () => {
controller.abort(new DOMException("client disconnected", "AbortError"));
return new Response(new ReadableStream<Uint8Array>({
pull() { pulls += 1; },
cancel() { markCancelled(); },
}, { highWaterMark: 0 }));
}) as typeof fetch;
const req = new Request("http://localhost/api/codex-auth/reset-credits?accountId=credit-pre-reader-cancel", {
signal: controller.signal,
});

const resp = await handleCodexAuthAPI(req, new URL(req.url), config);

expect(resp?.status).toBe(502);
expect(await resp?.json()).toEqual({ error: "Invalid upstream reset-credit response" });
await cancelled;
expect(pulls).toBe(0);
});

test("reset-credit lookup sanitizes cancellation before upstream response headers", async () => {
const config = makeConfig();
seedPoolAccount(config, { id: "credit-fetch-cancel", email: "fetch-cancel@example.test" });
const controller = new AbortController();
let markFetchStarted!: () => void;
const fetchStarted = new Promise<void>(resolve => { markFetchStarted = resolve; });
let upstreamSignal: AbortSignal | null = null;
globalThis.fetch = (async (_input, init) => {
upstreamSignal = init?.signal ?? null;
markFetchStarted();
return await new Promise<Response>((_resolve, reject) => {
const rejectForAbort = () => reject(upstreamSignal?.reason);
if (upstreamSignal?.aborted) {
rejectForAbort();
return;
}
upstreamSignal?.addEventListener("abort", rejectForAbort, { once: true });
});
}) as typeof fetch;
const req = new Request("http://localhost/api/codex-auth/reset-credits?accountId=credit-fetch-cancel", {
signal: controller.signal,
});

const pending = handleCodexAuthAPI(req, new URL(req.url), config);
await fetchStarted;
controller.abort(new Error("private reset-credit abort detail"));
const resp = await pending;

expect(upstreamSignal?.aborted).toBe(true);
expect(resp?.status).toBe(502);
expect(await resp?.json()).toEqual({ error: "Invalid upstream reset-credit response" });
});

test.each([
{ label: "invalid UTF-8", body: new Uint8Array([0xff]) },
{ label: "malformed JSON", body: "{" },
])("reset-credit lookup rejects $label without reflecting it", async ({ body }) => {
const config = makeConfig();
seedPoolAccount(config, { id: "credit-invalid", email: "invalid@example.test" });
globalThis.fetch = (async () => new Response(body)) as typeof fetch;

const req = new Request("http://localhost/api/codex-auth/reset-credits?accountId=credit-invalid");
const resp = await handleCodexAuthAPI(req, new URL(req.url), config);

expect(resp?.status).toBe(502);
expect(await resp?.json()).toEqual({ error: "Invalid upstream reset-credit response" });
});

test("reset-credit lookup returns only validated fields from a bounded response", async () => {
const config = makeConfig();
seedPoolAccount(config, { id: "credit-fields", email: "fields@example.test" });
globalThis.fetch = (async () => Response.json({
credits: [
{ granted_at: "2026-01-01T00:00:00Z", expires_at: "2026-02-01T00:00:00Z", secret: "drop-me" },
{ granted_at: 123, expires_at: "invalid" },
],
rate_limit_reset_credits: { available_count: 1 },
unexpected: "drop-me",
})) as typeof fetch;

const req = new Request("http://localhost/api/codex-auth/reset-credits?accountId=credit-fields");
const resp = await handleCodexAuthAPI(req, new URL(req.url), config);

expect(resp?.status).toBe(200);
expect(await resp?.json()).toEqual({
credits: [{ granted_at: "2026-01-01T00:00:00Z", expires_at: "2026-02-01T00:00:00Z" }],
available_count: 1,
});
});

test("reset-credit consume rejects invalid account ids before credential lookup", async () => {
const req = new Request("http://localhost/api/codex-auth/reset-credits/consume", {
method: "POST",
Expand Down
Loading