# `jwks-cache` — TTL-bounded JWKS resolution _(no submodules)_ ## Purpose In-process JWKS cache for `@suaveplan/api-auth-jwt`. Fetches a JWKS document from a remote endpoint, holds it under a positive TTL, negative-caches transient failures, dedupes concurrent fetches into a single network request, and supports forced refresh on a `kid` miss (the standard "the IdP rotated keys" recovery path). Throws `AUTH_JWT.JWKS_FETCH_FAILED` only when neither a fresh fetch nor a stale cached document is available — under partial outages we prefer stale-but-valid keys to a 401 storm. ## Features - Positive TTL (default 10 minutes) with monotonic clock injection. - Negative cache window (default 30 seconds) on fetch failure — within the window callers see the stale document if any, otherwise the error. - In-flight dedup: concurrent `getKeyForKid` callers share a single network round-trip. - `forceRefresh()` for operator-triggered rotation. - `kid` miss against a cached document triggers exactly one refresh. - JSON shape validation (`{ keys: object[] }`) at the boundary. ## Usage: Basic ```ts import { JwksCache } from "@suaveplan/api-auth-jwt/jwks-cache"; const cache = new JwksCache({ url: "https://idp.example/.well-known/jwks.json", ttlMs: 10 * 60 * 1000, negativeMs: 30 * 1000, clock: { nowMs: () => Date.now() }, fetchImpl: fetch, }); const jwk = await cache.getKeyForKid("kid-2024-01"); ``` ## Usage: Advanced Inject a fake clock and a controllable fetch impl for tests of the expiry and dedup behaviours: ```ts let now = 0; const calls: number[] = []; const cache = new JwksCache({ url: "https://idp.example/.well-known/jwks.json", ttlMs: 1000, negativeMs: 100, clock: { nowMs: () => now }, fetchImpl: async () => { calls.push(now); return new Response(JSON.stringify({ keys: [{ kid: "a", kty: "RSA", n: "...", e: "AQAB" }] }), { status: 200, headers: { "content-type": "application/json" }, }); }, }); await cache.getKeyForKid("a"); now = 500; await cache.getKeyForKid("a"); // cache hit, no second fetch ``` ## API Reference - `class JwksCache` — constructor takes `{ url, ttlMs, negativeMs, clock, fetchImpl }`. - `getKeyForKid(kid?: string): Promise | undefined>` — fetch-as-needed, returns `undefined` when no key matches. - `forceRefresh(): Promise` — clear cache and refetch. - `JwksDocument` — `{ keys: ReadonlyArray> }`. ## Implementation Notes When the token carries no `kid`, the cache only returns a key when the JWKS contains exactly one entry — ambiguous lookups must be a 401, not a guess. The negative cache window is short by design (30s default); longer windows accumulate authentication outages, shorter windows hammer the IdP. The in-flight dedup uses a single `Promise` field rather than a queue — the second concurrent caller awaits the same promise as the first. The "kid miss after a just-fetched document" guard prevents infinite refresh loops when the IdP legitimately does not have a matching key.