3.1 KiB
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
getKeyForKidcallers share a single network round-trip. forceRefresh()for operator-triggered rotation.kidmiss against a cached document triggers exactly one refresh.- JSON shape validation (
{ keys: object[] }) at the boundary.
Usage: Basic
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:
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<Record<string, unknown> | undefined>— fetch-as-needed, returnsundefinedwhen no key matches.forceRefresh(): Promise<JwksDocument>— clear cache and refetch.JwksDocument—{ keys: ReadonlyArray<Record<string, unknown>> }.
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.