FN-5917: fix Codex OAuth option selection
Prevent multi-option OAuth prompts from cancelling Codex browser login. - add OAuth option selection logic that prefers the browser flow for openai-codex prompts - fall back to default-labelled or first options for other multi-option OAuth prompts - add API route coverage for Codex multi-option, single-option, and generic default-labelled prompt handling Files changed: .changeset/fn-5917-codex-oauth-login.md | 5 ++ packages/dashboard/src/__tests__/routes-auth.test.ts | 72 ++++++++++++++++++++++ packages/dashboard/src/routes/register-auth-routes.ts | 26 ++++++-- 3 files changed, 97 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-5917 Fusion-Task-Lineage: 3b069280-9dbf-47ee-ae04-b7c54ff14d63
This commit is contained in:
5
.changeset/fn-5917-codex-oauth-login.md
Normal file
5
.changeset/fn-5917-codex-oauth-login.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Fix the dashboard OAuth login flow for ChatGPT Plus/Pro (Codex Subscription) so multi-option provider selection prompts no longer cancel the login before browser auth starts.
|
||||
@@ -1499,6 +1499,78 @@ describe("POST /auth/login", () => {
|
||||
expect(observedPromptInput).toBe("manual-code");
|
||||
});
|
||||
|
||||
it("prefers browser login for openai-codex multi-option prompts", async () => {
|
||||
let selectedOption: string | undefined;
|
||||
(authStorage.getOAuthProviders as ReturnType<typeof vi.fn>).mockReturnValue([{ id: "openai-codex", name: "OpenAI Codex" }]);
|
||||
(authStorage.login as ReturnType<typeof vi.fn>).mockImplementation(async (_provider: string, callbacks: any) => {
|
||||
selectedOption = await callbacks.onSelect({
|
||||
message: "Select OpenAI Codex login method:",
|
||||
options: [
|
||||
{ id: "browser", label: "Browser login (default)" },
|
||||
{ id: "device_code", label: "Device code login (headless)" },
|
||||
],
|
||||
});
|
||||
if (!selectedOption) {
|
||||
throw new Error("Login cancelled");
|
||||
}
|
||||
callbacks.onAuth({
|
||||
url: "https://auth.openai.com/oauth/authorize?state=test-state&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback",
|
||||
});
|
||||
});
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/auth/login", JSON.stringify({ provider: "openai-codex" }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(selectedOption).toBe("browser");
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.url).toBe(
|
||||
"https://auth.openai.com/oauth/authorize?state=test-state&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps returning the only option id for single-option prompts", async () => {
|
||||
let selectedOption: string | undefined;
|
||||
(authStorage.getOAuthProviders as ReturnType<typeof vi.fn>).mockReturnValue([{ id: "openai-codex", name: "OpenAI Codex" }]);
|
||||
(authStorage.login as ReturnType<typeof vi.fn>).mockImplementation(async (_provider: string, callbacks: any) => {
|
||||
selectedOption = await callbacks.onSelect({
|
||||
message: "Only one choice",
|
||||
options: [{ id: "browser", label: "Browser login" }],
|
||||
});
|
||||
callbacks.onAuth({ url: "https://auth.example.com/login" });
|
||||
});
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/auth/login", JSON.stringify({ provider: "openai-codex" }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(selectedOption).toBe("browser");
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("prefers the default-labelled option for generic multi-option prompts", async () => {
|
||||
let selectedOption: string | undefined;
|
||||
(authStorage.getOAuthProviders as ReturnType<typeof vi.fn>).mockReturnValue([{ id: "anthropic", name: "Anthropic" }]);
|
||||
(authStorage.login as ReturnType<typeof vi.fn>).mockImplementation(async (_provider: string, callbacks: any) => {
|
||||
selectedOption = await callbacks.onSelect({
|
||||
message: "Select login method:",
|
||||
options: [
|
||||
{ id: "device_code", label: "Device code login" },
|
||||
{ id: "browser", label: "Browser login (DEFAULT)" },
|
||||
{ id: "manual", label: "Manual login" },
|
||||
],
|
||||
});
|
||||
callbacks.onAuth({ url: "https://claude.ai/oauth/authorize?state=test-state&redirect_uri=http%3A%2F%2Flocalhost%3A3210%2Fauth%2Fcallback" });
|
||||
});
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/auth/login", JSON.stringify({ provider: "anthropic" }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(selectedOption).toBe("browser");
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("returns 400 when provider is missing", async () => {
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/auth/login", JSON.stringify({}), {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -189,6 +189,25 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
return providerId === "github-copilot";
|
||||
}
|
||||
|
||||
function selectOauthOption(
|
||||
providerId: string,
|
||||
prompt: { options: Array<{ id: string; label?: string }> },
|
||||
): string | undefined {
|
||||
if (prompt.options.length === 1) {
|
||||
return prompt.options[0]?.id;
|
||||
}
|
||||
|
||||
const defaultLabeledOption = prompt.options.find((option) => /\(default\)/i.test(option.label ?? ""));
|
||||
|
||||
// FN-5917: returning undefined here caused pi-ai's openai-codex login
|
||||
// flow to throw "Login cancelled" before it could open browser auth.
|
||||
if (providerId === "openai-codex") {
|
||||
return prompt.options.find((option) => option.id === "browser")?.id ?? defaultLabeledOption?.id ?? prompt.options[0]?.id;
|
||||
}
|
||||
|
||||
return defaultLabeledOption?.id ?? prompt.options[0]?.id;
|
||||
}
|
||||
|
||||
async function probeDroidCliWithEffectiveBinary(req?: Request) {
|
||||
let pluginSettings: Record<string, unknown> | undefined;
|
||||
if (req) {
|
||||
@@ -866,12 +885,7 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
// to race pasted codes against the localhost callback server.
|
||||
onManualCodeInput: async () => await pendingLogin.inputPromise,
|
||||
onProgress: () => {}, // no-op for web UI
|
||||
onSelect: async (prompt) => {
|
||||
if (prompt.options.length === 1) {
|
||||
return prompt.options[0]?.id;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
onSelect: async (prompt) => selectOauthOption(provider, prompt),
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user