From b157f784914e2cf7f4b1cf2536158c43b41b97ba Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Mon, 14 Sep 2026 11:05:29 +0900 Subject: [PATCH 1/2] fix(codex): bound the reset-credit consume response like every other read Both consume call sites parsed the upstream answer with resp.json(), which buffers the whole body before anything checks its size. Every neighbouring reset-credit read already goes through readResetCreditJson, which short-circuits an oversized declared length, reads through the shared 64 KiB bounded reader with fatal UTF-8, and rejects a truncated or empty answer. Only these two were left unbounded. The background auto-redeemer now treats an unreadable answer the same way its sibling availability read does and raises. The manual handler marks the operation ambiguous and answers 502, because the spend may already have landed upstream while its outcome code is unreadable, and a replay of that id must never be admitted as new work. --- src/codex/auth-api.ts | 13 +++++- structure/providers/openai-tiers.md | 4 ++ .../codex-integration/codex-auth-api.test.ts | 42 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts index 8f92f2c8c8..f67a0458da 100644 --- a/src/codex/auth-api.ts +++ b/src/codex/auth-api.ts @@ -529,7 +529,9 @@ export function createResetCreditWhamClient(config: OcxConfig, accountId: string signal: AbortSignal.timeout(10_000), }); if (!resp.ok) { await resp.body?.cancel().catch(() => {}); throw new Error(`upstream ${resp.status}`); } - return safeResetCreditConsumeDto(await resp.json()); + const parsed = await readResetCreditJson(resp, AbortSignal.timeout(10_000)); + if (!parsed.ok) throw new Error("invalid upstream reset-credit consume response"); + return safeResetCreditConsumeDto(parsed.value); }), }; } @@ -2701,7 +2703,14 @@ export async function handleCodexAuthAPI( if (identity) markManualResetCreditOperationAmbiguous(identity); return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status); } - const result = safeResetCreditConsumeDto(await resp.json()); + const consumed = await readResetCreditJson(resp, AbortSignal.timeout(10_000)); + if (!consumed.ok) { + // The spend may already have landed upstream and its outcome code is unreadable, + // so this id must never come back as a new operation. + if (identity) markManualResetCreditOperationAmbiguous(identity); + return jsonResponse({ error: "Invalid upstream reset-credit consume response" }, 502); + } + const result = safeResetCreditConsumeDto(consumed.value); if (identity) { // Narrow explicitly rather than casting: `safeResetCreditConsumeDto` // normalizes anything unrecognized to "unknown", and settling that diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index 527aacba6b..9d9bfe3a94 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -120,6 +120,10 @@ A confirmed manual reset-credit consumption may immediately reconcile that accou eligible pre-existing ordinary reset-derived cooldown after a complete, non-exhausted usage observation started after the reset. Paused or reauthentication-required accounts and cooldowns held by another in-flight probe remain excluded; their cooldowns are retained. +Confirmation requires a readable answer. Every reset-credit read, including the consume +response on both the manual and background paths, goes through the shared bounded-body +reader, so an upstream answer past that bound is unconfirmed rather than buffered whole. +An unconfirmed manual consume leaves its operation ambiguous and reconciles nothing. Recovery owns the specific cooldown and authenticates main and added Pool accounts through their respective credential contracts. Main usage publication keeps the latest successfully published observation authoritative. Pool recovery diff --git a/tests/codex-integration/codex-auth-api.test.ts b/tests/codex-integration/codex-auth-api.test.ts index 34e29bf334..b0758ad220 100644 --- a/tests/codex-integration/codex-auth-api.test.ts +++ b/tests/codex-integration/codex-auth-api.test.ts @@ -2922,6 +2922,48 @@ describe("codex-auth API", () => { } }); + test("reset-credit consume refuses an upstream body past the shared bound instead of buffering it", async () => { + const config = makeConfig(); + seedPoolAccount(config, { id: "pool-oversized", email: "oversized@example.test" }); + const originalFetch = globalThis.fetch; + let usageCalls = 0; + try { + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + if (url.includes("/backend-api/wham/rate-limit-reset-credits/consume")) { + // A 200 with an unbounded body was read whole by resp.json() before anything + // looked at its size, unlike every other reset-credit read on this path. + const padding = "x".repeat(BOUNDED_BODY_MAX_BYTES * 2); + return new Response(`{"code":"reset","padding":"${padding}"}`, { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + if (url.includes("/backend-api/wham/usage")) { + usageCalls += 1; + return Response.json({ + rate_limit: { primary_window: { used_percent: 10, reset_at: 1782000000 } }, + rate_limit_reset_credits: { available_count: 2 }, + }); + } + return originalFetch(input, init); + }) as typeof fetch; + + const req = new Request("http://localhost/api/codex-auth/reset-credits/consume", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ accountId: "pool-oversized" }), + }); + 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 consume response" }); + // The outcome is unconfirmed, so nothing downstream may treat the redeem as observed. + expect(usageCalls).toBe(0); + } finally { + globalThis.fetch = originalFetch; + } + }); + test("reset-credit already_redeemed refreshes quota and never invents a local decrement", async () => { const config = makeConfig(); seedPoolAccount(config, { id: "pool-idempotent", email: "idem@example.test" }); From b5c313edac2fa1d6eb55027bf4a94a8ab5a88c69 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:33:43 +0900 Subject: [PATCH 2/2] refactor: split changed contracts to respect the file-size ratchet Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- .../codex-integration/codex-auth-api.test.ts | 54 +--------------- .../reset-credit-consume-validation.ts | 63 +++++++++++++++++++ 2 files changed, 65 insertions(+), 52 deletions(-) create mode 100644 tests/helpers/reset-credit-consume-validation.ts diff --git a/tests/codex-integration/codex-auth-api.test.ts b/tests/codex-integration/codex-auth-api.test.ts index 73f700834b..7f49b31cb6 100644 --- a/tests/codex-integration/codex-auth-api.test.ts +++ b/tests/codex-integration/codex-auth-api.test.ts @@ -1,3 +1,4 @@ +import { registerResetCreditConsumeValidationTests } from "../helpers/reset-credit-consume-validation"; import * as usageHistoryModule from "../../src/usage/log"; import { getAccountQuotaHistory } from "../../src/codex/quota"; import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; @@ -2871,16 +2872,7 @@ describe("codex-auth API", () => { }); }); - 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", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ accountId: "../bad" }), - }); - const resp = await handleCodexAuthAPI(req, new URL(req.url), makeConfig()); - expect(resp!.status).toBe(400); - expect(await resp!.json()).toMatchObject({ error: "Invalid account id format" }); - }); + registerResetCreditConsumeValidationTests(makeConfig, seedPoolAccount); test("reset-credit consume returns remaining from refreshed quota, not the consume payload", async () => { const config = makeConfig(); @@ -2922,48 +2914,6 @@ describe("codex-auth API", () => { } }); - test("reset-credit consume refuses an upstream body past the shared bound instead of buffering it", async () => { - const config = makeConfig(); - seedPoolAccount(config, { id: "pool-oversized", email: "oversized@example.test" }); - const originalFetch = globalThis.fetch; - let usageCalls = 0; - try { - globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { - const url = String(input); - if (url.includes("/backend-api/wham/rate-limit-reset-credits/consume")) { - // A 200 with an unbounded body was read whole by resp.json() before anything - // looked at its size, unlike every other reset-credit read on this path. - const padding = "x".repeat(BOUNDED_BODY_MAX_BYTES * 2); - return new Response(`{"code":"reset","padding":"${padding}"}`, { - status: 200, - headers: { "content-type": "application/json" }, - }); - } - if (url.includes("/backend-api/wham/usage")) { - usageCalls += 1; - return Response.json({ - rate_limit: { primary_window: { used_percent: 10, reset_at: 1782000000 } }, - rate_limit_reset_credits: { available_count: 2 }, - }); - } - return originalFetch(input, init); - }) as typeof fetch; - - const req = new Request("http://localhost/api/codex-auth/reset-credits/consume", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ accountId: "pool-oversized" }), - }); - 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 consume response" }); - // The outcome is unconfirmed, so nothing downstream may treat the redeem as observed. - expect(usageCalls).toBe(0); - } finally { - globalThis.fetch = originalFetch; - } - }); - test("reset-credit already_redeemed refreshes quota and never invents a local decrement", async () => { const config = makeConfig(); seedPoolAccount(config, { id: "pool-idempotent", email: "idem@example.test" }); diff --git a/tests/helpers/reset-credit-consume-validation.ts b/tests/helpers/reset-credit-consume-validation.ts new file mode 100644 index 0000000000..3f06e08dc8 --- /dev/null +++ b/tests/helpers/reset-credit-consume-validation.ts @@ -0,0 +1,63 @@ +import { expect, test } from "bun:test"; +import { handleCodexAuthAPI } from "../../src/codex/auth-api"; +import { BOUNDED_BODY_MAX_BYTES } from "../../src/lib/bounded-body"; +import type { OcxConfig } from "../../src/types"; + +export function registerResetCreditConsumeValidationTests( + makeConfig: () => OcxConfig, + seedPoolAccount: (config: OcxConfig, options: { id: string; email: string }) => unknown, +): void { + 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", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ accountId: "../bad" }), + }); + const resp = await handleCodexAuthAPI(req, new URL(req.url), makeConfig()); + expect(resp!.status).toBe(400); + expect(await resp!.json()).toMatchObject({ error: "Invalid account id format" }); + }); + + test("reset-credit consume refuses an upstream body past the shared bound instead of buffering it", async () => { + const config = makeConfig(); + seedPoolAccount(config, { id: "pool-oversized", email: "oversized@example.test" }); + const originalFetch = globalThis.fetch; + let usageCalls = 0; + try { + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + if (url.includes("/backend-api/wham/rate-limit-reset-credits/consume")) { + // A 200 with an unbounded body was read whole by resp.json() before anything + // looked at its size, unlike every other reset-credit read on this path. + const padding = "x".repeat(BOUNDED_BODY_MAX_BYTES * 2); + return new Response(`{"code":"reset","padding":"${padding}"}`, { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + if (url.includes("/backend-api/wham/usage")) { + usageCalls += 1; + return Response.json({ + rate_limit: { primary_window: { used_percent: 10, reset_at: 1782000000 } }, + rate_limit_reset_credits: { available_count: 2 }, + }); + } + return originalFetch(input, init); + }) as typeof fetch; + + const req = new Request("http://localhost/api/codex-auth/reset-credits/consume", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ accountId: "pool-oversized" }), + }); + 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 consume response" }); + // The outcome is unconfirmed, so nothing downstream may treat the redeem as observed. + expect(usageCalls).toBe(0); + } finally { + globalThis.fetch = originalFetch; + } + }); + +}