diff --git a/CHANGELOG.md b/CHANGELOG.md index 9433791c6b..bcafea25a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ - **settings (model aliases — self-heal after restart):** the Settings → Routing page showed "No exact-match aliases configured" after a server restart even though the aliases were persisted in the DB. Aliases are held in a module-local `_customAliases` map in `modelDeprecation.ts` that the boot path hydrates, but Next.js compiles the app-route module graph separately from the startup graph (the same webpack chunk-splitting class as #5312), so the `GET /api/settings/model-aliases` handler read a different, un-hydrated copy. The handler now self-heals: when its in-memory alias map is empty it reads `settings.modelAliases` from the DB (via the existing `getSettings()` db module — no raw SQL in the route) and repopulates the map, so the UI reflects the persisted aliases on the first GET after a restart. Regression guard: `tests/unit/model-aliases-settings-route-selfheal.test.ts`. ([#5777](https://github.com/diegosouzapw/OmniRoute/pull/5777) — thanks [@jleonar2](https://github.com/jleonar2)) -- **providers (grok-cli token auto-refresh):** grok-cli OAuth tokens were never proactively refreshed before their real expiry. `mapTokens` hardcoded `expiresIn: 21600` (6 h) regardless of the token's actual lifetime, so the persisted `expiresAt` was always "now + 6 h" and the proactive `tokenHealthCheck` sweep (refresh when `expiresAt - now < 5 min`) fired 6 h after import instead of shortly before the token really expired. `mapTokens` now computes `expiresIn` from the authoritative `expires_at` field in `~/.grok/auth.json` (ISO → epoch-seconds) with a fallback to the JWT `exp` claim (payload-only decode, no signature trust); the hardcoded `21600` is kept only when neither is present. Regression guards: 3 new cases in `tests/unit/grok-cli-oauth.test.ts` (JWT `exp`, JSON `expires_at`, and the fallback). ([#5775](https://github.com/diegosouzapw/OmniRoute/pull/5775) — thanks [@Chewji9875](https://github.com/Chewji9875)) +- **providers (grok-cli token auto-refresh):** grok-cli OAuth tokens were never proactively refreshed before their real expiry. `mapTokens` hardcoded `expiresIn: 21600` (6 h) regardless of the token's actual lifetime, so the persisted `expiresAt` was always "now + 6 h" and the proactive `tokenHealthCheck` sweep (refresh when `expiresAt - now < 5 min`) fired 6 h after import instead of shortly before the token really expired. `mapTokens` now computes `expiresIn` from the authoritative `expires_at` field in `~/.grok/auth.json` (ISO → epoch-seconds) with a fallback to the JWT `exp` claim (payload-only decode, no signature trust); the hardcoded `21600` is kept only when neither is present. An already-expired token (real `expires_at`/`exp` in the past) is now clamped to a positive `expiresIn` via `Math.max(1, …)`, so the import route stores a near-future `expiresAt` and AutoCombo refreshes the connection instead of reading a past date and excluding it outright. Regression guards: 5 cases in `tests/unit/grok-cli-oauth.test.ts` (JWT `exp`, JSON `expires_at`, the `21600` fallback, and the two expired-token clamps). ([#5775](https://github.com/diegosouzapw/OmniRoute/pull/5775) — thanks [@Chewji9875](https://github.com/Chewji9875)) - **compression (CCR retrieve via MCP HTTP):** the `omniroute_ccr_retrieve` MCP tool returned `"CCR block not found"` for blocks stored earlier in the **same** session when called over the MCP HTTP transports (SSE / Streamable HTTP), e.g. from OpenCode in a Docker deployment. Compression stores each block keyed by the API-key principal (`String(apiKeyInfo.id)`), but the tool resolved the caller via `extra.authInfo.clientId` — which the MCP SDK never populates for API-key auth — so it fell back to `"anonymous"` and the compound store-key never matched. The retrieve tool now resolves the caller's API-key id from the MCP HTTP auth context (`httpAuthContext`) using the **same** `getApiKeyMetadata` lookup used at storage time, so retrieval matches storage. Cross-tenant IDOR isolation is preserved: a different key resolves to a different id → miss; no key → the anonymous bucket only. Regression guard: `tests/unit/compression/ccr-mcp-principal-5649.test.ts` (extraction, distinct-principal isolation, fail-closed, end-to-end store→retrieve). ([#5649](https://github.com/diegosouzapw/OmniRoute/issues/5649)) diff --git a/src/lib/oauth/providers/grok-cli.ts b/src/lib/oauth/providers/grok-cli.ts index 849602d6e3..59c7cf4c80 100644 --- a/src/lib/oauth/providers/grok-cli.ts +++ b/src/lib/oauth/providers/grok-cli.ts @@ -131,6 +131,13 @@ export const grokCli = { expiresIn = exp - currentSec; } + // #5775 follow-up: guard against an already-expired token yielding a negative + // expiresIn. A negative value is truthy downstream (import-token route) and maps + // to a PAST expiresAt, which AutoCombo reads as "already expired" and excludes the + // connection instead of refreshing it. Clamp to a tiny positive TTL so the token is + // treated as due-for-refresh. + expiresIn = Math.max(1, expiresIn); + return { accessToken, refreshToken, diff --git a/tests/unit/grok-cli-oauth.test.ts b/tests/unit/grok-cli-oauth.test.ts index 7cbb7195a2..199deeaa47 100644 --- a/tests/unit/grok-cli-oauth.test.ts +++ b/tests/unit/grok-cli-oauth.test.ts @@ -152,3 +152,32 @@ test("Grok Build OAuth Provider - mapTokens falls back to 21600 if no exp or exp assert.equal(result.expiresIn, 21600); }); + +// #5775 follow-up: an already-expired token must NOT produce a negative expiresIn. +// A negative value is truthy in the import-token route (route.ts), yielding a PAST +// expiresAt that AutoCombo (virtualFactory.ts) reads as "already expired" and excludes +// the connection immediately — instead of clamping to a tiny positive TTL so the token +// is treated as due-for-refresh. Clamp with Math.max(1, …). +test("Grok Build OAuth Provider - mapTokens clamps expired JWT exp to a positive expiresIn", () => { + const pastSec = Math.floor(Date.now() / 1000) - 3600; // expired 1h ago + const payload = { sub: "12345", email: "test@example.com", exp: pastSec }; + const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString("base64url"); + const mockJwt = `eyJhbGciOiJFUzI1NiJ9.${payloadBase64}.signature`; + const result = grokCli.mapTokens(mockJwt, null); + + assert.ok(result.expiresIn >= 1, `expected expiresIn >= 1, got ${result.expiresIn}`); +}); + +test("Grok Build OAuth Provider - mapTokens clamps expired JSON expires_at to a positive expiresIn", () => { + const pastDateStr = new Date(Date.now() - 3600 * 1000).toISOString(); // expired 1h ago + const authJson = { + "https://auth.x.ai::clientId": { + key: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.signature", + refresh_token: "test-refresh-token", + expires_at: pastDateStr, + }, + }; + const result = grokCli.mapTokens(authJson, null); + + assert.ok(result.expiresIn >= 1, `expected expiresIn >= 1, got ${result.expiresIn}`); +});