// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import { BrowserWindow, shell, type IpcMain } from 'electron'; import log from 'electron-log'; import http from 'node:http'; import { DEFAULT_CODEX_OAUTH_CALLBACK_HOST, DEFAULT_CODEX_OAUTH_CALLBACK_PATH, DEFAULT_CODEX_OAUTH_CALLBACK_PORT, codexOAuthAuthUrl, codexOAuthClientId, codexOAuthOriginator, codexOAuthRedirectUri, codexOAuthScopes, codexOAuthTokenUrl, } from './codexOAuth'; import { clearCodexCredential, getCodexAccountStatus, saveCodexCredential, } from './credentialStore'; import { codexAccountMetadataFromAccessToken, expiresAtFromTokenPayload, } from './oauthToken'; import { createPkcePair } from './pkce'; import { getCodexResolverEnv } from './resolverServer'; import type { CodexSubscriptionIpcResult } from './types'; const CODEX_OAUTH_STATE_TTL_MS = 10 * 60 * 1000; type PendingCodexOAuth = { email: string; codeVerifier: string; redirectUri: string; expiresAtMs: number; }; export type CodexOAuthCallbackResult = { handled: boolean; error_code?: string; }; const pendingCodexOAuthByState = new Map(); // The local OAuth callback server only needs to run during an active login. // We start it on demand and stop it once no login is pending (or the state TTL // lapses), so we don't hold port 1455 for the whole app lifetime. let oauthCallbackServer: http.Server | null = null; let oauthCallbackServerStarting: Promise | null = null; let oauthCallbackIdleTimer: ReturnType | null = null; const OAUTH_CALLBACK_IDLE_GRACE_MS = 30 * 1000; function normalizeEmail(email: unknown): string { return typeof email === 'string' ? email.trim() : ''; } function pruneExpiredOAuthState(now = Date.now()) { for (const [state, pending] of pendingCodexOAuthByState.entries()) { if (pending.expiresAtMs <= now) { pendingCodexOAuthByState.delete(state); } } } function notifyCodexStatusChanged(payload?: { error_code?: string }) { for (const browserWindow of BrowserWindow.getAllWindows()) { if (!browserWindow.isDestroyed()) { browserWindow.webContents.send( 'subscription-auth:codex-status-changed', payload ); } } } function writeOAuthHtml( res: http.ServerResponse, statusCode: number, title: string, body: string ) { res.writeHead(statusCode, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', // Close the socket after responding so the callback server can stop // promptly instead of lingering on a keep-alive connection. connection: 'close', }); res.end(` ${title}

${title}

${body}

`); } async function handleLocalOAuthCallback( req: http.IncomingMessage, res: http.ServerResponse ) { const callbackUrl = new URL( req.url || '/', `http://${DEFAULT_CODEX_OAUTH_CALLBACK_HOST}:${DEFAULT_CODEX_OAUTH_CALLBACK_PORT}` ); if (callbackUrl.pathname !== DEFAULT_CODEX_OAUTH_CALLBACK_PATH) { writeOAuthHtml( res, 404, 'Authentication failed', 'Callback route not found.' ); return; } const result = await completeCodexOAuthCallback(callbackUrl); if (!result.handled) { writeOAuthHtml( res, 400, 'Authentication failed', 'Unexpected OAuth callback.' ); return; } notifyCodexStatusChanged( result.error_code ? { error_code: result.error_code } : undefined ); if (result.error_code) { writeOAuthHtml( res, 400, 'Authentication failed', 'Return to Eigent and try signing in again.' ); stopCallbackServerWhenIdle(); return; } writeOAuthHtml( res, 200, 'Authentication successful', 'OpenAI Codex is connected. You can close this window.' ); stopCallbackServerWhenIdle(); } function clearCallbackServerIdleTimer() { if (oauthCallbackIdleTimer) { clearTimeout(oauthCallbackIdleTimer); oauthCallbackIdleTimer = null; } } function stopLocalOAuthCallbackServer() { clearCallbackServerIdleTimer(); const server = oauthCallbackServer; oauthCallbackServer = null; oauthCallbackServerStarting = null; if (!server) return; server.close((error) => { if (error) { log.warn('[CODEX AUTH] Failed to stop OAuth callback server:', error); } else { log.info('[CODEX AUTH] Local OAuth callback server stopped'); } }); } // Stop the server once nothing is pending; otherwise keep it for the other // in-flight login(s). function stopCallbackServerWhenIdle() { if (pendingCodexOAuthByState.size === 0) { stopLocalOAuthCallbackServer(); } } // Auto-stop after the state TTL (+ grace) so an abandoned login never leaves // the port held indefinitely. function armCallbackServerIdleTimeout() { clearCallbackServerIdleTimer(); oauthCallbackIdleTimer = setTimeout(() => { oauthCallbackIdleTimer = null; pruneExpiredOAuthState(); if (pendingCodexOAuthByState.size === 0) { stopLocalOAuthCallbackServer(); } else { armCallbackServerIdleTimeout(); } }, CODEX_OAUTH_STATE_TTL_MS + OAUTH_CALLBACK_IDLE_GRACE_MS); // Never keep the app alive just for this timer. oauthCallbackIdleTimer.unref?.(); } async function ensureLocalOAuthCallbackServer(): Promise { if (oauthCallbackServer) { armCallbackServerIdleTimeout(); return; } if (oauthCallbackServerStarting) return oauthCallbackServerStarting; oauthCallbackServerStarting = new Promise((resolve, reject) => { const server = http.createServer((req, res) => { void handleLocalOAuthCallback(req, res).catch((error) => { log.warn('[CODEX AUTH] Local OAuth callback failed:', error); writeOAuthHtml( res, 500, 'Authentication failed', 'Eigent could not complete the OAuth callback.' ); }); }); const handleListenError = (error: NodeJS.ErrnoException) => { oauthCallbackServer = null; oauthCallbackServerStarting = null; reject( new Error( error.code === 'EADDRINUSE' ? 'oauth_callback_port_in_use' : 'oauth_callback_unavailable' ) ); }; server.once('error', handleListenError); server.listen( DEFAULT_CODEX_OAUTH_CALLBACK_PORT, DEFAULT_CODEX_OAUTH_CALLBACK_HOST, () => { server.off('error', handleListenError); // Keep logging post-startup errors instead of crashing the process. server.on('error', (error) => { log.warn('[CODEX AUTH] OAuth callback server error:', error); }); oauthCallbackServer = server; oauthCallbackServerStarting = null; armCallbackServerIdleTimeout(); log.info('[CODEX AUTH] Local OAuth callback server started', { host: DEFAULT_CODEX_OAUTH_CALLBACK_HOST, port: DEFAULT_CODEX_OAUTH_CALLBACK_PORT, }); resolve(); } ); }); return oauthCallbackServerStarting; } export function registerCodexSubscriptionAuthIpcHandlers(ipcMain: IpcMain) { ipcMain.handle('subscription-auth:codex-status', (_event, email: unknown) => { const normalizedEmail = normalizeEmail(email); if (!normalizedEmail) { return { connected: false, status: 'not_connected', last_error_code: 'email_required', }; } return getCodexAccountStatus(normalizedEmail); }); ipcMain.handle( 'subscription-auth:codex-disconnect', (_event, email: unknown): CodexSubscriptionIpcResult => { const normalizedEmail = normalizeEmail(email); if (!normalizedEmail) { return { success: false, error_code: 'email_required' }; } clearCodexCredential(normalizedEmail); return { success: true }; } ); ipcMain.handle( 'subscription-auth:codex-login', async (_event, email: unknown): Promise => { const normalizedEmail = normalizeEmail(email); if (!normalizedEmail) { return { success: false, error_code: 'email_required' }; } try { await ensureLocalOAuthCallbackServer(); } catch (error: any) { log.warn('[CODEX AUTH] Failed to start local OAuth callback:', error); const errorCode = error?.message === 'oauth_callback_port_in_use' ? 'oauth_callback_port_in_use' : 'oauth_callback_unavailable'; return { success: false, error_code: errorCode }; } pruneExpiredOAuthState(); const pkce = createPkcePair(); const redirectUri = codexOAuthRedirectUri(); pendingCodexOAuthByState.set(pkce.state, { email: normalizedEmail, codeVerifier: pkce.codeVerifier, redirectUri, expiresAtMs: Date.now() + CODEX_OAUTH_STATE_TTL_MS, }); const authUrl = codexOAuthAuthUrl(); const clientId = codexOAuthClientId(); const url = new URL(authUrl); url.searchParams.set('response_type', 'code'); url.searchParams.set('client_id', clientId); url.searchParams.set('redirect_uri', redirectUri); url.searchParams.set('state', pkce.state); url.searchParams.set('code_challenge', pkce.codeChallenge); url.searchParams.set('code_challenge_method', pkce.codeChallengeMethod); url.searchParams.set('scope', codexOAuthScopes()); url.searchParams.set('id_token_add_organizations', 'true'); url.searchParams.set('codex_cli_simplified_flow', 'true'); url.searchParams.set('originator', codexOAuthOriginator()); try { await shell.openExternal(url.toString()); } catch (error) { pendingCodexOAuthByState.delete(pkce.state); stopCallbackServerWhenIdle(); log.warn('[CODEX AUTH] Failed to open OAuth URL:', error); return { success: false, error_code: 'oauth_open_browser_failed' }; } return { success: true }; } ); } async function exchangeCodexOAuthCode( code: string, pending: PendingCodexOAuth ): Promise> { const tokenUrl = codexOAuthTokenUrl(); const clientId = codexOAuthClientId(); const body = new URLSearchParams(); body.set('grant_type', 'authorization_code'); body.set('client_id', clientId); body.set('code', code); body.set('redirect_uri', pending.redirectUri); body.set('code_verifier', pending.codeVerifier); const response = await fetch(tokenUrl, { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/x-www-form-urlencoded', }, body, }); if (!response.ok) { throw new Error(`token_exchange_failed_${response.status}`); } const payload = (await response.json()) as Record; const accessToken = payload.access_token; if (typeof accessToken !== 'string' || !accessToken.trim()) { throw new Error('token_exchange_missing_access_token'); } return payload; } export async function completeCodexOAuthCallback( callbackUrl: URL ): Promise { pruneExpiredOAuthState(); const state = callbackUrl.searchParams.get('state'); const provider = callbackUrl.searchParams.get('provider'); const pending = state ? pendingCodexOAuthByState.get(state) : undefined; if (!pending && provider !== 'codex') { return { handled: false }; } if (!state || !pending) { log.warn('[CODEX AUTH] Ignoring OAuth callback without matching state'); return { handled: true, error_code: 'oauth_state_mismatch' }; } if (pending.expiresAtMs <= Date.now()) { pendingCodexOAuthByState.delete(state); log.warn('[CODEX AUTH] Ignoring expired OAuth callback state'); return { handled: true, error_code: 'oauth_state_expired' }; } pendingCodexOAuthByState.delete(state); const error = callbackUrl.searchParams.get('error'); if (error) { log.warn('[CODEX AUTH] OAuth callback returned an error:', error); return { handled: true, error_code: error }; } const code = callbackUrl.searchParams.get('code'); if (!code) { log.warn('[CODEX AUTH] OAuth callback did not include a code'); return { handled: true, error_code: 'oauth_code_missing' }; } try { const tokenPayload = await exchangeCodexOAuthCode(code, pending); const accountMetadata = codexAccountMetadataFromAccessToken( tokenPayload.access_token as string ); saveCodexCredential(pending.email, { access_token: tokenPayload.access_token as string, refresh_token: typeof tokenPayload.refresh_token === 'string' ? tokenPayload.refresh_token : null, token_type: typeof tokenPayload.token_type === 'string' ? tokenPayload.token_type : 'Bearer', expires_at: expiresAtFromTokenPayload(tokenPayload), account_label: typeof tokenPayload.account_label === 'string' ? tokenPayload.account_label : accountMetadata.account_label || pending.email, external_account_id: typeof tokenPayload.external_account_id === 'string' ? tokenPayload.external_account_id : accountMetadata.external_account_id, scopes: typeof tokenPayload.scope === 'string' ? tokenPayload.scope.split(/\s+/).filter(Boolean) : codexOAuthScopes().split(/\s+/).filter(Boolean), status: typeof tokenPayload.refresh_token === 'string' ? 'connected' : 'connected_non_refreshable', updated_at: new Date().toISOString(), }); } catch (error: any) { const errorCode = typeof error?.message === 'string' ? error.message : 'token_exchange_failed'; log.warn('[CODEX AUTH] Failed to complete OAuth callback:', errorCode); return { handled: true, error_code: errorCode }; } return { handled: true }; } export { getCodexResolverEnv };