Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |

---

Expand Down
9 changes: 7 additions & 2 deletions lib/auth/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -51,8 +52,12 @@ export function startLocalOAuthServer({ state }: { state: string }): Promise<OAu
ready: true,
close: () => server.close(),
waitForCode: async () => {
const poll = () => new Promise<void>((r) => setTimeout(r, 100));
for (let i = 0; i < 600; i++) {
const poll = () =>
new Promise<void>((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();
Expand Down
25 changes: 25 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
45 changes: 45 additions & 0 deletions test/constants.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});