summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorRohan Godha <[email protected]>2026-01-27 19:05:52 -0500
committerGitHub <[email protected]>2026-01-27 19:05:52 -0500
commit898118bafbf8a12c0ef9d6673d36305472991906 (patch)
tree8a4dbb45b96f9d6ac817b9a5a540d73773e91245 /packages
parentb4a9e1b1906af28ad58aa76afa43c194b2581ffd (diff)
downloadopencode-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.ts86
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",
},