feat: support headless authentication for chatgpt/codex (#10890)

Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
This commit is contained in:
Rohan Godha
2026-01-27 19:05:52 -05:00
committed by GitHub
parent b4a9e1b190
commit 898118bafb

View File

@@ -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()
@@ -490,6 +491,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",