From 6c0dca30b85f9a2e6a4b421e1610401a62eafa3b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Feb 2026 02:52:36 +0000 Subject: [PATCH] fix: accept auth code in chutes oauth manual flow --- src/cli/program.nodes-media.e2e.test.ts | 8 ---- src/commands/chutes-oauth.ts | 50 ++++++++++++++++--------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/cli/program.nodes-media.e2e.test.ts b/src/cli/program.nodes-media.e2e.test.ts index 16dd6f64c6f..e31f52d406d 100644 --- a/src/cli/program.nodes-media.e2e.test.ts +++ b/src/cli/program.nodes-media.e2e.test.ts @@ -20,14 +20,6 @@ const IOS_NODE = { connected: true, } as const; -function getFirstRuntimeLogLine(): string { - const value = runtime.log.mock.calls[0]?.[0]; - if (typeof value !== "string") { - throw new Error(`expected runtime.log first arg to be string, got ${typeof value}`); - } - return value; -} - function mockCameraGateway( command: "camera.snap" | "camera.clip", payload: Record, diff --git a/src/commands/chutes-oauth.ts b/src/commands/chutes-oauth.ts index 161ae621db0..e979907931e 100644 --- a/src/commands/chutes-oauth.ts +++ b/src/commands/chutes-oauth.ts @@ -15,6 +15,34 @@ type OAuthPrompt = { placeholder?: string; }; +function parseManualOAuthInput( + input: string, + expectedState: string, +): { code: string; state: string } { + const trimmed = String(input ?? "").trim(); + if (!trimmed) { + throw new Error("Missing OAuth redirect URL or authorization code."); + } + + // Support pasting either: + // - Full redirect URL (preferred; validates state) + // - Raw authorization code (legacy/manual copy flows) + const looksLikeRedirect = + /^https?:\/\//i.test(trimmed) || trimmed.includes("://") || trimmed.includes("?"); + if (!looksLikeRedirect) { + return { code: trimmed, state: expectedState }; + } + + const parsed = parseOAuthCallbackInput(trimmed, expectedState); + if ("error" in parsed) { + throw new Error(parsed.error); + } + if (parsed.state !== expectedState) { + throw new Error("Invalid OAuth state"); + } + return parsed; +} + function buildAuthorizeUrl(params: { clientId: string; redirectUri: string; @@ -156,17 +184,10 @@ export async function loginChutes(params: { await params.onAuth({ url }); params.onProgress?.("Waiting for redirect URL…"); const input = await params.onPrompt({ - message: "Paste the redirect URL", + message: "Paste the redirect URL (or authorization code)", placeholder: `${params.app.redirectUri}?code=...&state=...`, }); - const parsed = parseOAuthCallbackInput(String(input), state); - if ("error" in parsed) { - throw new Error(parsed.error); - } - if (parsed.state !== state) { - throw new Error("Invalid OAuth state"); - } - codeAndState = parsed; + codeAndState = parseManualOAuthInput(input, state); } else { const callback = waitForLocalCallback({ redirectUri: params.app.redirectUri, @@ -176,17 +197,10 @@ export async function loginChutes(params: { }).catch(async () => { params.onProgress?.("OAuth callback not detected; paste redirect URL…"); const input = await params.onPrompt({ - message: "Paste the redirect URL", + message: "Paste the redirect URL (or authorization code)", placeholder: `${params.app.redirectUri}?code=...&state=...`, }); - const parsed = parseOAuthCallbackInput(String(input), state); - if ("error" in parsed) { - throw new Error(parsed.error); - } - if (parsed.state !== state) { - throw new Error("Invalid OAuth state"); - } - return parsed; + return parseManualOAuthInput(input, state); }); await params.onAuth({ url });