diff options
| author | Rohan Godha <[email protected]> | 2026-01-27 19:05:52 -0500 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-01-27 19:05:52 -0500 |
| commit | 898118bafbf8a12c0ef9d6673d36305472991906 (patch) | |
| tree | 8a4dbb45b96f9d6ac817b9a5a540d73773e91245 /packages | |
| parent | b4a9e1b1906af28ad58aa76afa43c194b2581ffd (diff) | |
| download | opencode-898118bafbf8a12c0ef9d6673d36305472991906.tar.gz opencode-898118bafbf8a12c0ef9d6673d36305472991906.zip | |
feat: support headless authentication for chatgpt/codex (#10890)
Co-authored-by: Aiden Cline <[email protected]>
Co-authored-by: Aiden Cline <[email protected]>
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/opencode/src/plugin/codex.ts | 86 |
1 files changed, 85 insertions, 1 deletions
diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index 198e8ce25..b6f1a96a9 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -10,6 +10,7 @@ const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann" const ISSUER = "https://auth.openai.com" const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses" const OAUTH_PORT = 1455 +const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000 interface PkceCodes { verifier: string @@ -461,7 +462,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> { }, methods: [ { - label: "ChatGPT Pro/Plus", + label: "ChatGPT Pro/Plus (browser)", type: "oauth", authorize: async () => { const { redirectUri } = await startOAuthServer() @@ -491,6 +492,89 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> { }, }, { + label: "ChatGPT Pro/Plus (headless)", + type: "oauth", + authorize: async () => { + const deviceResponse = await fetch(`${ISSUER}/api/accounts/deviceauth/usercode`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": `opencode/${Installation.VERSION}`, + }, + body: JSON.stringify({ client_id: CLIENT_ID }), + }) + + if (!deviceResponse.ok) throw new Error("Failed to initiate device authorization") + + const deviceData = (await deviceResponse.json()) as { + device_auth_id: string + user_code: string + interval: string + } + const interval = Math.max(parseInt(deviceData.interval) || 5, 1) * 1000 + + return { + url: `${ISSUER}/codex/device`, + instructions: `Enter code: ${deviceData.user_code}`, + method: "auto" as const, + async callback() { + while (true) { + const response = await fetch(`${ISSUER}/api/accounts/deviceauth/token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": `opencode/${Installation.VERSION}`, + }, + body: JSON.stringify({ + device_auth_id: deviceData.device_auth_id, + user_code: deviceData.user_code, + }), + }) + + if (response.ok) { + const data = (await response.json()) as { + authorization_code: string + code_verifier: string + } + + const tokenResponse = await fetch(`${ISSUER}/oauth/token`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code: data.authorization_code, + redirect_uri: `${ISSUER}/deviceauth/callback`, + client_id: CLIENT_ID, + code_verifier: data.code_verifier, + }).toString(), + }) + + if (!tokenResponse.ok) { + throw new Error(`Token exchange failed: ${tokenResponse.status}`) + } + + const tokens: TokenResponse = await tokenResponse.json() + + return { + type: "success" as const, + refresh: tokens.refresh_token, + access: tokens.access_token, + expires: Date.now() + (tokens.expires_in ?? 3600) * 1000, + accountId: extractAccountId(tokens), + } + } + + if (response.status !== 403 && response.status !== 404) { + return { type: "failed" as const } + } + + await Bun.sleep(interval + OAUTH_POLLING_SAFETY_MARGIN_MS) + } + }, + } + }, + }, + { label: "Manually enter API Key", type: "api", }, |
