mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-01 22:48:16 +00:00
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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user