From a028e63324c85f673a7c202d3ef3d0c8cc81f724 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 20 Apr 2026 20:50:23 -0700 Subject: [PATCH] fix(openai): honor absolute codex jwt expiry fallback --- .../openai/openai-codex-device-code.test.ts | 40 +++++++++++++++++++ extensions/openai/openai-codex-device-code.ts | 6 ++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/extensions/openai/openai-codex-device-code.test.ts b/extensions/openai/openai-codex-device-code.test.ts index 4dbb4e8a2dc..c1d03b821b6 100644 --- a/extensions/openai/openai-codex-device-code.test.ts +++ b/extensions/openai/openai-codex-device-code.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from "vitest"; +import { resolveCodexAccessTokenExpiry } from "./openai-codex-auth-identity.js"; import { loginOpenAICodexDeviceCode } from "./openai-codex-device-code.js"; function createJwt(payload: Record): string { @@ -88,6 +89,45 @@ describe("loginOpenAICodexDeviceCode", () => { expect(credentials.expires).toBeGreaterThan(Date.now()); }); + it("treats JWT-derived expiry fallback as an absolute timestamp", async () => { + const accessToken = createJwt({ + exp: Math.floor(Date.now() / 1000) + 600, + "https://api.openai.com/auth": { + chatgpt_account_id: "acct_123", + }, + }); + const expectedExpiry = resolveCodexAccessTokenExpiry(accessToken); + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + createJsonResponse({ + device_auth_id: "device-auth-123", + user_code: "CODE-12345", + interval: "0", + }), + ) + .mockResolvedValueOnce( + createJsonResponse({ + authorization_code: "authorization-code-123", + code_verifier: "code-verifier-123", + }), + ) + .mockResolvedValueOnce( + createJsonResponse({ + access_token: accessToken, + refresh_token: "refresh-token-123", + }), + ); + + const credentials = await loginOpenAICodexDeviceCode({ + fetchFn: fetchMock as typeof fetch, + onVerification: async () => {}, + }); + + expect(expectedExpiry).toBeDefined(); + expect(credentials.expires).toBe(expectedExpiry); + }); + it("surfaces user-code request failures", async () => { const fetchMock = vi.fn().mockResolvedValueOnce(new Response(null, { status: 503 })); diff --git a/extensions/openai/openai-codex-device-code.ts b/extensions/openai/openai-codex-device-code.ts index 436ed7fbba7..8179fe43f87 100644 --- a/extensions/openai/openai-codex-device-code.ts +++ b/extensions/openai/openai-codex-device-code.ts @@ -240,9 +240,11 @@ async function exchangeOpenAICodexDeviceCode(params: { throw new Error("OpenAI token exchange succeeded but did not return OAuth tokens."); } + const expiresInMs = normalizeTokenLifetimeMs(body?.expires_in); const expires = - Date.now() + - (normalizeTokenLifetimeMs(body?.expires_in) ?? resolveCodexAccessTokenExpiry(access) ?? 0); + expiresInMs !== undefined + ? Date.now() + expiresInMs + : (resolveCodexAccessTokenExpiry(access) ?? Date.now()); const accountId = resolveCodexChatgptAccountId(access) ?? (idToken && resolveCodexChatgptAccountId(idToken));