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 ISSUER = "https://auth.openai.com"
|
||||||
const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
|
const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
|
||||||
const OAUTH_PORT = 1455
|
const OAUTH_PORT = 1455
|
||||||
|
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000
|
||||||
|
|
||||||
interface PkceCodes {
|
interface PkceCodes {
|
||||||
verifier: string
|
verifier: string
|
||||||
@@ -461,7 +462,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|||||||
},
|
},
|
||||||
methods: [
|
methods: [
|
||||||
{
|
{
|
||||||
label: "ChatGPT Pro/Plus",
|
label: "ChatGPT Pro/Plus (browser)",
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
authorize: async () => {
|
authorize: async () => {
|
||||||
const { redirectUri } = await startOAuthServer()
|
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",
|
label: "Manually enter API Key",
|
||||||
type: "api",
|
type: "api",
|
||||||
|
|||||||
Reference in New Issue
Block a user