| import { |
| getRefreshLeadMs, |
| isUnrecoverableRefreshError, |
| refreshTokenByProvider, |
| } from "./tokenRefresh.js"; |
|
|
| export const CODEX_MAX_REFRESH_AGE_MS = 8 * 24 * 60 * 60 * 1000; |
|
|
| const refreshLocks = new Map(); |
|
|
| function parseTimeMs(value) { |
| if (value === undefined || value === null || value === "") return null; |
| if (typeof value === "number") { |
| return value < 1e12 ? value * 1000 : value; |
| } |
|
|
| const parsed = new Date(value).getTime(); |
| return Number.isFinite(parsed) ? parsed : null; |
| } |
|
|
| function toExpiresAt(expiresIn, nowMs = Date.now()) { |
| if (!expiresIn) return null; |
| return new Date(nowMs + expiresIn * 1000).toISOString(); |
| } |
|
|
| export function getCredentialExpiryMs(credentials) { |
| return parseTimeMs(credentials?.expiresAt ?? credentials?.tokenExpiresAt); |
| } |
|
|
| export function getCredentialLastRefreshMs(credentials) { |
| return parseTimeMs( |
| credentials?.lastRefreshAt ?? |
| credentials?.lastRefresh ?? |
| credentials?.providerSpecificData?.lastRefreshAt |
| ); |
| } |
|
|
| export function isCodexRefreshStale(credentials, nowMs = Date.now()) { |
| const lastRefreshMs = getCredentialLastRefreshMs(credentials); |
| return !lastRefreshMs || nowMs - lastRefreshMs >= CODEX_MAX_REFRESH_AGE_MS; |
| } |
|
|
| export function shouldRefreshCredentials(provider, credentials, nowMs = Date.now()) { |
| if (!credentials) return false; |
|
|
| const expiresAtMs = getCredentialExpiryMs(credentials); |
| if (expiresAtMs !== null && expiresAtMs - nowMs < getRefreshLeadMs(provider)) { |
| return true; |
| } |
|
|
| if (provider === "codex" && credentials.refreshToken && isCodexRefreshStale(credentials, nowMs)) { |
| return true; |
| } |
|
|
| return false; |
| } |
|
|
| export function mergeProviderSpecificData(existing, next) { |
| if (!next || typeof next !== "object") return existing; |
| return { |
| ...(existing || {}), |
| ...next, |
| }; |
| } |
|
|
| export function mergeRefreshedCredentials(provider, currentCredentials, refreshedCredentials, nowMs = Date.now()) { |
| if (!refreshedCredentials) return null; |
| if (isUnrecoverableRefreshError(refreshedCredentials)) return refreshedCredentials; |
|
|
| const next = {}; |
| const nowIso = new Date(nowMs).toISOString(); |
|
|
| if (refreshedCredentials.accessToken) next.accessToken = refreshedCredentials.accessToken; |
| if (refreshedCredentials.apiKey) next.apiKey = refreshedCredentials.apiKey; |
| if (refreshedCredentials.token) next.token = refreshedCredentials.token; |
|
|
| const refreshToken = refreshedCredentials.refreshToken ?? currentCredentials?.refreshToken; |
| if (refreshToken) next.refreshToken = refreshToken; |
|
|
| const idToken = refreshedCredentials.idToken ?? currentCredentials?.idToken; |
| if (idToken) next.idToken = idToken; |
|
|
| if (refreshedCredentials.expiresIn) { |
| next.expiresIn = refreshedCredentials.expiresIn; |
| next.expiresAt = toExpiresAt(refreshedCredentials.expiresIn, nowMs); |
| } else if (refreshedCredentials.expiresAt) { |
| next.expiresAt = refreshedCredentials.expiresAt; |
| } |
|
|
| if (refreshedCredentials.projectId) next.projectId = refreshedCredentials.projectId; |
|
|
| if (refreshedCredentials.providerSpecificData) { |
| next.providerSpecificData = mergeProviderSpecificData( |
| currentCredentials?.providerSpecificData, |
| refreshedCredentials.providerSpecificData |
| ); |
| } |
|
|
| if (refreshedCredentials.copilotToken) next.copilotToken = refreshedCredentials.copilotToken; |
| if (refreshedCredentials.copilotTokenExpiresAt) { |
| next.copilotTokenExpiresAt = refreshedCredentials.copilotTokenExpiresAt; |
| } |
|
|
| if ( |
| provider === "codex" || |
| next.accessToken || |
| next.apiKey || |
| next.token || |
| next.refreshToken || |
| next.copilotToken |
| ) { |
| next.lastRefreshAt = refreshedCredentials.lastRefreshAt || nowIso; |
| } |
|
|
| return next; |
| } |
|
|
| function getRefreshLockKey(provider, credentials) { |
| const stableId = |
| credentials?.connectionId || |
| credentials?.id || |
| credentials?.email || |
| credentials?.name || |
| credentials?.refreshToken?.slice?.(-16) || |
| "default"; |
| return `${provider}:${stableId}`; |
| } |
|
|
| export async function withCredentialRefreshLock(provider, credentials, refreshFn) { |
| const key = getRefreshLockKey(provider, credentials); |
| const existing = refreshLocks.get(key); |
| if (existing) return existing; |
|
|
| const pending = Promise.resolve() |
| .then(refreshFn) |
| .finally(() => { |
| refreshLocks.delete(key); |
| }); |
|
|
| refreshLocks.set(key, pending); |
| return pending; |
| } |
|
|
| export async function refreshProviderCredentials(provider, credentials, log) { |
| if (!credentials) return null; |
|
|
| return withCredentialRefreshLock(provider, credentials, async () => { |
| const refreshed = await refreshTokenByProvider(provider, credentials, log); |
| return mergeRefreshedCredentials(provider, credentials, refreshed); |
| }); |
| } |
|
|