74 lines
3.0 KiB
TypeScript
74 lines
3.0 KiB
TypeScript
/**
|
|
* Codex multi-account family-revocation cascade — manual/auto token refresh guard.
|
|
*
|
|
* `POST /api/providers/[id]/refresh` is the explicit "refresh this token" endpoint.
|
|
* It rotates the refresh_token via getAccessToken. For rotating-refresh providers
|
|
* (Codex/OpenAI share one Auth0 client_id) rotating several sibling accounts —
|
|
* which happens when the dashboard auto-refreshes every expiring connection on a
|
|
* page load, or when an OLD cached frontend bulk-calls this endpoint — makes Auth0
|
|
* revoke the whole token family (openai/codex#9648) and kills every account but
|
|
* the last. This was the LAST unguarded proactive-refresh entry point for rotating
|
|
* providers (refreshAndUpdateCredentials and the connection-test route are already
|
|
* guarded). It must skip the proactive refresh and defer to the reactive,
|
|
* serialized 401 path. Non-rotating providers keep refreshing on demand.
|
|
*
|
|
* Mirrors the source-assertion style of token-refresh-race-comprehensive.test.ts.
|
|
*/
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
const root = path.resolve(import.meta.dirname, "../..");
|
|
const ROUTE = path.join(root, "src/app/api/providers/[id]/refresh/route.ts");
|
|
const read = () => 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 the OpenAI Auth0 family BEFORE calling getAccessToken", async () => {
|
|
const src = await read();
|
|
|
|
const guardIdx = src.search(/rotationGroup\s*===\s*["']openai-auth0["']/);
|
|
assert.ok(
|
|
guardIdx >= 0,
|
|
"refresh route must only skip proactive refresh for the OpenAI Auth0 family"
|
|
);
|
|
|
|
const getAccessTokenIdx = src.indexOf("getAccessToken(");
|
|
assert.ok(getAccessTokenIdx >= 0, "refresh route still calls getAccessToken for non-rotating providers");
|
|
|
|
assert.ok(
|
|
guardIdx < getAccessTokenIdx,
|
|
"the OpenAI Auth0 guard must run BEFORE getAccessToken so the risky 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 OpenAI Auth0 guard must return early (defer to the reactive 401 path) instead of refreshing"
|
|
);
|
|
});
|
|
|
|
test("manual refresh route does not skip Kiro just because it is serialized", async () => {
|
|
const src = await read();
|
|
assert.doesNotMatch(
|
|
src,
|
|
/rotationGroupFor\s*\(\s*[\w.]*provider[\w.]*\s*\)\s*!==\s*null/,
|
|
"a blanket rotation-group skip blocks Kiro manual refresh"
|
|
);
|
|
assert.match(
|
|
src,
|
|
/rotationGroup\s*===\s*["']openai-auth0["']/,
|
|
"only the OpenAI Auth0 family should be skipped by the manual refresh route"
|
|
);
|
|
});
|