fix(opencode): dedupe concurrent Codex OAuth refreshes

This commit is contained in:
Cooper Gamble 2026-05-18 20:26:42 +00:00
parent b396b71c6f
commit fd2c3f3f13
2 changed files with 115 additions and 16 deletions

View file

@ -405,6 +405,13 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
const auth = await getAuth()
if (auth.type !== "oauth") return {}
let refreshPromise:
| Promise<{
access: string
accountId: string | undefined
}>
| undefined
return {
apiKey: OAUTH_DUMMY_KEY,
async fetch(requestInput: RequestInfo | URL, init?: RequestInit) {
@ -429,21 +436,34 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
// Check if token needs refresh
if (!currentAuth.access || currentAuth.expires < Date.now()) {
log.info("refreshing codex access token")
const tokens = await refreshAccessToken(currentAuth.refresh)
const newAccountId = extractAccountId(tokens) || authWithAccount.accountId
await input.client.auth.set({
path: { id: "openai" },
body: {
type: "oauth",
refresh: tokens.refresh_token,
access: tokens.access_token,
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
...(newAccountId && { accountId: newAccountId }),
},
})
currentAuth.access = tokens.access_token
authWithAccount.accountId = newAccountId
if (!refreshPromise) {
log.info("refreshing codex access token")
refreshPromise = refreshAccessToken(currentAuth.refresh)
.then(async (tokens) => {
const accountId = extractAccountId(tokens) || authWithAccount.accountId
await input.client.auth.set({
path: { id: "openai" },
body: {
type: "oauth",
refresh: tokens.refresh_token,
access: tokens.access_token,
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
...(accountId && { accountId }),
},
})
return {
access: tokens.access_token,
accountId,
}
})
.finally(() => {
refreshPromise = undefined
})
}
const refreshed = await refreshPromise
currentAuth.access = refreshed.access
authWithAccount.accountId = refreshed.accountId
}
// Build headers

View file

@ -1,11 +1,18 @@
import { describe, expect, test } from "bun:test"
import { afterEach, describe, expect, mock, test } from "bun:test"
import {
CodexAuthPlugin,
parseJwtClaims,
extractAccountIdFromClaims,
extractAccountId,
type IdTokenClaims,
} from "../../src/plugin/codex"
const originalFetch = globalThis.fetch
afterEach(() => {
globalThis.fetch = originalFetch
})
function createTestJwt(payload: object): string {
const header = Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url")
const body = Buffer.from(JSON.stringify(payload)).toString("base64url")
@ -120,4 +127,76 @@ describe("plugin.codex", () => {
).toBe("acc-123")
})
})
test("deduplicates concurrent Codex token refreshes", async () => {
let auth = {
type: "oauth" as const,
refresh: "refresh-old",
access: "",
expires: 0,
}
const authUpdates: unknown[] = []
let resolveRefresh: (() => void) | undefined
const refreshReady = new Promise<void>((resolve) => {
resolveRefresh = resolve
})
let refreshRequests = 0
globalThis.fetch = mock(async (request: RequestInfo | URL) => {
const url = request instanceof URL ? request.href : typeof request === "string" ? request : request.url
if (url === "https://auth.openai.com/oauth/token") {
refreshRequests += 1
await refreshReady
return new Response(
JSON.stringify({
id_token: createTestJwt({ chatgpt_account_id: "acc-123" }),
access_token: "access-new",
refresh_token: "refresh-new",
expires_in: 3600,
}),
{ status: 200 },
)
}
return new Response("{}", { status: 200 })
}) as unknown as typeof fetch
const hooks = await CodexAuthPlugin({
client: {
auth: {
async set(input: { body: { refresh: string; access: string; expires: number } }) {
authUpdates.push(input)
auth = {
type: "oauth",
refresh: input.body.refresh,
access: input.body.access,
expires: input.body.expires,
}
},
},
} as never,
project: {} as never,
directory: "",
worktree: "",
experimental_workspace: {
register() {},
},
serverUrl: new URL("https://example.com"),
$: {} as never,
})
const loaded = await hooks.auth!.loader!(async () => auth as never, {} as never)
const first = loaded.fetch!("https://api.openai.com/v1/responses")
const second = loaded.fetch!("https://api.openai.com/v1/responses")
await Promise.resolve()
await Promise.resolve()
expect(refreshRequests).toBe(1)
resolveRefresh!()
await Promise.all([first, second])
expect(refreshRequests).toBe(1)
expect(authUpdates).toHaveLength(1)
})
})