From 6ee70c5009079ffd3153aab100ff953a2c3d6604 Mon Sep 17 00:00:00 2001 From: ryu-changhoon Date: Tue, 2 Jun 2026 16:06:11 +0900 Subject: [PATCH] feat: make OAuth login callback timeout configurable The local OAuth callback server waited a fixed 600 polls (60s) for the browser redirect, then fell back to manual paste. Logins involving 2FA, account switching, or password managers can exceed that window. Add OPENCODE_OPENAI_LOGIN_TIMEOUT_MS to override the wait, with the poll count derived from the timeout and a shared poll interval. The default is raised to 5 minutes. Invalid or non-positive values fall back to the default via resolveOAuthLoginTimeoutMs(), which is unit tested. --- README.md | 1 + lib/auth/server.ts | 9 +++++++-- lib/constants.ts | 25 +++++++++++++++++++++++ test/constants.test.ts | 45 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 test/constants.test.ts diff --git a/README.md b/README.md index d872f22..b3d1001 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ All accounts are pooled - when one person's account is rate limited, the plugin | `OPENCODE_OPENAI_DEBUG=1` | Enable debug logging | Off | | `OPENCODE_OPENAI_STRATEGY` | Account selection strategy | `sticky` | | `OPENCODE_OPENAI_PID_OFFSET=1` | Offset account selection by PID | Off | +| `OPENCODE_OPENAI_LOGIN_TIMEOUT_MS` | OAuth login callback timeout (ms) | `300000` (5 min) | --- diff --git a/lib/auth/server.ts b/lib/auth/server.ts index 1a3dcb7..90ccbfa 100644 --- a/lib/auth/server.ts +++ b/lib/auth/server.ts @@ -3,6 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import type { OAuthServerInfo } from "../types.js"; +import { OAUTH_LOGIN_TIMEOUT_MS, OAUTH_POLL_INTERVAL_MS } from "../constants.js"; // Resolve path to oauth-success.html (one level up from auth/ subfolder) const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -51,8 +52,12 @@ export function startLocalOAuthServer({ state }: { state: string }): Promise server.close(), waitForCode: async () => { - const poll = () => new Promise((r) => setTimeout(r, 100)); - for (let i = 0; i < 600; i++) { + const poll = () => + new Promise((r) => setTimeout(r, OAUTH_POLL_INTERVAL_MS)); + const maxAttempts = Math.ceil( + OAUTH_LOGIN_TIMEOUT_MS / OAUTH_POLL_INTERVAL_MS, + ); + for (let i = 0; i < maxAttempts; i++) { const lastCode = (server as http.Server & { _lastCode?: string })._lastCode; if (lastCode) return { code: lastCode }; await poll(); diff --git a/lib/constants.ts b/lib/constants.ts index efb2eff..7a2e509 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -97,3 +97,28 @@ export const AUTH_LABELS = { INSTRUCTIONS_MANUAL: "After logging in, copy the full redirect URL and paste it here.", } as const; + +/** Poll interval (ms) while waiting for the OAuth callback to arrive */ +export const OAUTH_POLL_INTERVAL_MS = 100; + +/** Default timeout (ms) for the OAuth login callback when none is configured */ +export const OAUTH_LOGIN_TIMEOUT_DEFAULT_MS = 5 * 60 * 1000; + +/** + * Resolve the OAuth login callback timeout from a raw env value. + * + * Some users need more than the previous fixed 60s to complete a browser + * login (2FA, account switching, password managers). Invalid or non-positive + * values fall back to {@link OAUTH_LOGIN_TIMEOUT_DEFAULT_MS}. + */ +export function resolveOAuthLoginTimeoutMs( + raw: string | undefined = process.env.OPENCODE_OPENAI_LOGIN_TIMEOUT_MS, +): number { + const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN; + return Number.isFinite(parsed) && parsed > 0 + ? parsed + : OAUTH_LOGIN_TIMEOUT_DEFAULT_MS; +} + +/** Timeout (ms) for waiting on the OAuth callback, overridable via OPENCODE_OPENAI_LOGIN_TIMEOUT_MS */ +export const OAUTH_LOGIN_TIMEOUT_MS = resolveOAuthLoginTimeoutMs(); diff --git a/test/constants.test.ts b/test/constants.test.ts new file mode 100644 index 0000000..023ab26 --- /dev/null +++ b/test/constants.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from 'vitest'; +import { + OAUTH_LOGIN_TIMEOUT_DEFAULT_MS, + OAUTH_POLL_INTERVAL_MS, + resolveOAuthLoginTimeoutMs, +} from '../lib/constants.js'; + +describe('resolveOAuthLoginTimeoutMs', () => { + it('falls back to the default when no value is provided', () => { + expect(resolveOAuthLoginTimeoutMs(undefined)).toBe( + OAUTH_LOGIN_TIMEOUT_DEFAULT_MS, + ); + }); + + it('parses a valid positive integer', () => { + expect(resolveOAuthLoginTimeoutMs('120000')).toBe(120000); + }); + + it('ignores trailing units and parses the leading integer', () => { + expect(resolveOAuthLoginTimeoutMs('90000ms')).toBe(90000); + }); + + it('falls back to the default for non-numeric input', () => { + expect(resolveOAuthLoginTimeoutMs('abc')).toBe( + OAUTH_LOGIN_TIMEOUT_DEFAULT_MS, + ); + }); + + it('falls back to the default for zero or negative values', () => { + expect(resolveOAuthLoginTimeoutMs('0')).toBe( + OAUTH_LOGIN_TIMEOUT_DEFAULT_MS, + ); + expect(resolveOAuthLoginTimeoutMs('-5000')).toBe( + OAUTH_LOGIN_TIMEOUT_DEFAULT_MS, + ); + }); + + it('keeps the default timeout above the previous fixed 60s', () => { + expect(OAUTH_LOGIN_TIMEOUT_DEFAULT_MS).toBeGreaterThan(60000); + }); + + it('uses a positive poll interval', () => { + expect(OAUTH_POLL_INTERVAL_MS).toBeGreaterThan(0); + }); +});