// Server-side TypeScript with Node.js 22+ and DOM fetch types. // Create ONE provider per credential/audience outside request handlers. export interface TokenProviderOptions { clientId: string; clientSecret: string; audience: string; } export interface TokenProvider { getToken(): Promise; invalidate(rejectedToken: string): void; } export function createTokenProvider({ clientId, clientSecret, audience }: TokenProviderOptions): TokenProvider { if (![clientId, clientSecret, audience].every(value => typeof value === "string" && value.trim())) { throw new Error("Client ID, client secret and audience are required."); } let cached: { token: string; refreshAt: number } | null = null; let refreshInFlight: Promise | null = null; async function requestToken(): Promise { const response = await fetch("https://identity.vinquery.com/connect/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, clientSecret, audience }), signal: AbortSignal.timeout(30_000) }); if (!response.ok) { throw new Error(`VINquery token request failed (HTTP ${response.status}).`); } const result: unknown = await response.json(); if (typeof result !== "object" || result === null) { throw new Error("VINquery returned an invalid token response."); } const { jwtToken, expiresUtc } = result as { jwtToken?: unknown; expiresUtc?: unknown }; const expiresAt = typeof expiresUtc === "string" ? Date.parse(expiresUtc) : NaN; const now = Date.now(); if (typeof jwtToken !== "string" || !jwtToken.trim() || !Number.isFinite(expiresAt) || expiresAt <= now) { throw new Error("VINquery returned an empty token or invalid expiry."); } cached = { token: jwtToken, refreshAt: expiresAt - Math.min(60_000, (expiresAt - now) * 0.1) }; return cached.token; } async function getToken(): Promise { if (cached && Date.now() < cached.refreshAt) return cached.token; if (!refreshInFlight) { refreshInFlight = requestToken().finally(() => { refreshInFlight = null; }); } return refreshInFlight; } function invalidate(rejectedToken: string): void { if (cached?.token === rejectedToken) cached = null; } return { getToken, invalidate }; }