import { PROVIDERS } from "../config/providers.js"; import { OAUTH_ENDPOINTS, GITHUB_COPILOT } from "../config/appConstants.js"; // Token expiry buffer (refresh if expires within 5 minutes) export const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; /** * Refresh OAuth access token using refresh token */ export async function refreshAccessToken(provider, refreshToken, credentials, log) { const config = PROVIDERS[provider]; if (!config || !config.refreshUrl) { log?.warn?.("TOKEN_REFRESH", `No refresh URL configured for provider: ${provider}`); return null; } if (!refreshToken) { log?.warn?.("TOKEN_REFRESH", `No refresh token available for provider: ${provider}`); return null; } try { const response = await fetch(config.refreshUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: config.clientId, client_secret: config.clientSecret, }), }); if (!response.ok) { const errorText = await response.text(); log?.error?.("TOKEN_REFRESH", `Failed to refresh token for ${provider}`, { status: response.status, error: errorText, }); return null; } const tokens = await response.json(); log?.info?.("TOKEN_REFRESH", `Successfully refreshed token for ${provider}`, { hasNewAccessToken: !!tokens.access_token, hasNewRefreshToken: !!tokens.refresh_token, expiresIn: tokens.expires_in, }); return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in, }; } catch (error) { log?.error?.("TOKEN_REFRESH", `Error refreshing token for ${provider}`, { error: error.message, }); return null; } } /** * Specialized refresh for Claude OAuth tokens */ export async function refreshClaudeOAuthToken(refreshToken, log) { try { const response = await fetch(OAUTH_ENDPOINTS.anthropic.token, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: PROVIDERS.claude.clientId, }), }); if (!response.ok) { const errorText = await response.text(); log?.error?.("TOKEN_REFRESH", "Failed to refresh Claude OAuth token", { status: response.status, error: errorText }); return null; } const tokens = await response.json(); log?.info?.("TOKEN_REFRESH", "Successfully refreshed Claude OAuth token", { hasNewAccessToken: !!tokens.access_token, expiresIn: tokens.expires_in }); return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; } catch (error) { log?.error?.("TOKEN_REFRESH", `Network error refreshing Claude token: ${error.message}`); return null; } } /** * Specialized refresh for Google providers (Gemini, Antigravity) */ export async function refreshGoogleToken(refreshToken, clientId, clientSecret, log) { try { const response = await fetch(OAUTH_ENDPOINTS.google.token, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: clientId, client_secret: clientSecret, }), }); if (!response.ok) { const errorText = await response.text(); log?.error?.("TOKEN_REFRESH", "Failed to refresh Google token", { status: response.status, error: errorText }); return null; } const tokens = await response.json(); log?.info?.("TOKEN_REFRESH", "Successfully refreshed Google token", { hasNewAccessToken: !!tokens.access_token, expiresIn: tokens.expires_in }); return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in }; } catch (error) { log?.error?.("TOKEN_REFRESH", `Network error refreshing Google token: ${error.message}`); return null; } } /** * Specialized refresh for Qwen OAuth tokens */ export async function refreshQwenToken(refreshToken, log) { const endpoint = OAUTH_ENDPOINTS.qwen.token; try { const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: PROVIDERS.qwen.clientId, }), }); if (response.status === 200) { const tokens = await response.json(); log?.info?.("TOKEN_REFRESH", "Successfully refreshed Qwen token", { hasNewAccessToken: !!tokens.access_token, hasNewRefreshToken: !!tokens.refresh_token, expiresIn: tokens.expires_in, }); return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in, providerSpecificData: tokens.resource_url ? { resourceUrl: tokens.resource_url } : undefined, }; } else { const errorText = await response.text().catch(() => ""); log?.warn?.("TOKEN_REFRESH", `Error with Qwen endpoint`, { status: response.status, error: errorText, }); } } catch (error) { log?.warn?.("TOKEN_REFRESH", `Network error trying Qwen endpoint`, { error: error.message, }); } log?.error?.("TOKEN_REFRESH", "Failed to refresh Qwen token"); return null; } /** * Specialized refresh for Codex (OpenAI) OAuth tokens */ export async function refreshCodexToken(refreshToken, log) { try { const response = await fetch(OAUTH_ENDPOINTS.openai.token, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: PROVIDERS.codex.clientId, scope: "openid profile email offline_access", }), }); if (!response.ok) { const errorText = await response.text(); log?.error?.("TOKEN_REFRESH", "Failed to refresh Codex token", { status: response.status, error: errorText, }); return null; } const tokens = await response.json(); log?.info?.("TOKEN_REFRESH", "Successfully refreshed Codex token", { hasNewAccessToken: !!tokens.access_token, hasNewRefreshToken: !!tokens.refresh_token, expiresIn: tokens.expires_in, }); return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in, }; } catch (error) { log?.error?.("TOKEN_REFRESH", `Network error refreshing Codex token: ${error.message}`); return null; } } /** * Specialized refresh for Kiro (AWS CodeWhisperer) tokens * Supports both AWS SSO OIDC (Builder ID/IDC) and Social Auth (Google/GitHub) */ export async function refreshKiroToken(refreshToken, providerSpecificData, log) { const authMethod = providerSpecificData?.authMethod; const clientId = providerSpecificData?.clientId; const clientSecret = providerSpecificData?.clientSecret; const region = providerSpecificData?.region; // AWS SSO OIDC (Builder ID or IDC) // If clientId and clientSecret exist, assume AWS SSO OIDC (default to builder-id if authMethod not specified) if (clientId && clientSecret) { const isIDC = authMethod === "idc"; const endpoint = isIDC && region ? `https://oidc.${region}.amazonaws.com/token` : "https://oidc.us-east-1.amazonaws.com/token"; const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ clientId: clientId, clientSecret: clientSecret, refreshToken: refreshToken, grantType: "refresh_token", }), }); if (!response.ok) { const errorText = await response.text(); log?.error?.("TOKEN_REFRESH", "Failed to refresh Kiro AWS token", { status: response.status, error: errorText, }); return null; } const tokens = await response.json(); log?.info?.("TOKEN_REFRESH", "Successfully refreshed Kiro AWS token", { hasNewAccessToken: !!tokens.accessToken, expiresIn: tokens.expiresIn, }); return { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken || refreshToken, expiresIn: tokens.expiresIn, }; } // Social Auth (Google/GitHub) - use Kiro's refresh endpoint const response = await fetch(PROVIDERS.kiro.tokenUrl, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "kiro-cli/1.0.0", }, body: JSON.stringify({ refreshToken: refreshToken, }), }); if (!response.ok) { const errorText = await response.text(); log?.error?.("TOKEN_REFRESH", "Failed to refresh Kiro social token", { status: response.status, error: errorText, }); return null; } const tokens = await response.json(); log?.info?.("TOKEN_REFRESH", "Successfully refreshed Kiro social token", { hasNewAccessToken: !!tokens.accessToken, expiresIn: tokens.expiresIn, }); return { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken || refreshToken, expiresIn: tokens.expiresIn, }; } /** * Specialized refresh for iFlow OAuth tokens */ export async function refreshIflowToken(refreshToken, log) { const basicAuth = btoa(`${PROVIDERS.iflow.clientId}:${PROVIDERS.iflow.clientSecret}`); const response = await fetch(OAUTH_ENDPOINTS.iflow.token, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", Authorization: `Basic ${basicAuth}`, }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: PROVIDERS.iflow.clientId, client_secret: PROVIDERS.iflow.clientSecret, }), }); if (!response.ok) { const errorText = await response.text(); log?.error?.("TOKEN_REFRESH", "Failed to refresh iFlow token", { status: response.status, error: errorText, }); return null; } const tokens = await response.json(); log?.info?.("TOKEN_REFRESH", "Successfully refreshed iFlow token", { hasNewAccessToken: !!tokens.access_token, hasNewRefreshToken: !!tokens.refresh_token, expiresIn: tokens.expires_in, }); return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in, }; } /** * Specialized refresh for GitHub Copilot OAuth tokens */ export async function refreshGitHubToken(refreshToken, log) { const params = { grant_type: "refresh_token", refresh_token: refreshToken, client_id: PROVIDERS.github.clientId, }; if (PROVIDERS.github.clientSecret) { params.client_secret = PROVIDERS.github.clientSecret; } const response = await fetch(OAUTH_ENDPOINTS.github.token, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, body: new URLSearchParams(params), }); if (!response.ok) { const errorText = await response.text(); log?.error?.("TOKEN_REFRESH", "Failed to refresh GitHub token", { status: response.status, error: errorText, }); return null; } const tokens = await response.json(); log?.info?.("TOKEN_REFRESH", "Successfully refreshed GitHub token", { hasNewAccessToken: !!tokens.access_token, hasNewRefreshToken: !!tokens.refresh_token, expiresIn: tokens.expires_in, }); return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in, }; } /** * Refresh GitHub Copilot token using GitHub access token */ export async function refreshCopilotToken(githubAccessToken, log) { try { const response = await fetch("https://api.github.com/copilot_internal/v2/token", { headers: { "Authorization": `token ${githubAccessToken}`, "User-Agent": GITHUB_COPILOT.USER_AGENT, "Editor-Version": `vscode/${GITHUB_COPILOT.VSCODE_VERSION}`, "Editor-Plugin-Version": `copilot-chat/${GITHUB_COPILOT.COPILOT_CHAT_VERSION}`, "Accept": "application/json", "x-github-api-version": GITHUB_COPILOT.API_VERSION } }); if (!response.ok) { const errorText = await response.text(); log?.error?.("TOKEN_REFRESH", "Failed to refresh Copilot token", { status: response.status, error: errorText }); return null; } const data = await response.json(); log?.info?.("TOKEN_REFRESH", "Successfully refreshed Copilot token", { hasToken: !!data.token, expiresAt: data.expires_at }); return { token: data.token, expiresAt: data.expires_at }; } catch (error) { log?.error?.("TOKEN_REFRESH", "Error refreshing Copilot token", { error: error.message }); return null; } } /** * Get access token for a specific provider */ export async function getAccessToken(provider, credentials, log) { if (!credentials || !credentials.refreshToken) { log?.warn?.("TOKEN_REFRESH", `No refresh token available for provider: ${provider}`); return null; } switch (provider) { case "gemini": case "gemini-cli": case "antigravity": return await refreshGoogleToken( credentials.refreshToken, PROVIDERS[provider].clientId, PROVIDERS[provider].clientSecret, log ); case "claude": return await refreshClaudeOAuthToken(credentials.refreshToken, log); case "codex": return await refreshCodexToken(credentials.refreshToken, log); case "qwen": return await refreshQwenToken(credentials.refreshToken, log); case "iflow": return await refreshIflowToken(credentials.refreshToken, log); case "github": return await refreshGitHubToken(credentials.refreshToken, log); case "kiro": return await refreshKiroToken( credentials.refreshToken, credentials.providerSpecificData, log ); case "vertex": case "vertex-partner": { const saJson = parseVertexSaJson(credentials.apiKey); if (!saJson) return null; return await refreshVertexToken(saJson, log); } default: log?.warn?.("TOKEN_REFRESH", `Unsupported provider for token refresh: ${provider}`); return null; } } /** * Refresh token by provider type (helper for handlers) */ export async function refreshTokenByProvider(provider, credentials, log) { if (!credentials.refreshToken) return null; switch (provider) { case "gemini-cli": case "antigravity": return refreshGoogleToken( credentials.refreshToken, PROVIDERS[provider].clientId, PROVIDERS[provider].clientSecret, log ); case "claude": return refreshClaudeOAuthToken(credentials.refreshToken, log); case "codex": return refreshCodexToken(credentials.refreshToken, log); case "qwen": return refreshQwenToken(credentials.refreshToken, log); case "iflow": return refreshIflowToken(credentials.refreshToken, log); case "github": return refreshGitHubToken(credentials.refreshToken, log); case "kiro": return refreshKiroToken( credentials.refreshToken, credentials.providerSpecificData, log ); case "vertex": case "vertex-partner": { const saJson = parseVertexSaJson(credentials.apiKey); if (!saJson) return null; return refreshVertexToken(saJson, log); } default: return refreshAccessToken(provider, credentials.refreshToken, credentials, log); } } /** * Format credentials for provider */ 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": 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 }; } } /** * Get all access tokens for a user */ 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; } /** * Parse Vertex AI Service Account JSON from apiKey string */ 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(); /** * Mint a short-lived OAuth2 Bearer token for Google Cloud Vertex AI * using Service Account JSON + jose (RS256 JWT assertion flow). * Token is cached until 5 minutes before expiry. */ export async function refreshVertexToken(saJson, log) { const cacheKey = saJson.client_email; const cached = vertexTokenCache.get(cacheKey); // Return cached token if still valid (5-min buffer) 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("https://oauth2.googleapis.com/token") .setIssuedAt(now) .setExpirationTime(now + 3600) .sign(privateKey); const res = await fetch("https://oauth2.googleapis.com/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; } } /** * Refresh token with retry and exponential backoff * Retries on failure with increasing delay: 1s, 2s, 3s... * @param {function} refreshFn - Async function that returns token or null * @param {number} maxRetries - Max retry attempts (default 3) * @param {object} log - Logger instance (optional) * @returns {Promise} Token result or null if all retries fail */ 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; }