Repository navigation
fix(api): guard rotating providers in manual token-refresh route (Codex family-revocation) - #3030
Conversation
…ex family-revocation) POST /api/providers/[id]/refresh was the last unguarded proactive-refresh entry point for rotating-refresh providers. The dashboard auto-refreshes every expiring connection on a page load (and an old cached frontend bulk-calls this endpoint), so each Codex account's single-use refresh_token got rotated; Auth0 then revoked the whole token family (openai/codex#9648) — every account but the last died with [403] <!DOCTYPE. refreshAndUpdateCredentials (quota-sync) and the connection-test route were already guarded; this closes the gap. The route now skips proactive rotation when rotationGroupFor(provider) !== null and returns {success,skipped,message}, deferring genuine expiry to the reactive, serialized 401 path. Non-rotating providers keep refreshing on demand. Test: codex-manual-refresh-rotating-guard (source-assertion, matches the token-refresh-race-comprehensive #2941 style).
There was a problem hiding this comment.
Code Review
This pull request introduces a guard in the manual/auto token refresh endpoint (POST /api/providers/[id]/refresh) to skip proactive token rotation for rotating-refresh providers (such as Codex/OpenAI sharing an Auth0 client ID). This prevents Auth0 token-family revocation when multiple sibling accounts are refreshed concurrently or in bulk. A unit test has been added to verify that the route imports rotationGroupFor and applies the guard before calling getAccessToken, and the CHANGELOG.md has been updated. I have no feedback to provide.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge This PR addresses a critical issue with Codex multi-account family-revocation cascade by adding a guard in the manual refresh endpoint to skip proactive token rotation for rotating-refresh providers (Codex/OpenAI share an Auth0 Overview
Files Reviewed (3 files)
ObservationsSecurity: The error handling at line 142 uses
Consistency: The import path Guard Logic Verification: The guard at line 61 correctly:
Reviewed by laguna-m.1-20260312:free · 2,185,764 tokens |
…add contributor hall Audited all 687 commits / 60 release/v3.8.8 PRs since v3.8.7 against the CHANGELOG: - Added 5 missing Fixed entries: #3052 (heap-pressure auto-calibration), #3051/#3048 (proxy fail-closed + registry assignments, @terence71-glitch), #3049/#3046 (session-pool fingerprint rotation + claude-web cf_clearance, @oyi77). - Credited previously-uncredited contributors: @branben (#2958 scope fix, #2959 Notion context source) and @JxnLexn (per-API-key stream default mode). - Added an Added entry for the per-API-key stream default mode feature. - Added the "🏆 Contributors" hall (24 contributors), matching the v3.8.6 format. Maintainer fix-PRs (#2966–#3030) are intentionally referenced by their original issue numbers in the body rather than the fix-PR number; @diegosouzapw is in the hall.
…add contributor hall Audited all 687 commits / 60 release/v3.8.8 PRs since v3.8.7 against the CHANGELOG: - Added 5 missing Fixed entries: cheaper-inference#3052 (heap-pressure auto-calibration), cheaper-inference#3051/cheaper-inference#3048 (proxy fail-closed + registry assignments, @terence71-glitch), cheaper-inference#3049/cheaper-inference#3046 (session-pool fingerprint rotation + claude-web cf_clearance, @oyi77). - Credited previously-uncredited contributors: @branben (cheaper-inference#2958 scope fix, cheaper-inference#2959 Notion context source) and @JxnLexn (per-API-key stream default mode). - Added an Added entry for the per-API-key stream default mode feature. - Added the "🏆 Contributors" hall (24 contributors), matching the v3.8.6 format. Maintainer fix-PRs (cheaper-inference#2966–cheaper-inference#3030) are intentionally referenced by their original issue numbers in the body rather than the fix-PR number; @diegosouzapw is in the hall.
…add contributor hall Audited all 687 commits / 60 release/v3.8.8 PRs since v3.8.7 against the CHANGELOG: - Added 5 missing Fixed entries: cheaper-inference#3052 (heap-pressure auto-calibration), cheaper-inference#3051/cheaper-inference#3048 (proxy fail-closed + registry assignments, @terence71-glitch), cheaper-inference#3049/cheaper-inference#3046 (session-pool fingerprint rotation + claude-web cf_clearance, @oyi77). - Credited previously-uncredited contributors: @branben (cheaper-inference#2958 scope fix, cheaper-inference#2959 Notion context source) and @JxnLexn (per-API-key stream default mode). - Added an Added entry for the per-API-key stream default mode feature. - Added the "🏆 Contributors" hall (24 contributors), matching the v3.8.6 format. Maintainer fix-PRs (cheaper-inference#2966–cheaper-inference#3030) are intentionally referenced by their original issue numbers in the body rather than the fix-PR number; @diegosouzapw is in the hall.
…add contributor hall Audited all 687 commits / 60 release/v3.8.8 PRs since v3.8.7 against the CHANGELOG: - Added 5 missing Fixed entries: cheaper-inference#3052 (heap-pressure auto-calibration), cheaper-inference#3051/cheaper-inference#3048 (proxy fail-closed + registry assignments, @terence71-glitch), cheaper-inference#3049/cheaper-inference#3046 (session-pool fingerprint rotation + claude-web cf_clearance, @oyi77). - Credited previously-uncredited contributors: @branben (cheaper-inference#2958 scope fix, cheaper-inference#2959 Notion context source) and @JxnLexn (per-API-key stream default mode). - Added an Added entry for the per-API-key stream default mode feature. - Added the "🏆 Contributors" hall (24 contributors), matching the v3.8.6 format. Maintainer fix-PRs (cheaper-inference#2966–cheaper-inference#3030) are intentionally referenced by their original issue numbers in the body rather than the fix-PR number; @diegosouzapw is in the hall.
Problem
Even after the quota-sync (
refreshAndUpdateCredentials) and connection-test guards (#2941), opening the Quota / Providers dashboard still cascaded Codex multi-account setups — all accounts of the same domain died with[403] <!DOCTYPE.Root cause (traced live on the VM via call_logs + proxy_logs + authenticated reproduction)
The deployed quota-page paths are clean:
GET /api/usage/[id](quota data) → skips rotating providers ✅ (verified, HTTP 200 no refresh)POST /api/providers/[id]/test(status) → skips rotating providers ✅ (fix(auth): prevent Codex multi-account refresh_token family revocation (openai/codex#9648) #2941 guard, verified)But
POST /api/providers/[id]/refresh(the manual/auto "refresh token" endpoint) had no rotating-provider guard — verified empirically: calling it on a Codex connection triggeredrefresh_token_reused. The dashboard auto-refreshes every expiring connection on a page load (and an old cached frontend still bulk-calls it), so each Codex account's single-use refresh_token got rotated. Under the shared Auth0client_id, Auth0 revoked the whole token family (openai/codex#9648) → every account but the last died.This was the last unguarded proactive-refresh entry point for rotating providers.
Fix
POST /api/providers/[id]/refreshnow skips proactive rotation whenrotationGroupFor(provider) !== null, returning{ success: true, skipped: true, message }and deferring genuine expiry to the reactive, serialized 401 path on the next real request. Non-rotating providers keep refreshing on demand. Same guard pattern asrefreshAndUpdateCredentialsand the connection-test route.Test (TDD, red→green)
tests/unit/codex-manual-refresh-rotating-guard.test.ts— source-assertion style (matchestoken-refresh-race-comprehensive.test.ts#2941): asserts the route importsrotationGroupForand guards beforegetAccessToken, returning early.Regression:
token-refresh-race-comprehensive,refresh-serializer,codex-quota-sync-no-proactive-refresh,providers-test-route-cli-mapall pass. lint + typecheck clean.Operator note
Existing dashboard tabs should be hard-refreshed (Ctrl+Shift+R) to drop the old cached frontend; this backend guard makes the cascade impossible regardless.