mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 02:50:35 +00:00
fix: await codex oauth node runtime imports
This commit is contained in:
13
extensions/openai/openai-codex-oauth-flow.runtime.test.ts
Normal file
13
extensions/openai/openai-codex-oauth-flow.runtime.test.ts
Normal file
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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<NodeOAuthRuntime> | null = null;
|
||||
|
||||
function loadNodeOAuthRuntime(): Promise<NodeOAuthRuntime> {
|
||||
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<TokenResult> {
|
||||
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<OAuthServerInfo> {
|
||||
if (!http) {
|
||||
throw new Error("OpenAI Codex OAuth is only available in Node.js environments");
|
||||
}
|
||||
|
||||
async function startLocalOAuthServer(state: string): Promise<OAuthServerInfo> {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<NodeOAuthRuntime> | null = null;
|
||||
|
||||
function loadNodeOAuthRuntime(): Promise<NodeOAuthRuntime> {
|
||||
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<TokenResult> {
|
||||
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<OAuthServerInfo> {
|
||||
if (!http) {
|
||||
throw new Error("OpenAI Codex OAuth is only available in Node.js environments");
|
||||
}
|
||||
|
||||
async function startLocalOAuthServer(state: string): Promise<OAuthServerInfo> {
|
||||
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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user