From 0904e53c85cfb131aa1dfc7e32fa8bc669a66a6f Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Sat, 28 Feb 2026 14:42:32 -0800 Subject: [PATCH] fix: surface OAuth denial error immediately instead of waiting 120s (#2039) When a user denies OAuth access on OpenRouter or DigitalOcean, the CLI now immediately shows a clear error message and falls back to manual key entry, instead of silently waiting the full 120s poll timeout. Changes: - OpenRouter OAuth: check for `error` query param on callback, set `oauthDenied` flag, show denial-specific HTML page in browser, break polling loop early, and log a clear terminal error - DigitalOcean OAuth: add `oauthDenied` flag (error detection already existed but the polling loop still waited 120s), break loop early Fixes #2038 Agent: code-health Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 --- packages/cli/src/digitalocean/digitalocean.ts | 11 +++++++- packages/cli/src/shared/oauth.ts | 28 ++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/digitalocean/digitalocean.ts b/packages/cli/src/digitalocean/digitalocean.ts index 2c2ddd32..c23f3e3e 100644 --- a/packages/cli/src/digitalocean/digitalocean.ts +++ b/packages/cli/src/digitalocean/digitalocean.ts @@ -330,6 +330,7 @@ async function tryDoOAuth(): Promise { const csrfState = generateCsrfState(); let oauthCode: string | null = null; + let oauthDenied = false; let server: ReturnType | null = null; // Try ports in range @@ -347,6 +348,7 @@ async function tryDoOAuth(): Promise { if (error) { const desc = url.searchParams.get("error_description") || error; logError(`DigitalOcean authorization denied: ${desc}`); + oauthDenied = true; return new Response(OAUTH_ERROR_HTML, { status: 403, headers: { @@ -434,12 +436,19 @@ async function tryDoOAuth(): Promise { // Wait up to 120 seconds logStep("Waiting for authorization in browser (timeout: 120s)..."); const deadline = Date.now() + 120_000; - while (!oauthCode && Date.now() < deadline) { + while (!oauthCode && !oauthDenied && Date.now() < deadline) { await sleep(500); } server.stop(true); + if (oauthDenied) { + logError("OAuth authorization was denied by the user"); + logError("Alternative: Use a manual API token instead"); + logError(" export DO_API_TOKEN=dop_v1_..."); + return null; + } + if (!oauthCode) { logError("OAuth authentication timed out after 120 seconds"); logError("Alternative: Use a manual API token instead"); diff --git a/packages/cli/src/shared/oauth.ts b/packages/cli/src/shared/oauth.ts index 0a157cf0..410b975e 100644 --- a/packages/cli/src/shared/oauth.ts +++ b/packages/cli/src/shared/oauth.ts @@ -56,6 +56,8 @@ const SUCCESS_HTML = `

Authentication Failed

Invalid or missing state parameter (CSRF protection). Please try again.

`; +const DENIAL_HTML = `

Authorization Denied

You denied access to OpenRouter. You can close this tab and return to your terminal.

`; + async function tryOauthFlow(callbackPort = 5180, agentSlug?: string, cloudSlug?: string): Promise { logStep("Attempting OAuth authentication..."); @@ -72,6 +74,7 @@ async function tryOauthFlow(callbackPort = 5180, agentSlug?: string, cloudSlug?: const csrfState = generateCsrfState(); let oauthCode: string | null = null; + let oauthDenied = false; let server: ReturnType | null = null; // Try ports in range @@ -83,6 +86,22 @@ async function tryOauthFlow(callbackPort = 5180, agentSlug?: string, cloudSlug?: hostname: "127.0.0.1", fetch(req) { const url = new URL(req.url); + if (url.pathname === "/callback") { + // Check for OAuth denial / error + const error = url.searchParams.get("error"); + if (error) { + const desc = url.searchParams.get("error_description") || error; + logError(`OpenRouter authorization denied: ${desc}`); + oauthDenied = true; + return new Response(DENIAL_HTML, { + status: 403, + headers: { + "Content-Type": "text/html", + Connection: "close", + }, + }); + } + } const code = url.searchParams.get("code"); if (url.pathname === "/callback" && code) { // CSRF check @@ -145,12 +164,19 @@ async function tryOauthFlow(callbackPort = 5180, agentSlug?: string, cloudSlug?: // Wait up to 120 seconds logStep("Waiting for authentication in browser (timeout: 120s)..."); const deadline = Date.now() + 120_000; - while (!oauthCode && Date.now() < deadline) { + while (!oauthCode && !oauthDenied && Date.now() < deadline) { await new Promise((r) => setTimeout(r, 500)); } server.stop(true); + if (oauthDenied) { + logError("OAuth authorization was denied by the user"); + logError("Alternative: Use a manual API key instead"); + logError(" export OPENROUTER_API_KEY=sk-or-v1-..."); + return null; + } + if (!oauthCode) { logError("OAuth authentication timed out after 120 seconds"); logError("Alternative: Use a manual API key instead");