mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Merge PR 3030 into release/v3.8.8
This commit is contained in:
@@ -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] <!DOCTYPE`. The endpoint now skips proactive
|
||||
rotation for rotating providers and defers to the reactive, serialized 401 path
|
||||
(same guard as `refreshAndUpdateCredentials` and the connection-test route).
|
||||
- **codex/quota:** opening the Quota / Providers dashboard no longer disconnects
|
||||
Codex multi-account setups. The quota-sync path
|
||||
(`refreshAndUpdateCredentials`) proactively refreshed every connection — for
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderConnectionById, updateProviderConnection } from "@/lib/db/providers";
|
||||
import { getAccessToken, updateProviderCredentials } from "@/sse/services/tokenRefresh";
|
||||
import { rotationGroupFor } from "@omniroute/open-sse/services/refreshSerializer.ts";
|
||||
|
||||
type RefreshResult = {
|
||||
accessToken?: string;
|
||||
@@ -44,6 +45,33 @@ export async function POST(_request: Request, { params }: { params: Promise<{ id
|
||||
}
|
||||
|
||||
const provider = connection.provider;
|
||||
|
||||
// Codex multi-account family-revocation cascade guard.
|
||||
// Rotating-refresh providers (Codex/OpenAI share one Auth0 client_id, etc.)
|
||||
// mint a single-use refresh_token on every refresh. This endpoint is invoked
|
||||
// per-connection by the dashboard (incl. an OLD cached frontend that bulk-
|
||||
// refreshes every expiring connection on a page load); rotating several
|
||||
// sibling accounts makes Auth0 revoke the whole token family
|
||||
// (openai/codex#9648), killing every account but the last. Never proactively
|
||||
// rotate a rotating provider here — the access_token is reused as-is and
|
||||
// genuine expiry is handled by the reactive, serialized 401 path on the next
|
||||
// real request. This was the last unguarded proactive-refresh entry point
|
||||
// (refreshAndUpdateCredentials and the connection-test route are already
|
||||
// guarded). Non-rotating providers keep refreshing on demand below.
|
||||
if (rotationGroupFor(provider) !== null) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
skipped: true,
|
||||
connectionId: id,
|
||||
provider,
|
||||
message:
|
||||
"Rotating-refresh provider: the token refreshes automatically on the next request. " +
|
||||
"Manual/bulk refresh is intentionally skipped to avoid Auth0 token-family revocation.",
|
||||
expiresAt: connection.tokenExpiresAt || connection.expiresAt || null,
|
||||
refreshedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
const credentials = {
|
||||
connectionId: id,
|
||||
accessToken: connection.accessToken,
|
||||
|
||||
59
tests/unit/codex-manual-refresh-rotating-guard.test.ts
Normal file
59
tests/unit/codex-manual-refresh-rotating-guard.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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 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"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user