CoolFace
Apppublic

Leon4gr45/builder

sourceHugging Facemitupdated 2d agoView on Hugging Face
0likes
codex-auth.ts131 linesDownload Raw Back to auth
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