+21







![opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)
![opencode-agent[bot]](/assets/img/avatar_default.png)
opencode-agent[bot]
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Aiden Cline
usrnk1
Aarav Sareen
Luke Parker
Brendan Allan
Kit Langton
James Long
Adam
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Jay
Simon Klee
Jay
Jack
David Hill
Brendan Allan
opencode
Aiden Cline
James Long
Dustin Deus
冯基魁
Frank
Aiden Cline
Victor Navarro
Dax Raad
Dax
Nabs
Vladimir Glafirov
AidenGeunGeun
Mark
634386fe0f
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Jay <air@live.ca> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: James Long <jlongster@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com> Co-authored-by: Victor Navarro <vn4varro@gmail.com> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Nabs <nabil@instafork.com> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: AidenGeunGeun <eastlandwyvern@gmail.com> Co-authored-by: Mark <geraint0923@users.noreply.github.com>
300 lines
10 KiB
TypeScript
300 lines
10 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import {
|
|
CodexAuthPlugin,
|
|
parseJwtClaims,
|
|
extractAccountIdFromClaims,
|
|
extractAccountId,
|
|
renderOAuthError,
|
|
type IdTokenClaims,
|
|
} from "../../src/plugin/openai/codex"
|
|
|
|
function createTestJwt(payload: object): string {
|
|
const header = Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url")
|
|
const body = Buffer.from(JSON.stringify(payload)).toString("base64url")
|
|
return `${header}.${body}.sig`
|
|
}
|
|
|
|
describe("plugin.codex", () => {
|
|
test("escapes provider errors in callback HTML", () => {
|
|
const error = `</div><script>alert("xss" & 'more')</script>`
|
|
const html = renderOAuthError(error)
|
|
|
|
expect(html).toContain("</div><script>alert("xss" & 'more')</script>")
|
|
expect(html).not.toContain(error)
|
|
})
|
|
|
|
describe("parseJwtClaims", () => {
|
|
test("parses valid JWT with claims", () => {
|
|
const payload = { email: "test@example.com", chatgpt_account_id: "acc-123" }
|
|
const jwt = createTestJwt(payload)
|
|
const claims = parseJwtClaims(jwt)
|
|
expect(claims).toEqual(payload)
|
|
})
|
|
|
|
test("returns undefined for JWT with less than 3 parts", () => {
|
|
expect(parseJwtClaims("invalid")).toBeUndefined()
|
|
expect(parseJwtClaims("only.two")).toBeUndefined()
|
|
})
|
|
|
|
test("returns undefined for invalid base64", () => {
|
|
expect(parseJwtClaims("a.!!!invalid!!!.b")).toBeUndefined()
|
|
})
|
|
|
|
test("returns undefined for invalid JSON payload", () => {
|
|
const header = Buffer.from("{}").toString("base64url")
|
|
const invalidJson = Buffer.from("not json").toString("base64url")
|
|
expect(parseJwtClaims(`${header}.${invalidJson}.sig`)).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe("extractAccountIdFromClaims", () => {
|
|
test("extracts chatgpt_account_id from root", () => {
|
|
const claims: IdTokenClaims = { chatgpt_account_id: "acc-root" }
|
|
expect(extractAccountIdFromClaims(claims)).toBe("acc-root")
|
|
})
|
|
|
|
test("extracts chatgpt_account_id from nested https://api.openai.com/auth", () => {
|
|
const claims: IdTokenClaims = {
|
|
"https://api.openai.com/auth": { chatgpt_account_id: "acc-nested" },
|
|
}
|
|
expect(extractAccountIdFromClaims(claims)).toBe("acc-nested")
|
|
})
|
|
|
|
test("prefers root over nested", () => {
|
|
const claims: IdTokenClaims = {
|
|
chatgpt_account_id: "acc-root",
|
|
"https://api.openai.com/auth": { chatgpt_account_id: "acc-nested" },
|
|
}
|
|
expect(extractAccountIdFromClaims(claims)).toBe("acc-root")
|
|
})
|
|
|
|
test("extracts from organizations array as fallback", () => {
|
|
const claims: IdTokenClaims = {
|
|
organizations: [{ id: "org-123" }, { id: "org-456" }],
|
|
}
|
|
expect(extractAccountIdFromClaims(claims)).toBe("org-123")
|
|
})
|
|
|
|
test("returns undefined when no accountId found", () => {
|
|
const claims: IdTokenClaims = { email: "test@example.com" }
|
|
expect(extractAccountIdFromClaims(claims)).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe("extractAccountId", () => {
|
|
test("extracts from id_token first", () => {
|
|
const idToken = createTestJwt({ chatgpt_account_id: "from-id-token" })
|
|
const accessToken = createTestJwt({ chatgpt_account_id: "from-access-token" })
|
|
expect(
|
|
extractAccountId({
|
|
id_token: idToken,
|
|
access_token: accessToken,
|
|
refresh_token: "rt",
|
|
}),
|
|
).toBe("from-id-token")
|
|
})
|
|
|
|
test("falls back to access_token when id_token has no accountId", () => {
|
|
const idToken = createTestJwt({ email: "test@example.com" })
|
|
const accessToken = createTestJwt({
|
|
"https://api.openai.com/auth": { chatgpt_account_id: "from-access" },
|
|
})
|
|
expect(
|
|
extractAccountId({
|
|
id_token: idToken,
|
|
access_token: accessToken,
|
|
refresh_token: "rt",
|
|
}),
|
|
).toBe("from-access")
|
|
})
|
|
|
|
test("returns undefined when no tokens have accountId", () => {
|
|
const token = createTestJwt({ email: "test@example.com" })
|
|
expect(
|
|
extractAccountId({
|
|
id_token: token,
|
|
access_token: token,
|
|
refresh_token: "rt",
|
|
}),
|
|
).toBeUndefined()
|
|
})
|
|
|
|
test("handles missing id_token", () => {
|
|
const accessToken = createTestJwt({ chatgpt_account_id: "acc-123" })
|
|
expect(
|
|
extractAccountId({
|
|
id_token: "",
|
|
access_token: accessToken,
|
|
refresh_token: "rt",
|
|
}),
|
|
).toBe("acc-123")
|
|
})
|
|
})
|
|
|
|
test("installs websocket transport only when experimental websockets are enabled", async () => {
|
|
const disabled = await CodexAuthPlugin({} as never)
|
|
const enabled = await CodexAuthPlugin({} as never, { experimentalWebSockets: true })
|
|
|
|
const disabledOptions = await disabled.auth!.loader!(
|
|
async () => ({ type: "api", key: "sk-test" }) as never,
|
|
{} as never,
|
|
)
|
|
const enabledOptions = await enabled.auth!.loader!(
|
|
async () => ({ type: "api", key: "sk-test" }) as never,
|
|
{} as never,
|
|
)
|
|
|
|
expect(disabledOptions.fetch).toBeUndefined()
|
|
expect(enabledOptions.fetch).toBeFunction()
|
|
await enabled.dispose?.()
|
|
})
|
|
|
|
test("filters unsupported modes and uses Codex context limits for OAuth GPT models", async () => {
|
|
const hooks = await CodexAuthPlugin({} as never)
|
|
const limit = { context: 1_050_000, input: 922_000, output: 128_000 }
|
|
const provider = {
|
|
models: {
|
|
...Object.fromEntries(
|
|
["gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.7-pro"].map((id) => [
|
|
id,
|
|
{ id, api: { id }, limit, cost: {}, options: {} },
|
|
]),
|
|
),
|
|
"gpt-5.4-pro": {
|
|
id: "gpt-5.4-pro",
|
|
api: { id: "gpt-5.4" },
|
|
limit,
|
|
cost: {},
|
|
options: { reasoningMode: "pro" },
|
|
},
|
|
"gpt-5.6-sol-high": {
|
|
id: "gpt-5.6-sol-high",
|
|
api: { id: "gpt-5.6-sol" },
|
|
limit,
|
|
cost: {},
|
|
options: { reasoningEffort: "high" },
|
|
},
|
|
},
|
|
}
|
|
|
|
const models = await hooks.provider!.models!(provider as never, { auth: { type: "oauth" } } as never)
|
|
|
|
expect(models["gpt-5.4"]?.limit).toEqual(limit)
|
|
expect(models["gpt-5.5"]?.limit).toEqual({ context: 400_000, input: 272_000, output: 128_000 })
|
|
expect(models["gpt-5.6-sol"]?.limit).toEqual({ context: 500_000, input: 372_000, output: 128_000 })
|
|
expect(models["gpt-5.6-terra"]?.limit).toEqual({ context: 500_000, input: 372_000, output: 128_000 })
|
|
expect(models["gpt-5.6-luna"]?.limit).toEqual({ context: 500_000, input: 372_000, output: 128_000 })
|
|
expect(models["gpt-5.4-pro"]).toBeUndefined()
|
|
expect(models["gpt-5.7-pro"]).toBeDefined()
|
|
expect(models["gpt-5.6-sol-high"]).toBeDefined()
|
|
expect(await hooks.provider!.models!(provider as never, { auth: { type: "api" } } as never)).toBe(
|
|
provider.models as never,
|
|
)
|
|
})
|
|
|
|
test("deduplicates concurrent Codex token refreshes", async () => {
|
|
let auth = {
|
|
type: "oauth" as const,
|
|
refresh: "refresh-old",
|
|
access: "",
|
|
expires: 0,
|
|
}
|
|
const authUpdates: Array<{
|
|
body: { refresh: string; access: string; expires: number; accountId?: string }
|
|
}> = []
|
|
let resolveRefresh: (() => void) | undefined
|
|
const refreshReady = new Promise<void>((resolve) => {
|
|
resolveRefresh = resolve
|
|
})
|
|
let refreshRequests = 0
|
|
const apiRequests: { authorization: string | null; accountId: string | null }[] = []
|
|
|
|
using server = Bun.serve({
|
|
port: 0,
|
|
async fetch(request) {
|
|
const url = new URL(request.url)
|
|
if (url.pathname === "/oauth/token") {
|
|
expect(await request.text()).toContain("refresh_token=refresh-old")
|
|
refreshRequests += 1
|
|
await refreshReady
|
|
return Response.json({
|
|
id_token: createTestJwt({ chatgpt_account_id: "acc-123" }),
|
|
access_token: "access-new",
|
|
refresh_token: "refresh-new",
|
|
expires_in: 3600,
|
|
})
|
|
}
|
|
|
|
if (url.pathname === "/backend-api/codex/responses") {
|
|
apiRequests.push({
|
|
authorization: request.headers.get("authorization"),
|
|
accountId: request.headers.get("ChatGPT-Account-Id"),
|
|
})
|
|
return new Response("{}", { status: 200 })
|
|
}
|
|
|
|
return new Response("unexpected request", { status: 500 })
|
|
},
|
|
})
|
|
|
|
const hooks = await CodexAuthPlugin(
|
|
{
|
|
client: {
|
|
auth: {
|
|
async set(input: { body: { refresh: string; access: string; expires: number; accountId?: string } }) {
|
|
authUpdates.push(input)
|
|
auth = {
|
|
type: "oauth",
|
|
refresh: input.body.refresh,
|
|
access: input.body.access,
|
|
expires: input.body.expires,
|
|
...(input.body.accountId && { accountId: input.body.accountId }),
|
|
}
|
|
},
|
|
},
|
|
} as never,
|
|
project: {} as never,
|
|
directory: "",
|
|
worktree: "",
|
|
experimental_workspace: {
|
|
register() {},
|
|
},
|
|
serverUrl: new URL("https://example.com"),
|
|
$: {} as never,
|
|
},
|
|
{
|
|
issuer: server.url.origin,
|
|
codexApiEndpoint: new URL("/backend-api/codex/responses", server.url).toString(),
|
|
},
|
|
)
|
|
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 waitFor(() => refreshRequests === 1)
|
|
expect(apiRequests).toHaveLength(0)
|
|
|
|
resolveRefresh!()
|
|
await Promise.all([first, second])
|
|
|
|
expect(refreshRequests).toBe(1)
|
|
expect(authUpdates).toHaveLength(1)
|
|
expect(authUpdates[0]?.body.refresh).toBe("refresh-new")
|
|
expect(authUpdates[0]?.body.access).toBe("access-new")
|
|
expect(authUpdates[0]?.body.accountId).toBe("acc-123")
|
|
expect(apiRequests).toEqual([
|
|
{ authorization: "Bearer access-new", accountId: "acc-123" },
|
|
{ authorization: "Bearer access-new", accountId: "acc-123" },
|
|
])
|
|
})
|
|
})
|
|
|
|
async function waitFor(predicate: () => boolean) {
|
|
const started = Date.now()
|
|
while (!predicate()) {
|
|
if (Date.now() - started > 1_000) throw new Error("timed out waiting for condition")
|
|
await new Promise((resolve) => setTimeout(resolve, 1))
|
|
}
|
|
}
|