mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-20 01:11:18 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
44f67462ed
commit
0904e53c85
2 changed files with 37 additions and 2 deletions
|
|
@ -330,6 +330,7 @@ async function tryDoOAuth(): Promise<string | null> {
|
|||
|
||||
const csrfState = generateCsrfState();
|
||||
let oauthCode: string | null = null;
|
||||
let oauthDenied = false;
|
||||
let server: ReturnType<typeof Bun.serve> | null = null;
|
||||
|
||||
// Try ports in range
|
||||
|
|
@ -347,6 +348,7 @@ async function tryDoOAuth(): Promise<string | null> {
|
|||
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<string | null> {
|
|||
// 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");
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ const SUCCESS_HTML = `<html><head><meta name="viewport" content="width=device-wi
|
|||
|
||||
const ERROR_HTML = `<html><head><meta name="viewport" content="width=device-width,initial-scale=1"><style>${OAUTH_CSS}h1{color:#dc2626}@media(prefers-color-scheme:dark){h1{color:#ef4444}}</style></head><body><div class="card"><div class="icon">✗</div><h1>Authentication Failed</h1><p>Invalid or missing state parameter (CSRF protection). Please try again.</p></div></body></html>`;
|
||||
|
||||
const DENIAL_HTML = `<html><head><meta name="viewport" content="width=device-width,initial-scale=1"><style>${OAUTH_CSS}h1{color:#dc2626}@media(prefers-color-scheme:dark){h1{color:#ef4444}}</style></head><body><div class="card"><div class="icon">✗</div><h1>Authorization Denied</h1><p>You denied access to OpenRouter. You can close this tab and return to your terminal.</p></div></body></html>`;
|
||||
|
||||
async function tryOauthFlow(callbackPort = 5180, agentSlug?: string, cloudSlug?: string): Promise<string | null> {
|
||||
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<typeof Bun.serve> | 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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue