first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
# `jwks-cache` — TTL-bounded JWKS resolution
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<Record<string, unknown> | undefined>`
— fetch-as-needed, returns `undefined` when 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.