(
url: URL,
options: CachedRemoteJWKSetOptions = {},
)
| 257 | * auth paths — see module header for why we don't just use jose's built-in. |
| 258 | */ |
| 259 | export const createCachedRemoteJWKSet = ( |
| 260 | url: URL, |
| 261 | options: CachedRemoteJWKSetOptions = {}, |
| 262 | ): CachedRemoteJWKSet => { |
| 263 | const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS; |
| 264 | const staleMaxMs = Math.max(options.staleMaxMs ?? DEFAULT_STALE_MAX_MS, ttlMs); |
| 265 | const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; |
| 266 | const store = options.store === undefined ? workersCacheStore(staleMaxMs) : options.store; |
| 267 | // Capture the fetch impl lazily so consumers can swap globalThis.fetch |
| 268 | // (tests do this) without us snapshotting a stale reference. |
| 269 | const fetchImpl = (): typeof globalThis.fetch => |
| 270 | options.fetch ?? globalThis.fetch.bind(globalThis); |
| 271 | |
| 272 | let entry: CacheEntry | null = null; |
| 273 | let inflight: Promise<CacheEntry> | null = null; |
| 274 | let fetchCount = 0; |
| 275 | let fetchFailureCount = 0; |
| 276 | let blockingFetchCount = 0; |
| 277 | let storeHitCount = 0; |
| 278 | let lastFetchDurationMs: number | null = null; |
| 279 | let lastStoreReadMs: number | null = null; |
| 280 | let lastResolveMs: number | null = null; |
| 281 | |
| 282 | const isFresh = (candidate: CacheEntry): boolean => Date.now() - candidate.fetchedAt < ttlMs; |
| 283 | const isUsable = (candidate: CacheEntry): boolean => |
| 284 | Date.now() - candidate.fetchedAt < staleMaxMs; |
| 285 | |
| 286 | const refresh = (): Promise<CacheEntry> => { |
| 287 | if (inflight) return inflight; |
| 288 | const startedAt = Date.now(); |
| 289 | fetchCount += 1; |
| 290 | inflight = (async () => { |
| 291 | const jwks = await fetchJwksOnce(url, fetchImpl(), timeoutMs); |
| 292 | const next = entryFrom({ jwks, fetchedAt: Date.now() }); |
| 293 | entry = next; |
| 294 | if (store) { |
| 295 | await ignoreFailure(store.put(url, { jwks: next.jwks, fetchedAt: next.fetchedAt })); |
| 296 | } |
| 297 | return next; |
| 298 | })() |
| 299 | .then( |
| 300 | (next) => next, |
| 301 | (error: unknown) => { |
| 302 | fetchFailureCount += 1; |
| 303 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: counting a fetch failure must preserve the original rejection for jose |
| 304 | throw error; |
| 305 | }, |
| 306 | ) |
| 307 | .finally(() => { |
| 308 | lastFetchDurationMs = Date.now() - startedAt; |
| 309 | inflight = null; |
| 310 | }); |
| 311 | return inflight; |
| 312 | }; |
| 313 | |
| 314 | /** Fire-and-forget revalidation behind a stale hit. */ |
| 315 | const refreshInBackground = (): void => { |
| 316 | void ignoreFailure(refresh()); |
no test coverage detected