mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-22 23:41:07 +00:00
fix(auth): support remote Codex OAuth manual input (#51631)
* fix(auth): support remote codex oauth manual input * fix: support remote Codex OAuth manual input (#51631) (thanks @cash-echo-bot) --------- Co-authored-by: Cash Williams <cashwilliams@gmail.com> Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
@@ -118,6 +118,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Control UI/chat sessions: show human-readable labels in the grouped session dropdown again, keep unique scoped fallbacks when metadata is missing, and disambiguate duplicate labels only when needed. (#45130) Thanks @luzhidong.
|
||||
- Control UI: scope persisted session selection per gateway, prevent stale session bleed across tokenized gateway opens, and cap stored gateway session history. (#47453) Thanks @sallyom.
|
||||
- Control UI/dashboard: preserve structured gateway shutdown reasons across restart disconnects so config-triggered restarts no longer fall back to `disconnected (1006): no reason`. (#46580) Fixes #46532. Thanks @vincentkoc.
|
||||
- Models/OpenAI Codex OAuth: start the remote manual-input race for Codex login and keep the pasted-input prompt aligned with the actual accepted values, so remote/VPS auth no longer stalls waiting on an unreachable localhost callback. (#51631) Thanks @cash-echo-bot.
|
||||
- Android/chat: theme the thinking dropdown and TLS trust dialogs explicitly so popup surfaces match the active app theme instead of falling back to mismatched Material defaults.
|
||||
- Group mention gating: reject invalid and unsafe nested-repetition `mentionPatterns`, reuse the shared safe config-regex compiler across mention stripping and detection, and cache strip-time regex compilation so noisy groups avoid repeated recompiles.
|
||||
- Browser/profiles: drop the auto-created `chrome-relay` browser profile; users who need the Chrome extension relay must now create their own profile via `openclaw browser create-profile`. (#46596) Fixes #45777. Thanks @odysseus0.
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loginOpenAICodex: vi.fn(),
|
||||
createVpsAwareOAuthHandlers: vi.fn(),
|
||||
runOpenAIOAuthTlsPreflight: vi.fn(),
|
||||
formatOpenAIOAuthTlsPreflightFix: vi.fn(),
|
||||
}));
|
||||
@@ -13,22 +12,19 @@ vi.mock("@mariozechner/pi-ai/oauth", () => ({
|
||||
loginOpenAICodex: mocks.loginOpenAICodex,
|
||||
}));
|
||||
|
||||
vi.mock("./oauth-flow.js", () => ({
|
||||
createVpsAwareOAuthHandlers: mocks.createVpsAwareOAuthHandlers,
|
||||
}));
|
||||
|
||||
vi.mock("./oauth-tls-preflight.js", () => ({
|
||||
vi.mock("../plugins/provider-openai-codex-oauth-tls.js", () => ({
|
||||
runOpenAIOAuthTlsPreflight: mocks.runOpenAIOAuthTlsPreflight,
|
||||
formatOpenAIOAuthTlsPreflightFix: mocks.formatOpenAIOAuthTlsPreflightFix,
|
||||
}));
|
||||
|
||||
import { loginOpenAICodexOAuth } from "./openai-codex-oauth.js";
|
||||
import { loginOpenAICodexOAuth } from "../plugins/provider-openai-codex-oauth.js";
|
||||
|
||||
function createPrompter() {
|
||||
const spin = { update: vi.fn(), stop: vi.fn() };
|
||||
const prompter: Pick<WizardPrompter, "note" | "progress"> = {
|
||||
const prompter: Pick<WizardPrompter, "note" | "progress" | "text"> = {
|
||||
note: vi.fn(async () => {}),
|
||||
progress: vi.fn(() => spin),
|
||||
text: vi.fn(async () => "http://localhost:1455/auth/callback?code=test"),
|
||||
};
|
||||
return { prompter: prompter as unknown as WizardPrompter, spin };
|
||||
}
|
||||
@@ -43,14 +39,17 @@ function createRuntime(): RuntimeEnv {
|
||||
};
|
||||
}
|
||||
|
||||
async function runCodexOAuth(params: { isRemote: boolean }) {
|
||||
async function runCodexOAuth(params: {
|
||||
isRemote: boolean;
|
||||
openUrl?: (url: string) => Promise<void>;
|
||||
}) {
|
||||
const { prompter, spin } = createPrompter();
|
||||
const runtime = createRuntime();
|
||||
const result = await loginOpenAICodexOAuth({
|
||||
prompter,
|
||||
runtime,
|
||||
isRemote: params.isRemote,
|
||||
openUrl: async () => {},
|
||||
openUrl: params.openUrl ?? (async () => {}),
|
||||
});
|
||||
return { result, prompter, spin, runtime };
|
||||
}
|
||||
@@ -70,10 +69,6 @@ describe("loginOpenAICodexOAuth", () => {
|
||||
expires: Date.now() + 60_000,
|
||||
email: "user@example.com",
|
||||
};
|
||||
mocks.createVpsAwareOAuthHandlers.mockReturnValue({
|
||||
onAuth: vi.fn(),
|
||||
onPrompt: vi.fn(),
|
||||
});
|
||||
mocks.loginOpenAICodex.mockResolvedValue(creds);
|
||||
|
||||
const { result, spin, runtime } = await runCodexOAuth({ isRemote: false });
|
||||
@@ -92,11 +87,6 @@ describe("loginOpenAICodexOAuth", () => {
|
||||
expires: Date.now() + 60_000,
|
||||
email: "user@example.com",
|
||||
};
|
||||
const onAuthSpy = vi.fn();
|
||||
mocks.createVpsAwareOAuthHandlers.mockReturnValue({
|
||||
onAuth: onAuthSpy,
|
||||
onPrompt: vi.fn(),
|
||||
});
|
||||
mocks.loginOpenAICodex.mockImplementation(
|
||||
async (opts: { onAuth: (event: { url: string }) => Promise<void> }) => {
|
||||
await opts.onAuth({
|
||||
@@ -106,20 +96,18 @@ describe("loginOpenAICodexOAuth", () => {
|
||||
},
|
||||
);
|
||||
|
||||
await runCodexOAuth({ isRemote: false });
|
||||
const openUrl = vi.fn(async () => {});
|
||||
const { runtime } = await runCodexOAuth({ isRemote: false, openUrl });
|
||||
|
||||
expect(onAuthSpy).toHaveBeenCalledTimes(1);
|
||||
const event = onAuthSpy.mock.calls[0]?.[0] as { url: string };
|
||||
expect(event.url).toBe(
|
||||
expect(openUrl).toHaveBeenCalledWith(
|
||||
"https://auth.openai.com/oauth/authorize?scope=openid+profile+email+offline_access&state=abc",
|
||||
);
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
"Open: https://auth.openai.com/oauth/authorize?scope=openid+profile+email+offline_access&state=abc",
|
||||
);
|
||||
});
|
||||
|
||||
it("reports oauth errors and rethrows", async () => {
|
||||
mocks.createVpsAwareOAuthHandlers.mockReturnValue({
|
||||
onAuth: vi.fn(),
|
||||
onPrompt: vi.fn(),
|
||||
});
|
||||
mocks.loginOpenAICodex.mockRejectedValue(new Error("oauth failed"));
|
||||
|
||||
const { prompter, spin } = createPrompter();
|
||||
@@ -141,6 +129,37 @@ describe("loginOpenAICodexOAuth", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("passes manual code input hook for remote oauth flows", async () => {
|
||||
const creds = {
|
||||
provider: "openai-codex" as const,
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
email: "user@example.com",
|
||||
};
|
||||
mocks.loginOpenAICodex.mockImplementation(
|
||||
async (opts: {
|
||||
onAuth: (event: { url: string }) => Promise<void>;
|
||||
onManualCodeInput?: () => Promise<string>;
|
||||
}) => {
|
||||
await opts.onAuth({
|
||||
url: "https://auth.openai.com/oauth/authorize?state=abc",
|
||||
});
|
||||
expect(opts.onManualCodeInput).toBeTypeOf("function");
|
||||
await expect(opts.onManualCodeInput?.()).resolves.toContain("code=test");
|
||||
return creds;
|
||||
},
|
||||
);
|
||||
|
||||
const { result, prompter } = await runCodexOAuth({ isRemote: true });
|
||||
|
||||
expect(result).toEqual(creds);
|
||||
expect(prompter.text).toHaveBeenCalledWith({
|
||||
message: "Paste the authorization code (or full redirect URL):",
|
||||
validate: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("continues OAuth flow on non-certificate preflight failures", async () => {
|
||||
const creds = {
|
||||
provider: "openai-codex" as const,
|
||||
@@ -154,10 +173,6 @@ describe("loginOpenAICodexOAuth", () => {
|
||||
kind: "network",
|
||||
message: "Client network socket disconnected before secure TLS connection was established",
|
||||
});
|
||||
mocks.createVpsAwareOAuthHandlers.mockReturnValue({
|
||||
onAuth: vi.fn(),
|
||||
onPrompt: vi.fn(),
|
||||
});
|
||||
mocks.loginOpenAICodex.mockResolvedValue(creds);
|
||||
|
||||
const { result, prompter, runtime } = await runCodexOAuth({ isRemote: false });
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
runOpenAIOAuthTlsPreflight,
|
||||
} from "./provider-openai-codex-oauth-tls.js";
|
||||
|
||||
const manualInputPromptMessage = "Paste the authorization code (or full redirect URL):";
|
||||
|
||||
export async function loginOpenAICodexOAuth(params: {
|
||||
prompter: WizardPrompter;
|
||||
runtime: RuntimeEnv;
|
||||
@@ -47,11 +49,18 @@ export async function loginOpenAICodexOAuth(params: {
|
||||
spin,
|
||||
openUrl,
|
||||
localBrowserMessage: localBrowserMessage ?? "Complete sign-in in browser…",
|
||||
manualPromptMessage: manualInputPromptMessage,
|
||||
});
|
||||
|
||||
const creds = await loginOpenAICodex({
|
||||
onAuth: baseOnAuth,
|
||||
onPrompt,
|
||||
onManualCodeInput: isRemote
|
||||
? async () =>
|
||||
await onPrompt({
|
||||
message: manualInputPromptMessage,
|
||||
})
|
||||
: undefined,
|
||||
onProgress: (msg: string) => spin.update(msg),
|
||||
});
|
||||
spin.stop("OpenAI OAuth complete");
|
||||
|
||||
Reference in New Issue
Block a user