Leon4gr45/builder
0
1/**2 * Codex CLI OAuth — Token management for ChatGPT subscription access3 *4 * The long-lived refresh_token is stored in an HttpOnly cookie (osw_codex_rt)5 * and never exposed to JavaScript. Only the short-lived access_token (~1 hour)6 * is kept in localStorage.7 */8 9import { CodexAuthData } from '@/lib/llm/providers/types';10import { configManager } from '@/lib/config/storage';11 12/**13 * Send the full auth payload to the server. The server stores the14 * refresh_token in an HttpOnly cookie and returns the non-sensitive fields.15 */16export async function connectCodex(auth: CodexAuthData): Promise<CodexAuthData> {17 const res = await fetch('/api/auth/codex/connect', {18 method: 'POST',19 headers: { 'Content-Type': 'application/json' },20 credentials: 'same-origin',21 body: JSON.stringify(auth),22 });23 24 if (!res.ok) {25 const data = await res.json().catch(() => ({ error: 'Connect failed' }));26 throw new Error(data.error || 'Failed to connect Codex session');27 }28 29 // Server returns { access_token, expires_at, user_email } — no refresh_token30 return res.json();31}32 33/**34 * Delete the HttpOnly refresh token cookie and clear localStorage.35 */36export async function disconnectCodex(): Promise<void> {37 const res = await fetch('/api/auth/codex/disconnect', {38 method: 'POST',39 credentials: 'same-origin',40 });41 if (!res.ok) {42 throw new Error('Failed to clear server session');43 }44 configManager.clearCodexAuth();45}46 47/**48 * Check whether the server has a refresh token cookie set.49 */50export async function checkCodexStatus(): Promise<boolean> {51 const res = await fetch('/api/auth/codex/status', {52 credentials: 'same-origin',53 });54 if (!res.ok) return false;55 const data = await res.json();56 return !!data.hasRefreshToken;57}58 59/**60 * Refresh the access token using the HttpOnly cookie. The client sends no61 * token — the server reads it from the cookie automatically.62 */63export async function refreshAccessToken(): Promise<CodexAuthData> {64 const res = await fetch('/api/auth/codex/token', {65 method: 'POST',66 credentials: 'same-origin',67 });68 69 if (!res.ok) {70 const data = await res.json().catch(() => ({ error: 'Token refresh failed' }));71 throw new Error(data.error || `Token refresh failed: ${res.status}`);72 }73 74 // Server returns { access_token, expires_at }75 return res.json();76}77 78/**79 * Ensure the stored Codex token is valid. Refreshes if expired.80 * Returns the valid access token, or throws if refresh fails.81 */82export async function ensureValidCodexToken(): Promise<string> {83 const auth = configManager.getCodexAuth();84 if (!auth) {85 throw new Error('ChatGPT session not found. Please log in via Settings.');86 }87 88 if (!configManager.isCodexTokenExpired()) {89 return auth.access_token;90 }91 92 // Token expired or near-expiry — refresh via HttpOnly cookie93 try {94 const refreshed = await refreshAccessToken();95 configManager.setCodexAuth(refreshed);96 return refreshed.access_token;97 } catch {98 configManager.clearCodexAuth();99 throw new Error('ChatGPT session expired. Please re-authenticate in Settings.');100 }101}102 103/**104 * Parse a pasted auth JSON (from running `codex login` locally).105 * Handles the actual ~/.codex/auth.json format where tokens are nested:106 * { "tokens": { "access_token": "...", "refresh_token": "...", ... }, ... }107 * Also accepts a flat format with top-level access_token/refresh_token.108 */109export function parseCodexAuthJson(json: string): CodexAuthData {110 const parsed = JSON.parse(json);111 112 // Codex CLI nests tokens under a "tokens" key113 const tokens = parsed.tokens || parsed;114 const accessToken = tokens.access_token || tokens.token;115 const refreshToken = tokens.refresh_token;116 117 if (!accessToken) {118 throw new Error('Missing access_token in pasted JSON');119 }120 if (!refreshToken) {121 throw new Error('Missing refresh_token in pasted JSON');122 }123 124 return {125 access_token: accessToken,126 refresh_token: refreshToken,127 expires_at: tokens.expires_at || parsed.expires_at || Math.floor(Date.now() / 1000) + 3600,128 user_email: tokens.user_email || parsed.user_email || tokens.email || parsed.email,129 };130}131 