File size: 4,699 Bytes
88c4c60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | 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);
});
}
|