import { randomUUID } from 'crypto' import { join, dirname } from 'path' import { homedir } from 'os' import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs' import { getActiveProfileName, getProfileDir } from '../../services/hermes/hermes-profile' import { logger } from '../../services/logger' // --- OAuth Constants --- const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann' const CODEX_DEVICE_AUTH_URL = 'https://auth.openai.com/api/accounts/deviceauth/usercode' const CODEX_DEVICE_TOKEN_URL = 'https://auth.openai.com/api/accounts/deviceauth/token' const CODEX_OAUTH_TOKEN_URL = 'https://auth.openai.com/oauth/token' const CODEX_DEFAULT_BASE_URL = 'https://chatgpt.com/backend-api/codex' const CODEX_REDIRECT_URI = 'https://auth.openai.com/deviceauth/callback' const CODEX_VERIFICATION_URL = 'https://auth.openai.com/codex/device' const CODEX_HOME = join(homedir(), '.codex') const POLL_MAX_DURATION = 15 * 60 * 1000 const POLL_DEFAULT_INTERVAL = 5000 // --- Session Store --- interface CodexSession { id: string; userCode: string; deviceAuthId: string profile: string status: 'pending' | 'approved' | 'expired' | 'error' error?: string; accessToken?: string; refreshToken?: string; createdAt: number } const sessions = new Map() function cleanupExpiredSessions() { const now = Date.now() sessions.forEach((session, id) => { if (now - session.createdAt > POLL_MAX_DURATION + 60000) { sessions.delete(id) } }) } // --- Auth file helpers --- interface AuthJson { version?: number; active_provider?: string; providers?: Record; credential_pool?: Record; updated_at?: string } interface CodexCredentialRef { accessToken: string refreshToken?: string lastRefresh?: string provider?: any poolEntry?: any } function loadAuthJson(authPath: string): AuthJson { try { return JSON.parse(readFileSync(authPath, 'utf-8')) as AuthJson } catch { return { version: 1 } } } function saveAuthJson(authPath: string, data: AuthJson): void { data.updated_at = new Date().toISOString() const dir = dirname(authPath) if (!existsSync(dir)) mkdirSync(dir, { recursive: true }) writeFileSync(authPath, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 }) } function saveCodexCliTokens(accessToken: string, refreshToken: string): void { const codexHome = process.env.CODEX_HOME || CODEX_HOME const codexAuthPath = join(codexHome, 'auth.json') const dir = dirname(codexAuthPath) if (!existsSync(dir)) mkdirSync(dir, { recursive: true }) writeFileSync(codexAuthPath, JSON.stringify({ tokens: { access_token: accessToken, refresh_token: refreshToken }, last_refresh: new Date().toISOString() }, null, 2) + '\n', { mode: 0o600 }) } function requestedProfile(ctx: any): string { const headerProfile = typeof ctx.get === 'function' ? ctx.get('x-hermes-profile') : '' const queryProfile = typeof ctx.query?.profile === 'string' ? ctx.query.profile : '' const bodyProfile = typeof ctx.request?.body?.profile === 'string' ? ctx.request.body.profile : '' return ctx.state?.profile?.name || headerProfile.trim() || queryProfile.trim() || bodyProfile.trim() || getActiveProfileName() || 'default' } function authPathForProfile(profile: string): string { return join(getProfileDir(profile), 'auth.json') } function decodeJwtExp(token: string): number | null { try { const parts = token.split('.') if (parts.length !== 3) return null const payload = Buffer.from(parts[1], 'base64url').toString('utf-8') const claims = JSON.parse(payload) return typeof claims.exp === 'number' ? claims.exp : null } catch { return null } } function getCodexCredential(auth: AuthJson): CodexCredentialRef | null { const provider = auth.providers?.['openai-codex'] const providerTokens = provider?.tokens const providerAccessToken = providerTokens?.access_token || provider?.access_token const pool = auth.credential_pool?.['openai-codex'] const poolEntry = Array.isArray(pool) ? pool.find(entry => entry?.access_token) : undefined if (providerAccessToken) { return { accessToken: providerAccessToken, refreshToken: providerTokens?.refresh_token || provider?.refresh_token, lastRefresh: provider.last_refresh, provider, poolEntry, } } if (poolEntry?.access_token) { return { accessToken: poolEntry.access_token, refreshToken: poolEntry.refresh_token, lastRefresh: poolEntry.last_refresh, poolEntry, } } return null } // --- Background login worker --- export function saveCodexOAuthTokensForProfile(profile: string, accessToken: string, refreshToken: string): void { const authPath = authPathForProfile(profile) const auth = loadAuthJson(authPath) if (!auth.providers) auth.providers = {} auth.providers['openai-codex'] = { tokens: { access_token: accessToken, refresh_token: refreshToken }, last_refresh: new Date().toISOString(), auth_mode: 'chatgpt' } if (!auth.credential_pool) auth.credential_pool = {} auth.credential_pool['openai-codex'] = [{ id: `openai-codex-${Date.now()}`, label: 'OpenAI Codex', base_url: CODEX_DEFAULT_BASE_URL, access_token: accessToken, last_status: null }] saveAuthJson(authPath, auth) saveCodexCliTokens(accessToken, refreshToken) } async function codexLoginWorker(session: CodexSession): Promise { const startTime = Date.now() const interval = POLL_DEFAULT_INTERVAL while (Date.now() - startTime < POLL_MAX_DURATION) { await new Promise(resolve => setTimeout(resolve, interval)) if (session.status !== 'pending') return try { const pollRes = await fetch(CODEX_DEVICE_TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ device_auth_id: session.deviceAuthId, user_code: session.userCode }), signal: AbortSignal.timeout(10000), }) if (pollRes.status === 200) { const pollData = await pollRes.json() as { authorization_code: string; code_verifier: string } const tokenRes = await fetch(CODEX_OAUTH_TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code: pollData.authorization_code, redirect_uri: CODEX_REDIRECT_URI, client_id: CODEX_CLIENT_ID, code_verifier: pollData.code_verifier }).toString(), signal: AbortSignal.timeout(15000), }) if (!tokenRes.ok) { const errText = await tokenRes.text(); logger.error('Token exchange failed: %d %s', tokenRes.status, errText); session.status = 'error'; session.error = `Token exchange failed: ${tokenRes.status}`; return } const tokenData = await tokenRes.json() as { access_token: string; refresh_token?: string } const refreshToken = tokenData.refresh_token || '' session.accessToken = tokenData.access_token; session.refreshToken = refreshToken; session.status = 'approved' saveCodexOAuthTokensForProfile(session.profile, tokenData.access_token, refreshToken) logger.info('Login successful') return } if (pollRes.status === 403 || pollRes.status === 404) { continue } logger.error('Poll failed: %d', pollRes.status); session.status = 'error'; session.error = `Poll failed: ${pollRes.status}`; return } catch (err: any) { if (err.name === 'TimeoutError' || err.name === 'AbortError') { continue } logger.error(err, 'Poll error'); session.status = 'error'; session.error = err.message; return } } session.status = 'expired' } // --- Controller functions --- export async function start(ctx: any) { try { cleanupExpiredSessions() const res = await fetch(CODEX_DEVICE_AUTH_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'node-fetch' }, body: JSON.stringify({ client_id: CODEX_CLIENT_ID }), signal: AbortSignal.timeout(10000), }) if (!res.ok) { let errorBody: any = null; try { errorBody = await res.json() } catch { } logger.error('Device code request failed: %d %s', res.status, errorBody) let errorMessage = `Device code request failed: ${res.status}` if (errorBody?.error?.code === 'unsupported_country_region_territory') { errorMessage = 'OpenAI does not support your region. You may need to use a proxy or VPN to access Codex.' } ctx.status = 502; ctx.body = { error: errorMessage, code: errorBody?.error?.code }; return } const data = await res.json() as { user_code: string; device_auth_id: string; interval?: string } const sessionId = randomUUID() const session: CodexSession = { id: sessionId, userCode: data.user_code, deviceAuthId: data.device_auth_id, profile: requestedProfile(ctx), status: 'pending', createdAt: Date.now() } sessions.set(sessionId, session) codexLoginWorker(session).catch(err => { logger.error(err, 'Worker error'); session.status = 'error'; session.error = err.message }) ctx.body = { session_id: sessionId, user_code: data.user_code, verification_url: CODEX_VERIFICATION_URL, expires_in: 900 } } catch (err: any) { ctx.status = 500; ctx.body = { error: err.message } } } export async function poll(ctx: any) { const session = sessions.get(ctx.params.sessionId) if (!session) { ctx.status = 404; ctx.body = { error: 'Session not found' }; return } ctx.body = { status: session.status, error: session.error || null } } export async function status(ctx: any) { try { const authPath = authPathForProfile(requestedProfile(ctx)) const auth = loadAuthJson(authPath) const credential = getCodexCredential(auth) if (!credential) { ctx.body = { authenticated: false }; return } const exp = decodeJwtExp(credential.accessToken) if (exp && exp <= Date.now() / 1000 + 120) { if (credential.refreshToken) { try { const refreshRes = await fetch(CODEX_OAUTH_TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: credential.refreshToken, client_id: CODEX_CLIENT_ID }).toString(), signal: AbortSignal.timeout(15000), }) if (refreshRes.ok) { const newTokens = await refreshRes.json() as { access_token: string; refresh_token?: string } const lastRefresh = new Date().toISOString() if (credential.provider?.tokens) { credential.provider.tokens.access_token = newTokens.access_token if (newTokens.refresh_token) { credential.provider.tokens.refresh_token = newTokens.refresh_token } credential.provider.last_refresh = lastRefresh } else if (credential.provider) { credential.provider.access_token = newTokens.access_token if (newTokens.refresh_token) { credential.provider.refresh_token = newTokens.refresh_token } credential.provider.last_refresh = lastRefresh } if (credential.poolEntry) { credential.poolEntry.access_token = newTokens.access_token if (newTokens.refresh_token) { credential.poolEntry.refresh_token = newTokens.refresh_token } credential.poolEntry.last_refresh = lastRefresh } saveAuthJson(authPath, auth) saveCodexCliTokens(newTokens.access_token, newTokens.refresh_token || credential.refreshToken) ctx.body = { authenticated: true, last_refresh: lastRefresh }; return } } catch { } } ctx.body = { authenticated: false }; return } ctx.body = { authenticated: true, last_refresh: credential.lastRefresh } } catch { ctx.body = { authenticated: false } } }