diff --git a/src/agents/chutes-oauth.test.ts b/src/agents/chutes-oauth.test.ts index 8ef91281b..26e99539d 100644 --- a/src/agents/chutes-oauth.test.ts +++ b/src/agents/chutes-oauth.test.ts @@ -21,10 +21,9 @@ describe("parseOAuthCallbackInput", () => { expect((result as { error: string }).error).toMatch(/state mismatch/i); }); - it("rejects bare code input without fabricating state", () => { + it("accepts bare code input for manual flow", () => { const result = parseOAuthCallbackInput("bare_auth_code", EXPECTED_STATE); - expect(result).toHaveProperty("error"); - expect(result).not.toHaveProperty("code"); + expect(result).toEqual({ code: "bare_auth_code", state: EXPECTED_STATE }); }); it("rejects empty input", () => { diff --git a/src/agents/chutes-oauth.ts b/src/agents/chutes-oauth.ts index 405c3d841..da36f7506 100644 --- a/src/agents/chutes-oauth.ts +++ b/src/agents/chutes-oauth.ts @@ -50,14 +50,19 @@ export function parseOAuthCallbackInput( return { error: "Missing 'code' parameter in URL" }; } if (!state) { - return { error: "Missing 'state' parameter. Paste the full URL." }; + return { error: "Missing 'state' parameter. Paste the full URL (or just the code)." }; } if (state !== expectedState) { return { error: "OAuth state mismatch - possible CSRF attack. Please retry login." }; } return { code, state }; } catch { - return { error: "Paste the full redirect URL, not just the code." }; + // Manual flow: users often paste only the authorization code. + // In that case we can't validate state, but the user is explicitly opting in by pasting it. + if (!/\s/.test(trimmed) && !trimmed.includes("://") && trimmed.length > 0) { + return { code: trimmed, state: expectedState }; + } + return { error: "Paste the redirect URL (or authorization code)." }; } } diff --git a/src/commands/chutes-oauth.ts b/src/commands/chutes-oauth.ts index 161ae621d..1925649bb 100644 --- a/src/commands/chutes-oauth.ts +++ b/src/commands/chutes-oauth.ts @@ -156,7 +156,7 @@ 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); @@ -176,7 +176,7 @@ 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);