import { PROVIDERS } from "../config/providers.js"; import { OAUTH_ENDPOINTS, REFRESH_LEAD_MS } from "../config/appConstants.js"; import { refreshXaiToken, refreshAccessToken, refreshClaudeOAuthToken, refreshGoogleToken, refreshQwenToken, refreshCodexToken, refreshKiroToken, refreshIflowToken, refreshGitHubToken, refreshCopilotToken, classifyOAuthRefreshError, } from "./tokenRefresh/providers.js"; // Re-export all provider refresh functions (preserves public API for all consumers) export { refreshAccessToken, refreshClaudeOAuthToken, refreshGoogleToken, refreshQwenToken, refreshCodexToken, refreshKiroToken, refreshIflowToken, refreshGitHubToken, refreshCopilotToken, classifyOAuthRefreshError, }; export const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; export function isUnrecoverableRefreshError(result) { return ( result && typeof result === "object" && (result.error === "unrecoverable_refresh_error" || result.error === "refresh_token_reused" || result.error === "invalid_request" || result.error === "invalid_grant") ); } export function getRefreshLeadMs(provider) { return REFRESH_LEAD_MS[provider] || TOKEN_EXPIRY_BUFFER_MS; } export function parseVertexSaJson(apiKey) { if (typeof apiKey !== "string") return null; try { const parsed = JSON.parse(apiKey); if (parsed.type === "service_account" && parsed.client_email && parsed.private_key && parsed.project_id) { return parsed; } return null; } catch { return null; } } // Cache Vertex tokens keyed by service account email { token, expiresAt } const vertexTokenCache = new Map(); export async function refreshVertexToken(saJson, log) { const cacheKey = saJson.client_email; const cached = vertexTokenCache.get(cacheKey); if (cached && cached.expiresAt - Date.now() > 5 * 60 * 1000) { return { accessToken: cached.token, expiresAt: cached.expiresAt }; } try { const { SignJWT, importPKCS8 } = await import("jose"); log?.debug?.("TOKEN_REFRESH", `Vertex minting token for ${saJson.client_email}`); const privateKey = await importPKCS8(saJson.private_key.replace(/\\n/g, "\n"), "RS256"); const now = Math.floor(Date.now() / 1000); const jwt = await new SignJWT({ scope: "https://www.googleapis.com/auth/cloud-platform" }) .setProtectedHeader({ alg: "RS256" }) .setIssuer(saJson.client_email) .setAudience(OAUTH_ENDPOINTS.google.token) .setIssuedAt(now) .setExpirationTime(now + 3600) .sign(privateKey); const res = await fetch(OAUTH_ENDPOINTS.google.token, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: jwt, }), }); if (!res.ok) { const err = await res.text(); log?.error?.("TOKEN_REFRESH", `Vertex token mint failed: ${err}`); return null; } const { access_token, expires_in } = await res.json(); const expiresAt = Date.now() + (expires_in ?? 3600) * 1000; vertexTokenCache.set(cacheKey, { token: access_token, expiresAt }); log?.info?.("TOKEN_REFRESH", `Vertex token minted for ${saJson.client_email}`); return { accessToken: access_token, expiresAt }; } catch (error) { log?.error?.("TOKEN_REFRESH", `Vertex token error: ${error.message}`); return null; } } function vertexRefreshHandler(c, log) { const saJson = parseVertexSaJson(c.apiKey); if (!saJson) return null; return refreshVertexToken(saJson, log); } const REFRESH_HANDLERS = { "gemini-cli": (c, log) => refreshGoogleToken(c.refreshToken, PROVIDERS["gemini-cli"].clientId, PROVIDERS["gemini-cli"].clientSecret, log), antigravity: (c, log) => refreshGoogleToken(c.refreshToken, PROVIDERS.antigravity.clientId, PROVIDERS.antigravity.clientSecret, log), claude: (c, log) => refreshClaudeOAuthToken(c.refreshToken, log), codex: (c, log) => refreshCodexToken(c.refreshToken, log), qwen: (c, log) => refreshQwenToken(c.refreshToken, log), iflow: (c, log) => refreshIflowToken(c.refreshToken, log), github: (c, log) => refreshGitHubToken(c.refreshToken, log), kiro: (c, log) => refreshKiroToken(c.refreshToken, c.providerSpecificData, log), xai: (c, log) => refreshXaiToken(c.refreshToken, log), vertex: vertexRefreshHandler, "vertex-partner": vertexRefreshHandler }; export async function getAccessToken(provider, credentials, log) { if (!credentials || !credentials.refreshToken || typeof credentials.refreshToken !== "string") { log?.warn?.("TOKEN_REFRESH", `No valid refresh token available for provider: ${provider}`); return null; } return _getAccessTokenInternal(provider, credentials, log); } async function _getAccessTokenInternal(provider, credentials, log) { if (provider === "gemini") { return refreshGoogleToken(credentials.refreshToken, PROVIDERS.gemini.clientId, PROVIDERS.gemini.clientSecret, log); } const handler = REFRESH_HANDLERS[provider]; if (!handler) { log?.warn?.("TOKEN_REFRESH", `Unsupported provider for token refresh: ${provider}`); return null; } return handler(credentials, log); } export async function refreshTokenByProvider(provider, credentials, log) { if (!credentials.refreshToken) return null; const handler = REFRESH_HANDLERS[provider]; return handler ? handler(credentials, log) : refreshAccessToken(provider, credentials.refreshToken, credentials, log); } export function formatProviderCredentials(provider, credentials, log) { const config = PROVIDERS[provider]; if (!config) { log?.warn?.("TOKEN_REFRESH", `No configuration found for provider: ${provider}`); return null; } switch (provider) { case "gemini": return { apiKey: credentials.apiKey, accessToken: credentials.accessToken, projectId: credentials.projectId }; case "claude": return { apiKey: credentials.apiKey, accessToken: credentials.accessToken }; case "codex": case "qwen": case "iflow": case "openai": case "openrouter": case "xai": return { apiKey: credentials.apiKey, accessToken: credentials.accessToken }; case "antigravity": case "gemini-cli": return { accessToken: credentials.accessToken, refreshToken: credentials.refreshToken, projectId: credentials.projectId }; default: return { apiKey: credentials.apiKey, accessToken: credentials.accessToken, refreshToken: credentials.refreshToken }; } } export async function getAllAccessTokens(userInfo, log) { const results = {}; if (userInfo.connections && Array.isArray(userInfo.connections)) { for (const connection of userInfo.connections) { if (connection.isActive && connection.provider) { const token = await getAccessToken(connection.provider, { refreshToken: connection.refreshToken }, log); if (token) { results[connection.provider] = token; } } } } return results; } export async function refreshWithRetry(refreshFn, maxRetries = 3, log = null) { for (let attempt = 0; attempt < maxRetries; attempt++) { if (attempt > 0) { const delay = attempt * 1000; log?.debug?.("TOKEN_REFRESH", `Retry ${attempt}/${maxRetries} after ${delay}ms`); await new Promise(r => setTimeout(r, delay)); } try { const result = await refreshFn(); if (result) return result; } catch (error) { log?.warn?.("TOKEN_REFRESH", `Attempt ${attempt + 1}/${maxRetries} failed: ${error.message}`); } } log?.error?.("TOKEN_REFRESH", `All ${maxRetries} retry attempts failed`); return null; }