diff --git a/CHANGELOG.md b/CHANGELOG.md index ac6e0c6200..44e2f31617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,15 @@ ### Fixed +- **codex/providers:** `POST /api/providers/[id]/refresh` (the manual/auto "refresh + token" endpoint) no longer rotates rotating-refresh providers (Codex/OpenAI share + one Auth0 `client_id`). This was the last unguarded proactive-refresh entry point: + when the dashboard auto-refreshed every expiring connection on a page load (or an + old cached frontend bulk-called it), each Codex account's single-use refresh_token + was rotated, and Auth0 revoked the whole token family (`openai/codex#9648`) — every + account but the last died with `[403] readFile(ROUTE, "utf8"); + +test("manual refresh route imports rotationGroupFor", async () => { + const src = await read(); + assert.match( + src, + /import\s*\{[^}]*rotationGroupFor[^}]*\}\s*from\s*["'][^"']*refreshSerializer/, + "refresh route must import rotationGroupFor to detect rotating providers" + ); +}); + +test("manual refresh route skips proactive refresh for rotating providers BEFORE calling getAccessToken", async () => { + const src = await read(); + + const guardIdx = src.search(/rotationGroupFor\s*\(\s*[\w.]*provider[\w.]*\s*\)\s*!==\s*null/); + assert.ok( + guardIdx >= 0, + "refresh route must guard with `rotationGroupFor(provider) !== null` to skip rotating providers" + ); + + const getAccessTokenIdx = src.indexOf("getAccessToken("); + assert.ok(getAccessTokenIdx >= 0, "refresh route still calls getAccessToken for non-rotating providers"); + + assert.ok( + guardIdx < getAccessTokenIdx, + "the rotating-provider guard must run BEFORE getAccessToken so the rotating refresh_token is never exercised" + ); + + // The guard short-circuits with an early return (no token rotation). + const guardBlock = src.slice(guardIdx, getAccessTokenIdx); + assert.match( + guardBlock, + /return\b/, + "the rotating-provider guard must return early (defer to the reactive 401 path) instead of refreshing" + ); +});