diff --git a/bun.lock b/bun.lock index 43d9e060a..786ed4cb3 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,7 @@ "@corbits/code-agent-reviewer": "workspace:*", "@corbits/codex-provider": "0.1.3", "@corbits/meta-provider": "0.1.1", - "@corbits/oauth-core": "0.3.2", + "@corbits/oauth-core": "0.3.3", "@corbits/openai-responses": "0.2.2", "@corbits/xai-provider": "0.1.4", "@intx/agent": "workspace:*", @@ -315,7 +315,7 @@ "@corbits/meta-provider": ["@corbits/meta-provider@0.1.1", "", { "dependencies": { "arktype": "^2.2.3" }, "peerDependencies": { "@corbits/oauth-core": "^0.3.0", "@corbits/openai-responses": "^0.2.2", "@intx/inference": "^0.5.0", "@intx/types": "^0.5.0" } }, "sha512-tMuQkSMeR/0w/ctX9SwirXNVEm74XS/K5O5EC4NFYBp5vHn/wlm//ocCNo24dMtrhCH+7rr+orEOh4jIy1+RVg=="], - "@corbits/oauth-core": ["@corbits/oauth-core@0.3.2", "", { "dependencies": { "@intx/db": "^0.5.0", "@intx/hub-api": "^0.5.0", "@intx/hub-common": "^0.5.0", "@intx/types": "^0.5.0", "arktype": "^2.2.3", "drizzle-orm": "^0.45.2", "hono": "^4.11.9" } }, "sha512-2wBKzaWA/hEObDq+yehuHFh6ZsPDRoyGgBBhBiN/gPPWOw+DCCGogTkzTwL0yPPM04NU+RUKEcgBka7M7LVmGw=="], + "@corbits/oauth-core": ["@corbits/oauth-core@0.3.3", "", { "dependencies": { "@intx/db": "^0.5.0", "@intx/hub-api": "^0.5.0", "@intx/hub-common": "^0.5.0", "@intx/types": "^0.5.0", "arktype": "^2.2.3", "drizzle-orm": "^0.45.2", "hono": "^4.11.9" } }, "sha512-scHRRjyP8rQAn9qxTr2z/fQjzfp8pe6ygMXkvli36dPjaa9t8dLJc3jsaRd9YEZdDvmnMdYva65kK2cmKEau8w=="], "@corbits/openai-responses": ["@corbits/openai-responses@0.2.2", "", { "dependencies": { "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": "^0.5.0", "@intx/types": "^0.5.0" } }, "sha512-dp9ZVHUL+EuwveezrJr3EMQc963xBVxUlQg32P+1yUP7TZfR1rT7fwCWIUYDQIfAkeaOFTDfl6y/QRTIi6bVxw=="], diff --git a/package.json b/package.json index c72736022..f947ee80a 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "@anthropic-ai/sandbox-runtime": "0.0.79", "@corbits/codex-provider": "0.1.3", "@corbits/meta-provider": "0.1.1", - "@corbits/oauth-core": "0.3.2", + "@corbits/oauth-core": "0.3.3", "@corbits/openai-responses": "0.2.2", "@corbits/xai-provider": "0.1.4", "@intx/agent": "workspace:*", diff --git a/src/auth/codex/auth.test.ts b/src/auth/codex/auth.test.ts index 9cc0624a7..7d952cf8b 100644 --- a/src/auth/codex/auth.test.ts +++ b/src/auth/codex/auth.test.ts @@ -96,10 +96,10 @@ describe("codexTokensFromResponse", () => { expect(tokens.refresh).toBe("old-refresh"); }); - test("throws when no refresh token is available anywhere", () => { - expect(() => codexTokensFromResponse({ access_token: "a" }, 0)).toThrow( - /refresh_token/, - ); + test("omits refresh when no refresh token is available anywhere", () => { + const tokens = codexTokensFromResponse({ access_token: "a" }, 0); + expect(tokens.access).toBe("a"); + expect(tokens.refresh).toBeUndefined(); }); test("falls back to a default lifetime when expires_in is absent", () => { diff --git a/src/auth/codex/session-failure.test.ts b/src/auth/codex/session-failure.test.ts index 2f4a585d9..f45df5edd 100644 --- a/src/auth/codex/session-failure.test.ts +++ b/src/auth/codex/session-failure.test.ts @@ -106,7 +106,7 @@ describe("codex auth failure surface", () => { const auth = failure as CodexAuthError; const surfaced = authFailureSurface(auth); expect(surfaced).not.toContain(refresh); - expect(surfaced).toContain("grant rejected"); + expect(surfaced).toContain("OAuth token endpoint returned 400"); expect(surfaced).toContain("Re-authenticate"); } finally { globalThis.fetch = originalFetch; diff --git a/src/auth/meta/session.ts b/src/auth/meta/session.ts index fbd701176..f7d9a7f70 100644 --- a/src/auth/meta/session.ts +++ b/src/auth/meta/session.ts @@ -1,6 +1,7 @@ import { createTokenSession, isTokenExpired, + OAuthMissingRefreshTokenError, OAuthProfileNotFoundError, OAuthRefreshFailedError, type TokenSession, @@ -145,11 +146,15 @@ export async function refreshStagedMetaTokens( now: number = Date.now(), ): Promise { if (!isMetaTokenExpired(tokens, now)) return tokens; + const refreshToken = tokens.refresh; + if (refreshToken === undefined || refreshToken.trim().length === 0) { + throw new OAuthMissingRefreshTokenError(); + } let refreshed: MetaOAuthTokens; try { - refreshed = await refreshMetaTokens(tokens.refresh, now); + refreshed = await refreshMetaTokens(refreshToken, now); } catch (error) { - throw sanitizedRefreshFailure(error, tokens.refresh); + throw sanitizedRefreshFailure(error, refreshToken); } replaceMutableTokens(tokens, refreshed); return tokens; diff --git a/src/auth/provider-token-session.ts b/src/auth/provider-token-session.ts index 937a6ca8a..981a3c902 100644 --- a/src/auth/provider-token-session.ts +++ b/src/auth/provider-token-session.ts @@ -1,6 +1,7 @@ import { createTokenSession, isTokenExpired, + OAuthMissingRefreshTokenError, OAuthProfileNotFoundError, OAuthRefreshFailedError, type AuthProfile, @@ -195,11 +196,15 @@ export function createProviderTokenSession( }, refreshStaged: async (tokens, now = Date.now()) => { if (!isExpired(tokens, now)) return tokens; + const refreshToken = tokens.refresh; + if (refreshToken === undefined || refreshToken.trim().length === 0) { + throw new OAuthMissingRefreshTokenError(); + } let refreshed: TTokens; try { - refreshed = await spec.refreshTokens(tokens.refresh, now, tokens); + refreshed = await spec.refreshTokens(refreshToken, now, tokens); } catch (error) { - throw sanitizedRefreshFailure(error, tokens.refresh); + throw sanitizedRefreshFailure(error, refreshToken); } replaceMutableTokens(tokens, refreshed); return tokens; diff --git a/src/auth/xai/session-refresh-race.test.ts b/src/auth/xai/session-refresh-race.test.ts index 4423d4370..a7f41d6fd 100644 --- a/src/auth/xai/session-refresh-race.test.ts +++ b/src/auth/xai/session-refresh-race.test.ts @@ -235,7 +235,7 @@ describe("xAI shared-credential refresh race", () => { const auth = failure as XaiAuthError; const surfaced = authFailureSurface(auth); expect(surfaced).not.toContain(refresh); - expect(surfaced).toContain("grant rejected"); + expect(surfaced).toContain("OAuth token endpoint returned 400"); expect(surfaced).toContain("Re-authenticate"); } finally { globalThis.fetch = originalFetch;