Files
2026-07-13 13:39:12 +08:00

114 lines
3.3 KiB
TypeScript

import { randomUUID } from "crypto";
type GigachatTokenResult = {
accessToken: string;
expiresAt: string;
};
type GigachatTokenOptions = {
credentials?: string | null;
scope?: string;
authUrl?: string;
signal?: AbortSignal;
};
const DEFAULT_GIGACHAT_AUTH_URL = "https://ngw.devices.sberbank.ru:9443/api/v2/oauth";
const DEFAULT_GIGACHAT_SCOPE = "GIGACHAT_API_PERS";
const CACHE_SKEW_MS = 60_000;
const tokenCache = new Map<string, GigachatTokenResult>();
const inflightRequests = new Map<string, Promise<GigachatTokenResult>>();
function getCacheKey(credentials: string, authUrl: string, scope: string) {
return `${authUrl}::${scope}::${credentials}`;
}
function isFreshToken(token: GigachatTokenResult | undefined) {
if (!token?.accessToken || !token?.expiresAt) return false;
const expiresAtMs = new Date(token.expiresAt).getTime();
return Number.isFinite(expiresAtMs) && expiresAtMs - Date.now() > CACHE_SKEW_MS;
}
function normalizeExpiry(rawExpiry: unknown) {
if (typeof rawExpiry === "number" && Number.isFinite(rawExpiry)) {
return new Date(rawExpiry).toISOString();
}
if (typeof rawExpiry === "string" && rawExpiry.trim().length > 0) {
const numericValue = Number(rawExpiry);
if (Number.isFinite(numericValue)) {
return new Date(numericValue).toISOString();
}
const dateMs = Date.parse(rawExpiry);
if (Number.isFinite(dateMs)) {
return new Date(dateMs).toISOString();
}
}
return new Date(Date.now() + 30 * 60 * 1000).toISOString();
}
export async function getGigachatAccessToken(
options: GigachatTokenOptions = {}
): Promise<GigachatTokenResult> {
const credentials = (options.credentials || "").trim();
if (!credentials) {
throw new Error("Missing GigaChat credentials");
}
const authUrl = (options.authUrl || DEFAULT_GIGACHAT_AUTH_URL).trim();
const scope = (options.scope || DEFAULT_GIGACHAT_SCOPE).trim();
const cacheKey = getCacheKey(credentials, authUrl, scope);
const cached = tokenCache.get(cacheKey);
if (isFreshToken(cached)) {
return cached;
}
const inflight = inflightRequests.get(cacheKey);
if (inflight) {
return inflight;
}
const requestPromise = (async () => {
const response = await fetch(authUrl, {
method: "POST",
headers: {
Authorization: `Basic ${credentials}`,
RqUID: randomUUID(),
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({ scope }).toString(),
signal: options.signal,
});
if (!response.ok) {
const bodyText = await response.text();
throw new Error(`GigaChat token request failed: ${response.status} ${bodyText}`.trim());
}
const data = (await response.json()) as Record<string, unknown>;
const accessToken =
typeof data.tok === "string"
? data.tok
: typeof data.access_token === "string"
? data.access_token
: "";
if (!accessToken) {
throw new Error("GigaChat token response missing access token");
}
const token = {
accessToken,
expiresAt: normalizeExpiry(data.exp ?? data.expires_at),
};
tokenCache.set(cacheKey, token);
return token;
})();
inflightRequests.set(cacheKey, requestPromise);
try {
return await requestPromise;
} finally {
inflightRequests.delete(cacheKey);
}
}