{"version":3,"sources":["../../src/stores/cloudflare-kv.ts"],"sourcesContent":["import type { DPoPNonceStore } from \"./types.js\";\n\nconst DEFAULT_KEY_PREFIX = \"dpop:jti:\";\n\n/** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */\nexport interface KVNamespaceLike {\n\tget(key: string, options?: { type?: \"text\" }): Promise<string | null>;\n\tput(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;\n}\n\nexport interface KVStoreOptions {\n\t/** Cloudflare Workers KV namespace binding. */\n\tnamespace: KVNamespaceLike;\n\t/** Key prefix to namespace replay-cache entries (default: \"dpop:jti:\"). */\n\tkeyPrefix?: string;\n\t/**\n\t * Optional best-effort race-detection delay in milliseconds. When > 0 the store\n\t * sleeps for this long after `put` and re-reads the key, returning false if the\n\t * value disagrees with the one it just wrote. Default 0 (disabled) — KV's\n\t * eventual consistency makes any value tunable but never fully reliable.\n\t */\n\traceWindowMs?: number;\n}\n\n/**\n * Cloudflare Workers KV-backed replay cache.\n *\n * IMPORTANT: KV is eventually consistent across edge locations. Two requests routed to\n * different POPs may both observe the key as absent and both succeed in `put`, allowing\n * a brief replay window. RFC 9449 §11.1 acknowledges this: jti enforcement is at-most-once\n * but is allowed to degrade to best-effort under partition. For strict atomicity, use\n * `d1Store` (single primary) or `durableObjectStore` (single-writer per object).\n *\n * Caches expire automatically via `expirationTtl` derived from the proof's `expiresAt`,\n * so `purge()` is a no-op returning 0. The KV minimum is 60 seconds — shorter requested\n * TTLs are clamped to that floor.\n */\nexport function kvStore(options: KVStoreOptions): DPoPNonceStore {\n\tconst { namespace: kv, keyPrefix = DEFAULT_KEY_PREFIX, raceWindowMs = 0 } = options;\n\n\treturn {\n\t\tasync check(jti, expiresAt) {\n\t\t\tconst remainingMs = expiresAt - Date.now();\n\t\t\tif (remainingMs <= 0) return false;\n\t\t\tconst key = `${keyPrefix}${jti}`;\n\n\t\t\tconst existing = await kv.get(key);\n\t\t\tif (existing !== null) return false;\n\n\t\t\t// KV minimum expirationTtl is 60s — clamp.\n\t\t\tconst expirationTtl = Math.max(60, Math.ceil(remainingMs / 1000));\n\t\t\tconst marker = crypto.randomUUID();\n\t\t\tawait kv.put(key, marker, { expirationTtl });\n\n\t\t\tif (raceWindowMs > 0) {\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, raceWindowMs));\n\t\t\t\tconst readback = await kv.get(key);\n\t\t\t\tif (readback !== marker) return false;\n\t\t\t}\n\n\t\t\treturn true;\n\t\t},\n\n\t\tasync purge() {\n\t\t\t// KV handles expiration automatically via expirationTtl — no manual purge needed\n\t\t\treturn 0;\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,IAAM,qBAAqB;AAmCpB,SAAS,QAAQ,SAAyC;AAChE,QAAM,EAAE,WAAW,IAAI,YAAY,oBAAoB,eAAe,EAAE,IAAI;AAE5E,SAAO;AAAA,IACN,MAAM,MAAM,KAAK,WAAW;AAC3B,YAAM,cAAc,YAAY,KAAK,IAAI;AACzC,UAAI,eAAe,EAAG,QAAO;AAC7B,YAAM,MAAM,GAAG,SAAS,GAAG,GAAG;AAE9B,YAAM,WAAW,MAAM,GAAG,IAAI,GAAG;AACjC,UAAI,aAAa,KAAM,QAAO;AAG9B,YAAM,gBAAgB,KAAK,IAAI,IAAI,KAAK,KAAK,cAAc,GAAI,CAAC;AAChE,YAAM,SAAS,OAAO,WAAW;AACjC,YAAM,GAAG,IAAI,KAAK,QAAQ,EAAE,cAAc,CAAC;AAE3C,UAAI,eAAe,GAAG;AACrB,cAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,YAAY,CAAC;AAChE,cAAM,WAAW,MAAM,GAAG,IAAI,GAAG;AACjC,YAAI,aAAa,OAAQ,QAAO;AAAA,MACjC;AAEA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,QAAQ;AAEb,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}