hono-idempotency 0.8.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@ Stripe-style Idempotency-Key middleware for [Hono](https://hono.dev). IETF [draf
13
13
  - Idempotency-Key header support for POST/PATCH (configurable)
14
14
  - Request fingerprinting (SHA-256) prevents key reuse with different payloads
15
15
  - Concurrent request protection with optimistic locking
16
- - RFC 9457 Problem Details error responses with error codes (`MISSING_KEY`, `KEY_TOO_LONG`, `FINGERPRINT_MISMATCH`, `CONFLICT`)
16
+ - RFC 9457 Problem Details error responses with error codes (`MISSING_KEY`, `KEY_TOO_LONG`, `BODY_TOO_LARGE`, `FINGERPRINT_MISMATCH`, `CONFLICT`)
17
17
  - Replayed responses include `Idempotency-Replayed: true` header
18
18
  - Non-2xx responses are not cached (Stripe pattern — allows client retry)
19
19
  - Per-request opt-out via `skipRequest`
@@ -33,6 +33,12 @@ npm install hono-idempotency
33
33
  pnpm add hono-idempotency
34
34
  ```
35
35
 
36
+ ### Requirements
37
+
38
+ - Hono `>= 4.0.0` (peer dependency)
39
+ - TypeScript `>= 5.0` — the published `.d.ts` files are CI-tested against TS 5.0, 5.4, 5.7, 5.9, and 6.0. Older TS versions may work but are not verified.
40
+ - Node.js `>= 22` — Node.js 20 reached end-of-life on 2026-04-30 and is no longer tested.
41
+
36
42
  ## Quick Start
37
43
 
38
44
  ```ts
@@ -79,6 +85,12 @@ idempotency({
79
85
  // Maximum key length (default: 256)
80
86
  maxKeyLength: 256,
81
87
 
88
+ // Maximum request body size in bytes (no default — disabled when unset).
89
+ // Pre-checked via Content-Length, then enforced against actual body bytes.
90
+ // Returns 413 BODY_TOO_LARGE when exceeded. Only applies when an
91
+ // Idempotency-Key header is present.
92
+ maxBodySize: 1024 * 1024,
93
+
82
94
  // Custom fingerprint function (default: SHA-256 of method + path + body)
83
95
  fingerprint: (c) => `${c.req.method}:${c.req.path}`,
84
96
 
@@ -90,6 +102,11 @@ idempotency({
90
102
 
91
103
  // Custom error response handler (default: RFC 9457 Problem Details)
92
104
  onError: (error, c) => c.json({ error: error.title }, error.status),
105
+
106
+ // Observability hooks. Errors thrown inside hooks are swallowed —
107
+ // hooks must not affect request processing.
108
+ onCacheHit: (key, c) => console.log("idempotency hit", { key }),
109
+ onCacheMiss: (key, c) => console.log("idempotency miss", { key }),
93
110
  });
94
111
  ```
95
112
 
@@ -130,6 +147,7 @@ Override the default RFC 9457 error responses with a custom handler. Each error
130
147
  | `MISSING_KEY` | 400 | `required: true` and no header |
131
148
  | `KEY_TOO_LONG` | 400 | Key exceeds `maxKeyLength` |
132
149
  | `CONFLICT` | 409 | Concurrent request with same key |
150
+ | `BODY_TOO_LARGE` | 413 | Body exceeds `maxBodySize` |
133
151
  | `FINGERPRINT_MISMATCH` | 422 | Same key, different request body |
134
152
 
135
153
  ```ts
@@ -344,6 +362,7 @@ All errors follow [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9
344
362
  | 400 | `MISSING_KEY` | `/errors/missing-key` | `required: true` and no header |
345
363
  | 400 | `KEY_TOO_LONG` | `/errors/key-too-long` | Key exceeds `maxKeyLength` |
346
364
  | 409 | `CONFLICT` | `/errors/conflict` | Concurrent request with same key |
365
+ | 413 | `BODY_TOO_LARGE` | `/errors/body-too-large` | Body exceeds `maxBodySize` |
347
366
  | 422 | `FINGERPRINT_MISMATCH` | `/errors/fingerprint-mismatch` | Same key, different request body |
348
367
 
349
368
  When [hono-problem-details](https://github.com/paveg/hono-problem-details) is installed, error responses are generated using its `problemDetails().getResponse()`. Otherwise, a built-in fallback is used. No configuration needed — detection is automatic.
@@ -6,4 +6,4 @@ export {
6
6
  RECORD_STATUS_PROCESSING,
7
7
  RECORD_STATUS_COMPLETED
8
8
  };
9
- //# sourceMappingURL=chunk-I5ECYFYR.js.map
9
+ //# sourceMappingURL=chunk-ET3EW23V.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n}\n"],"mappings":";AAIO,IAAM,2BAA2B;AACjC,IAAM,0BAA0B;","names":[]}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Opt out of the multi-tenant safety warning.\n\t *\n\t * When `cacheKeyPrefix` is not set and `methods` includes any state-mutating\n\t * method (POST/PATCH/PUT/DELETE), the middleware emits a one-time\n\t * `console.warn` at factory construction time. Set this to `true` to\n\t * acknowledge that the deployment is single-tenant and silence the warning.\n\t *\n\t * @default false\n\t */\n\tdangerouslyAllowGlobalKeys?: boolean;\n}\n"],"mappings":";AAIO,IAAM,2BAA2B;AACjC,IAAM,0BAA0B;","names":[]}
package/dist/index.cjs CHANGED
@@ -162,7 +162,23 @@ var EXCLUDED_STORE_HEADERS = /* @__PURE__ */ new Set(["set-cookie", "content-len
162
162
  var DEFAULT_RETRY_AFTER = "1";
163
163
  var REPLAY_HEADER = "Idempotency-Replayed";
164
164
  var encoder2 = new TextEncoder();
165
+ var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PATCH", "PUT", "DELETE"]);
166
+ var GLOBAL_KEYS_WARNING = `[hono-idempotency] WARNING: cacheKeyPrefix is not configured.
167
+ Two users sending the same Idempotency-Key will replay each other's cached responses (cross-tenant data leak).
168
+ Fix:
169
+ cacheKeyPrefix: (c) => \`\${c.get("user")?.id ?? "anon"}:\`
170
+ Single-tenant? Set dangerouslyAllowGlobalKeys: true to silence.
171
+ Docs: https://github.com/paveg/hono-idempotency#cachekeyprefix`;
172
+ function shouldWarnGlobalKeys(options) {
173
+ if (options.dangerouslyAllowGlobalKeys === true) return false;
174
+ if (options.cacheKeyPrefix !== void 0) return false;
175
+ const methods = options.methods ?? DEFAULT_METHODS;
176
+ return methods.some((m) => MUTATING_METHODS.has(m.toUpperCase()));
177
+ }
165
178
  function idempotency(options) {
179
+ if (shouldWarnGlobalKeys(options)) {
180
+ console.warn(GLOBAL_KEYS_WARNING);
181
+ }
166
182
  const {
167
183
  store,
168
184
  headerName = "Idempotency-Key",
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/middleware.ts","../src/compat.ts","../src/errors.ts","../src/fingerprint.ts","../src/types.ts"],"sourcesContent":["export { idempotency } from \"./middleware.js\";\nexport { IdempotencyErrors, clampHttpStatus, problemResponse } from \"./errors.js\";\nexport { RECORD_STATUS_COMPLETED, RECORD_STATUS_PROCESSING } from \"./types.js\";\nexport type {\n\tIdempotencyEnv,\n\tIdempotencyOptions,\n\tIdempotencyRecord,\n\tStoredResponse,\n} from \"./types.js\";\nexport type { IdempotencyStore } from \"./stores/types.js\";\nexport type { IdempotencyErrorCode, ProblemDetail } from \"./errors.js\";\nexport type { MemoryStore, MemoryStoreOptions } from \"./stores/memory.js\";\nexport type { RedisClientLike, RedisStoreOptions } from \"./stores/redis.js\";\nexport type { KVNamespaceLike, KVStoreOptions } from \"./stores/cloudflare-kv.js\";\nexport type {\n\tD1DatabaseLike,\n\tD1PreparedStatementLike,\n\tD1StoreOptions,\n} from \"./stores/cloudflare-d1.js\";\nexport type {\n\tDurableObjectStorageLike,\n\tDurableObjectStoreOptions,\n} from \"./stores/durable-objects.js\";\n","import { createMiddleware } from \"hono/factory\";\nimport { getHonoProblemDetails } from \"./compat.js\";\nimport {\n\tIdempotencyErrors,\n\ttype ProblemDetail,\n\tclampHttpStatus,\n\tproblemResponse,\n} from \"./errors.js\";\nimport { generateFingerprint, timingSafeEqual } from \"./fingerprint.js\";\nimport {\n\ttype IdempotencyEnv,\n\ttype IdempotencyOptions,\n\tRECORD_STATUS_PROCESSING,\n\ttype StoredResponse,\n} from \"./types.js\";\n\nconst DEFAULT_METHODS = [\"POST\", \"PATCH\"];\nconst DEFAULT_MAX_KEY_LENGTH = 256;\n// Headers unsafe to replay — session cookies could leak across users\nconst EXCLUDED_STORE_HEADERS = new Set([\"set-cookie\", \"content-length\", \"transfer-encoding\"]);\nconst DEFAULT_RETRY_AFTER = \"1\";\nconst REPLAY_HEADER = \"Idempotency-Replayed\";\nconst encoder = new TextEncoder();\n\nexport function idempotency(options: IdempotencyOptions) {\n\tconst {\n\t\tstore,\n\t\theaderName = \"Idempotency-Key\",\n\t\tfingerprint: customFingerprint,\n\t\trequired = false,\n\t\tmethods = DEFAULT_METHODS,\n\t\tmaxKeyLength = DEFAULT_MAX_KEY_LENGTH,\n\t\tskipRequest,\n\t\tonError,\n\t\tcacheKeyPrefix,\n\t\tmaxBodySize,\n\t\tonCacheHit,\n\t\tonCacheMiss,\n\t} = options;\n\n\treturn createMiddleware<IdempotencyEnv>(async (c, next) => {\n\t\tif (!methods.includes(c.req.method)) {\n\t\t\treturn next();\n\t\t}\n\n\t\tif (skipRequest && (await skipRequest(c))) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst errorResponse = async (problem: ProblemDetail, extraHeaders?: Record<string, string>) => {\n\t\t\tif (onError) return onError(problem, c);\n\t\t\tconst pd = await getHonoProblemDetails();\n\t\t\tif (pd) {\n\t\t\t\tconst response = pd\n\t\t\t\t\t.problemDetails({\n\t\t\t\t\t\ttype: problem.type,\n\t\t\t\t\t\ttitle: problem.title,\n\t\t\t\t\t\tstatus: problem.status,\n\t\t\t\t\t\tdetail: problem.detail,\n\t\t\t\t\t\textensions: { code: problem.code },\n\t\t\t\t\t})\n\t\t\t\t\t.getResponse();\n\t\t\t\tif (extraHeaders) {\n\t\t\t\t\tfor (const [key, value] of Object.entries(extraHeaders)) {\n\t\t\t\t\t\tresponse.headers.set(key, value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn response;\n\t\t\t}\n\t\t\treturn problemResponse(problem, extraHeaders);\n\t\t};\n\n\t\tconst key = c.req.header(headerName);\n\n\t\tif (!key) {\n\t\t\tif (required) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.missingKey());\n\t\t\t}\n\t\t\treturn next();\n\t\t}\n\n\t\tif (encoder.encode(key).length > maxKeyLength) {\n\t\t\treturn errorResponse(IdempotencyErrors.keyTooLong(maxKeyLength));\n\t\t}\n\n\t\t// Pre-check Content-Length before reading body\n\t\tif (maxBodySize != null) {\n\t\t\tconst cl = c.req.header(\"Content-Length\");\n\t\t\tif (cl) {\n\t\t\t\tconst parsed = Number.parseInt(cl, 10);\n\t\t\t\tif (parsed < 0 || parsed > maxBodySize) {\n\t\t\t\t\treturn errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst rawPrefix =\n\t\t\ttypeof cacheKeyPrefix === \"function\" ? await cacheKeyPrefix(c) : cacheKeyPrefix;\n\t\t// Encode user-controlled components to prevent delimiter injection\n\t\tconst encodedKey = encodeURIComponent(key);\n\t\tconst baseKey = `${c.req.method}:${c.req.path}:${encodedKey}`;\n\t\tconst storeKey = rawPrefix ? `${encodeURIComponent(rawPrefix)}:${baseKey}` : baseKey;\n\n\t\tconst existing = await store.get(storeKey);\n\n\t\tif (existing) {\n\t\t\tif (existing.status === RECORD_STATUS_PROCESSING) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.conflict(), {\n\t\t\t\t\t\"Retry-After\": DEFAULT_RETRY_AFTER,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst body = await c.req.text();\n\n\t\t\tif (maxBodySize != null) {\n\t\t\t\tconst byteLength = encoder.encode(body).length;\n\t\t\t\tif (byteLength > maxBodySize) {\n\t\t\t\t\treturn errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst fp = customFingerprint\n\t\t\t\t? await customFingerprint(c)\n\t\t\t\t: await generateFingerprint(c.req.method, c.req.path, body);\n\n\t\t\tif (!timingSafeEqual(existing.fingerprint, fp)) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.fingerprintMismatch());\n\t\t\t}\n\n\t\t\tif (existing.response) {\n\t\t\t\tawait safeHook(onCacheHit, key, c);\n\t\t\t\treturn replayResponse(existing.response);\n\t\t\t}\n\n\t\t\t// Completed but no response — corrupt record; delete so lock() can re-acquire\n\t\t\tawait store.delete(storeKey);\n\t\t}\n\n\t\tconst body = await c.req.text();\n\n\t\tif (maxBodySize != null) {\n\t\t\tconst byteLength = encoder.encode(body).length;\n\t\t\tif (byteLength > maxBodySize) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));\n\t\t\t}\n\t\t}\n\n\t\tconst fp = customFingerprint\n\t\t\t? await customFingerprint(c)\n\t\t\t: await generateFingerprint(c.req.method, c.req.path, body);\n\n\t\tconst record = {\n\t\t\tkey,\n\t\t\tfingerprint: fp,\n\t\t\tstatus: RECORD_STATUS_PROCESSING,\n\t\t\tcreatedAt: Date.now(),\n\t\t};\n\n\t\tconst locked = await store.lock(storeKey, record);\n\t\tif (!locked) {\n\t\t\treturn errorResponse(IdempotencyErrors.conflict(), {\n\t\t\t\t\"Retry-After\": DEFAULT_RETRY_AFTER,\n\t\t\t});\n\t\t}\n\n\t\tc.set(\"idempotencyKey\", key);\n\t\tawait safeHook(onCacheMiss, key, c);\n\n\t\ttry {\n\t\t\tawait next();\n\t\t} catch (err) {\n\t\t\tawait store.delete(storeKey);\n\t\t\tthrow err;\n\t\t}\n\n\t\tconst res = c.res;\n\t\tif (!res.ok) {\n\t\t\t// Non-2xx: delete key (Stripe pattern) so client can retry\n\t\t\tawait store.delete(storeKey);\n\t\t\treturn;\n\t\t}\n\n\t\tconst resBody = await res.text();\n\t\tconst resHeaders: Record<string, string> = {};\n\t\tres.headers.forEach((v, k) => {\n\t\t\tif (!EXCLUDED_STORE_HEADERS.has(k.toLowerCase())) {\n\t\t\t\tresHeaders[k] = v;\n\t\t\t}\n\t\t});\n\n\t\tconst storedResponse: StoredResponse = {\n\t\t\tstatus: res.status,\n\t\t\theaders: resHeaders,\n\t\t\tbody: resBody,\n\t\t};\n\n\t\tawait store.complete(storeKey, storedResponse);\n\n\t\t// Rebuild response since we consumed body\n\t\tc.res = new Response(resBody, {\n\t\t\tstatus: res.status,\n\t\t\theaders: res.headers,\n\t\t});\n\t});\n}\n\n// Hook errors must not break idempotency guarantees\nasync function safeHook<C>(\n\tfn: ((key: string, c: C) => void | Promise<void>) | undefined,\n\tkey: string,\n\tc: C,\n): Promise<void> {\n\tif (!fn) return;\n\ttry {\n\t\tawait fn(key, c);\n\t} catch {\n\t\t// Swallow — hooks are for observability, not control flow\n\t}\n}\n\nfunction replayResponse(stored: StoredResponse) {\n\tconst headers = new Headers(stored.headers);\n\theaders.set(REPLAY_HEADER, \"true\");\n\n\treturn new Response(stored.body, {\n\t\tstatus: clampHttpStatus(stored.status),\n\t\theaders,\n\t});\n}\n","type HonoProblemDetails = typeof import(\"hono-problem-details\");\n\nlet cached: HonoProblemDetails | null | undefined;\n\nexport async function getHonoProblemDetails(): Promise<HonoProblemDetails | null> {\n\tif (cached === undefined) {\n\t\ttry {\n\t\t\tcached = await import(\"hono-problem-details\");\n\t\t} catch {\n\t\t\tcached = null;\n\t\t}\n\t}\n\treturn cached;\n}\n","export type IdempotencyErrorCode =\n\t| \"MISSING_KEY\"\n\t| \"KEY_TOO_LONG\"\n\t| \"BODY_TOO_LARGE\"\n\t| \"FINGERPRINT_MISMATCH\"\n\t| \"CONFLICT\";\n\nexport interface ProblemDetail {\n\ttype: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail: string;\n\tcode: IdempotencyErrorCode;\n}\n\n/** Clamp HTTP status to 200-599 integer range; returns 500 for out-of-range or non-integer values. */\nexport function clampHttpStatus(status: number): number {\n\treturn Number.isInteger(status) && status >= 200 && status <= 599 ? status : 500;\n}\n\nexport function problemResponse(\n\tproblem: ProblemDetail,\n\textraHeaders?: Record<string, string>,\n): Response {\n\tlet body: string;\n\tlet status: number;\n\ttry {\n\t\tbody = JSON.stringify(problem);\n\t\tstatus = clampHttpStatus(problem.status);\n\t} catch {\n\t\tbody = '{\"title\":\"Internal Server Error\",\"status\":500}';\n\t\tstatus = 500;\n\t}\n\treturn new Response(body, {\n\t\tstatus,\n\t\theaders: {\n\t\t\t\"Content-Type\": PROBLEM_CONTENT_TYPE,\n\t\t\t...extraHeaders,\n\t\t},\n\t});\n}\n\nconst PROBLEM_CONTENT_TYPE = \"application/problem+json; charset=utf-8\";\nconst BASE_URL = \"https://hono-idempotency.dev/errors\";\n\nexport const IdempotencyErrors = {\n\tmissingKey(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/missing-key`,\n\t\t\ttitle: \"Idempotency-Key header is required\",\n\t\t\tstatus: 400,\n\t\t\tdetail: \"This endpoint requires an Idempotency-Key header\",\n\t\t\tcode: \"MISSING_KEY\",\n\t\t};\n\t},\n\n\tkeyTooLong(maxLength: number): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/key-too-long`,\n\t\t\ttitle: \"Idempotency-Key is too long\",\n\t\t\tstatus: 400,\n\t\t\tdetail: `Idempotency-Key must be at most ${maxLength} characters`,\n\t\t\tcode: \"KEY_TOO_LONG\",\n\t\t};\n\t},\n\n\tbodyTooLarge(maxSize: number): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/body-too-large`,\n\t\t\ttitle: \"Request body is too large\",\n\t\t\tstatus: 413,\n\t\t\tdetail: `Request body must be at most ${maxSize} bytes`,\n\t\t\tcode: \"BODY_TOO_LARGE\",\n\t\t};\n\t},\n\n\tfingerprintMismatch(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/fingerprint-mismatch`,\n\t\t\ttitle: \"Idempotency-Key is already used with a different request\",\n\t\t\tstatus: 422,\n\t\t\tdetail:\n\t\t\t\t\"A request with the same idempotency key but different parameters was already processed\",\n\t\t\tcode: \"FINGERPRINT_MISMATCH\",\n\t\t};\n\t},\n\n\tconflict(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/conflict`,\n\t\t\ttitle: \"A request is outstanding for this idempotency key\",\n\t\t\tstatus: 409,\n\t\t\tdetail: \"A request with the same idempotency key is currently being processed\",\n\t\t\tcode: \"CONFLICT\",\n\t\t};\n\t},\n} as const;\n","const encoder = new TextEncoder();\nconst HEX_TABLE = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, \"0\"));\n\nexport async function generateFingerprint(\n\tmethod: string,\n\tpath: string,\n\tbody: string,\n): Promise<string> {\n\tconst data = `${method}:${path}:${body}`;\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", encoder.encode(data));\n\tconst bytes = new Uint8Array(hashBuffer);\n\tlet hex = \"\";\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\thex += HEX_TABLE[bytes[i]];\n\t}\n\treturn hex;\n}\n\n/** Constant-time string comparison to prevent timing side-channel attacks on fingerprint matching. */\nexport function timingSafeEqual(a: string, b: string): boolean {\n\tif (a.length !== b.length) return false;\n\tconst aBytes = encoder.encode(a);\n\tconst bBytes = encoder.encode(b);\n\tlet diff = 0;\n\tfor (let i = 0; i < aBytes.length; i++) {\n\t\tdiff |= aBytes[i] ^ bBytes[i];\n\t}\n\treturn diff === 0;\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n}\n"],"mappings":"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","names":["encoder","key","body","fp"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/middleware.ts","../src/compat.ts","../src/errors.ts","../src/fingerprint.ts","../src/types.ts"],"sourcesContent":["export { idempotency } from \"./middleware.js\";\nexport { IdempotencyErrors, clampHttpStatus, problemResponse } from \"./errors.js\";\nexport { RECORD_STATUS_COMPLETED, RECORD_STATUS_PROCESSING } from \"./types.js\";\nexport type {\n\tIdempotencyEnv,\n\tIdempotencyOptions,\n\tIdempotencyRecord,\n\tStoredResponse,\n} from \"./types.js\";\nexport type { IdempotencyStore } from \"./stores/types.js\";\nexport type { IdempotencyErrorCode, ProblemDetail } from \"./errors.js\";\nexport type { MemoryStore, MemoryStoreOptions } from \"./stores/memory.js\";\nexport type { RedisClientLike, RedisStoreOptions } from \"./stores/redis.js\";\nexport type { KVNamespaceLike, KVStoreOptions } from \"./stores/cloudflare-kv.js\";\nexport type {\n\tD1DatabaseLike,\n\tD1PreparedStatementLike,\n\tD1StoreOptions,\n} from \"./stores/cloudflare-d1.js\";\nexport type {\n\tDurableObjectStorageLike,\n\tDurableObjectStoreOptions,\n} from \"./stores/durable-objects.js\";\n","import { createMiddleware } from \"hono/factory\";\nimport { getHonoProblemDetails } from \"./compat.js\";\nimport {\n\tIdempotencyErrors,\n\ttype ProblemDetail,\n\tclampHttpStatus,\n\tproblemResponse,\n} from \"./errors.js\";\nimport { generateFingerprint, timingSafeEqual } from \"./fingerprint.js\";\nimport {\n\ttype IdempotencyEnv,\n\ttype IdempotencyOptions,\n\tRECORD_STATUS_PROCESSING,\n\ttype StoredResponse,\n} from \"./types.js\";\n\nconst DEFAULT_METHODS = [\"POST\", \"PATCH\"];\nconst DEFAULT_MAX_KEY_LENGTH = 256;\n// Headers unsafe to replay — session cookies could leak across users\nconst EXCLUDED_STORE_HEADERS = new Set([\"set-cookie\", \"content-length\", \"transfer-encoding\"]);\nconst DEFAULT_RETRY_AFTER = \"1\";\nconst REPLAY_HEADER = \"Idempotency-Replayed\";\nconst encoder = new TextEncoder();\nconst MUTATING_METHODS = new Set([\"POST\", \"PATCH\", \"PUT\", \"DELETE\"]);\n\nconst GLOBAL_KEYS_WARNING = `[hono-idempotency] WARNING: cacheKeyPrefix is not configured.\n Two users sending the same Idempotency-Key will replay each other's cached responses (cross-tenant data leak).\n Fix:\n cacheKeyPrefix: (c) => \\`\\${c.get(\"user\")?.id ?? \"anon\"}:\\`\n Single-tenant? Set dangerouslyAllowGlobalKeys: true to silence.\n Docs: https://github.com/paveg/hono-idempotency#cachekeyprefix`;\n\nfunction shouldWarnGlobalKeys(options: IdempotencyOptions): boolean {\n\tif (options.dangerouslyAllowGlobalKeys === true) return false;\n\tif (options.cacheKeyPrefix !== undefined) return false;\n\tconst methods = options.methods ?? DEFAULT_METHODS;\n\treturn methods.some((m) => MUTATING_METHODS.has(m.toUpperCase()));\n}\n\nexport function idempotency(options: IdempotencyOptions) {\n\tif (shouldWarnGlobalKeys(options)) {\n\t\tconsole.warn(GLOBAL_KEYS_WARNING);\n\t}\n\tconst {\n\t\tstore,\n\t\theaderName = \"Idempotency-Key\",\n\t\tfingerprint: customFingerprint,\n\t\trequired = false,\n\t\tmethods = DEFAULT_METHODS,\n\t\tmaxKeyLength = DEFAULT_MAX_KEY_LENGTH,\n\t\tskipRequest,\n\t\tonError,\n\t\tcacheKeyPrefix,\n\t\tmaxBodySize,\n\t\tonCacheHit,\n\t\tonCacheMiss,\n\t} = options;\n\n\treturn createMiddleware<IdempotencyEnv>(async (c, next) => {\n\t\tif (!methods.includes(c.req.method)) {\n\t\t\treturn next();\n\t\t}\n\n\t\tif (skipRequest && (await skipRequest(c))) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst errorResponse = async (problem: ProblemDetail, extraHeaders?: Record<string, string>) => {\n\t\t\tif (onError) return onError(problem, c);\n\t\t\tconst pd = await getHonoProblemDetails();\n\t\t\tif (pd) {\n\t\t\t\tconst response = pd\n\t\t\t\t\t.problemDetails({\n\t\t\t\t\t\ttype: problem.type,\n\t\t\t\t\t\ttitle: problem.title,\n\t\t\t\t\t\tstatus: problem.status,\n\t\t\t\t\t\tdetail: problem.detail,\n\t\t\t\t\t\textensions: { code: problem.code },\n\t\t\t\t\t})\n\t\t\t\t\t.getResponse();\n\t\t\t\tif (extraHeaders) {\n\t\t\t\t\tfor (const [key, value] of Object.entries(extraHeaders)) {\n\t\t\t\t\t\tresponse.headers.set(key, value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn response;\n\t\t\t}\n\t\t\treturn problemResponse(problem, extraHeaders);\n\t\t};\n\n\t\tconst key = c.req.header(headerName);\n\n\t\tif (!key) {\n\t\t\tif (required) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.missingKey());\n\t\t\t}\n\t\t\treturn next();\n\t\t}\n\n\t\tif (encoder.encode(key).length > maxKeyLength) {\n\t\t\treturn errorResponse(IdempotencyErrors.keyTooLong(maxKeyLength));\n\t\t}\n\n\t\t// Pre-check Content-Length before reading body\n\t\tif (maxBodySize != null) {\n\t\t\tconst cl = c.req.header(\"Content-Length\");\n\t\t\tif (cl) {\n\t\t\t\tconst parsed = Number.parseInt(cl, 10);\n\t\t\t\tif (parsed < 0 || parsed > maxBodySize) {\n\t\t\t\t\treturn errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst rawPrefix =\n\t\t\ttypeof cacheKeyPrefix === \"function\" ? await cacheKeyPrefix(c) : cacheKeyPrefix;\n\t\t// Encode user-controlled components to prevent delimiter injection\n\t\tconst encodedKey = encodeURIComponent(key);\n\t\tconst baseKey = `${c.req.method}:${c.req.path}:${encodedKey}`;\n\t\tconst storeKey = rawPrefix ? `${encodeURIComponent(rawPrefix)}:${baseKey}` : baseKey;\n\n\t\tconst existing = await store.get(storeKey);\n\n\t\tif (existing) {\n\t\t\tif (existing.status === RECORD_STATUS_PROCESSING) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.conflict(), {\n\t\t\t\t\t\"Retry-After\": DEFAULT_RETRY_AFTER,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst body = await c.req.text();\n\n\t\t\tif (maxBodySize != null) {\n\t\t\t\tconst byteLength = encoder.encode(body).length;\n\t\t\t\tif (byteLength > maxBodySize) {\n\t\t\t\t\treturn errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst fp = customFingerprint\n\t\t\t\t? await customFingerprint(c)\n\t\t\t\t: await generateFingerprint(c.req.method, c.req.path, body);\n\n\t\t\tif (!timingSafeEqual(existing.fingerprint, fp)) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.fingerprintMismatch());\n\t\t\t}\n\n\t\t\tif (existing.response) {\n\t\t\t\tawait safeHook(onCacheHit, key, c);\n\t\t\t\treturn replayResponse(existing.response);\n\t\t\t}\n\n\t\t\t// Completed but no response — corrupt record; delete so lock() can re-acquire\n\t\t\tawait store.delete(storeKey);\n\t\t}\n\n\t\tconst body = await c.req.text();\n\n\t\tif (maxBodySize != null) {\n\t\t\tconst byteLength = encoder.encode(body).length;\n\t\t\tif (byteLength > maxBodySize) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));\n\t\t\t}\n\t\t}\n\n\t\tconst fp = customFingerprint\n\t\t\t? await customFingerprint(c)\n\t\t\t: await generateFingerprint(c.req.method, c.req.path, body);\n\n\t\tconst record = {\n\t\t\tkey,\n\t\t\tfingerprint: fp,\n\t\t\tstatus: RECORD_STATUS_PROCESSING,\n\t\t\tcreatedAt: Date.now(),\n\t\t};\n\n\t\tconst locked = await store.lock(storeKey, record);\n\t\tif (!locked) {\n\t\t\treturn errorResponse(IdempotencyErrors.conflict(), {\n\t\t\t\t\"Retry-After\": DEFAULT_RETRY_AFTER,\n\t\t\t});\n\t\t}\n\n\t\tc.set(\"idempotencyKey\", key);\n\t\tawait safeHook(onCacheMiss, key, c);\n\n\t\ttry {\n\t\t\tawait next();\n\t\t} catch (err) {\n\t\t\tawait store.delete(storeKey);\n\t\t\tthrow err;\n\t\t}\n\n\t\tconst res = c.res;\n\t\tif (!res.ok) {\n\t\t\t// Non-2xx: delete key (Stripe pattern) so client can retry\n\t\t\tawait store.delete(storeKey);\n\t\t\treturn;\n\t\t}\n\n\t\tconst resBody = await res.text();\n\t\tconst resHeaders: Record<string, string> = {};\n\t\tres.headers.forEach((v, k) => {\n\t\t\tif (!EXCLUDED_STORE_HEADERS.has(k.toLowerCase())) {\n\t\t\t\tresHeaders[k] = v;\n\t\t\t}\n\t\t});\n\n\t\tconst storedResponse: StoredResponse = {\n\t\t\tstatus: res.status,\n\t\t\theaders: resHeaders,\n\t\t\tbody: resBody,\n\t\t};\n\n\t\tawait store.complete(storeKey, storedResponse);\n\n\t\t// Rebuild response since we consumed body\n\t\tc.res = new Response(resBody, {\n\t\t\tstatus: res.status,\n\t\t\theaders: res.headers,\n\t\t});\n\t});\n}\n\n// Hook errors must not break idempotency guarantees\nasync function safeHook<C>(\n\tfn: ((key: string, c: C) => void | Promise<void>) | undefined,\n\tkey: string,\n\tc: C,\n): Promise<void> {\n\tif (!fn) return;\n\ttry {\n\t\tawait fn(key, c);\n\t} catch {\n\t\t// Swallow — hooks are for observability, not control flow\n\t}\n}\n\nfunction replayResponse(stored: StoredResponse) {\n\tconst headers = new Headers(stored.headers);\n\theaders.set(REPLAY_HEADER, \"true\");\n\n\treturn new Response(stored.body, {\n\t\tstatus: clampHttpStatus(stored.status),\n\t\theaders,\n\t});\n}\n","type HonoProblemDetails = typeof import(\"hono-problem-details\");\n\nlet cached: HonoProblemDetails | null | undefined;\n\nexport async function getHonoProblemDetails(): Promise<HonoProblemDetails | null> {\n\tif (cached === undefined) {\n\t\ttry {\n\t\t\tcached = await import(\"hono-problem-details\");\n\t\t} catch {\n\t\t\tcached = null;\n\t\t}\n\t}\n\treturn cached;\n}\n","export type IdempotencyErrorCode =\n\t| \"MISSING_KEY\"\n\t| \"KEY_TOO_LONG\"\n\t| \"BODY_TOO_LARGE\"\n\t| \"FINGERPRINT_MISMATCH\"\n\t| \"CONFLICT\";\n\nexport interface ProblemDetail {\n\ttype: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail: string;\n\tcode: IdempotencyErrorCode;\n}\n\n/** Clamp HTTP status to 200-599 integer range; returns 500 for out-of-range or non-integer values. */\nexport function clampHttpStatus(status: number): number {\n\treturn Number.isInteger(status) && status >= 200 && status <= 599 ? status : 500;\n}\n\nexport function problemResponse(\n\tproblem: ProblemDetail,\n\textraHeaders?: Record<string, string>,\n): Response {\n\tlet body: string;\n\tlet status: number;\n\ttry {\n\t\tbody = JSON.stringify(problem);\n\t\tstatus = clampHttpStatus(problem.status);\n\t} catch {\n\t\tbody = '{\"title\":\"Internal Server Error\",\"status\":500}';\n\t\tstatus = 500;\n\t}\n\treturn new Response(body, {\n\t\tstatus,\n\t\theaders: {\n\t\t\t\"Content-Type\": PROBLEM_CONTENT_TYPE,\n\t\t\t...extraHeaders,\n\t\t},\n\t});\n}\n\nconst PROBLEM_CONTENT_TYPE = \"application/problem+json; charset=utf-8\";\nconst BASE_URL = \"https://hono-idempotency.dev/errors\";\n\nexport const IdempotencyErrors = {\n\tmissingKey(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/missing-key`,\n\t\t\ttitle: \"Idempotency-Key header is required\",\n\t\t\tstatus: 400,\n\t\t\tdetail: \"This endpoint requires an Idempotency-Key header\",\n\t\t\tcode: \"MISSING_KEY\",\n\t\t};\n\t},\n\n\tkeyTooLong(maxLength: number): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/key-too-long`,\n\t\t\ttitle: \"Idempotency-Key is too long\",\n\t\t\tstatus: 400,\n\t\t\tdetail: `Idempotency-Key must be at most ${maxLength} characters`,\n\t\t\tcode: \"KEY_TOO_LONG\",\n\t\t};\n\t},\n\n\tbodyTooLarge(maxSize: number): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/body-too-large`,\n\t\t\ttitle: \"Request body is too large\",\n\t\t\tstatus: 413,\n\t\t\tdetail: `Request body must be at most ${maxSize} bytes`,\n\t\t\tcode: \"BODY_TOO_LARGE\",\n\t\t};\n\t},\n\n\tfingerprintMismatch(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/fingerprint-mismatch`,\n\t\t\ttitle: \"Idempotency-Key is already used with a different request\",\n\t\t\tstatus: 422,\n\t\t\tdetail:\n\t\t\t\t\"A request with the same idempotency key but different parameters was already processed\",\n\t\t\tcode: \"FINGERPRINT_MISMATCH\",\n\t\t};\n\t},\n\n\tconflict(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/conflict`,\n\t\t\ttitle: \"A request is outstanding for this idempotency key\",\n\t\t\tstatus: 409,\n\t\t\tdetail: \"A request with the same idempotency key is currently being processed\",\n\t\t\tcode: \"CONFLICT\",\n\t\t};\n\t},\n} as const;\n","const encoder = new TextEncoder();\nconst HEX_TABLE = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, \"0\"));\n\nexport async function generateFingerprint(\n\tmethod: string,\n\tpath: string,\n\tbody: string,\n): Promise<string> {\n\tconst data = `${method}:${path}:${body}`;\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", encoder.encode(data));\n\tconst bytes = new Uint8Array(hashBuffer);\n\tlet hex = \"\";\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\thex += HEX_TABLE[bytes[i]];\n\t}\n\treturn hex;\n}\n\n/** Constant-time string comparison to prevent timing side-channel attacks on fingerprint matching. */\nexport function timingSafeEqual(a: string, b: string): boolean {\n\tif (a.length !== b.length) return false;\n\tconst aBytes = encoder.encode(a);\n\tconst bBytes = encoder.encode(b);\n\tlet diff = 0;\n\tfor (let i = 0; i < aBytes.length; i++) {\n\t\tdiff |= aBytes[i] ^ bBytes[i];\n\t}\n\treturn diff === 0;\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Opt out of the multi-tenant safety warning.\n\t *\n\t * When `cacheKeyPrefix` is not set and `methods` includes any state-mutating\n\t * method (POST/PATCH/PUT/DELETE), the middleware emits a one-time\n\t * `console.warn` at factory construction time. Set this to `true` to\n\t * acknowledge that the deployment is single-tenant and silence the warning.\n\t *\n\t * @default false\n\t */\n\tdangerouslyAllowGlobalKeys?: boolean;\n}\n"],"mappings":"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","names":["encoder","key","body","fp"]}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as hono from 'hono';
2
- import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-Kb-9sxwk.cjs';
3
- export { b as IdempotencyErrorCode, c as IdempotencyErrors, d as IdempotencyRecord, e as IdempotencyStore, P as ProblemDetail, R as RECORD_STATUS_COMPLETED, f as RECORD_STATUS_PROCESSING, S as StoredResponse, g as clampHttpStatus, p as problemResponse } from './types-Kb-9sxwk.cjs';
2
+ import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-CI-upMvQ.cjs';
3
+ export { b as IdempotencyErrorCode, c as IdempotencyErrors, d as IdempotencyRecord, e as IdempotencyStore, P as ProblemDetail, R as RECORD_STATUS_COMPLETED, f as RECORD_STATUS_PROCESSING, S as StoredResponse, g as clampHttpStatus, p as problemResponse } from './types-CI-upMvQ.cjs';
4
4
  export { MemoryStore, MemoryStoreOptions } from './stores/memory.cjs';
5
5
  export { RedisClientLike, RedisStoreOptions } from './stores/redis.cjs';
6
6
  export { KVNamespaceLike, KVStoreOptions } from './stores/cloudflare-kv.cjs';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as hono from 'hono';
2
- import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-Kb-9sxwk.js';
3
- export { b as IdempotencyErrorCode, c as IdempotencyErrors, d as IdempotencyRecord, e as IdempotencyStore, P as ProblemDetail, R as RECORD_STATUS_COMPLETED, f as RECORD_STATUS_PROCESSING, S as StoredResponse, g as clampHttpStatus, p as problemResponse } from './types-Kb-9sxwk.js';
2
+ import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-CI-upMvQ.js';
3
+ export { b as IdempotencyErrorCode, c as IdempotencyErrors, d as IdempotencyRecord, e as IdempotencyStore, P as ProblemDetail, R as RECORD_STATUS_COMPLETED, f as RECORD_STATUS_PROCESSING, S as StoredResponse, g as clampHttpStatus, p as problemResponse } from './types-CI-upMvQ.js';
4
4
  export { MemoryStore, MemoryStoreOptions } from './stores/memory.js';
5
5
  export { RedisClientLike, RedisStoreOptions } from './stores/redis.js';
6
6
  export { KVNamespaceLike, KVStoreOptions } from './stores/cloudflare-kv.js';
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  RECORD_STATUS_COMPLETED,
3
3
  RECORD_STATUS_PROCESSING
4
- } from "./chunk-I5ECYFYR.js";
4
+ } from "./chunk-ET3EW23V.js";
5
5
 
6
6
  // src/middleware.ts
7
7
  import { createMiddleware } from "hono/factory";
@@ -122,7 +122,23 @@ var EXCLUDED_STORE_HEADERS = /* @__PURE__ */ new Set(["set-cookie", "content-len
122
122
  var DEFAULT_RETRY_AFTER = "1";
123
123
  var REPLAY_HEADER = "Idempotency-Replayed";
124
124
  var encoder2 = new TextEncoder();
125
+ var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PATCH", "PUT", "DELETE"]);
126
+ var GLOBAL_KEYS_WARNING = `[hono-idempotency] WARNING: cacheKeyPrefix is not configured.
127
+ Two users sending the same Idempotency-Key will replay each other's cached responses (cross-tenant data leak).
128
+ Fix:
129
+ cacheKeyPrefix: (c) => \`\${c.get("user")?.id ?? "anon"}:\`
130
+ Single-tenant? Set dangerouslyAllowGlobalKeys: true to silence.
131
+ Docs: https://github.com/paveg/hono-idempotency#cachekeyprefix`;
132
+ function shouldWarnGlobalKeys(options) {
133
+ if (options.dangerouslyAllowGlobalKeys === true) return false;
134
+ if (options.cacheKeyPrefix !== void 0) return false;
135
+ const methods = options.methods ?? DEFAULT_METHODS;
136
+ return methods.some((m) => MUTATING_METHODS.has(m.toUpperCase()));
137
+ }
125
138
  function idempotency(options) {
139
+ if (shouldWarnGlobalKeys(options)) {
140
+ console.warn(GLOBAL_KEYS_WARNING);
141
+ }
126
142
  const {
127
143
  store,
128
144
  headerName = "Idempotency-Key",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/middleware.ts","../src/compat.ts","../src/errors.ts","../src/fingerprint.ts"],"sourcesContent":["import { createMiddleware } from \"hono/factory\";\nimport { getHonoProblemDetails } from \"./compat.js\";\nimport {\n\tIdempotencyErrors,\n\ttype ProblemDetail,\n\tclampHttpStatus,\n\tproblemResponse,\n} from \"./errors.js\";\nimport { generateFingerprint, timingSafeEqual } from \"./fingerprint.js\";\nimport {\n\ttype IdempotencyEnv,\n\ttype IdempotencyOptions,\n\tRECORD_STATUS_PROCESSING,\n\ttype StoredResponse,\n} from \"./types.js\";\n\nconst DEFAULT_METHODS = [\"POST\", \"PATCH\"];\nconst DEFAULT_MAX_KEY_LENGTH = 256;\n// Headers unsafe to replay — session cookies could leak across users\nconst EXCLUDED_STORE_HEADERS = new Set([\"set-cookie\", \"content-length\", \"transfer-encoding\"]);\nconst DEFAULT_RETRY_AFTER = \"1\";\nconst REPLAY_HEADER = \"Idempotency-Replayed\";\nconst encoder = new TextEncoder();\n\nexport function idempotency(options: IdempotencyOptions) {\n\tconst {\n\t\tstore,\n\t\theaderName = \"Idempotency-Key\",\n\t\tfingerprint: customFingerprint,\n\t\trequired = false,\n\t\tmethods = DEFAULT_METHODS,\n\t\tmaxKeyLength = DEFAULT_MAX_KEY_LENGTH,\n\t\tskipRequest,\n\t\tonError,\n\t\tcacheKeyPrefix,\n\t\tmaxBodySize,\n\t\tonCacheHit,\n\t\tonCacheMiss,\n\t} = options;\n\n\treturn createMiddleware<IdempotencyEnv>(async (c, next) => {\n\t\tif (!methods.includes(c.req.method)) {\n\t\t\treturn next();\n\t\t}\n\n\t\tif (skipRequest && (await skipRequest(c))) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst errorResponse = async (problem: ProblemDetail, extraHeaders?: Record<string, string>) => {\n\t\t\tif (onError) return onError(problem, c);\n\t\t\tconst pd = await getHonoProblemDetails();\n\t\t\tif (pd) {\n\t\t\t\tconst response = pd\n\t\t\t\t\t.problemDetails({\n\t\t\t\t\t\ttype: problem.type,\n\t\t\t\t\t\ttitle: problem.title,\n\t\t\t\t\t\tstatus: problem.status,\n\t\t\t\t\t\tdetail: problem.detail,\n\t\t\t\t\t\textensions: { code: problem.code },\n\t\t\t\t\t})\n\t\t\t\t\t.getResponse();\n\t\t\t\tif (extraHeaders) {\n\t\t\t\t\tfor (const [key, value] of Object.entries(extraHeaders)) {\n\t\t\t\t\t\tresponse.headers.set(key, value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn response;\n\t\t\t}\n\t\t\treturn problemResponse(problem, extraHeaders);\n\t\t};\n\n\t\tconst key = c.req.header(headerName);\n\n\t\tif (!key) {\n\t\t\tif (required) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.missingKey());\n\t\t\t}\n\t\t\treturn next();\n\t\t}\n\n\t\tif (encoder.encode(key).length > maxKeyLength) {\n\t\t\treturn errorResponse(IdempotencyErrors.keyTooLong(maxKeyLength));\n\t\t}\n\n\t\t// Pre-check Content-Length before reading body\n\t\tif (maxBodySize != null) {\n\t\t\tconst cl = c.req.header(\"Content-Length\");\n\t\t\tif (cl) {\n\t\t\t\tconst parsed = Number.parseInt(cl, 10);\n\t\t\t\tif (parsed < 0 || parsed > maxBodySize) {\n\t\t\t\t\treturn errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst rawPrefix =\n\t\t\ttypeof cacheKeyPrefix === \"function\" ? await cacheKeyPrefix(c) : cacheKeyPrefix;\n\t\t// Encode user-controlled components to prevent delimiter injection\n\t\tconst encodedKey = encodeURIComponent(key);\n\t\tconst baseKey = `${c.req.method}:${c.req.path}:${encodedKey}`;\n\t\tconst storeKey = rawPrefix ? `${encodeURIComponent(rawPrefix)}:${baseKey}` : baseKey;\n\n\t\tconst existing = await store.get(storeKey);\n\n\t\tif (existing) {\n\t\t\tif (existing.status === RECORD_STATUS_PROCESSING) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.conflict(), {\n\t\t\t\t\t\"Retry-After\": DEFAULT_RETRY_AFTER,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst body = await c.req.text();\n\n\t\t\tif (maxBodySize != null) {\n\t\t\t\tconst byteLength = encoder.encode(body).length;\n\t\t\t\tif (byteLength > maxBodySize) {\n\t\t\t\t\treturn errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst fp = customFingerprint\n\t\t\t\t? await customFingerprint(c)\n\t\t\t\t: await generateFingerprint(c.req.method, c.req.path, body);\n\n\t\t\tif (!timingSafeEqual(existing.fingerprint, fp)) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.fingerprintMismatch());\n\t\t\t}\n\n\t\t\tif (existing.response) {\n\t\t\t\tawait safeHook(onCacheHit, key, c);\n\t\t\t\treturn replayResponse(existing.response);\n\t\t\t}\n\n\t\t\t// Completed but no response — corrupt record; delete so lock() can re-acquire\n\t\t\tawait store.delete(storeKey);\n\t\t}\n\n\t\tconst body = await c.req.text();\n\n\t\tif (maxBodySize != null) {\n\t\t\tconst byteLength = encoder.encode(body).length;\n\t\t\tif (byteLength > maxBodySize) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));\n\t\t\t}\n\t\t}\n\n\t\tconst fp = customFingerprint\n\t\t\t? await customFingerprint(c)\n\t\t\t: await generateFingerprint(c.req.method, c.req.path, body);\n\n\t\tconst record = {\n\t\t\tkey,\n\t\t\tfingerprint: fp,\n\t\t\tstatus: RECORD_STATUS_PROCESSING,\n\t\t\tcreatedAt: Date.now(),\n\t\t};\n\n\t\tconst locked = await store.lock(storeKey, record);\n\t\tif (!locked) {\n\t\t\treturn errorResponse(IdempotencyErrors.conflict(), {\n\t\t\t\t\"Retry-After\": DEFAULT_RETRY_AFTER,\n\t\t\t});\n\t\t}\n\n\t\tc.set(\"idempotencyKey\", key);\n\t\tawait safeHook(onCacheMiss, key, c);\n\n\t\ttry {\n\t\t\tawait next();\n\t\t} catch (err) {\n\t\t\tawait store.delete(storeKey);\n\t\t\tthrow err;\n\t\t}\n\n\t\tconst res = c.res;\n\t\tif (!res.ok) {\n\t\t\t// Non-2xx: delete key (Stripe pattern) so client can retry\n\t\t\tawait store.delete(storeKey);\n\t\t\treturn;\n\t\t}\n\n\t\tconst resBody = await res.text();\n\t\tconst resHeaders: Record<string, string> = {};\n\t\tres.headers.forEach((v, k) => {\n\t\t\tif (!EXCLUDED_STORE_HEADERS.has(k.toLowerCase())) {\n\t\t\t\tresHeaders[k] = v;\n\t\t\t}\n\t\t});\n\n\t\tconst storedResponse: StoredResponse = {\n\t\t\tstatus: res.status,\n\t\t\theaders: resHeaders,\n\t\t\tbody: resBody,\n\t\t};\n\n\t\tawait store.complete(storeKey, storedResponse);\n\n\t\t// Rebuild response since we consumed body\n\t\tc.res = new Response(resBody, {\n\t\t\tstatus: res.status,\n\t\t\theaders: res.headers,\n\t\t});\n\t});\n}\n\n// Hook errors must not break idempotency guarantees\nasync function safeHook<C>(\n\tfn: ((key: string, c: C) => void | Promise<void>) | undefined,\n\tkey: string,\n\tc: C,\n): Promise<void> {\n\tif (!fn) return;\n\ttry {\n\t\tawait fn(key, c);\n\t} catch {\n\t\t// Swallow — hooks are for observability, not control flow\n\t}\n}\n\nfunction replayResponse(stored: StoredResponse) {\n\tconst headers = new Headers(stored.headers);\n\theaders.set(REPLAY_HEADER, \"true\");\n\n\treturn new Response(stored.body, {\n\t\tstatus: clampHttpStatus(stored.status),\n\t\theaders,\n\t});\n}\n","type HonoProblemDetails = typeof import(\"hono-problem-details\");\n\nlet cached: HonoProblemDetails | null | undefined;\n\nexport async function getHonoProblemDetails(): Promise<HonoProblemDetails | null> {\n\tif (cached === undefined) {\n\t\ttry {\n\t\t\tcached = await import(\"hono-problem-details\");\n\t\t} catch {\n\t\t\tcached = null;\n\t\t}\n\t}\n\treturn cached;\n}\n","export type IdempotencyErrorCode =\n\t| \"MISSING_KEY\"\n\t| \"KEY_TOO_LONG\"\n\t| \"BODY_TOO_LARGE\"\n\t| \"FINGERPRINT_MISMATCH\"\n\t| \"CONFLICT\";\n\nexport interface ProblemDetail {\n\ttype: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail: string;\n\tcode: IdempotencyErrorCode;\n}\n\n/** Clamp HTTP status to 200-599 integer range; returns 500 for out-of-range or non-integer values. */\nexport function clampHttpStatus(status: number): number {\n\treturn Number.isInteger(status) && status >= 200 && status <= 599 ? status : 500;\n}\n\nexport function problemResponse(\n\tproblem: ProblemDetail,\n\textraHeaders?: Record<string, string>,\n): Response {\n\tlet body: string;\n\tlet status: number;\n\ttry {\n\t\tbody = JSON.stringify(problem);\n\t\tstatus = clampHttpStatus(problem.status);\n\t} catch {\n\t\tbody = '{\"title\":\"Internal Server Error\",\"status\":500}';\n\t\tstatus = 500;\n\t}\n\treturn new Response(body, {\n\t\tstatus,\n\t\theaders: {\n\t\t\t\"Content-Type\": PROBLEM_CONTENT_TYPE,\n\t\t\t...extraHeaders,\n\t\t},\n\t});\n}\n\nconst PROBLEM_CONTENT_TYPE = \"application/problem+json; charset=utf-8\";\nconst BASE_URL = \"https://hono-idempotency.dev/errors\";\n\nexport const IdempotencyErrors = {\n\tmissingKey(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/missing-key`,\n\t\t\ttitle: \"Idempotency-Key header is required\",\n\t\t\tstatus: 400,\n\t\t\tdetail: \"This endpoint requires an Idempotency-Key header\",\n\t\t\tcode: \"MISSING_KEY\",\n\t\t};\n\t},\n\n\tkeyTooLong(maxLength: number): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/key-too-long`,\n\t\t\ttitle: \"Idempotency-Key is too long\",\n\t\t\tstatus: 400,\n\t\t\tdetail: `Idempotency-Key must be at most ${maxLength} characters`,\n\t\t\tcode: \"KEY_TOO_LONG\",\n\t\t};\n\t},\n\n\tbodyTooLarge(maxSize: number): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/body-too-large`,\n\t\t\ttitle: \"Request body is too large\",\n\t\t\tstatus: 413,\n\t\t\tdetail: `Request body must be at most ${maxSize} bytes`,\n\t\t\tcode: \"BODY_TOO_LARGE\",\n\t\t};\n\t},\n\n\tfingerprintMismatch(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/fingerprint-mismatch`,\n\t\t\ttitle: \"Idempotency-Key is already used with a different request\",\n\t\t\tstatus: 422,\n\t\t\tdetail:\n\t\t\t\t\"A request with the same idempotency key but different parameters was already processed\",\n\t\t\tcode: \"FINGERPRINT_MISMATCH\",\n\t\t};\n\t},\n\n\tconflict(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/conflict`,\n\t\t\ttitle: \"A request is outstanding for this idempotency key\",\n\t\t\tstatus: 409,\n\t\t\tdetail: \"A request with the same idempotency key is currently being processed\",\n\t\t\tcode: \"CONFLICT\",\n\t\t};\n\t},\n} as const;\n","const encoder = new TextEncoder();\nconst HEX_TABLE = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, \"0\"));\n\nexport async function generateFingerprint(\n\tmethod: string,\n\tpath: string,\n\tbody: string,\n): Promise<string> {\n\tconst data = `${method}:${path}:${body}`;\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", encoder.encode(data));\n\tconst bytes = new Uint8Array(hashBuffer);\n\tlet hex = \"\";\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\thex += HEX_TABLE[bytes[i]];\n\t}\n\treturn hex;\n}\n\n/** Constant-time string comparison to prevent timing side-channel attacks on fingerprint matching. */\nexport function timingSafeEqual(a: string, b: string): boolean {\n\tif (a.length !== b.length) return false;\n\tconst aBytes = encoder.encode(a);\n\tconst bBytes = encoder.encode(b);\n\tlet diff = 0;\n\tfor (let i = 0; i < aBytes.length; i++) {\n\t\tdiff |= aBytes[i] ^ bBytes[i];\n\t}\n\treturn diff === 0;\n}\n"],"mappings":"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","names":["encoder","key","body","fp"]}
1
+ {"version":3,"sources":["../src/middleware.ts","../src/compat.ts","../src/errors.ts","../src/fingerprint.ts"],"sourcesContent":["import { createMiddleware } from \"hono/factory\";\nimport { getHonoProblemDetails } from \"./compat.js\";\nimport {\n\tIdempotencyErrors,\n\ttype ProblemDetail,\n\tclampHttpStatus,\n\tproblemResponse,\n} from \"./errors.js\";\nimport { generateFingerprint, timingSafeEqual } from \"./fingerprint.js\";\nimport {\n\ttype IdempotencyEnv,\n\ttype IdempotencyOptions,\n\tRECORD_STATUS_PROCESSING,\n\ttype StoredResponse,\n} from \"./types.js\";\n\nconst DEFAULT_METHODS = [\"POST\", \"PATCH\"];\nconst DEFAULT_MAX_KEY_LENGTH = 256;\n// Headers unsafe to replay — session cookies could leak across users\nconst EXCLUDED_STORE_HEADERS = new Set([\"set-cookie\", \"content-length\", \"transfer-encoding\"]);\nconst DEFAULT_RETRY_AFTER = \"1\";\nconst REPLAY_HEADER = \"Idempotency-Replayed\";\nconst encoder = new TextEncoder();\nconst MUTATING_METHODS = new Set([\"POST\", \"PATCH\", \"PUT\", \"DELETE\"]);\n\nconst GLOBAL_KEYS_WARNING = `[hono-idempotency] WARNING: cacheKeyPrefix is not configured.\n Two users sending the same Idempotency-Key will replay each other's cached responses (cross-tenant data leak).\n Fix:\n cacheKeyPrefix: (c) => \\`\\${c.get(\"user\")?.id ?? \"anon\"}:\\`\n Single-tenant? Set dangerouslyAllowGlobalKeys: true to silence.\n Docs: https://github.com/paveg/hono-idempotency#cachekeyprefix`;\n\nfunction shouldWarnGlobalKeys(options: IdempotencyOptions): boolean {\n\tif (options.dangerouslyAllowGlobalKeys === true) return false;\n\tif (options.cacheKeyPrefix !== undefined) return false;\n\tconst methods = options.methods ?? DEFAULT_METHODS;\n\treturn methods.some((m) => MUTATING_METHODS.has(m.toUpperCase()));\n}\n\nexport function idempotency(options: IdempotencyOptions) {\n\tif (shouldWarnGlobalKeys(options)) {\n\t\tconsole.warn(GLOBAL_KEYS_WARNING);\n\t}\n\tconst {\n\t\tstore,\n\t\theaderName = \"Idempotency-Key\",\n\t\tfingerprint: customFingerprint,\n\t\trequired = false,\n\t\tmethods = DEFAULT_METHODS,\n\t\tmaxKeyLength = DEFAULT_MAX_KEY_LENGTH,\n\t\tskipRequest,\n\t\tonError,\n\t\tcacheKeyPrefix,\n\t\tmaxBodySize,\n\t\tonCacheHit,\n\t\tonCacheMiss,\n\t} = options;\n\n\treturn createMiddleware<IdempotencyEnv>(async (c, next) => {\n\t\tif (!methods.includes(c.req.method)) {\n\t\t\treturn next();\n\t\t}\n\n\t\tif (skipRequest && (await skipRequest(c))) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst errorResponse = async (problem: ProblemDetail, extraHeaders?: Record<string, string>) => {\n\t\t\tif (onError) return onError(problem, c);\n\t\t\tconst pd = await getHonoProblemDetails();\n\t\t\tif (pd) {\n\t\t\t\tconst response = pd\n\t\t\t\t\t.problemDetails({\n\t\t\t\t\t\ttype: problem.type,\n\t\t\t\t\t\ttitle: problem.title,\n\t\t\t\t\t\tstatus: problem.status,\n\t\t\t\t\t\tdetail: problem.detail,\n\t\t\t\t\t\textensions: { code: problem.code },\n\t\t\t\t\t})\n\t\t\t\t\t.getResponse();\n\t\t\t\tif (extraHeaders) {\n\t\t\t\t\tfor (const [key, value] of Object.entries(extraHeaders)) {\n\t\t\t\t\t\tresponse.headers.set(key, value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn response;\n\t\t\t}\n\t\t\treturn problemResponse(problem, extraHeaders);\n\t\t};\n\n\t\tconst key = c.req.header(headerName);\n\n\t\tif (!key) {\n\t\t\tif (required) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.missingKey());\n\t\t\t}\n\t\t\treturn next();\n\t\t}\n\n\t\tif (encoder.encode(key).length > maxKeyLength) {\n\t\t\treturn errorResponse(IdempotencyErrors.keyTooLong(maxKeyLength));\n\t\t}\n\n\t\t// Pre-check Content-Length before reading body\n\t\tif (maxBodySize != null) {\n\t\t\tconst cl = c.req.header(\"Content-Length\");\n\t\t\tif (cl) {\n\t\t\t\tconst parsed = Number.parseInt(cl, 10);\n\t\t\t\tif (parsed < 0 || parsed > maxBodySize) {\n\t\t\t\t\treturn errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst rawPrefix =\n\t\t\ttypeof cacheKeyPrefix === \"function\" ? await cacheKeyPrefix(c) : cacheKeyPrefix;\n\t\t// Encode user-controlled components to prevent delimiter injection\n\t\tconst encodedKey = encodeURIComponent(key);\n\t\tconst baseKey = `${c.req.method}:${c.req.path}:${encodedKey}`;\n\t\tconst storeKey = rawPrefix ? `${encodeURIComponent(rawPrefix)}:${baseKey}` : baseKey;\n\n\t\tconst existing = await store.get(storeKey);\n\n\t\tif (existing) {\n\t\t\tif (existing.status === RECORD_STATUS_PROCESSING) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.conflict(), {\n\t\t\t\t\t\"Retry-After\": DEFAULT_RETRY_AFTER,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst body = await c.req.text();\n\n\t\t\tif (maxBodySize != null) {\n\t\t\t\tconst byteLength = encoder.encode(body).length;\n\t\t\t\tif (byteLength > maxBodySize) {\n\t\t\t\t\treturn errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst fp = customFingerprint\n\t\t\t\t? await customFingerprint(c)\n\t\t\t\t: await generateFingerprint(c.req.method, c.req.path, body);\n\n\t\t\tif (!timingSafeEqual(existing.fingerprint, fp)) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.fingerprintMismatch());\n\t\t\t}\n\n\t\t\tif (existing.response) {\n\t\t\t\tawait safeHook(onCacheHit, key, c);\n\t\t\t\treturn replayResponse(existing.response);\n\t\t\t}\n\n\t\t\t// Completed but no response — corrupt record; delete so lock() can re-acquire\n\t\t\tawait store.delete(storeKey);\n\t\t}\n\n\t\tconst body = await c.req.text();\n\n\t\tif (maxBodySize != null) {\n\t\t\tconst byteLength = encoder.encode(body).length;\n\t\t\tif (byteLength > maxBodySize) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));\n\t\t\t}\n\t\t}\n\n\t\tconst fp = customFingerprint\n\t\t\t? await customFingerprint(c)\n\t\t\t: await generateFingerprint(c.req.method, c.req.path, body);\n\n\t\tconst record = {\n\t\t\tkey,\n\t\t\tfingerprint: fp,\n\t\t\tstatus: RECORD_STATUS_PROCESSING,\n\t\t\tcreatedAt: Date.now(),\n\t\t};\n\n\t\tconst locked = await store.lock(storeKey, record);\n\t\tif (!locked) {\n\t\t\treturn errorResponse(IdempotencyErrors.conflict(), {\n\t\t\t\t\"Retry-After\": DEFAULT_RETRY_AFTER,\n\t\t\t});\n\t\t}\n\n\t\tc.set(\"idempotencyKey\", key);\n\t\tawait safeHook(onCacheMiss, key, c);\n\n\t\ttry {\n\t\t\tawait next();\n\t\t} catch (err) {\n\t\t\tawait store.delete(storeKey);\n\t\t\tthrow err;\n\t\t}\n\n\t\tconst res = c.res;\n\t\tif (!res.ok) {\n\t\t\t// Non-2xx: delete key (Stripe pattern) so client can retry\n\t\t\tawait store.delete(storeKey);\n\t\t\treturn;\n\t\t}\n\n\t\tconst resBody = await res.text();\n\t\tconst resHeaders: Record<string, string> = {};\n\t\tres.headers.forEach((v, k) => {\n\t\t\tif (!EXCLUDED_STORE_HEADERS.has(k.toLowerCase())) {\n\t\t\t\tresHeaders[k] = v;\n\t\t\t}\n\t\t});\n\n\t\tconst storedResponse: StoredResponse = {\n\t\t\tstatus: res.status,\n\t\t\theaders: resHeaders,\n\t\t\tbody: resBody,\n\t\t};\n\n\t\tawait store.complete(storeKey, storedResponse);\n\n\t\t// Rebuild response since we consumed body\n\t\tc.res = new Response(resBody, {\n\t\t\tstatus: res.status,\n\t\t\theaders: res.headers,\n\t\t});\n\t});\n}\n\n// Hook errors must not break idempotency guarantees\nasync function safeHook<C>(\n\tfn: ((key: string, c: C) => void | Promise<void>) | undefined,\n\tkey: string,\n\tc: C,\n): Promise<void> {\n\tif (!fn) return;\n\ttry {\n\t\tawait fn(key, c);\n\t} catch {\n\t\t// Swallow — hooks are for observability, not control flow\n\t}\n}\n\nfunction replayResponse(stored: StoredResponse) {\n\tconst headers = new Headers(stored.headers);\n\theaders.set(REPLAY_HEADER, \"true\");\n\n\treturn new Response(stored.body, {\n\t\tstatus: clampHttpStatus(stored.status),\n\t\theaders,\n\t});\n}\n","type HonoProblemDetails = typeof import(\"hono-problem-details\");\n\nlet cached: HonoProblemDetails | null | undefined;\n\nexport async function getHonoProblemDetails(): Promise<HonoProblemDetails | null> {\n\tif (cached === undefined) {\n\t\ttry {\n\t\t\tcached = await import(\"hono-problem-details\");\n\t\t} catch {\n\t\t\tcached = null;\n\t\t}\n\t}\n\treturn cached;\n}\n","export type IdempotencyErrorCode =\n\t| \"MISSING_KEY\"\n\t| \"KEY_TOO_LONG\"\n\t| \"BODY_TOO_LARGE\"\n\t| \"FINGERPRINT_MISMATCH\"\n\t| \"CONFLICT\";\n\nexport interface ProblemDetail {\n\ttype: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail: string;\n\tcode: IdempotencyErrorCode;\n}\n\n/** Clamp HTTP status to 200-599 integer range; returns 500 for out-of-range or non-integer values. */\nexport function clampHttpStatus(status: number): number {\n\treturn Number.isInteger(status) && status >= 200 && status <= 599 ? status : 500;\n}\n\nexport function problemResponse(\n\tproblem: ProblemDetail,\n\textraHeaders?: Record<string, string>,\n): Response {\n\tlet body: string;\n\tlet status: number;\n\ttry {\n\t\tbody = JSON.stringify(problem);\n\t\tstatus = clampHttpStatus(problem.status);\n\t} catch {\n\t\tbody = '{\"title\":\"Internal Server Error\",\"status\":500}';\n\t\tstatus = 500;\n\t}\n\treturn new Response(body, {\n\t\tstatus,\n\t\theaders: {\n\t\t\t\"Content-Type\": PROBLEM_CONTENT_TYPE,\n\t\t\t...extraHeaders,\n\t\t},\n\t});\n}\n\nconst PROBLEM_CONTENT_TYPE = \"application/problem+json; charset=utf-8\";\nconst BASE_URL = \"https://hono-idempotency.dev/errors\";\n\nexport const IdempotencyErrors = {\n\tmissingKey(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/missing-key`,\n\t\t\ttitle: \"Idempotency-Key header is required\",\n\t\t\tstatus: 400,\n\t\t\tdetail: \"This endpoint requires an Idempotency-Key header\",\n\t\t\tcode: \"MISSING_KEY\",\n\t\t};\n\t},\n\n\tkeyTooLong(maxLength: number): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/key-too-long`,\n\t\t\ttitle: \"Idempotency-Key is too long\",\n\t\t\tstatus: 400,\n\t\t\tdetail: `Idempotency-Key must be at most ${maxLength} characters`,\n\t\t\tcode: \"KEY_TOO_LONG\",\n\t\t};\n\t},\n\n\tbodyTooLarge(maxSize: number): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/body-too-large`,\n\t\t\ttitle: \"Request body is too large\",\n\t\t\tstatus: 413,\n\t\t\tdetail: `Request body must be at most ${maxSize} bytes`,\n\t\t\tcode: \"BODY_TOO_LARGE\",\n\t\t};\n\t},\n\n\tfingerprintMismatch(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/fingerprint-mismatch`,\n\t\t\ttitle: \"Idempotency-Key is already used with a different request\",\n\t\t\tstatus: 422,\n\t\t\tdetail:\n\t\t\t\t\"A request with the same idempotency key but different parameters was already processed\",\n\t\t\tcode: \"FINGERPRINT_MISMATCH\",\n\t\t};\n\t},\n\n\tconflict(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/conflict`,\n\t\t\ttitle: \"A request is outstanding for this idempotency key\",\n\t\t\tstatus: 409,\n\t\t\tdetail: \"A request with the same idempotency key is currently being processed\",\n\t\t\tcode: \"CONFLICT\",\n\t\t};\n\t},\n} as const;\n","const encoder = new TextEncoder();\nconst HEX_TABLE = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, \"0\"));\n\nexport async function generateFingerprint(\n\tmethod: string,\n\tpath: string,\n\tbody: string,\n): Promise<string> {\n\tconst data = `${method}:${path}:${body}`;\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", encoder.encode(data));\n\tconst bytes = new Uint8Array(hashBuffer);\n\tlet hex = \"\";\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\thex += HEX_TABLE[bytes[i]];\n\t}\n\treturn hex;\n}\n\n/** Constant-time string comparison to prevent timing side-channel attacks on fingerprint matching. */\nexport function timingSafeEqual(a: string, b: string): boolean {\n\tif (a.length !== b.length) return false;\n\tconst aBytes = encoder.encode(a);\n\tconst bBytes = encoder.encode(b);\n\tlet diff = 0;\n\tfor (let i = 0; i < aBytes.length; i++) {\n\t\tdiff |= aBytes[i] ^ bBytes[i];\n\t}\n\treturn diff === 0;\n}\n"],"mappings":"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","names":["encoder","key","body","fp"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/cloudflare-d1.ts","../../src/types.ts"],"sourcesContent":["import {\n\ttype IdempotencyRecord,\n\tRECORD_STATUS_COMPLETED,\n\ttype RECORD_STATUS_PROCESSING,\n\ttype StoredResponse,\n} from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TABLE = \"idempotency_keys\";\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\nconst TABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/** Minimal D1Database subset used by d1Store (avoids @cloudflare/workers-types dependency). */\nexport interface D1DatabaseLike {\n\tprepare(sql: string): D1PreparedStatementLike;\n}\n\nexport interface D1PreparedStatementLike {\n\tbind(...params: unknown[]): D1PreparedStatementLike;\n\trun(): Promise<{ success: boolean; meta: { changes: number } }>;\n\tfirst(): Promise<Record<string, unknown> | null>;\n}\n\nexport interface D1StoreOptions {\n\t/** Cloudflare D1 database binding. */\n\tdatabase: D1DatabaseLike;\n\t/** Table name (default: \"idempotency_keys\"). Must match /^[a-zA-Z_][a-zA-Z0-9_]*$/. */\n\ttableName?: string;\n\t/** TTL in seconds (default: 86400 = 24h). Expired rows are ignored by get/lock. */\n\tttl?: number;\n}\n\nexport function d1Store(options: D1StoreOptions): IdempotencyStore {\n\tconst { database: db, tableName = DEFAULT_TABLE, ttl = DEFAULT_TTL } = options;\n\n\tif (!TABLE_NAME_RE.test(tableName)) {\n\t\tthrow new Error(`Invalid table name: \"${tableName}\". Must match ${TABLE_NAME_RE}`);\n\t}\n\n\tlet initialized = false;\n\n\tconst ensureTable = async (): Promise<void> => {\n\t\tif (initialized) return;\n\t\tawait db\n\t\t\t.prepare(\n\t\t\t\t`CREATE TABLE IF NOT EXISTS ${tableName} (\n\t\t\t\tkey TEXT PRIMARY KEY,\n\t\t\t\tfingerprint TEXT NOT NULL,\n\t\t\t\tstatus TEXT NOT NULL,\n\t\t\t\tresponse TEXT,\n\t\t\t\tcreated_at INTEGER NOT NULL\n\t\t\t)`,\n\t\t\t)\n\t\t\t.run();\n\t\tinitialized = true;\n\t};\n\n\tconst ttlThreshold = (): number => {\n\t\treturn Date.now() - ttl * 1000;\n\t};\n\n\tconst toRecord = (row: Record<string, unknown>): IdempotencyRecord => {\n\t\tlet response: StoredResponse | undefined;\n\t\tif (row.response) {\n\t\t\ttry {\n\t\t\t\tresponse = JSON.parse(row.response as string) as StoredResponse;\n\t\t\t} catch {\n\t\t\t\t// Corrupt JSON in storage — degrade gracefully like other stores\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tkey: row.key as string,\n\t\t\tfingerprint: row.fingerprint as string,\n\t\t\tstatus: row.status as typeof RECORD_STATUS_PROCESSING | typeof RECORD_STATUS_COMPLETED,\n\t\t\tresponse,\n\t\t\tcreatedAt: row.created_at as number,\n\t\t};\n\t};\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tawait ensureTable();\n\t\t\tconst row = await db\n\t\t\t\t.prepare(`SELECT * FROM ${tableName} WHERE key = ? AND created_at >= ?`)\n\t\t\t\t.bind(key, ttlThreshold())\n\t\t\t\t.first();\n\t\t\tif (!row) return undefined;\n\t\t\treturn toRecord(row);\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tawait ensureTable();\n\t\t\tconst result = await db\n\t\t\t\t.prepare(\n\t\t\t\t\t`INSERT OR IGNORE INTO ${tableName} (key, fingerprint, status, response, created_at)\n\t\t\t\t\tSELECT ?, ?, ?, ?, ?\n\t\t\t\t\tWHERE NOT EXISTS (SELECT 1 FROM ${tableName} WHERE key = ? AND created_at >= ?)`,\n\t\t\t\t)\n\t\t\t\t.bind(key, record.fingerprint, record.status, null, record.createdAt, key, ttlThreshold())\n\t\t\t\t.run();\n\t\t\treturn result.meta.changes > 0;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tawait ensureTable();\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(response);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait db\n\t\t\t\t.prepare(`UPDATE ${tableName} SET status = ?, response = ? WHERE key = ?`)\n\t\t\t\t.bind(RECORD_STATUS_COMPLETED, serialized, key)\n\t\t\t\t.run();\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait ensureTable();\n\t\t\tawait db.prepare(`DELETE FROM ${tableName} WHERE key = ?`).bind(key).run();\n\t\t},\n\n\t\tasync purge() {\n\t\t\tawait ensureTable();\n\t\t\tconst result = await db\n\t\t\t\t.prepare(`DELETE FROM ${tableName} WHERE created_at < ?`)\n\t\t\t\t.bind(ttlThreshold())\n\t\t\t\t.run();\n\t\t\treturn result.meta.changes;\n\t\t},\n\t};\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,0BAA0B;;;ADGvC,IAAM,gBAAgB;AACtB,IAAM,cAAc;AACpB,IAAM,gBAAgB;AAsBf,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,UAAU,IAAI,YAAY,eAAe,MAAM,YAAY,IAAI;AAEvE,MAAI,CAAC,cAAc,KAAK,SAAS,GAAG;AACnC,UAAM,IAAI,MAAM,wBAAwB,SAAS,iBAAiB,aAAa,EAAE;AAAA,EAClF;AAEA,MAAI,cAAc;AAElB,QAAM,cAAc,YAA2B;AAC9C,QAAI,YAAa;AACjB,UAAM,GACJ;AAAA,MACA,8BAA8B,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOxC,EACC,IAAI;AACN,kBAAc;AAAA,EACf;AAEA,QAAM,eAAe,MAAc;AAClC,WAAO,KAAK,IAAI,IAAI,MAAM;AAAA,EAC3B;AAEA,QAAM,WAAW,CAAC,QAAoD;AACrE,QAAI;AACJ,QAAI,IAAI,UAAU;AACjB,UAAI;AACH,mBAAW,KAAK,MAAM,IAAI,QAAkB;AAAA,MAC7C,QAAQ;AAAA,MAER;AAAA,IACD;AACA,WAAO;AAAA,MACN,KAAK,IAAI;AAAA,MACT,aAAa,IAAI;AAAA,MACjB,QAAQ,IAAI;AAAA,MACZ;AAAA,MACA,WAAW,IAAI;AAAA,IAChB;AAAA,EACD;AAEA,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,YAAY;AAClB,YAAM,MAAM,MAAM,GAChB,QAAQ,iBAAiB,SAAS,oCAAoC,EACtE,KAAK,KAAK,aAAa,CAAC,EACxB,MAAM;AACR,UAAI,CAAC,IAAK,QAAO;AACjB,aAAO,SAAS,GAAG;AAAA,IACpB;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,YAAY;AAClB,YAAM,SAAS,MAAM,GACnB;AAAA,QACA,yBAAyB,SAAS;AAAA;AAAA,uCAEA,SAAS;AAAA,MAC5C,EACC,KAAK,KAAK,OAAO,aAAa,OAAO,QAAQ,MAAM,OAAO,WAAW,KAAK,aAAa,CAAC,EACxF,IAAI;AACN,aAAO,OAAO,KAAK,UAAU;AAAA,IAC9B;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,YAAY;AAClB,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,QAAQ;AAAA,MACrC,QAAQ;AACP;AAAA,MACD;AACA,YAAM,GACJ,QAAQ,UAAU,SAAS,6CAA6C,EACxE,KAAK,yBAAyB,YAAY,GAAG,EAC7C,IAAI;AAAA,IACP;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,YAAY;AAClB,YAAM,GAAG,QAAQ,eAAe,SAAS,gBAAgB,EAAE,KAAK,GAAG,EAAE,IAAI;AAAA,IAC1E;AAAA,IAEA,MAAM,QAAQ;AACb,YAAM,YAAY;AAClB,YAAM,SAAS,MAAM,GACnB,QAAQ,eAAe,SAAS,uBAAuB,EACvD,KAAK,aAAa,CAAC,EACnB,IAAI;AACN,aAAO,OAAO,KAAK;AAAA,IACpB;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/stores/cloudflare-d1.ts","../../src/types.ts"],"sourcesContent":["import {\n\ttype IdempotencyRecord,\n\tRECORD_STATUS_COMPLETED,\n\ttype RECORD_STATUS_PROCESSING,\n\ttype StoredResponse,\n} from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TABLE = \"idempotency_keys\";\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\nconst TABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/** Minimal D1Database subset used by d1Store (avoids @cloudflare/workers-types dependency). */\nexport interface D1DatabaseLike {\n\tprepare(sql: string): D1PreparedStatementLike;\n}\n\nexport interface D1PreparedStatementLike {\n\tbind(...params: unknown[]): D1PreparedStatementLike;\n\trun(): Promise<{ success: boolean; meta: { changes: number } }>;\n\tfirst(): Promise<Record<string, unknown> | null>;\n}\n\nexport interface D1StoreOptions {\n\t/** Cloudflare D1 database binding. */\n\tdatabase: D1DatabaseLike;\n\t/** Table name (default: \"idempotency_keys\"). Must match /^[a-zA-Z_][a-zA-Z0-9_]*$/. */\n\ttableName?: string;\n\t/** TTL in seconds (default: 86400 = 24h). Expired rows are ignored by get/lock. */\n\tttl?: number;\n}\n\nexport function d1Store(options: D1StoreOptions): IdempotencyStore {\n\tconst { database: db, tableName = DEFAULT_TABLE, ttl = DEFAULT_TTL } = options;\n\n\tif (!TABLE_NAME_RE.test(tableName)) {\n\t\tthrow new Error(`Invalid table name: \"${tableName}\". Must match ${TABLE_NAME_RE}`);\n\t}\n\n\tlet initialized = false;\n\n\tconst ensureTable = async (): Promise<void> => {\n\t\tif (initialized) return;\n\t\tawait db\n\t\t\t.prepare(\n\t\t\t\t`CREATE TABLE IF NOT EXISTS ${tableName} (\n\t\t\t\tkey TEXT PRIMARY KEY,\n\t\t\t\tfingerprint TEXT NOT NULL,\n\t\t\t\tstatus TEXT NOT NULL,\n\t\t\t\tresponse TEXT,\n\t\t\t\tcreated_at INTEGER NOT NULL\n\t\t\t)`,\n\t\t\t)\n\t\t\t.run();\n\t\tinitialized = true;\n\t};\n\n\tconst ttlThreshold = (): number => {\n\t\treturn Date.now() - ttl * 1000;\n\t};\n\n\tconst toRecord = (row: Record<string, unknown>): IdempotencyRecord => {\n\t\tlet response: StoredResponse | undefined;\n\t\tif (row.response) {\n\t\t\ttry {\n\t\t\t\tresponse = JSON.parse(row.response as string) as StoredResponse;\n\t\t\t} catch {\n\t\t\t\t// Corrupt JSON in storage — degrade gracefully like other stores\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tkey: row.key as string,\n\t\t\tfingerprint: row.fingerprint as string,\n\t\t\tstatus: row.status as typeof RECORD_STATUS_PROCESSING | typeof RECORD_STATUS_COMPLETED,\n\t\t\tresponse,\n\t\t\tcreatedAt: row.created_at as number,\n\t\t};\n\t};\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tawait ensureTable();\n\t\t\tconst row = await db\n\t\t\t\t.prepare(`SELECT * FROM ${tableName} WHERE key = ? AND created_at >= ?`)\n\t\t\t\t.bind(key, ttlThreshold())\n\t\t\t\t.first();\n\t\t\tif (!row) return undefined;\n\t\t\treturn toRecord(row);\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tawait ensureTable();\n\t\t\tconst result = await db\n\t\t\t\t.prepare(\n\t\t\t\t\t`INSERT OR IGNORE INTO ${tableName} (key, fingerprint, status, response, created_at)\n\t\t\t\t\tSELECT ?, ?, ?, ?, ?\n\t\t\t\t\tWHERE NOT EXISTS (SELECT 1 FROM ${tableName} WHERE key = ? AND created_at >= ?)`,\n\t\t\t\t)\n\t\t\t\t.bind(key, record.fingerprint, record.status, null, record.createdAt, key, ttlThreshold())\n\t\t\t\t.run();\n\t\t\treturn result.meta.changes > 0;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tawait ensureTable();\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(response);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait db\n\t\t\t\t.prepare(`UPDATE ${tableName} SET status = ?, response = ? WHERE key = ?`)\n\t\t\t\t.bind(RECORD_STATUS_COMPLETED, serialized, key)\n\t\t\t\t.run();\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait ensureTable();\n\t\t\tawait db.prepare(`DELETE FROM ${tableName} WHERE key = ?`).bind(key).run();\n\t\t},\n\n\t\tasync purge() {\n\t\t\tawait ensureTable();\n\t\t\tconst result = await db\n\t\t\t\t.prepare(`DELETE FROM ${tableName} WHERE created_at < ?`)\n\t\t\t\t.bind(ttlThreshold())\n\t\t\t\t.run();\n\t\t\treturn result.meta.changes;\n\t\t},\n\t};\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Opt out of the multi-tenant safety warning.\n\t *\n\t * When `cacheKeyPrefix` is not set and `methods` includes any state-mutating\n\t * method (POST/PATCH/PUT/DELETE), the middleware emits a one-time\n\t * `console.warn` at factory construction time. Set this to `true` to\n\t * acknowledge that the deployment is single-tenant and silence the warning.\n\t *\n\t * @default false\n\t */\n\tdangerouslyAllowGlobalKeys?: boolean;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,0BAA0B;;;ADGvC,IAAM,gBAAgB;AACtB,IAAM,cAAc;AACpB,IAAM,gBAAgB;AAsBf,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,UAAU,IAAI,YAAY,eAAe,MAAM,YAAY,IAAI;AAEvE,MAAI,CAAC,cAAc,KAAK,SAAS,GAAG;AACnC,UAAM,IAAI,MAAM,wBAAwB,SAAS,iBAAiB,aAAa,EAAE;AAAA,EAClF;AAEA,MAAI,cAAc;AAElB,QAAM,cAAc,YAA2B;AAC9C,QAAI,YAAa;AACjB,UAAM,GACJ;AAAA,MACA,8BAA8B,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOxC,EACC,IAAI;AACN,kBAAc;AAAA,EACf;AAEA,QAAM,eAAe,MAAc;AAClC,WAAO,KAAK,IAAI,IAAI,MAAM;AAAA,EAC3B;AAEA,QAAM,WAAW,CAAC,QAAoD;AACrE,QAAI;AACJ,QAAI,IAAI,UAAU;AACjB,UAAI;AACH,mBAAW,KAAK,MAAM,IAAI,QAAkB;AAAA,MAC7C,QAAQ;AAAA,MAER;AAAA,IACD;AACA,WAAO;AAAA,MACN,KAAK,IAAI;AAAA,MACT,aAAa,IAAI;AAAA,MACjB,QAAQ,IAAI;AAAA,MACZ;AAAA,MACA,WAAW,IAAI;AAAA,IAChB;AAAA,EACD;AAEA,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,YAAY;AAClB,YAAM,MAAM,MAAM,GAChB,QAAQ,iBAAiB,SAAS,oCAAoC,EACtE,KAAK,KAAK,aAAa,CAAC,EACxB,MAAM;AACR,UAAI,CAAC,IAAK,QAAO;AACjB,aAAO,SAAS,GAAG;AAAA,IACpB;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,YAAY;AAClB,YAAM,SAAS,MAAM,GACnB;AAAA,QACA,yBAAyB,SAAS;AAAA;AAAA,uCAEA,SAAS;AAAA,MAC5C,EACC,KAAK,KAAK,OAAO,aAAa,OAAO,QAAQ,MAAM,OAAO,WAAW,KAAK,aAAa,CAAC,EACxF,IAAI;AACN,aAAO,OAAO,KAAK,UAAU;AAAA,IAC9B;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,YAAY;AAClB,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,QAAQ;AAAA,MACrC,QAAQ;AACP;AAAA,MACD;AACA,YAAM,GACJ,QAAQ,UAAU,SAAS,6CAA6C,EACxE,KAAK,yBAAyB,YAAY,GAAG,EAC7C,IAAI;AAAA,IACP;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,YAAY;AAClB,YAAM,GAAG,QAAQ,eAAe,SAAS,gBAAgB,EAAE,KAAK,GAAG,EAAE,IAAI;AAAA,IAC1E;AAAA,IAEA,MAAM,QAAQ;AACb,YAAM,YAAY;AAClB,YAAM,SAAS,MAAM,GACnB,QAAQ,eAAe,SAAS,uBAAuB,EACvD,KAAK,aAAa,CAAC,EACnB,IAAI;AACN,aAAO,OAAO,KAAK;AAAA,IACpB;AAAA,EACD;AACD;","names":[]}
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-Kb-9sxwk.cjs';
1
+ import { e as IdempotencyStore } from '../types-CI-upMvQ.cjs';
2
2
  import 'hono';
3
3
 
4
4
  /** Minimal D1Database subset used by d1Store (avoids @cloudflare/workers-types dependency). */
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-Kb-9sxwk.js';
1
+ import { e as IdempotencyStore } from '../types-CI-upMvQ.js';
2
2
  import 'hono';
3
3
 
4
4
  /** Minimal D1Database subset used by d1Store (avoids @cloudflare/workers-types dependency). */
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RECORD_STATUS_COMPLETED
3
- } from "../chunk-I5ECYFYR.js";
3
+ } from "../chunk-ET3EW23V.js";
4
4
 
5
5
  // src/stores/cloudflare-d1.ts
6
6
  var DEFAULT_TABLE = "idempotency_keys";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/cloudflare-kv.ts","../../src/types.ts"],"sourcesContent":["import { type IdempotencyRecord, RECORD_STATUS_COMPLETED, type StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */\nexport interface KVNamespaceLike {\n\tget(key: string, options: { type: \"json\" }): Promise<unknown>;\n\tput(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;\n\tdelete(key: string): Promise<void>;\n}\n\nexport interface KVStoreOptions {\n\t/** Cloudflare Workers KV namespace binding. */\n\tnamespace: KVNamespaceLike;\n\t/** TTL in seconds (default: 86400 = 24h). KV minimum is 60 seconds. */\n\tttl?: number;\n}\n\n/**\n * KV is eventually consistent — lock() uses write-first with read-back\n * verification for best-effort race detection but cannot guarantee atomicity.\n * For strict concurrency guarantees, use d1Store or durableObjectStore.\n */\nexport function kvStore(options: KVStoreOptions): IdempotencyStore {\n\tconst { namespace: kv, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst raw = (await kv.get(key, { type: \"json\" })) as\n\t\t\t\t| (IdempotencyRecord & { lockId?: string })\n\t\t\t\t| null;\n\t\t\tif (!raw) return undefined;\n\t\t\t// Strip internal lockId before returning to consumers\n\t\t\tconst { lockId: _, ...record } = raw;\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\tif (existing) return false;\n\n\t\t\t// Embed a unique lockId to distinguish concurrent writers with the same fingerprint\n\t\t\tconst lockId = crypto.randomUUID();\n\t\t\tconst withLock = { ...record, lockId };\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(withLock);\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tawait kv.put(key, serialized, { expirationTtl: ttl });\n\n\t\t\t// Read-back verification using lockId (not fingerprint) for reliable race detection\n\t\t\tconst stored = (await kv.get(key, { type: \"json\" })) as\n\t\t\t\t| (IdempotencyRecord & { lockId?: string })\n\t\t\t\t| null;\n\t\t\treturn stored?.lockId === lockId;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst raw = (await kv.get(key, { type: \"json\" })) as\n\t\t\t\t| (IdempotencyRecord & { lockId?: string })\n\t\t\t\t| null;\n\t\t\tif (!raw) return;\n\t\t\tconst { lockId: _, ...record } = raw;\n\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\trecord.response = response;\n\t\t\tconst elapsed = Math.floor((Date.now() - record.createdAt) / 1000);\n\t\t\tconst remaining = Math.max(1, ttl - elapsed);\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(record);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait kv.put(key, serialized, { expirationTtl: remaining });\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait kv.delete(key);\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","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,0BAA0B;;;ADFvC,IAAM,cAAc;AAqBb,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,WAAW,IAAI,MAAM,YAAY,IAAI;AAE7C,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,MAAO,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAG/C,UAAI,CAAC,IAAK,QAAO;AAEjB,YAAM,EAAE,QAAQ,GAAG,GAAG,OAAO,IAAI;AACjC,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,WAAY,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AACpD,UAAI,SAAU,QAAO;AAGrB,YAAM,SAAS,OAAO,WAAW;AACjC,YAAM,WAAW,EAAE,GAAG,QAAQ,OAAO;AACrC,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,QAAQ;AAAA,MACrC,QAAQ;AACP,eAAO;AAAA,MACR;AACA,YAAM,GAAG,IAAI,KAAK,YAAY,EAAE,eAAe,IAAI,CAAC;AAGpD,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAGlD,aAAO,QAAQ,WAAW;AAAA,IAC3B;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,MAAO,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAG/C,UAAI,CAAC,IAAK;AACV,YAAM,EAAE,QAAQ,GAAG,GAAG,OAAO,IAAI;AACjC,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,UAAU,KAAK,OAAO,KAAK,IAAI,IAAI,OAAO,aAAa,GAAI;AACjE,YAAM,YAAY,KAAK,IAAI,GAAG,MAAM,OAAO;AAC3C,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,MAAM;AAAA,MACnC,QAAQ;AACP;AAAA,MACD;AACA,YAAM,GAAG,IAAI,KAAK,YAAY,EAAE,eAAe,UAAU,CAAC;AAAA,IAC3D;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,GAAG,OAAO,GAAG;AAAA,IACpB;AAAA,IAEA,MAAM,QAAQ;AAEb,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/stores/cloudflare-kv.ts","../../src/types.ts"],"sourcesContent":["import { type IdempotencyRecord, RECORD_STATUS_COMPLETED, type StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */\nexport interface KVNamespaceLike {\n\tget(key: string, options: { type: \"json\" }): Promise<unknown>;\n\tput(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;\n\tdelete(key: string): Promise<void>;\n}\n\nexport interface KVStoreOptions {\n\t/** Cloudflare Workers KV namespace binding. */\n\tnamespace: KVNamespaceLike;\n\t/** TTL in seconds (default: 86400 = 24h). KV minimum is 60 seconds. */\n\tttl?: number;\n}\n\n/**\n * KV is eventually consistent — lock() uses write-first with read-back\n * verification for best-effort race detection but cannot guarantee atomicity.\n * For strict concurrency guarantees, use d1Store or durableObjectStore.\n */\nexport function kvStore(options: KVStoreOptions): IdempotencyStore {\n\tconst { namespace: kv, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst raw = (await kv.get(key, { type: \"json\" })) as\n\t\t\t\t| (IdempotencyRecord & { lockId?: string })\n\t\t\t\t| null;\n\t\t\tif (!raw) return undefined;\n\t\t\t// Strip internal lockId before returning to consumers\n\t\t\tconst { lockId: _, ...record } = raw;\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\tif (existing) return false;\n\n\t\t\t// Embed a unique lockId to distinguish concurrent writers with the same fingerprint\n\t\t\tconst lockId = crypto.randomUUID();\n\t\t\tconst withLock = { ...record, lockId };\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(withLock);\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tawait kv.put(key, serialized, { expirationTtl: ttl });\n\n\t\t\t// Read-back verification using lockId (not fingerprint) for reliable race detection\n\t\t\tconst stored = (await kv.get(key, { type: \"json\" })) as\n\t\t\t\t| (IdempotencyRecord & { lockId?: string })\n\t\t\t\t| null;\n\t\t\treturn stored?.lockId === lockId;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst raw = (await kv.get(key, { type: \"json\" })) as\n\t\t\t\t| (IdempotencyRecord & { lockId?: string })\n\t\t\t\t| null;\n\t\t\tif (!raw) return;\n\t\t\tconst { lockId: _, ...record } = raw;\n\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\trecord.response = response;\n\t\t\tconst elapsed = Math.floor((Date.now() - record.createdAt) / 1000);\n\t\t\tconst remaining = Math.max(1, ttl - elapsed);\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(record);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait kv.put(key, serialized, { expirationTtl: remaining });\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait kv.delete(key);\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","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Opt out of the multi-tenant safety warning.\n\t *\n\t * When `cacheKeyPrefix` is not set and `methods` includes any state-mutating\n\t * method (POST/PATCH/PUT/DELETE), the middleware emits a one-time\n\t * `console.warn` at factory construction time. Set this to `true` to\n\t * acknowledge that the deployment is single-tenant and silence the warning.\n\t *\n\t * @default false\n\t */\n\tdangerouslyAllowGlobalKeys?: boolean;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,0BAA0B;;;ADFvC,IAAM,cAAc;AAqBb,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,WAAW,IAAI,MAAM,YAAY,IAAI;AAE7C,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,MAAO,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAG/C,UAAI,CAAC,IAAK,QAAO;AAEjB,YAAM,EAAE,QAAQ,GAAG,GAAG,OAAO,IAAI;AACjC,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,WAAY,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AACpD,UAAI,SAAU,QAAO;AAGrB,YAAM,SAAS,OAAO,WAAW;AACjC,YAAM,WAAW,EAAE,GAAG,QAAQ,OAAO;AACrC,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,QAAQ;AAAA,MACrC,QAAQ;AACP,eAAO;AAAA,MACR;AACA,YAAM,GAAG,IAAI,KAAK,YAAY,EAAE,eAAe,IAAI,CAAC;AAGpD,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAGlD,aAAO,QAAQ,WAAW;AAAA,IAC3B;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,MAAO,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAG/C,UAAI,CAAC,IAAK;AACV,YAAM,EAAE,QAAQ,GAAG,GAAG,OAAO,IAAI;AACjC,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,UAAU,KAAK,OAAO,KAAK,IAAI,IAAI,OAAO,aAAa,GAAI;AACjE,YAAM,YAAY,KAAK,IAAI,GAAG,MAAM,OAAO;AAC3C,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,MAAM;AAAA,MACnC,QAAQ;AACP;AAAA,MACD;AACA,YAAM,GAAG,IAAI,KAAK,YAAY,EAAE,eAAe,UAAU,CAAC;AAAA,IAC3D;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,GAAG,OAAO,GAAG;AAAA,IACpB;AAAA,IAEA,MAAM,QAAQ;AAEb,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-Kb-9sxwk.cjs';
1
+ import { e as IdempotencyStore } from '../types-CI-upMvQ.cjs';
2
2
  import 'hono';
3
3
 
4
4
  /** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-Kb-9sxwk.js';
1
+ import { e as IdempotencyStore } from '../types-CI-upMvQ.js';
2
2
  import 'hono';
3
3
 
4
4
  /** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RECORD_STATUS_COMPLETED
3
- } from "../chunk-I5ECYFYR.js";
3
+ } from "../chunk-ET3EW23V.js";
4
4
 
5
5
  // src/stores/cloudflare-kv.ts
6
6
  var DEFAULT_TTL = 86400;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/durable-objects.ts","../../src/types.ts"],"sourcesContent":["import { type IdempotencyRecord, RECORD_STATUS_COMPLETED, type StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours in ms\n\n/** Minimal DurableObjectStorage subset (avoids @cloudflare/workers-types dependency). */\nexport interface DurableObjectStorageLike {\n\tget<T>(key: string): Promise<T | undefined>;\n\tput<T>(key: string, value: T): Promise<void>;\n\tdelete(key: string): Promise<boolean>;\n\tlist(options?: { prefix?: string }): Promise<Map<string, unknown>>;\n}\n\nexport interface DurableObjectStoreOptions {\n\t/** Durable Object storage instance (from `this.ctx.storage` inside a DO class). */\n\tstorage: DurableObjectStorageLike;\n\t/** TTL in milliseconds (default: 86400000 = 24h). */\n\tttl?: number;\n}\n\nexport function durableObjectStore(options: DurableObjectStoreOptions): IdempotencyStore {\n\tconst { storage, ttl = DEFAULT_TTL } = options;\n\n\tconst isExpired = (record: IdempotencyRecord): boolean => {\n\t\treturn Date.now() - record.createdAt >= ttl;\n\t};\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst record = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (!record) return undefined;\n\t\t\tif (isExpired(record)) {\n\t\t\t\tawait storage.delete(key);\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (existing && !isExpired(existing)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tawait storage.put(key, record);\n\t\t\treturn true;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (!record) return;\n\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\trecord.response = response;\n\t\t\tawait storage.put(key, record);\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait storage.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\tconst entries = await storage.list();\n\t\t\tlet count = 0;\n\t\t\tfor (const [key, value] of entries) {\n\t\t\t\tconst record = value as IdempotencyRecord;\n\t\t\t\tif (record.createdAt !== undefined && isExpired(record)) {\n\t\t\t\t\tawait storage.delete(key);\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn count;\n\t\t},\n\t};\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,0BAA0B;;;ADFvC,IAAM,cAAc,KAAK,KAAK,KAAK;AAiB5B,SAAS,mBAAmB,SAAsD;AACxF,QAAM,EAAE,SAAS,MAAM,YAAY,IAAI;AAEvC,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,SAAS,MAAM,QAAQ,IAAuB,GAAG;AACvD,UAAI,CAAC,OAAQ,QAAO;AACpB,UAAI,UAAU,MAAM,GAAG;AACtB,cAAM,QAAQ,OAAO,GAAG;AACxB,eAAO;AAAA,MACR;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,WAAW,MAAM,QAAQ,IAAuB,GAAG;AACzD,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,YAAM,QAAQ,IAAI,KAAK,MAAM;AAC7B,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAS,MAAM,QAAQ,IAAuB,GAAG;AACvD,UAAI,CAAC,OAAQ;AACb,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,QAAQ,IAAI,KAAK,MAAM;AAAA,IAC9B;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,QAAQ,OAAO,GAAG;AAAA,IACzB;AAAA,IAEA,MAAM,QAAQ;AACb,YAAM,UAAU,MAAM,QAAQ,KAAK;AACnC,UAAI,QAAQ;AACZ,iBAAW,CAAC,KAAK,KAAK,KAAK,SAAS;AACnC,cAAM,SAAS;AACf,YAAI,OAAO,cAAc,UAAa,UAAU,MAAM,GAAG;AACxD,gBAAM,QAAQ,OAAO,GAAG;AACxB;AAAA,QACD;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/stores/durable-objects.ts","../../src/types.ts"],"sourcesContent":["import { type IdempotencyRecord, RECORD_STATUS_COMPLETED, type StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours in ms\n\n/** Minimal DurableObjectStorage subset (avoids @cloudflare/workers-types dependency). */\nexport interface DurableObjectStorageLike {\n\tget<T>(key: string): Promise<T | undefined>;\n\tput<T>(key: string, value: T): Promise<void>;\n\tdelete(key: string): Promise<boolean>;\n\tlist(options?: { prefix?: string }): Promise<Map<string, unknown>>;\n}\n\nexport interface DurableObjectStoreOptions {\n\t/** Durable Object storage instance (from `this.ctx.storage` inside a DO class). */\n\tstorage: DurableObjectStorageLike;\n\t/** TTL in milliseconds (default: 86400000 = 24h). */\n\tttl?: number;\n}\n\nexport function durableObjectStore(options: DurableObjectStoreOptions): IdempotencyStore {\n\tconst { storage, ttl = DEFAULT_TTL } = options;\n\n\tconst isExpired = (record: IdempotencyRecord): boolean => {\n\t\treturn Date.now() - record.createdAt >= ttl;\n\t};\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst record = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (!record) return undefined;\n\t\t\tif (isExpired(record)) {\n\t\t\t\tawait storage.delete(key);\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (existing && !isExpired(existing)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tawait storage.put(key, record);\n\t\t\treturn true;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (!record) return;\n\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\trecord.response = response;\n\t\t\tawait storage.put(key, record);\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait storage.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\tconst entries = await storage.list();\n\t\t\tlet count = 0;\n\t\t\tfor (const [key, value] of entries) {\n\t\t\t\tconst record = value as IdempotencyRecord;\n\t\t\t\tif (record.createdAt !== undefined && isExpired(record)) {\n\t\t\t\t\tawait storage.delete(key);\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn count;\n\t\t},\n\t};\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Opt out of the multi-tenant safety warning.\n\t *\n\t * When `cacheKeyPrefix` is not set and `methods` includes any state-mutating\n\t * method (POST/PATCH/PUT/DELETE), the middleware emits a one-time\n\t * `console.warn` at factory construction time. Set this to `true` to\n\t * acknowledge that the deployment is single-tenant and silence the warning.\n\t *\n\t * @default false\n\t */\n\tdangerouslyAllowGlobalKeys?: boolean;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,0BAA0B;;;ADFvC,IAAM,cAAc,KAAK,KAAK,KAAK;AAiB5B,SAAS,mBAAmB,SAAsD;AACxF,QAAM,EAAE,SAAS,MAAM,YAAY,IAAI;AAEvC,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,SAAS,MAAM,QAAQ,IAAuB,GAAG;AACvD,UAAI,CAAC,OAAQ,QAAO;AACpB,UAAI,UAAU,MAAM,GAAG;AACtB,cAAM,QAAQ,OAAO,GAAG;AACxB,eAAO;AAAA,MACR;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,WAAW,MAAM,QAAQ,IAAuB,GAAG;AACzD,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,YAAM,QAAQ,IAAI,KAAK,MAAM;AAC7B,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAS,MAAM,QAAQ,IAAuB,GAAG;AACvD,UAAI,CAAC,OAAQ;AACb,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,QAAQ,IAAI,KAAK,MAAM;AAAA,IAC9B;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,QAAQ,OAAO,GAAG;AAAA,IACzB;AAAA,IAEA,MAAM,QAAQ;AACb,YAAM,UAAU,MAAM,QAAQ,KAAK;AACnC,UAAI,QAAQ;AACZ,iBAAW,CAAC,KAAK,KAAK,KAAK,SAAS;AACnC,cAAM,SAAS;AACf,YAAI,OAAO,cAAc,UAAa,UAAU,MAAM,GAAG;AACxD,gBAAM,QAAQ,OAAO,GAAG;AACxB;AAAA,QACD;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-Kb-9sxwk.cjs';
1
+ import { e as IdempotencyStore } from '../types-CI-upMvQ.cjs';
2
2
  import 'hono';
3
3
 
4
4
  /** Minimal DurableObjectStorage subset (avoids @cloudflare/workers-types dependency). */
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-Kb-9sxwk.js';
1
+ import { e as IdempotencyStore } from '../types-CI-upMvQ.js';
2
2
  import 'hono';
3
3
 
4
4
  /** Minimal DurableObjectStorage subset (avoids @cloudflare/workers-types dependency). */
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RECORD_STATUS_COMPLETED
3
- } from "../chunk-I5ECYFYR.js";
3
+ } from "../chunk-ET3EW23V.js";
4
4
 
5
5
  // src/stores/durable-objects.ts
6
6
  var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/memory.ts","../../src/types.ts"],"sourcesContent":["import {\n\ttype IdempotencyRecord,\n\tRECORD_STATUS_COMPLETED,\n\tRECORD_STATUS_PROCESSING,\n\ttype StoredResponse,\n} from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours\n\nexport interface MemoryStoreOptions {\n\tttl?: number;\n\t/** Maximum number of entries. Oldest entries are evicted when exceeded. */\n\tmaxSize?: number;\n\t/** Minimum interval between sweeps in milliseconds (default: 60000). */\n\tsweepInterval?: number;\n}\n\nexport interface MemoryStore extends IdempotencyStore {\n\t/** Number of entries currently in the store (including expired but not yet swept). */\n\treadonly size: number;\n}\n\nexport function memoryStore(options: MemoryStoreOptions = {}): MemoryStore {\n\tconst ttl = options.ttl ?? DEFAULT_TTL;\n\tconst maxSize = options.maxSize;\n\tconst sweepInterval = options.sweepInterval ?? 60_000;\n\tconst map = new Map<string, IdempotencyRecord>();\n\tlet lastSweep = Number.NEGATIVE_INFINITY;\n\n\tconst isExpired = (record: IdempotencyRecord): boolean => {\n\t\treturn Date.now() - record.createdAt >= ttl;\n\t};\n\n\tconst sweepIfDue = (): void => {\n\t\tconst now = Date.now();\n\t\tif (now - lastSweep < sweepInterval) return;\n\t\tlastSweep = now;\n\t\tfor (const [key, record] of map) {\n\t\t\tif (isExpired(record)) {\n\t\t\t\tmap.delete(key);\n\t\t\t}\n\t\t}\n\t};\n\n\treturn {\n\t\tget size() {\n\t\t\treturn map.size;\n\t\t},\n\n\t\tasync get(key) {\n\t\t\tconst record = map.get(key);\n\t\t\tif (!record) return undefined;\n\t\t\tif (isExpired(record)) {\n\t\t\t\tmap.delete(key);\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tsweepIfDue();\n\t\t\tconst existing = map.get(key);\n\t\t\tif (existing && !isExpired(existing)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tmap.set(key, record);\n\t\t\tif (maxSize !== undefined) {\n\t\t\t\twhile (map.size > maxSize) {\n\t\t\t\t\tlet evicted = false;\n\t\t\t\t\tfor (const [k, r] of map) {\n\t\t\t\t\t\tif (r.status !== RECORD_STATUS_PROCESSING) {\n\t\t\t\t\t\t\tmap.delete(k);\n\t\t\t\t\t\t\tevicted = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (!evicted) break;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = map.get(key);\n\t\t\tif (record) {\n\t\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\t\trecord.response = response;\n\t\t\t}\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tmap.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\tlet count = 0;\n\t\t\tfor (const [key, record] of map) {\n\t\t\t\tif (isExpired(record)) {\n\t\t\t\t\tmap.delete(key);\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn count;\n\t\t},\n\t};\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIO,IAAM,2BAA2B;AACjC,IAAM,0BAA0B;;;ADGvC,IAAM,cAAc,KAAK,KAAK,KAAK;AAe5B,SAAS,YAAY,UAA8B,CAAC,GAAgB;AAC1E,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,UAAU,QAAQ;AACxB,QAAM,gBAAgB,QAAQ,iBAAiB;AAC/C,QAAM,MAAM,oBAAI,IAA+B;AAC/C,MAAI,YAAY,OAAO;AAEvB,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,QAAM,aAAa,MAAY;AAC9B,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,MAAM,YAAY,cAAe;AACrC,gBAAY;AACZ,eAAW,CAAC,KAAK,MAAM,KAAK,KAAK;AAChC,UAAI,UAAU,MAAM,GAAG;AACtB,YAAI,OAAO,GAAG;AAAA,MACf;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AAAA,IACN,IAAI,OAAO;AACV,aAAO,IAAI;AAAA,IACZ;AAAA,IAEA,MAAM,IAAI,KAAK;AACd,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,CAAC,OAAQ,QAAO;AACpB,UAAI,UAAU,MAAM,GAAG;AACtB,YAAI,OAAO,GAAG;AACd,eAAO;AAAA,MACR;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,iBAAW;AACX,YAAM,WAAW,IAAI,IAAI,GAAG;AAC5B,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,UAAI,IAAI,KAAK,MAAM;AACnB,UAAI,YAAY,QAAW;AAC1B,eAAO,IAAI,OAAO,SAAS;AAC1B,cAAI,UAAU;AACd,qBAAW,CAAC,GAAG,CAAC,KAAK,KAAK;AACzB,gBAAI,EAAE,WAAW,0BAA0B;AAC1C,kBAAI,OAAO,CAAC;AACZ,wBAAU;AACV;AAAA,YACD;AAAA,UACD;AACA,cAAI,CAAC,QAAS;AAAA,QACf;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,QAAQ;AACX,eAAO,SAAS;AAChB,eAAO,WAAW;AAAA,MACnB;AAAA,IACD;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,UAAI,OAAO,GAAG;AAAA,IACf;AAAA,IAEA,MAAM,QAAQ;AACb,UAAI,QAAQ;AACZ,iBAAW,CAAC,KAAK,MAAM,KAAK,KAAK;AAChC,YAAI,UAAU,MAAM,GAAG;AACtB,cAAI,OAAO,GAAG;AACd;AAAA,QACD;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/stores/memory.ts","../../src/types.ts"],"sourcesContent":["import {\n\ttype IdempotencyRecord,\n\tRECORD_STATUS_COMPLETED,\n\tRECORD_STATUS_PROCESSING,\n\ttype StoredResponse,\n} from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours\n\nexport interface MemoryStoreOptions {\n\tttl?: number;\n\t/** Maximum number of entries. Oldest entries are evicted when exceeded. */\n\tmaxSize?: number;\n\t/** Minimum interval between sweeps in milliseconds (default: 60000). */\n\tsweepInterval?: number;\n}\n\nexport interface MemoryStore extends IdempotencyStore {\n\t/** Number of entries currently in the store (including expired but not yet swept). */\n\treadonly size: number;\n}\n\nexport function memoryStore(options: MemoryStoreOptions = {}): MemoryStore {\n\tconst ttl = options.ttl ?? DEFAULT_TTL;\n\tconst maxSize = options.maxSize;\n\tconst sweepInterval = options.sweepInterval ?? 60_000;\n\tconst map = new Map<string, IdempotencyRecord>();\n\tlet lastSweep = Number.NEGATIVE_INFINITY;\n\n\tconst isExpired = (record: IdempotencyRecord): boolean => {\n\t\treturn Date.now() - record.createdAt >= ttl;\n\t};\n\n\tconst sweepIfDue = (): void => {\n\t\tconst now = Date.now();\n\t\tif (now - lastSweep < sweepInterval) return;\n\t\tlastSweep = now;\n\t\tfor (const [key, record] of map) {\n\t\t\tif (isExpired(record)) {\n\t\t\t\tmap.delete(key);\n\t\t\t}\n\t\t}\n\t};\n\n\treturn {\n\t\tget size() {\n\t\t\treturn map.size;\n\t\t},\n\n\t\tasync get(key) {\n\t\t\tconst record = map.get(key);\n\t\t\tif (!record) return undefined;\n\t\t\tif (isExpired(record)) {\n\t\t\t\tmap.delete(key);\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tsweepIfDue();\n\t\t\tconst existing = map.get(key);\n\t\t\tif (existing && !isExpired(existing)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tmap.set(key, record);\n\t\t\tif (maxSize !== undefined) {\n\t\t\t\twhile (map.size > maxSize) {\n\t\t\t\t\tlet evicted = false;\n\t\t\t\t\tfor (const [k, r] of map) {\n\t\t\t\t\t\tif (r.status !== RECORD_STATUS_PROCESSING) {\n\t\t\t\t\t\t\tmap.delete(k);\n\t\t\t\t\t\t\tevicted = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (!evicted) break;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = map.get(key);\n\t\t\tif (record) {\n\t\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\t\trecord.response = response;\n\t\t\t}\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tmap.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\tlet count = 0;\n\t\t\tfor (const [key, record] of map) {\n\t\t\t\tif (isExpired(record)) {\n\t\t\t\t\tmap.delete(key);\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn count;\n\t\t},\n\t};\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Opt out of the multi-tenant safety warning.\n\t *\n\t * When `cacheKeyPrefix` is not set and `methods` includes any state-mutating\n\t * method (POST/PATCH/PUT/DELETE), the middleware emits a one-time\n\t * `console.warn` at factory construction time. Set this to `true` to\n\t * acknowledge that the deployment is single-tenant and silence the warning.\n\t *\n\t * @default false\n\t */\n\tdangerouslyAllowGlobalKeys?: boolean;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIO,IAAM,2BAA2B;AACjC,IAAM,0BAA0B;;;ADGvC,IAAM,cAAc,KAAK,KAAK,KAAK;AAe5B,SAAS,YAAY,UAA8B,CAAC,GAAgB;AAC1E,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,UAAU,QAAQ;AACxB,QAAM,gBAAgB,QAAQ,iBAAiB;AAC/C,QAAM,MAAM,oBAAI,IAA+B;AAC/C,MAAI,YAAY,OAAO;AAEvB,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,QAAM,aAAa,MAAY;AAC9B,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,MAAM,YAAY,cAAe;AACrC,gBAAY;AACZ,eAAW,CAAC,KAAK,MAAM,KAAK,KAAK;AAChC,UAAI,UAAU,MAAM,GAAG;AACtB,YAAI,OAAO,GAAG;AAAA,MACf;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AAAA,IACN,IAAI,OAAO;AACV,aAAO,IAAI;AAAA,IACZ;AAAA,IAEA,MAAM,IAAI,KAAK;AACd,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,CAAC,OAAQ,QAAO;AACpB,UAAI,UAAU,MAAM,GAAG;AACtB,YAAI,OAAO,GAAG;AACd,eAAO;AAAA,MACR;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,iBAAW;AACX,YAAM,WAAW,IAAI,IAAI,GAAG;AAC5B,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,UAAI,IAAI,KAAK,MAAM;AACnB,UAAI,YAAY,QAAW;AAC1B,eAAO,IAAI,OAAO,SAAS;AAC1B,cAAI,UAAU;AACd,qBAAW,CAAC,GAAG,CAAC,KAAK,KAAK;AACzB,gBAAI,EAAE,WAAW,0BAA0B;AAC1C,kBAAI,OAAO,CAAC;AACZ,wBAAU;AACV;AAAA,YACD;AAAA,UACD;AACA,cAAI,CAAC,QAAS;AAAA,QACf;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,QAAQ;AACX,eAAO,SAAS;AAChB,eAAO,WAAW;AAAA,MACnB;AAAA,IACD;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,UAAI,OAAO,GAAG;AAAA,IACf;AAAA,IAEA,MAAM,QAAQ;AACb,UAAI,QAAQ;AACZ,iBAAW,CAAC,KAAK,MAAM,KAAK,KAAK;AAChC,YAAI,UAAU,MAAM,GAAG;AACtB,cAAI,OAAO,GAAG;AACd;AAAA,QACD;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-Kb-9sxwk.cjs';
1
+ import { e as IdempotencyStore } from '../types-CI-upMvQ.cjs';
2
2
  import 'hono';
3
3
 
4
4
  interface MemoryStoreOptions {
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-Kb-9sxwk.js';
1
+ import { e as IdempotencyStore } from '../types-CI-upMvQ.js';
2
2
  import 'hono';
3
3
 
4
4
  interface MemoryStoreOptions {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  RECORD_STATUS_COMPLETED,
3
3
  RECORD_STATUS_PROCESSING
4
- } from "../chunk-I5ECYFYR.js";
4
+ } from "../chunk-ET3EW23V.js";
5
5
 
6
6
  // src/stores/memory.ts
7
7
  var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/redis.ts","../../src/types.ts"],"sourcesContent":["import { type IdempotencyRecord, RECORD_STATUS_COMPLETED, type StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal Redis client subset compatible with ioredis, node-redis, and @upstash/redis. */\nexport interface RedisClientLike {\n\tget(key: string): Promise<string | null>;\n\tset(key: string, value: string, options?: { NX?: boolean; EX?: number }): Promise<string | null>;\n\tdel(...keys: string[]): Promise<number>;\n}\n\nexport interface RedisStoreOptions {\n\t/** Redis client instance (ioredis, node-redis, or @upstash/redis). */\n\tclient: RedisClientLike;\n\t/** TTL in seconds (default: 86400 = 24h). Passed as Redis EX option. */\n\tttl?: number;\n}\n\nexport function redisStore(options: RedisStoreOptions): IdempotencyStore {\n\tconst { client, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst raw = await client.get(key);\n\t\t\tif (!raw) return undefined;\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(raw) as IdempotencyRecord;\n\t\t\t} catch {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(record);\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst result = await client.set(key, serialized, { NX: true, EX: ttl });\n\t\t\treturn result === \"OK\";\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst raw = await client.get(key);\n\t\t\tif (!raw) return;\n\t\t\tlet record: IdempotencyRecord;\n\t\t\ttry {\n\t\t\t\trecord = JSON.parse(raw) as IdempotencyRecord;\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\trecord.response = response;\n\t\t\tconst elapsed = Math.floor((Date.now() - record.createdAt) / 1000);\n\t\t\tconst remaining = Math.max(1, ttl - elapsed);\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(record);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait client.set(key, serialized, { EX: remaining });\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait client.del(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\t// Redis handles expiration automatically via EX — no manual purge needed\n\t\t\treturn 0;\n\t\t},\n\t};\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,0BAA0B;;;ADFvC,IAAM,cAAc;AAgBb,SAAS,WAAW,SAA8C;AACxE,QAAM,EAAE,QAAQ,MAAM,YAAY,IAAI;AAEtC,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,MAAM,MAAM,OAAO,IAAI,GAAG;AAChC,UAAI,CAAC,IAAK,QAAO;AACjB,UAAI;AACH,eAAO,KAAK,MAAM,GAAG;AAAA,MACtB,QAAQ;AACP,eAAO;AAAA,MACR;AAAA,IACD;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,MAAM;AAAA,MACnC,QAAQ;AACP,eAAO;AAAA,MACR;AACA,YAAM,SAAS,MAAM,OAAO,IAAI,KAAK,YAAY,EAAE,IAAI,MAAM,IAAI,IAAI,CAAC;AACtE,aAAO,WAAW;AAAA,IACnB;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,MAAM,MAAM,OAAO,IAAI,GAAG;AAChC,UAAI,CAAC,IAAK;AACV,UAAI;AACJ,UAAI;AACH,iBAAS,KAAK,MAAM,GAAG;AAAA,MACxB,QAAQ;AACP;AAAA,MACD;AACA,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,UAAU,KAAK,OAAO,KAAK,IAAI,IAAI,OAAO,aAAa,GAAI;AACjE,YAAM,YAAY,KAAK,IAAI,GAAG,MAAM,OAAO;AAC3C,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,MAAM;AAAA,MACnC,QAAQ;AACP;AAAA,MACD;AACA,YAAM,OAAO,IAAI,KAAK,YAAY,EAAE,IAAI,UAAU,CAAC;AAAA,IACpD;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,OAAO,IAAI,GAAG;AAAA,IACrB;AAAA,IAEA,MAAM,QAAQ;AAEb,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/stores/redis.ts","../../src/types.ts"],"sourcesContent":["import { type IdempotencyRecord, RECORD_STATUS_COMPLETED, type StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal Redis client subset compatible with ioredis, node-redis, and @upstash/redis. */\nexport interface RedisClientLike {\n\tget(key: string): Promise<string | null>;\n\tset(key: string, value: string, options?: { NX?: boolean; EX?: number }): Promise<string | null>;\n\tdel(...keys: string[]): Promise<number>;\n}\n\nexport interface RedisStoreOptions {\n\t/** Redis client instance (ioredis, node-redis, or @upstash/redis). */\n\tclient: RedisClientLike;\n\t/** TTL in seconds (default: 86400 = 24h). Passed as Redis EX option. */\n\tttl?: number;\n}\n\nexport function redisStore(options: RedisStoreOptions): IdempotencyStore {\n\tconst { client, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst raw = await client.get(key);\n\t\t\tif (!raw) return undefined;\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(raw) as IdempotencyRecord;\n\t\t\t} catch {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(record);\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst result = await client.set(key, serialized, { NX: true, EX: ttl });\n\t\t\treturn result === \"OK\";\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst raw = await client.get(key);\n\t\t\tif (!raw) return;\n\t\t\tlet record: IdempotencyRecord;\n\t\t\ttry {\n\t\t\t\trecord = JSON.parse(raw) as IdempotencyRecord;\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\trecord.response = response;\n\t\t\tconst elapsed = Math.floor((Date.now() - record.createdAt) / 1000);\n\t\t\tconst remaining = Math.max(1, ttl - elapsed);\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(record);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait client.set(key, serialized, { EX: remaining });\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait client.del(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\t// Redis handles expiration automatically via EX — no manual purge needed\n\t\t\treturn 0;\n\t\t},\n\t};\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/**\n\t * Maximum request body size in bytes. Pre-checked via Content-Length header,\n\t * then enforced against actual body byte length.\n\t * Only applies when an Idempotency-Key header is present.\n\t * Requests without the key bypass this check regardless of this setting.\n\t */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Opt out of the multi-tenant safety warning.\n\t *\n\t * When `cacheKeyPrefix` is not set and `methods` includes any state-mutating\n\t * method (POST/PATCH/PUT/DELETE), the middleware emits a one-time\n\t * `console.warn` at factory construction time. Set this to `true` to\n\t * acknowledge that the deployment is single-tenant and silence the warning.\n\t *\n\t * @default false\n\t */\n\tdangerouslyAllowGlobalKeys?: boolean;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,0BAA0B;;;ADFvC,IAAM,cAAc;AAgBb,SAAS,WAAW,SAA8C;AACxE,QAAM,EAAE,QAAQ,MAAM,YAAY,IAAI;AAEtC,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,MAAM,MAAM,OAAO,IAAI,GAAG;AAChC,UAAI,CAAC,IAAK,QAAO;AACjB,UAAI;AACH,eAAO,KAAK,MAAM,GAAG;AAAA,MACtB,QAAQ;AACP,eAAO;AAAA,MACR;AAAA,IACD;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,MAAM;AAAA,MACnC,QAAQ;AACP,eAAO;AAAA,MACR;AACA,YAAM,SAAS,MAAM,OAAO,IAAI,KAAK,YAAY,EAAE,IAAI,MAAM,IAAI,IAAI,CAAC;AACtE,aAAO,WAAW;AAAA,IACnB;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,MAAM,MAAM,OAAO,IAAI,GAAG;AAChC,UAAI,CAAC,IAAK;AACV,UAAI;AACJ,UAAI;AACH,iBAAS,KAAK,MAAM,GAAG;AAAA,MACxB,QAAQ;AACP;AAAA,MACD;AACA,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,UAAU,KAAK,OAAO,KAAK,IAAI,IAAI,OAAO,aAAa,GAAI;AACjE,YAAM,YAAY,KAAK,IAAI,GAAG,MAAM,OAAO;AAC3C,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,MAAM;AAAA,MACnC,QAAQ;AACP;AAAA,MACD;AACA,YAAM,OAAO,IAAI,KAAK,YAAY,EAAE,IAAI,UAAU,CAAC;AAAA,IACpD;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,OAAO,IAAI,GAAG;AAAA,IACrB;AAAA,IAEA,MAAM,QAAQ;AAEb,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-Kb-9sxwk.cjs';
1
+ import { e as IdempotencyStore } from '../types-CI-upMvQ.cjs';
2
2
  import 'hono';
3
3
 
4
4
  /** Minimal Redis client subset compatible with ioredis, node-redis, and @upstash/redis. */
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-Kb-9sxwk.js';
1
+ import { e as IdempotencyStore } from '../types-CI-upMvQ.js';
2
2
  import 'hono';
3
3
 
4
4
  /** Minimal Redis client subset compatible with ioredis, node-redis, and @upstash/redis. */
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RECORD_STATUS_COMPLETED
3
- } from "../chunk-I5ECYFYR.js";
3
+ } from "../chunk-ET3EW23V.js";
4
4
 
5
5
  // src/stores/redis.ts
6
6
  var DEFAULT_TTL = 86400;
@@ -102,6 +102,17 @@ interface IdempotencyOptions {
102
102
  * Errors are swallowed — hooks must not affect request processing.
103
103
  */
104
104
  onCacheMiss?: (key: string, c: Context) => void | Promise<void>;
105
+ /**
106
+ * Opt out of the multi-tenant safety warning.
107
+ *
108
+ * When `cacheKeyPrefix` is not set and `methods` includes any state-mutating
109
+ * method (POST/PATCH/PUT/DELETE), the middleware emits a one-time
110
+ * `console.warn` at factory construction time. Set this to `true` to
111
+ * acknowledge that the deployment is single-tenant and silence the warning.
112
+ *
113
+ * @default false
114
+ */
115
+ dangerouslyAllowGlobalKeys?: boolean;
105
116
  }
106
117
 
107
118
  export { type IdempotencyOptions as I, type ProblemDetail as P, RECORD_STATUS_COMPLETED as R, type StoredResponse as S, type IdempotencyEnv as a, type IdempotencyErrorCode as b, IdempotencyErrors as c, type IdempotencyRecord as d, type IdempotencyStore as e, RECORD_STATUS_PROCESSING as f, clampHttpStatus as g, problemResponse as p };
@@ -102,6 +102,17 @@ interface IdempotencyOptions {
102
102
  * Errors are swallowed — hooks must not affect request processing.
103
103
  */
104
104
  onCacheMiss?: (key: string, c: Context) => void | Promise<void>;
105
+ /**
106
+ * Opt out of the multi-tenant safety warning.
107
+ *
108
+ * When `cacheKeyPrefix` is not set and `methods` includes any state-mutating
109
+ * method (POST/PATCH/PUT/DELETE), the middleware emits a one-time
110
+ * `console.warn` at factory construction time. Set this to `true` to
111
+ * acknowledge that the deployment is single-tenant and silence the warning.
112
+ *
113
+ * @default false
114
+ */
115
+ dangerouslyAllowGlobalKeys?: boolean;
105
116
  }
106
117
 
107
118
  export { type IdempotencyOptions as I, type ProblemDetail as P, RECORD_STATUS_COMPLETED as R, type StoredResponse as S, type IdempotencyEnv as a, type IdempotencyErrorCode as b, IdempotencyErrors as c, type IdempotencyRecord as d, type IdempotencyStore as e, RECORD_STATUS_PROCESSING as f, clampHttpStatus as g, problemResponse as p };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono-idempotency",
3
- "version": "0.8.4",
3
+ "version": "0.9.0",
4
4
  "description": "Stripe-style Idempotency-Key middleware for Hono. IETF draft-ietf-httpapi-idempotency-key-header compliant.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -109,12 +109,13 @@
109
109
  "url": "https://github.com/paveg/hono-idempotency/issues"
110
110
  },
111
111
  "license": "MIT",
112
+ "funding": "https://github.com/sponsors/paveg",
112
113
  "publishConfig": {
113
114
  "access": "public",
114
115
  "provenance": true
115
116
  },
116
117
  "engines": {
117
- "node": ">=20"
118
+ "node": ">=22"
118
119
  },
119
120
  "peerDependencies": {
120
121
  "hono": ">=4.0.0",
@@ -134,7 +135,7 @@
134
135
  "hono-problem-details": "0.1.4",
135
136
  "lefthook": "2.1.1",
136
137
  "tsup": "^8.5.1",
137
- "typescript": "^5.9.3",
138
+ "typescript": "^6.0.3",
138
139
  "vitest": "^3.2.4"
139
140
  },
140
141
  "scripts": {
@@ -145,6 +146,7 @@
145
146
  "lint:fix": "biome check --write .",
146
147
  "format": "biome format --write .",
147
148
  "typecheck": "tsc --noEmit",
149
+ "test:compat": "tsc -p tsconfig.compat.json",
148
150
  "release": "pnpm build && changeset publish",
149
151
  "version-packages": "changeset version && pnpm lint:fix"
150
152
  }