From ebc4fb2f5114f6293eff08a85b5ef8e9d837bcf0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 26 May 2026 21:27:13 +0100 Subject: [PATCH] fix: await codex oauth node runtime imports --- .../openai-codex-oauth-flow.runtime.test.ts | 13 +++++ .../openai/openai-codex-oauth-flow.runtime.ts | 49 ++++++++++--------- src/llm/utils/oauth/openai-codex.test.ts | 9 ++++ src/llm/utils/oauth/openai-codex.ts | 49 ++++++++++--------- 4 files changed, 76 insertions(+), 44 deletions(-) create mode 100644 extensions/openai/openai-codex-oauth-flow.runtime.test.ts diff --git a/extensions/openai/openai-codex-oauth-flow.runtime.test.ts b/extensions/openai/openai-codex-oauth-flow.runtime.test.ts new file mode 100644 index 00000000000..22bfe4d6af7 --- /dev/null +++ b/extensions/openai/openai-codex-oauth-flow.runtime.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; +import { testing } from "./openai-codex-oauth-flow.runtime.js"; + +describe("OpenAI Codex OAuth flow", () => { + it("waits for Node OAuth runtime before creating an authorization flow", async () => { + const flow = await testing.createAuthorizationFlow("openclaw-test"); + const url = new URL(flow.url); + + expect(flow.state).toMatch(/^[a-f0-9]{32}$/u); + expect(url.searchParams.get("state")).toBe(flow.state); + expect(url.searchParams.get("originator")).toBe("openclaw-test"); + }); +}); diff --git a/extensions/openai/openai-codex-oauth-flow.runtime.ts b/extensions/openai/openai-codex-oauth-flow.runtime.ts index 290313a166c..e0660756778 100644 --- a/extensions/openai/openai-codex-oauth-flow.runtime.ts +++ b/extensions/openai/openai-codex-oauth-flow.runtime.ts @@ -5,18 +5,6 @@ * It is only intended for CLI use, not browser environments. */ -// NEVER convert to top-level imports - breaks browser/Vite builds -let randomBytes: typeof import("node:crypto").randomBytes | null = null; -let http: typeof import("node:http") | null = null; -if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) { - void import("node:crypto").then((m) => { - randomBytes = m.randomBytes; - }); - void import("node:http").then((m) => { - http = m; - }); -} - import { resolveCodexAuthIdentity } from "./openai-codex-auth-identity.js"; import { oauthErrorHtml, oauthSuccessHtml } from "./openai-codex-oauth-page.runtime.js"; import type { @@ -42,11 +30,27 @@ type TokenResponseJson = { refresh_token?: string; expires_in?: number; }; +type NodeOAuthRuntime = { + randomBytes: typeof import("node:crypto").randomBytes; + http: typeof import("node:http"); +}; -function createState(): string { - if (!randomBytes) { - throw new Error("OpenAI Codex OAuth is only available in Node.js environments"); +let nodeOAuthRuntimePromise: Promise | null = null; + +function loadNodeOAuthRuntime(): Promise { + if (typeof process === "undefined" || (!process.versions?.node && !process.versions?.bun)) { + return Promise.reject(new Error("OpenAI Codex OAuth is only available in Node.js environments")); } + nodeOAuthRuntimePromise ??= Promise.all([import("node:crypto"), import("node:http")]).then( + ([cryptoModule, httpModule]) => ({ + randomBytes: cryptoModule.randomBytes, + http: httpModule, + }), + ); + return nodeOAuthRuntimePromise; +} + +function createState(randomBytes: typeof import("node:crypto").randomBytes): string { return randomBytes(16).toString("hex"); } @@ -186,8 +190,11 @@ async function refreshAccessToken(refreshToken: string): Promise { async function createAuthorizationFlow( originator: string = "openclaw", ): Promise<{ verifier: string; state: string; url: string }> { - const { verifier, challenge } = await generatePKCE(); - const state = createState(); + const [{ verifier, challenge }, runtime] = await Promise.all([ + generatePKCE(), + loadNodeOAuthRuntime(), + ]); + const state = createState(runtime.randomBytes); const url = new URL(AUTHORIZE_URL); url.searchParams.set("response_type", "code"); @@ -210,11 +217,8 @@ type OAuthServerInfo = { waitForCode: () => Promise<{ code: string } | null>; }; -function startLocalOAuthServer(state: string): Promise { - if (!http) { - throw new Error("OpenAI Codex OAuth is only available in Node.js environments"); - } - +async function startLocalOAuthServer(state: string): Promise { + const { http } = await loadNodeOAuthRuntime(); let settleWait: ((value: { code: string } | null) => void) | undefined; const waitForCodePromise = new Promise<{ code: string } | null>((resolve) => { let settled = false; @@ -457,6 +461,7 @@ export const openaiCodexOAuthProvider: OAuthProviderInterface = { }; export const testing = { + createAuthorizationFlow, exchangeAuthorizationCode, refreshAccessToken, }; diff --git a/src/llm/utils/oauth/openai-codex.test.ts b/src/llm/utils/oauth/openai-codex.test.ts index 8ca6459323f..1d45af420bc 100644 --- a/src/llm/utils/oauth/openai-codex.test.ts +++ b/src/llm/utils/oauth/openai-codex.test.ts @@ -19,6 +19,15 @@ afterEach(() => { }); describe("OpenAI Codex OAuth token responses", () => { + it("waits for Node OAuth runtime before creating an authorization flow", async () => { + const flow = await testing.createAuthorizationFlow("openclaw-test"); + const url = new URL(flow.url); + + expect(flow.state).toMatch(/^[a-f0-9]{32}$/u); + expect(url.searchParams.get("state")).toBe(flow.state); + expect(url.searchParams.get("originator")).toBe("openclaw-test"); + }); + it("does not echo token payload values when the exchange response is malformed", async () => { stubTokenResponse({ access_token: "secret-access-token", diff --git a/src/llm/utils/oauth/openai-codex.ts b/src/llm/utils/oauth/openai-codex.ts index bb3cee222f1..18f053a68fd 100644 --- a/src/llm/utils/oauth/openai-codex.ts +++ b/src/llm/utils/oauth/openai-codex.ts @@ -5,18 +5,6 @@ * It is only intended for CLI use, not browser environments. */ -// NEVER convert to top-level imports - breaks browser/Vite builds -let randomBytes: typeof import("node:crypto").randomBytes | null = null; -let http: typeof import("node:http") | null = null; -if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) { - void import("node:crypto").then((m) => { - randomBytes = m.randomBytes; - }); - void import("node:http").then((m) => { - http = m; - }); -} - import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.js"; import { resolveOpenAICodexAccountId } from "./openai-codex-jwt.js"; import { generatePKCE } from "./pkce.js"; @@ -41,11 +29,27 @@ type TokenResponseJson = { refresh_token?: string; expires_in?: number; }; +type NodeOAuthRuntime = { + randomBytes: typeof import("node:crypto").randomBytes; + http: typeof import("node:http"); +}; -function createState(): string { - if (!randomBytes) { - throw new Error("OpenAI Codex OAuth is only available in Node.js environments"); +let nodeOAuthRuntimePromise: Promise | null = null; + +function loadNodeOAuthRuntime(): Promise { + if (typeof process === "undefined" || (!process.versions?.node && !process.versions?.bun)) { + return Promise.reject(new Error("OpenAI Codex OAuth is only available in Node.js environments")); } + nodeOAuthRuntimePromise ??= Promise.all([import("node:crypto"), import("node:http")]).then( + ([cryptoModule, httpModule]) => ({ + randomBytes: cryptoModule.randomBytes, + http: httpModule, + }), + ); + return nodeOAuthRuntimePromise; +} + +function createState(randomBytes: typeof import("node:crypto").randomBytes): string { return randomBytes(16).toString("hex"); } @@ -185,8 +189,11 @@ async function refreshAccessToken(refreshToken: string): Promise { async function createAuthorizationFlow( originator: string = "openclaw", ): Promise<{ verifier: string; state: string; url: string }> { - const { verifier, challenge } = await generatePKCE(); - const state = createState(); + const [{ verifier, challenge }, runtime] = await Promise.all([ + generatePKCE(), + loadNodeOAuthRuntime(), + ]); + const state = createState(runtime.randomBytes); const url = new URL(AUTHORIZE_URL); url.searchParams.set("response_type", "code"); @@ -209,11 +216,8 @@ type OAuthServerInfo = { waitForCode: () => Promise<{ code: string } | null>; }; -function startLocalOAuthServer(state: string): Promise { - if (!http) { - throw new Error("OpenAI Codex OAuth is only available in Node.js environments"); - } - +async function startLocalOAuthServer(state: string): Promise { + const { http } = await loadNodeOAuthRuntime(); let settleWait: ((value: { code: string } | null) => void) | undefined; const waitForCodePromise = new Promise<{ code: string } | null>((resolve) => { let settled = false; @@ -455,6 +459,7 @@ export const openaiCodexOAuthProvider: OAuthProviderInterface = { }; export const testing = { + createAuthorizationFlow, exchangeAuthorizationCode, refreshAccessToken, };