import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts"; import { createHash } from "node:crypto"; // Token expiry buffer (refresh if expires within 5 minutes) export const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; // In-flight refresh promise cache to prevent race conditions // Key: "provider:sha256(refreshToken)" → Value: Promise const refreshPromiseCache = new Map(); function getRefreshCacheKey(provider, refreshToken) { const tokenHash = createHash("sha256").update(refreshToken).digest("hex"); return `${provider}:${tokenHash}`; } /** * Refresh OAuth access token using refresh token */ export async function refreshAccessToken(provider, refreshToken, credentials, log) { const config = PROVIDERS[provider]; const refreshEndpoint = config?.refreshUrl || config?.tokenUrl; if (!config || !refreshEndpoint) { log?.warn?.("TOKEN_REFRESH", `No refresh endpoint configured for provider: ${provider}`); return null; } if (!refreshToken) { log?.warn?.("TOKEN_REFRESH", `No refresh token available for provider: ${provider}`); return null; } try { const params = new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, }); if (config.clientId) params.set("client_id", config.clientId); if (config.clientSecret) params.set("client_secret", config.clientSecret); const response = await fetch(refreshEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, body: params, }); 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 Cline OAuth tokens. * Cline refresh endpoint expects JSON body and returns camelCase fields. */ export async function refreshClineToken(refreshToken, log) { const endpoint = PROVIDERS.cline?.refreshUrl; if (!endpoint) { log?.warn?.("TOKEN_REFRESH", "No refresh URL configured for Cline"); return null; } try { const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ refreshToken, grantType: "refresh_token", clientType: "extension", }), }); if (!response.ok) { const errorText = await response.text(); log?.error?.("TOKEN_REFRESH", "Failed to refresh Cline token", { status: response.status, error: errorText, }); return null; } const payload = await response.json(); const data = payload?.data || payload; const expiresAtIso = data?.expiresAt; const expiresIn = expiresAtIso ? Math.max(1, Math.floor((new Date(expiresAtIso).getTime() - Date.now()) / 1000)) : undefined; log?.info?.("TOKEN_REFRESH", "Successfully refreshed Cline token", { hasNewAccessToken: !!data?.accessToken, hasNewRefreshToken: !!data?.refreshToken, expiresIn, }); return { accessToken: data?.accessToken, refreshToken: data?.refreshToken || refreshToken, expiresIn, }; } catch (error) { log?.error?.("TOKEN_REFRESH", `Network error refreshing Cline token: ${error.message}`); return null; } } /** * Specialized refresh for Kimi Coding OAuth tokens. * Uses custom X-Msh-* headers required by Kimi OAuth API. */ export async function refreshKimiCodingToken(refreshToken, log) { const endpoint = PROVIDERS["kimi-coding"]?.refreshUrl || PROVIDERS["kimi-coding"]?.tokenUrl; if (!endpoint) { log?.warn?.("TOKEN_REFRESH", "No refresh URL configured for Kimi Coding"); return null; } // Generate device info for headers (same as OAuth flow) const deviceId = "kimi-refresh-" + Date.now(); const platform = "omniroute"; const version = "2.1.2"; const deviceModel = typeof process !== "undefined" ? `${process.platform} ${process.arch}` : "unknown"; try { const params = new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: PROVIDERS["kimi-coding"]?.clientId || "", }); const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", "X-Msh-Platform": platform, "X-Msh-Version": version, "X-Msh-Device-Model": deviceModel, "X-Msh-Device-Id": deviceId, }, body: params, }); if (!response.ok) { const errorText = await response.text(); log?.error?.("TOKEN_REFRESH", "Failed to refresh Kimi Coding token", { status: response.status, error: errorText, }); return null; } const tokens = await response.json(); log?.info?.("TOKEN_REFRESH", "Successfully refreshed Kimi Coding 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, tokenType: tokens.token_type, scope: tokens.scope, }; } catch (error) { log?.error?.("TOKEN_REFRESH", `Network error refreshing Kimi Coding token: ${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, 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 Claude token: ${error.message}`); return null; } } /** * Specialized refresh for Google providers (Gemini, Antigravity) */ export async function refreshGoogleToken(refreshToken, clientId, clientSecret, log) { 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, 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 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, }; } else { const errorText = await response.text().catch(() => ""); // Detect unrecoverable invalid_request (expired/revoked refresh token or bad client_id) let errorCode = null; try { const parsed = JSON.parse(errorText); errorCode = parsed?.error; } catch { // not JSON, ignore } if (errorCode === "invalid_request") { log?.error?.( "TOKEN_REFRESH", "Qwen refresh token is invalid or expired. Re-authentication required.", { status: response.status, } ); return { error: "invalid_request" }; } 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. * OpenAI uses rotating (one-time-use) refresh tokens. * Returns { error: 'refresh_token_reused' } when the token has already been consumed, * so callers can stop retrying and request re-authentication. */ 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(); // Detect unrecoverable "refresh_token_reused" error from OpenAI // This means the token was already consumed and a new one was issued. // Retrying with the same token will never succeed. let errorCode = null; try { const parsed = JSON.parse(errorText); errorCode = parsed?.error?.code; } catch { // not JSON, ignore } if (errorCode === "refresh_token_reused") { log?.error?.( "TOKEN_REFRESH", "Codex refresh token already used (rotating token consumed). Re-authentication required.", { status: response.status, } ); return { error: "refresh_token_reused" }; } 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) { try { 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", }, 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, }; } catch (error) { log?.error?.("TOKEN_REFRESH", `Network error refreshing Kiro token: ${error.message}`); return null; } } /** * 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 response = await fetch(OAUTH_ENDPOINTS.github.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.github.clientId, client_secret: PROVIDERS.github.clientSecret, }), }); 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": "GithubCopilot/1.0", "Editor-Version": "vscode/1.100.0", "Editor-Plugin-Version": "copilot/1.300.0", Accept: "application/json", }, }); 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 (internal, does the actual work) */ async function _getAccessTokenInternal(provider, credentials, log) { 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 "cline": return await refreshClineToken(credentials.refreshToken, log); case "kimi-coding": return await refreshKimiCodingToken(credentials.refreshToken, log); default: // Fallback to generic OAuth refresh for unknown providers return refreshAccessToken(provider, credentials.refreshToken, credentials, log); } } /** * Whether a provider has a supported refresh path in this service. */ export function supportsTokenRefresh(provider) { const explicitlySupported = new Set([ "gemini", "gemini-cli", "antigravity", "claude", "codex", "qwen", "iflow", "github", "kiro", "cline", "kimi-coding", ]); if (explicitlySupported.has(provider)) return true; const config = PROVIDERS[provider]; return !!(config?.refreshUrl || config?.tokenUrl); } /** * Check if a refresh result indicates an unrecoverable error * (e.g. the refresh token was already consumed and cannot be reused). * Callers should stop retrying and request re-authentication. */ export function isUnrecoverableRefreshError(result) { return ( result && typeof result === "object" && (result.error === "refresh_token_reused" || result.error === "invalid_request") ); } /** * Get access token for a specific provider (with deduplication). * If a refresh is already in-flight for the same provider+token, * subsequent calls share the existing promise instead of making * parallel OAuth requests. */ 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; } const cacheKey = getRefreshCacheKey(provider, credentials.refreshToken); // If a refresh is already in-flight, reuse it if (refreshPromiseCache.has(cacheKey)) { log?.info?.("TOKEN_REFRESH", `Reusing in-flight refresh for ${provider}`); return refreshPromiseCache.get(cacheKey); } // Start a new refresh and cache the promise const refreshPromise = _getAccessTokenInternal(provider, credentials, log).finally(() => { refreshPromiseCache.delete(cacheKey); }); refreshPromiseCache.set(cacheKey, refreshPromise); return refreshPromise; } /** * Refresh token by provider type (alias for getAccessToken) * @deprecated Since v0.2.70 — use getAccessToken() directly. * Still exported because open-sse/index.js and src/sse wrapper use it. * Will be removed in a future major version. */ export const refreshTokenByProvider = getAccessToken; /** * 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, }; 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; } /** * Refresh token with retry and exponential backoff * Retries on failure with increasing delay: 1s, 2s, 3s... * * Includes: * - Per-provider circuit breaker (5 consecutive failures → 30min pause) * - 30s timeout per refresh attempt to prevent hanging connections * * @param {function} refreshFn - Async function that returns token or null * @param {number} maxRetries - Max retry attempts (default 3) * @param {object} log - Logger instance (optional) * @param {string} provider - Provider ID for circuit breaker tracking (optional) * @returns {Promise} Token result or null if all retries fail */ // ─── Circuit Breaker State ────────────────────────────────────────────────── const _circuitBreaker: Record = {}; const CIRCUIT_BREAKER_THRESHOLD = 5; // consecutive failures before tripping const CIRCUIT_BREAKER_COOLDOWN = 30 * 60 * 1000; // 30 minutes const REFRESH_TIMEOUT_MS = 30_000; // 30s max per refresh attempt interface CircuitBreakerStatusEntry { failures: number; blocked: boolean; blockedUntil: string | null; remainingMs: number; } interface RefreshLoggerLike { error?: (scope: string, message: string) => void; warn?: (scope: string, message: string) => void; } /** * Check if a provider is circuit-breaker blocked. */ export function isProviderBlocked(provider: string): boolean { const state = _circuitBreaker[provider]; if (!state) return false; if (state.blockedUntil > Date.now()) return true; // Cooldown expired — reset delete _circuitBreaker[provider]; return false; } /** * Get circuit breaker status for all providers (for diagnostics). */ export function getCircuitBreakerStatus(): Record { const result: Record = {}; for (const [provider, state] of Object.entries(_circuitBreaker)) { result[provider] = { failures: state.failures, blocked: state.blockedUntil > Date.now(), blockedUntil: state.blockedUntil > Date.now() ? new Date(state.blockedUntil).toISOString() : null, remainingMs: Math.max(0, state.blockedUntil - Date.now()), }; } return result; } /** * Record a successful refresh — resets circuit breaker for provider. */ function recordSuccess(provider: string) { if (_circuitBreaker[provider]) { delete _circuitBreaker[provider]; } } /** * Record a failed refresh — increments circuit breaker counter. */ function recordFailure(provider: string, log: RefreshLoggerLike | null = null) { if (!_circuitBreaker[provider]) { _circuitBreaker[provider] = { failures: 0, blockedUntil: 0 }; } _circuitBreaker[provider].failures++; if (_circuitBreaker[provider].failures >= CIRCUIT_BREAKER_THRESHOLD) { _circuitBreaker[provider].blockedUntil = Date.now() + CIRCUIT_BREAKER_COOLDOWN; log?.error?.( "TOKEN_REFRESH", `🔴 Circuit breaker tripped for ${provider}: ${CIRCUIT_BREAKER_THRESHOLD} consecutive failures. ` + `Blocked for ${CIRCUIT_BREAKER_COOLDOWN / 60000}min. Provider needs re-authentication.` ); } } /** * Execute a function with a timeout. */ async function withTimeout(fn: () => Promise, timeoutMs: number): Promise { return Promise.race([ fn(), new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs)), ]); } export async function refreshWithRetry( refreshFn, maxRetries = 3, log = null, provider = "unknown" ) { // Circuit breaker check if (isProviderBlocked(provider)) { log?.warn?.("TOKEN_REFRESH", `⚡ Circuit breaker active for ${provider}, skipping refresh`); return 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 withTimeout(refreshFn, REFRESH_TIMEOUT_MS); if (result) { recordSuccess(provider); return result; } } catch (error) { log?.warn?.("TOKEN_REFRESH", `Attempt ${attempt + 1}/${maxRetries} failed: ${error.message}`); } } // All retries exhausted — record failure for circuit breaker recordFailure(provider, log); log?.error?.("TOKEN_REFRESH", `All ${maxRetries} retry attempts failed for ${provider}`); return null; }