| import { PROVIDERS } from "../config/providers.js"; |
| import { OAUTH_ENDPOINTS, GITHUB_COPILOT, REFRESH_LEAD_MS } from "../config/appConstants.js"; |
| import { proxyAwareFetch } from "../utils/proxyFetch.js"; |
|
|
| |
| |
| let _xaiServiceSingleton = null; |
| async function refreshXaiToken(refreshToken, log) { |
| if (!refreshToken) return null; |
| return dedupRefresh("xai", refreshToken, async () => { |
| try { |
| if (!_xaiServiceSingleton) { |
| const mod = await import("../../src/lib/oauth/services/xai.js"); |
| _xaiServiceSingleton = new mod.XaiService(); |
| } |
| const tokens = await _xaiServiceSingleton.refreshAccessToken(refreshToken); |
| return { |
| accessToken: tokens.access_token, |
| refreshToken: tokens.refresh_token || refreshToken, |
| expiresIn: tokens.expires_in, |
| idToken: tokens.id_token, |
| }; |
| } catch (e) { |
| log?.warn?.("TOKEN_REFRESH", `xai refresh failed: ${e?.message || e}`); |
| const msg = String(e?.message || ""); |
| if (msg.includes("invalid_grant") || msg.includes("invalid_request")) { |
| return { error: "invalid_grant" }; |
| } |
| return null; |
| } |
| }, log); |
| } |
|
|
| |
| export const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; |
|
|
| |
| const REFRESH_RESULT_TTL_MS = 10_000; |
| const refreshDedupCache = new Map(); |
|
|
| async function dedupRefresh(provider, oldToken, fn, log) { |
| if (!oldToken) return fn(); |
| const key = `${provider}:${oldToken}`; |
| const hit = refreshDedupCache.get(key); |
| if (hit) { |
| if (hit.promise) { |
| log?.info?.("TOKEN_REFRESH", `Reusing in-flight refresh for ${provider}`); |
| return hit.promise; |
| } |
| if (hit.expiresAt > Date.now()) { |
| log?.info?.("TOKEN_REFRESH", `Reusing recent refresh result for ${provider}`); |
| return hit.result; |
| } |
| refreshDedupCache.delete(key); |
| } |
| const promise = (async () => { |
| try { |
| const result = await fn(); |
| refreshDedupCache.set(key, { result, expiresAt: Date.now() + REFRESH_RESULT_TTL_MS }); |
| return result; |
| } catch (err) { |
| refreshDedupCache.delete(key); |
| throw err; |
| } |
| })(); |
| refreshDedupCache.set(key, { promise }); |
| return promise; |
| } |
|
|
| |
| 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 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; |
| } |
|
|
| return dedupRefresh(provider, refreshToken, async () => { |
| 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; |
| } |
| }, log); |
| } |
|
|
| |
| |
| |
| export async function refreshClaudeOAuthToken(refreshToken, log) { |
| if (!refreshToken) return null; |
| return dedupRefresh("claude", refreshToken, async () => { |
| 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; |
| } |
| }, log); |
| } |
|
|
| |
| |
| |
| export async function refreshGoogleToken(refreshToken, clientId, clientSecret, log) { |
| if (!refreshToken) return null; |
| return dedupRefresh(`google:${clientId}`, refreshToken, async () => { |
| 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; |
| } |
| }, log); |
| } |
|
|
| |
| |
| |
| export async function refreshQwenToken(refreshToken, log) { |
| if (!refreshToken) return null; |
| return dedupRefresh("qwen", refreshToken, async () => { |
| 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; |
| }, log); |
| } |
|
|
| export function classifyOAuthRefreshError(errorText = "", status = 0) { |
| let parsed = null; |
| try { |
| parsed = errorText ? JSON.parse(errorText) : null; |
| } catch { |
| parsed = null; |
| } |
|
|
| const code = parsed?.error?.code || parsed?.error || parsed?.error_code || ""; |
| const description = parsed?.error_description || parsed?.message || errorText || ""; |
| const combined = `${code} ${description}`.toLowerCase(); |
| const permanent = [ |
| "refresh_token_expired", |
| "refresh_token_reused", |
| "refresh_token_invalidated", |
| "invalid_grant", |
| ].some((marker) => combined.includes(marker)); |
|
|
| return { status, code, description, permanent }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function refreshCodexToken(refreshToken, log) { |
| if (!refreshToken) return null; |
| return dedupRefresh("codex", refreshToken, async () => { |
| try { |
| const response = await fetch(OAUTH_ENDPOINTS.openai.token, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| Accept: "application/json", |
| }, |
| body: JSON.stringify({ |
| client_id: PROVIDERS.codex.clientId, |
| grant_type: "refresh_token", |
| refresh_token: refreshToken, |
| }), |
| }); |
|
|
| if (!response.ok) { |
| const errorText = await response.text(); |
| const failure = classifyOAuthRefreshError(errorText, response.status); |
| if (failure.permanent) { |
| log?.error?.("TOKEN_REFRESH", "Codex refresh token already used or invalid. Re-auth required.", { |
| status: response.status, |
| code: failure.code, |
| }); |
| return { error: "unrecoverable_refresh_error", code: failure.code }; |
| } |
|
|
| log?.error?.("TOKEN_REFRESH", "Failed to refresh Codex token", { |
| status: response.status, |
| error: errorText, |
| code: failure.code, |
| permanent: failure.permanent, |
| }); |
| return null; |
| } |
|
|
| const tokens = await response.json(); |
|
|
| log?.info?.("TOKEN_REFRESH", "Successfully refreshed Codex token", { |
| hasNewAccessToken: !!tokens.access_token, |
| hasNewRefreshToken: !!tokens.refresh_token, |
| hasIdToken: !!tokens.id_token, |
| expiresIn: tokens.expires_in, |
| }); |
|
|
| return { |
| accessToken: tokens.access_token, |
| refreshToken: tokens.refresh_token || refreshToken, |
| idToken: tokens.id_token, |
| expiresIn: tokens.expires_in, |
| }; |
| } catch (error) { |
| log?.error?.("TOKEN_REFRESH", `Network error refreshing Codex token: ${error.message}`); |
| return null; |
| } |
| }, log); |
| } |
|
|
| |
| |
| |
| |
| |
| async function resolveKiroProfileArnPatch(providerSpecificData, accessToken, refreshedArn) { |
| if (providerSpecificData?.profileArn) return {}; |
| let profileArn = refreshedArn?.trim?.() || null; |
| if (!profileArn) { |
| const { fetchKiroProfileArn } = await import("../../src/lib/oauth/providers.js"); |
| profileArn = await fetchKiroProfileArn(accessToken); |
| } |
| return profileArn ? { providerSpecificData: { profileArn } } : {}; |
| } |
|
|
| export async function refreshKiroToken(refreshToken, providerSpecificData, log, proxyOptions = null) { |
| if (!refreshToken) return null; |
| return dedupRefresh("kiro", refreshToken, async () => { |
| const authMethod = providerSpecificData?.authMethod; |
| const clientId = providerSpecificData?.clientId; |
| const clientSecret = providerSpecificData?.clientSecret; |
| const region = providerSpecificData?.region; |
|
|
| |
| |
| 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 proxyAwareFetch(endpoint, { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| Accept: "application/json", |
| }, |
| body: JSON.stringify({ |
| clientId: clientId, |
| clientSecret: clientSecret, |
| refreshToken: refreshToken, |
| grantType: "refresh_token", |
| }), |
| }, proxyOptions); |
|
|
| 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, |
| ...(await resolveKiroProfileArnPatch(providerSpecificData, tokens.accessToken, tokens.profileArn)), |
| }; |
| } |
|
|
| |
| const response = await proxyAwareFetch(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, |
| }), |
| }, proxyOptions); |
|
|
| 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, |
| ...(await resolveKiroProfileArnPatch(providerSpecificData, tokens.accessToken, tokens.profileArn)), |
| }; |
| }, log); |
| } |
|
|
| |
| |
| |
| export async function refreshIflowToken(refreshToken, log) { |
| if (!refreshToken) return null; |
| return dedupRefresh("iflow", refreshToken, async () => { |
| 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, |
| }; |
| }, log); |
| } |
|
|
| |
| |
| |
| export async function refreshGitHubToken(refreshToken, log) { |
| if (!refreshToken) return null; |
| return dedupRefresh("github", refreshToken, async () => { |
| 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, |
| }; |
| }, log); |
| } |
|
|
| |
| |
| |
| export async function refreshCopilotToken(githubAccessToken, log) { |
| if (!githubAccessToken) return null; |
| return dedupRefresh("copilot", githubAccessToken, async () => { |
| 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; |
| } |
| }, log); |
| } |
|
|
| |
| |
| |
| |
| |
| 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) { |
| 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 "xai": |
| return await refreshXaiToken(credentials.refreshToken, 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; |
| } |
| } |
|
|
| |
| |
| |
| 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 "xai": |
| return refreshXaiToken(credentials.refreshToken, 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); |
| } |
| } |
|
|
| |
| |
| |
| 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 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; |
| } |
| } |
|
|
| |
| 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("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; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| 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; |
| } |
|
|