mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-22 11:25:15 +00:00
fix(opencode): dedupe concurrent Codex OAuth refreshes
This commit is contained in:
parent
b396b71c6f
commit
fd2c3f3f13
2 changed files with 115 additions and 16 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue