hono-idempotency 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryota Ikezawa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # hono-idempotency
2
+
3
+ Stripe-style Idempotency-Key middleware for [Hono](https://hono.dev). IETF [draft-ietf-httpapi-idempotency-key-header](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/) compliant.
4
+
5
+ ## Features
6
+
7
+ - Idempotency-Key header support for POST/PATCH (configurable)
8
+ - Request fingerprinting (SHA-256) prevents key reuse with different payloads
9
+ - Concurrent request protection with optimistic locking
10
+ - RFC 9457 Problem Details error responses
11
+ - Replayed responses include `Idempotency-Replayed: true` header
12
+ - Non-2xx responses are not cached (Stripe pattern — allows client retry)
13
+ - Pluggable store interface (memory, Cloudflare KV, Cloudflare D1)
14
+ - Works on Cloudflare Workers, Node.js, Deno, Bun, and any Web Standards runtime
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install hono-idempotency
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```ts
25
+ import { Hono } from "hono";
26
+ import { idempotency } from "hono-idempotency";
27
+ import { memoryStore } from "hono-idempotency/stores/memory";
28
+
29
+ const app = new Hono();
30
+
31
+ app.use("/api/*", idempotency({ store: memoryStore() }));
32
+
33
+ app.post("/api/payments", (c) => {
34
+ // This handler only runs once per unique Idempotency-Key.
35
+ // Retries with the same key return the cached response.
36
+ return c.json({ id: "pay_123", status: "succeeded" }, 201);
37
+ });
38
+ ```
39
+
40
+ Client usage:
41
+
42
+ ```bash
43
+ curl -X POST http://localhost:3000/api/payments \
44
+ -H "Idempotency-Key: unique-request-id-123" \
45
+ -H "Content-Type: application/json" \
46
+ -d '{"amount": 1000}'
47
+ ```
48
+
49
+ ## Options
50
+
51
+ ```ts
52
+ idempotency({
53
+ // Required: storage backend
54
+ store: memoryStore(),
55
+
56
+ // Header name (default: "Idempotency-Key")
57
+ headerName: "Idempotency-Key",
58
+
59
+ // Return 400 if header is missing (default: false)
60
+ required: false,
61
+
62
+ // HTTP methods to apply idempotency (default: ["POST", "PATCH"])
63
+ methods: ["POST", "PATCH"],
64
+
65
+ // Maximum key length (default: 256)
66
+ maxKeyLength: 256,
67
+
68
+ // Custom fingerprint function (default: SHA-256 of method + path + body)
69
+ fingerprint: (c) => `${c.req.method}:${c.req.path}`,
70
+ });
71
+ ```
72
+
73
+ ## Stores
74
+
75
+ ### Memory Store
76
+
77
+ Built-in, suitable for single-instance deployments and development.
78
+
79
+ ```ts
80
+ import { memoryStore } from "hono-idempotency/stores/memory";
81
+
82
+ const store = memoryStore({
83
+ ttl: 24 * 60 * 60 * 1000, // 24 hours (default)
84
+ });
85
+ ```
86
+
87
+ ### Custom Store
88
+
89
+ Implement the `IdempotencyStore` interface:
90
+
91
+ ```ts
92
+ import type { IdempotencyStore } from "hono-idempotency";
93
+
94
+ const customStore: IdempotencyStore = {
95
+ async get(key) { /* ... */ },
96
+ async lock(key, record) { /* return false if already locked */ },
97
+ async complete(key, response) { /* ... */ },
98
+ async delete(key) { /* ... */ },
99
+ };
100
+ ```
101
+
102
+ ## Error Responses
103
+
104
+ All errors follow [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457) with `Content-Type: application/problem+json`.
105
+
106
+ | Status | Type | When |
107
+ |--------|------|------|
108
+ | 400 | `/errors/missing-key` | `required: true` and no header |
109
+ | 400 | `/errors/key-too-long` | Key exceeds `maxKeyLength` |
110
+ | 409 | `/errors/conflict` | Concurrent request with same key |
111
+ | 422 | `/errors/fingerprint-mismatch` | Same key, different request body |
112
+
113
+ ## Accessing the Key in Handlers
114
+
115
+ The middleware sets `idempotencyKey` on the Hono context:
116
+
117
+ ```ts
118
+ import type { IdempotencyEnv } from "hono-idempotency";
119
+
120
+ app.post("/api/payments", (c: Context<IdempotencyEnv>) => {
121
+ const key = c.get("idempotencyKey");
122
+ return c.json({ idempotencyKey: key });
123
+ });
124
+ ```
125
+
126
+ ## License
127
+
128
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ idempotency: () => idempotency
24
+ });
25
+ module.exports = __toCommonJS(src_exports);
26
+
27
+ // src/middleware.ts
28
+ var import_factory = require("hono/factory");
29
+
30
+ // src/errors.ts
31
+ function problemResponse(problem, extraHeaders) {
32
+ return new Response(JSON.stringify(problem), {
33
+ status: problem.status,
34
+ headers: {
35
+ "Content-Type": "application/problem+json",
36
+ ...extraHeaders
37
+ }
38
+ });
39
+ }
40
+ var BASE_URL = "https://hono-idempotency.dev/errors";
41
+ var IdempotencyErrors = {
42
+ missingKey() {
43
+ return {
44
+ type: `${BASE_URL}/missing-key`,
45
+ title: "Idempotency-Key header is required",
46
+ status: 400,
47
+ detail: "This endpoint requires an Idempotency-Key header"
48
+ };
49
+ },
50
+ keyTooLong(maxLength) {
51
+ return {
52
+ type: `${BASE_URL}/key-too-long`,
53
+ title: "Idempotency-Key is too long",
54
+ status: 400,
55
+ detail: `Idempotency-Key must be at most ${maxLength} characters`
56
+ };
57
+ },
58
+ fingerprintMismatch() {
59
+ return {
60
+ type: `${BASE_URL}/fingerprint-mismatch`,
61
+ title: "Idempotency-Key is already used with a different request",
62
+ status: 422,
63
+ detail: "A request with the same idempotency key but different parameters was already processed"
64
+ };
65
+ },
66
+ conflict() {
67
+ return {
68
+ type: `${BASE_URL}/conflict`,
69
+ title: "A request is outstanding for this idempotency key",
70
+ status: 409,
71
+ detail: "A request with the same idempotency key is currently being processed"
72
+ };
73
+ }
74
+ };
75
+
76
+ // src/fingerprint.ts
77
+ async function generateFingerprint(method, path, body) {
78
+ const data = `${method}:${path}:${body}`;
79
+ const encoded = new TextEncoder().encode(data);
80
+ const hashBuffer = await crypto.subtle.digest("SHA-256", encoded);
81
+ const hashArray = new Uint8Array(hashBuffer);
82
+ return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
83
+ }
84
+
85
+ // src/middleware.ts
86
+ var DEFAULT_METHODS = ["POST", "PATCH"];
87
+ var DEFAULT_MAX_KEY_LENGTH = 256;
88
+ var EXCLUDED_STORE_HEADERS = /* @__PURE__ */ new Set(["set-cookie"]);
89
+ function idempotency(options) {
90
+ const {
91
+ store,
92
+ headerName = "Idempotency-Key",
93
+ fingerprint: customFingerprint,
94
+ required = false,
95
+ methods = DEFAULT_METHODS,
96
+ maxKeyLength = DEFAULT_MAX_KEY_LENGTH,
97
+ skipRequest,
98
+ onError,
99
+ cacheKeyPrefix
100
+ } = options;
101
+ return (0, import_factory.createMiddleware)(async (c, next) => {
102
+ if (!methods.includes(c.req.method)) {
103
+ return next();
104
+ }
105
+ if (skipRequest && await skipRequest(c)) {
106
+ return next();
107
+ }
108
+ const errorResponse = (problem, extraHeaders) => onError ? onError(problem, c) : problemResponse(problem, extraHeaders);
109
+ const key = c.req.header(headerName);
110
+ if (!key) {
111
+ if (required) {
112
+ return errorResponse(IdempotencyErrors.missingKey());
113
+ }
114
+ return next();
115
+ }
116
+ if (key.length > maxKeyLength) {
117
+ return errorResponse(IdempotencyErrors.keyTooLong(maxKeyLength));
118
+ }
119
+ const body = await c.req.text();
120
+ const fp = customFingerprint ? await customFingerprint(c) : await generateFingerprint(c.req.method, c.req.path, body);
121
+ const rawPrefix = typeof cacheKeyPrefix === "function" ? await cacheKeyPrefix(c) : cacheKeyPrefix;
122
+ const encodedKey = encodeURIComponent(key);
123
+ const baseKey = `${c.req.method}:${c.req.path}:${encodedKey}`;
124
+ const storeKey = rawPrefix ? `${encodeURIComponent(rawPrefix)}:${baseKey}` : baseKey;
125
+ const existing = await store.get(storeKey);
126
+ if (existing) {
127
+ if (existing.status === "processing") {
128
+ return errorResponse(IdempotencyErrors.conflict(), { "Retry-After": "1" });
129
+ }
130
+ if (existing.fingerprint !== fp) {
131
+ return errorResponse(IdempotencyErrors.fingerprintMismatch());
132
+ }
133
+ if (existing.response) {
134
+ return replayResponse(existing.response);
135
+ }
136
+ }
137
+ const record = {
138
+ key,
139
+ fingerprint: fp,
140
+ status: "processing",
141
+ createdAt: Date.now()
142
+ };
143
+ const locked = await store.lock(storeKey, record);
144
+ if (!locked) {
145
+ return errorResponse(IdempotencyErrors.conflict(), { "Retry-After": "1" });
146
+ }
147
+ c.set("idempotencyKey", key);
148
+ try {
149
+ await next();
150
+ } catch (err) {
151
+ await store.delete(storeKey);
152
+ throw err;
153
+ }
154
+ const res = c.res;
155
+ if (!res.ok) {
156
+ await store.delete(storeKey);
157
+ return;
158
+ }
159
+ const resBody = await res.text();
160
+ const resHeaders = {};
161
+ res.headers.forEach((v, k) => {
162
+ if (!EXCLUDED_STORE_HEADERS.has(k.toLowerCase())) {
163
+ resHeaders[k] = v;
164
+ }
165
+ });
166
+ const storedResponse = {
167
+ status: res.status,
168
+ headers: resHeaders,
169
+ body: resBody
170
+ };
171
+ await store.complete(storeKey, storedResponse);
172
+ c.res = new Response(resBody, {
173
+ status: res.status,
174
+ headers: res.headers
175
+ });
176
+ });
177
+ }
178
+ function replayResponse(stored) {
179
+ const headers = new Headers(stored.headers);
180
+ headers.set("Idempotency-Replayed", "true");
181
+ return new Response(stored.body, {
182
+ status: stored.status,
183
+ headers
184
+ });
185
+ }
186
+ // Annotate the CommonJS export names for ESM import in node:
187
+ 0 && (module.exports = {
188
+ idempotency
189
+ });
190
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/middleware.ts","../src/errors.ts","../src/fingerprint.ts"],"sourcesContent":["export { idempotency } from \"./middleware.js\";\nexport type {\n\tIdempotencyEnv,\n\tIdempotencyOptions,\n\tIdempotencyRecord,\n\tStoredResponse,\n} from \"./types.js\";\nexport type { IdempotencyStore } from \"./stores/types.js\";\nexport type { ProblemDetail } from \"./errors.js\";\n","import { createMiddleware } from \"hono/factory\";\nimport { IdempotencyErrors, type ProblemDetail, problemResponse } from \"./errors.js\";\nimport { generateFingerprint } from \"./fingerprint.js\";\nimport type { IdempotencyEnv, IdempotencyOptions, StoredResponse } 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\"]);\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} = 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 = (problem: ProblemDetail, extraHeaders?: Record<string, string>) =>\n\t\t\tonError ? onError(problem, c) : problemResponse(problem, extraHeaders);\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 (key.length > maxKeyLength) {\n\t\t\treturn errorResponse(IdempotencyErrors.keyTooLong(maxKeyLength));\n\t\t}\n\n\t\tconst body = await c.req.text();\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 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 === \"processing\") {\n\t\t\t\treturn errorResponse(IdempotencyErrors.conflict(), { \"Retry-After\": \"1\" });\n\t\t\t}\n\n\t\t\tif (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\treturn replayResponse(existing.response);\n\t\t\t}\n\t\t}\n\n\t\tconst record = {\n\t\t\tkey,\n\t\t\tfingerprint: fp,\n\t\t\tstatus: \"processing\" as const,\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(), { \"Retry-After\": \"1\" });\n\t\t}\n\n\t\tc.set(\"idempotencyKey\", key);\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\nfunction replayResponse(stored: StoredResponse) {\n\tconst headers = new Headers(stored.headers);\n\theaders.set(\"Idempotency-Replayed\", \"true\");\n\n\treturn new Response(stored.body, {\n\t\tstatus: stored.status,\n\t\theaders,\n\t});\n}\n","export interface ProblemDetail {\n\ttype: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail: string;\n}\n\nexport function problemResponse(\n\tproblem: ProblemDetail,\n\textraHeaders?: Record<string, string>,\n): Response {\n\treturn new Response(JSON.stringify(problem), {\n\t\tstatus: problem.status,\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/problem+json\",\n\t\t\t...extraHeaders,\n\t\t},\n\t});\n}\n\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};\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};\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};\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};\n\t},\n} as const;\n","export async function generateFingerprint(\n\tmethod: string,\n\tpath: string,\n\tbody: string,\n): Promise<string> {\n\tconst data = `${method}:${path}:${body}`;\n\tconst encoded = new TextEncoder().encode(data);\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", encoded);\n\tconst hashArray = new Uint8Array(hashBuffer);\n\treturn Array.from(hashArray)\n\t\t.map((b) => b.toString(16).padStart(2, \"0\"))\n\t\t.join(\"\");\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,7 @@
1
+ import * as hono from 'hono';
2
+ import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-YeEt4qLu.cjs';
3
+ export { b as IdempotencyRecord, c as IdempotencyStore, P as ProblemDetail, S as StoredResponse } from './types-YeEt4qLu.cjs';
4
+
5
+ declare function idempotency(options: IdempotencyOptions): hono.MiddlewareHandler<IdempotencyEnv, string, {}, Response>;
6
+
7
+ export { IdempotencyEnv, IdempotencyOptions, idempotency };
@@ -0,0 +1,7 @@
1
+ import * as hono from 'hono';
2
+ import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-YeEt4qLu.js';
3
+ export { b as IdempotencyRecord, c as IdempotencyStore, P as ProblemDetail, S as StoredResponse } from './types-YeEt4qLu.js';
4
+
5
+ declare function idempotency(options: IdempotencyOptions): hono.MiddlewareHandler<IdempotencyEnv, string, {}, Response>;
6
+
7
+ export { IdempotencyEnv, IdempotencyOptions, idempotency };
package/dist/index.js ADDED
@@ -0,0 +1,163 @@
1
+ // src/middleware.ts
2
+ import { createMiddleware } from "hono/factory";
3
+
4
+ // src/errors.ts
5
+ function problemResponse(problem, extraHeaders) {
6
+ return new Response(JSON.stringify(problem), {
7
+ status: problem.status,
8
+ headers: {
9
+ "Content-Type": "application/problem+json",
10
+ ...extraHeaders
11
+ }
12
+ });
13
+ }
14
+ var BASE_URL = "https://hono-idempotency.dev/errors";
15
+ var IdempotencyErrors = {
16
+ missingKey() {
17
+ return {
18
+ type: `${BASE_URL}/missing-key`,
19
+ title: "Idempotency-Key header is required",
20
+ status: 400,
21
+ detail: "This endpoint requires an Idempotency-Key header"
22
+ };
23
+ },
24
+ keyTooLong(maxLength) {
25
+ return {
26
+ type: `${BASE_URL}/key-too-long`,
27
+ title: "Idempotency-Key is too long",
28
+ status: 400,
29
+ detail: `Idempotency-Key must be at most ${maxLength} characters`
30
+ };
31
+ },
32
+ fingerprintMismatch() {
33
+ return {
34
+ type: `${BASE_URL}/fingerprint-mismatch`,
35
+ title: "Idempotency-Key is already used with a different request",
36
+ status: 422,
37
+ detail: "A request with the same idempotency key but different parameters was already processed"
38
+ };
39
+ },
40
+ conflict() {
41
+ return {
42
+ type: `${BASE_URL}/conflict`,
43
+ title: "A request is outstanding for this idempotency key",
44
+ status: 409,
45
+ detail: "A request with the same idempotency key is currently being processed"
46
+ };
47
+ }
48
+ };
49
+
50
+ // src/fingerprint.ts
51
+ async function generateFingerprint(method, path, body) {
52
+ const data = `${method}:${path}:${body}`;
53
+ const encoded = new TextEncoder().encode(data);
54
+ const hashBuffer = await crypto.subtle.digest("SHA-256", encoded);
55
+ const hashArray = new Uint8Array(hashBuffer);
56
+ return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
57
+ }
58
+
59
+ // src/middleware.ts
60
+ var DEFAULT_METHODS = ["POST", "PATCH"];
61
+ var DEFAULT_MAX_KEY_LENGTH = 256;
62
+ var EXCLUDED_STORE_HEADERS = /* @__PURE__ */ new Set(["set-cookie"]);
63
+ function idempotency(options) {
64
+ const {
65
+ store,
66
+ headerName = "Idempotency-Key",
67
+ fingerprint: customFingerprint,
68
+ required = false,
69
+ methods = DEFAULT_METHODS,
70
+ maxKeyLength = DEFAULT_MAX_KEY_LENGTH,
71
+ skipRequest,
72
+ onError,
73
+ cacheKeyPrefix
74
+ } = options;
75
+ return createMiddleware(async (c, next) => {
76
+ if (!methods.includes(c.req.method)) {
77
+ return next();
78
+ }
79
+ if (skipRequest && await skipRequest(c)) {
80
+ return next();
81
+ }
82
+ const errorResponse = (problem, extraHeaders) => onError ? onError(problem, c) : problemResponse(problem, extraHeaders);
83
+ const key = c.req.header(headerName);
84
+ if (!key) {
85
+ if (required) {
86
+ return errorResponse(IdempotencyErrors.missingKey());
87
+ }
88
+ return next();
89
+ }
90
+ if (key.length > maxKeyLength) {
91
+ return errorResponse(IdempotencyErrors.keyTooLong(maxKeyLength));
92
+ }
93
+ const body = await c.req.text();
94
+ const fp = customFingerprint ? await customFingerprint(c) : await generateFingerprint(c.req.method, c.req.path, body);
95
+ const rawPrefix = typeof cacheKeyPrefix === "function" ? await cacheKeyPrefix(c) : cacheKeyPrefix;
96
+ const encodedKey = encodeURIComponent(key);
97
+ const baseKey = `${c.req.method}:${c.req.path}:${encodedKey}`;
98
+ const storeKey = rawPrefix ? `${encodeURIComponent(rawPrefix)}:${baseKey}` : baseKey;
99
+ const existing = await store.get(storeKey);
100
+ if (existing) {
101
+ if (existing.status === "processing") {
102
+ return errorResponse(IdempotencyErrors.conflict(), { "Retry-After": "1" });
103
+ }
104
+ if (existing.fingerprint !== fp) {
105
+ return errorResponse(IdempotencyErrors.fingerprintMismatch());
106
+ }
107
+ if (existing.response) {
108
+ return replayResponse(existing.response);
109
+ }
110
+ }
111
+ const record = {
112
+ key,
113
+ fingerprint: fp,
114
+ status: "processing",
115
+ createdAt: Date.now()
116
+ };
117
+ const locked = await store.lock(storeKey, record);
118
+ if (!locked) {
119
+ return errorResponse(IdempotencyErrors.conflict(), { "Retry-After": "1" });
120
+ }
121
+ c.set("idempotencyKey", key);
122
+ try {
123
+ await next();
124
+ } catch (err) {
125
+ await store.delete(storeKey);
126
+ throw err;
127
+ }
128
+ const res = c.res;
129
+ if (!res.ok) {
130
+ await store.delete(storeKey);
131
+ return;
132
+ }
133
+ const resBody = await res.text();
134
+ const resHeaders = {};
135
+ res.headers.forEach((v, k) => {
136
+ if (!EXCLUDED_STORE_HEADERS.has(k.toLowerCase())) {
137
+ resHeaders[k] = v;
138
+ }
139
+ });
140
+ const storedResponse = {
141
+ status: res.status,
142
+ headers: resHeaders,
143
+ body: resBody
144
+ };
145
+ await store.complete(storeKey, storedResponse);
146
+ c.res = new Response(resBody, {
147
+ status: res.status,
148
+ headers: res.headers
149
+ });
150
+ });
151
+ }
152
+ function replayResponse(stored) {
153
+ const headers = new Headers(stored.headers);
154
+ headers.set("Idempotency-Replayed", "true");
155
+ return new Response(stored.body, {
156
+ status: stored.status,
157
+ headers
158
+ });
159
+ }
160
+ export {
161
+ idempotency
162
+ };
163
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/middleware.ts","../src/errors.ts","../src/fingerprint.ts"],"sourcesContent":["import { createMiddleware } from \"hono/factory\";\nimport { IdempotencyErrors, type ProblemDetail, problemResponse } from \"./errors.js\";\nimport { generateFingerprint } from \"./fingerprint.js\";\nimport type { IdempotencyEnv, IdempotencyOptions, StoredResponse } 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\"]);\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} = 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 = (problem: ProblemDetail, extraHeaders?: Record<string, string>) =>\n\t\t\tonError ? onError(problem, c) : problemResponse(problem, extraHeaders);\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 (key.length > maxKeyLength) {\n\t\t\treturn errorResponse(IdempotencyErrors.keyTooLong(maxKeyLength));\n\t\t}\n\n\t\tconst body = await c.req.text();\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 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 === \"processing\") {\n\t\t\t\treturn errorResponse(IdempotencyErrors.conflict(), { \"Retry-After\": \"1\" });\n\t\t\t}\n\n\t\t\tif (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\treturn replayResponse(existing.response);\n\t\t\t}\n\t\t}\n\n\t\tconst record = {\n\t\t\tkey,\n\t\t\tfingerprint: fp,\n\t\t\tstatus: \"processing\" as const,\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(), { \"Retry-After\": \"1\" });\n\t\t}\n\n\t\tc.set(\"idempotencyKey\", key);\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\nfunction replayResponse(stored: StoredResponse) {\n\tconst headers = new Headers(stored.headers);\n\theaders.set(\"Idempotency-Replayed\", \"true\");\n\n\treturn new Response(stored.body, {\n\t\tstatus: stored.status,\n\t\theaders,\n\t});\n}\n","export interface ProblemDetail {\n\ttype: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail: string;\n}\n\nexport function problemResponse(\n\tproblem: ProblemDetail,\n\textraHeaders?: Record<string, string>,\n): Response {\n\treturn new Response(JSON.stringify(problem), {\n\t\tstatus: problem.status,\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/problem+json\",\n\t\t\t...extraHeaders,\n\t\t},\n\t});\n}\n\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};\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};\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};\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};\n\t},\n} as const;\n","export async function generateFingerprint(\n\tmethod: string,\n\tpath: string,\n\tbody: string,\n): Promise<string> {\n\tconst data = `${method}:${path}:${body}`;\n\tconst encoded = new TextEncoder().encode(data);\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", encoded);\n\tconst hashArray = new Uint8Array(hashBuffer);\n\treturn Array.from(hashArray)\n\t\t.map((b) => b.toString(16).padStart(2, \"0\"))\n\t\t.join(\"\");\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/stores/cloudflare-d1.ts
21
+ var cloudflare_d1_exports = {};
22
+ __export(cloudflare_d1_exports, {
23
+ d1Store: () => d1Store
24
+ });
25
+ module.exports = __toCommonJS(cloudflare_d1_exports);
26
+ function d1Store(_options) {
27
+ throw new Error("cloudflare-d1 store is not yet implemented. Coming in Phase 2.");
28
+ }
29
+ // Annotate the CommonJS export names for ESM import in node:
30
+ 0 && (module.exports = {
31
+ d1Store
32
+ });
33
+ //# sourceMappingURL=cloudflare-d1.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/stores/cloudflare-d1.ts"],"sourcesContent":["import type { IdempotencyStore } from \"./types.js\";\n\ninterface D1StoreOptions {\n\tbinding: string;\n\ttableName?: string;\n}\n\n// Phase 2: Cloudflare D1 store implementation\nexport function d1Store(_options: D1StoreOptions): IdempotencyStore {\n\tthrow new Error(\"cloudflare-d1 store is not yet implemented. Coming in Phase 2.\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAQO,SAAS,QAAQ,UAA4C;AACnE,QAAM,IAAI,MAAM,gEAAgE;AACjF;","names":[]}
@@ -0,0 +1,10 @@
1
+ import { c as IdempotencyStore } from '../types-YeEt4qLu.cjs';
2
+ import 'hono';
3
+
4
+ interface D1StoreOptions {
5
+ binding: string;
6
+ tableName?: string;
7
+ }
8
+ declare function d1Store(_options: D1StoreOptions): IdempotencyStore;
9
+
10
+ export { d1Store };
@@ -0,0 +1,10 @@
1
+ import { c as IdempotencyStore } from '../types-YeEt4qLu.js';
2
+ import 'hono';
3
+
4
+ interface D1StoreOptions {
5
+ binding: string;
6
+ tableName?: string;
7
+ }
8
+ declare function d1Store(_options: D1StoreOptions): IdempotencyStore;
9
+
10
+ export { d1Store };
@@ -0,0 +1,8 @@
1
+ // src/stores/cloudflare-d1.ts
2
+ function d1Store(_options) {
3
+ throw new Error("cloudflare-d1 store is not yet implemented. Coming in Phase 2.");
4
+ }
5
+ export {
6
+ d1Store
7
+ };
8
+ //# sourceMappingURL=cloudflare-d1.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/stores/cloudflare-d1.ts"],"sourcesContent":["import type { IdempotencyStore } from \"./types.js\";\n\ninterface D1StoreOptions {\n\tbinding: string;\n\ttableName?: string;\n}\n\n// Phase 2: Cloudflare D1 store implementation\nexport function d1Store(_options: D1StoreOptions): IdempotencyStore {\n\tthrow new Error(\"cloudflare-d1 store is not yet implemented. Coming in Phase 2.\");\n}\n"],"mappings":";AAQO,SAAS,QAAQ,UAA4C;AACnE,QAAM,IAAI,MAAM,gEAAgE;AACjF;","names":[]}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/stores/cloudflare-kv.ts
21
+ var cloudflare_kv_exports = {};
22
+ __export(cloudflare_kv_exports, {
23
+ kvStore: () => kvStore
24
+ });
25
+ module.exports = __toCommonJS(cloudflare_kv_exports);
26
+ function kvStore(_options) {
27
+ throw new Error("cloudflare-kv store is not yet implemented. Coming in Phase 2.");
28
+ }
29
+ // Annotate the CommonJS export names for ESM import in node:
30
+ 0 && (module.exports = {
31
+ kvStore
32
+ });
33
+ //# sourceMappingURL=cloudflare-kv.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/stores/cloudflare-kv.ts"],"sourcesContent":["import type { IdempotencyStore } from \"./types.js\";\n\ninterface KVStoreOptions {\n\tbinding: string;\n\tttl?: number;\n}\n\n// Phase 2: Cloudflare KV store implementation\nexport function kvStore(_options: KVStoreOptions): IdempotencyStore {\n\tthrow new Error(\"cloudflare-kv store is not yet implemented. Coming in Phase 2.\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAQO,SAAS,QAAQ,UAA4C;AACnE,QAAM,IAAI,MAAM,gEAAgE;AACjF;","names":[]}
@@ -0,0 +1,10 @@
1
+ import { c as IdempotencyStore } from '../types-YeEt4qLu.cjs';
2
+ import 'hono';
3
+
4
+ interface KVStoreOptions {
5
+ binding: string;
6
+ ttl?: number;
7
+ }
8
+ declare function kvStore(_options: KVStoreOptions): IdempotencyStore;
9
+
10
+ export { kvStore };
@@ -0,0 +1,10 @@
1
+ import { c as IdempotencyStore } from '../types-YeEt4qLu.js';
2
+ import 'hono';
3
+
4
+ interface KVStoreOptions {
5
+ binding: string;
6
+ ttl?: number;
7
+ }
8
+ declare function kvStore(_options: KVStoreOptions): IdempotencyStore;
9
+
10
+ export { kvStore };
@@ -0,0 +1,8 @@
1
+ // src/stores/cloudflare-kv.ts
2
+ function kvStore(_options) {
3
+ throw new Error("cloudflare-kv store is not yet implemented. Coming in Phase 2.");
4
+ }
5
+ export {
6
+ kvStore
7
+ };
8
+ //# sourceMappingURL=cloudflare-kv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/stores/cloudflare-kv.ts"],"sourcesContent":["import type { IdempotencyStore } from \"./types.js\";\n\ninterface KVStoreOptions {\n\tbinding: string;\n\tttl?: number;\n}\n\n// Phase 2: Cloudflare KV store implementation\nexport function kvStore(_options: KVStoreOptions): IdempotencyStore {\n\tthrow new Error(\"cloudflare-kv store is not yet implemented. Coming in Phase 2.\");\n}\n"],"mappings":";AAQO,SAAS,QAAQ,UAA4C;AACnE,QAAM,IAAI,MAAM,gEAAgE;AACjF;","names":[]}
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/stores/memory.ts
21
+ var memory_exports = {};
22
+ __export(memory_exports, {
23
+ memoryStore: () => memoryStore
24
+ });
25
+ module.exports = __toCommonJS(memory_exports);
26
+ var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
27
+ function memoryStore(options = {}) {
28
+ const ttl = options.ttl ?? DEFAULT_TTL;
29
+ const map = /* @__PURE__ */ new Map();
30
+ const isExpired = (record) => {
31
+ return Date.now() - record.createdAt >= ttl;
32
+ };
33
+ return {
34
+ async get(key) {
35
+ const record = map.get(key);
36
+ if (!record) return void 0;
37
+ if (isExpired(record)) {
38
+ map.delete(key);
39
+ return void 0;
40
+ }
41
+ return record;
42
+ },
43
+ async lock(key, record) {
44
+ const existing = map.get(key);
45
+ if (existing && !isExpired(existing)) {
46
+ return false;
47
+ }
48
+ map.set(key, record);
49
+ return true;
50
+ },
51
+ async complete(key, response) {
52
+ const record = map.get(key);
53
+ if (record) {
54
+ record.status = "completed";
55
+ record.response = response;
56
+ }
57
+ },
58
+ async delete(key) {
59
+ map.delete(key);
60
+ }
61
+ };
62
+ }
63
+ // Annotate the CommonJS export names for ESM import in node:
64
+ 0 && (module.exports = {
65
+ memoryStore
66
+ });
67
+ //# sourceMappingURL=memory.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/stores/memory.ts"],"sourcesContent":["import type { IdempotencyRecord, StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours\n\ninterface MemoryStoreOptions {\n\tttl?: number;\n}\n\nexport function memoryStore(options: MemoryStoreOptions = {}): IdempotencyStore {\n\tconst ttl = options.ttl ?? DEFAULT_TTL;\n\tconst map = new Map<string, IdempotencyRecord>();\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 = 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\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\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 = \"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\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,IAAM,cAAc,KAAK,KAAK,KAAK;AAM5B,SAAS,YAAY,UAA8B,CAAC,GAAqB;AAC/E,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,MAAM,oBAAI,IAA+B;AAE/C,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,SAAO;AAAA,IACN,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,YAAM,WAAW,IAAI,IAAI,GAAG;AAC5B,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,UAAI,IAAI,KAAK,MAAM;AACnB,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,EACD;AACD;","names":[]}
@@ -0,0 +1,9 @@
1
+ import { c as IdempotencyStore } from '../types-YeEt4qLu.cjs';
2
+ import 'hono';
3
+
4
+ interface MemoryStoreOptions {
5
+ ttl?: number;
6
+ }
7
+ declare function memoryStore(options?: MemoryStoreOptions): IdempotencyStore;
8
+
9
+ export { memoryStore };
@@ -0,0 +1,9 @@
1
+ import { c as IdempotencyStore } from '../types-YeEt4qLu.js';
2
+ import 'hono';
3
+
4
+ interface MemoryStoreOptions {
5
+ ttl?: number;
6
+ }
7
+ declare function memoryStore(options?: MemoryStoreOptions): IdempotencyStore;
8
+
9
+ export { memoryStore };
@@ -0,0 +1,42 @@
1
+ // src/stores/memory.ts
2
+ var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
3
+ function memoryStore(options = {}) {
4
+ const ttl = options.ttl ?? DEFAULT_TTL;
5
+ const map = /* @__PURE__ */ new Map();
6
+ const isExpired = (record) => {
7
+ return Date.now() - record.createdAt >= ttl;
8
+ };
9
+ return {
10
+ async get(key) {
11
+ const record = map.get(key);
12
+ if (!record) return void 0;
13
+ if (isExpired(record)) {
14
+ map.delete(key);
15
+ return void 0;
16
+ }
17
+ return record;
18
+ },
19
+ async lock(key, record) {
20
+ const existing = map.get(key);
21
+ if (existing && !isExpired(existing)) {
22
+ return false;
23
+ }
24
+ map.set(key, record);
25
+ return true;
26
+ },
27
+ async complete(key, response) {
28
+ const record = map.get(key);
29
+ if (record) {
30
+ record.status = "completed";
31
+ record.response = response;
32
+ }
33
+ },
34
+ async delete(key) {
35
+ map.delete(key);
36
+ }
37
+ };
38
+ }
39
+ export {
40
+ memoryStore
41
+ };
42
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/stores/memory.ts"],"sourcesContent":["import type { IdempotencyRecord, StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours\n\ninterface MemoryStoreOptions {\n\tttl?: number;\n}\n\nexport function memoryStore(options: MemoryStoreOptions = {}): IdempotencyStore {\n\tconst ttl = options.ttl ?? DEFAULT_TTL;\n\tconst map = new Map<string, IdempotencyRecord>();\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 = 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\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\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 = \"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\t};\n}\n"],"mappings":";AAGA,IAAM,cAAc,KAAK,KAAK,KAAK;AAM5B,SAAS,YAAY,UAA8B,CAAC,GAAqB;AAC/E,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,MAAM,oBAAI,IAA+B;AAE/C,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,SAAO;AAAA,IACN,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,YAAM,WAAW,IAAI,IAAI,GAAG;AAC5B,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,UAAI,IAAI,KAAK,MAAM;AACnB,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,EACD;AACD;","names":[]}
@@ -0,0 +1,55 @@
1
+ import { Env, Context } from 'hono';
2
+
3
+ interface ProblemDetail {
4
+ type: string;
5
+ title: string;
6
+ status: number;
7
+ detail: string;
8
+ }
9
+
10
+ interface IdempotencyStore {
11
+ /** Get a record by key. Returns undefined if not found. */
12
+ get(key: string): Promise<IdempotencyRecord | undefined>;
13
+ /**
14
+ * Attempt to lock a key (save in "processing" state).
15
+ * Returns false if the key already exists (optimistic lock).
16
+ */
17
+ lock(key: string, record: IdempotencyRecord): Promise<boolean>;
18
+ /** Save the response and mark the record as "completed". */
19
+ complete(key: string, response: StoredResponse): Promise<void>;
20
+ /** Delete a key (cleanup on error). */
21
+ delete(key: string): Promise<void>;
22
+ }
23
+
24
+ interface IdempotencyEnv extends Env {
25
+ Variables: {
26
+ idempotencyKey: string;
27
+ };
28
+ }
29
+ interface StoredResponse {
30
+ status: number;
31
+ headers: Record<string, string>;
32
+ body: string;
33
+ }
34
+ interface IdempotencyRecord {
35
+ key: string;
36
+ fingerprint: string;
37
+ status: "processing" | "completed";
38
+ response?: StoredResponse;
39
+ createdAt: number;
40
+ }
41
+ interface IdempotencyOptions {
42
+ store: IdempotencyStore;
43
+ headerName?: string;
44
+ fingerprint?: (c: Context) => string | Promise<string>;
45
+ required?: boolean;
46
+ methods?: string[];
47
+ maxKeyLength?: number;
48
+ /** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */
49
+ skipRequest?: (c: Context) => boolean | Promise<boolean>;
50
+ /** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */
51
+ onError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;
52
+ cacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);
53
+ }
54
+
55
+ export type { IdempotencyOptions as I, ProblemDetail as P, StoredResponse as S, IdempotencyEnv as a, IdempotencyRecord as b, IdempotencyStore as c };
@@ -0,0 +1,55 @@
1
+ import { Env, Context } from 'hono';
2
+
3
+ interface ProblemDetail {
4
+ type: string;
5
+ title: string;
6
+ status: number;
7
+ detail: string;
8
+ }
9
+
10
+ interface IdempotencyStore {
11
+ /** Get a record by key. Returns undefined if not found. */
12
+ get(key: string): Promise<IdempotencyRecord | undefined>;
13
+ /**
14
+ * Attempt to lock a key (save in "processing" state).
15
+ * Returns false if the key already exists (optimistic lock).
16
+ */
17
+ lock(key: string, record: IdempotencyRecord): Promise<boolean>;
18
+ /** Save the response and mark the record as "completed". */
19
+ complete(key: string, response: StoredResponse): Promise<void>;
20
+ /** Delete a key (cleanup on error). */
21
+ delete(key: string): Promise<void>;
22
+ }
23
+
24
+ interface IdempotencyEnv extends Env {
25
+ Variables: {
26
+ idempotencyKey: string;
27
+ };
28
+ }
29
+ interface StoredResponse {
30
+ status: number;
31
+ headers: Record<string, string>;
32
+ body: string;
33
+ }
34
+ interface IdempotencyRecord {
35
+ key: string;
36
+ fingerprint: string;
37
+ status: "processing" | "completed";
38
+ response?: StoredResponse;
39
+ createdAt: number;
40
+ }
41
+ interface IdempotencyOptions {
42
+ store: IdempotencyStore;
43
+ headerName?: string;
44
+ fingerprint?: (c: Context) => string | Promise<string>;
45
+ required?: boolean;
46
+ methods?: string[];
47
+ maxKeyLength?: number;
48
+ /** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */
49
+ skipRequest?: (c: Context) => boolean | Promise<boolean>;
50
+ /** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */
51
+ onError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;
52
+ cacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);
53
+ }
54
+
55
+ export type { IdempotencyOptions as I, ProblemDetail as P, StoredResponse as S, IdempotencyEnv as a, IdempotencyRecord as b, IdempotencyStore as c };
package/package.json ADDED
@@ -0,0 +1,103 @@
1
+ {
2
+ "name": "hono-idempotency",
3
+ "version": "0.1.0",
4
+ "description": "Stripe-style Idempotency-Key middleware for Hono. IETF draft-ietf-httpapi-idempotency-key-header compliant.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ },
20
+ "./stores/memory": {
21
+ "import": {
22
+ "types": "./dist/stores/memory.d.ts",
23
+ "default": "./dist/stores/memory.js"
24
+ },
25
+ "require": {
26
+ "types": "./dist/stores/memory.d.cts",
27
+ "default": "./dist/stores/memory.cjs"
28
+ }
29
+ },
30
+ "./stores/cloudflare-kv": {
31
+ "import": {
32
+ "types": "./dist/stores/cloudflare-kv.d.ts",
33
+ "default": "./dist/stores/cloudflare-kv.js"
34
+ },
35
+ "require": {
36
+ "types": "./dist/stores/cloudflare-kv.d.cts",
37
+ "default": "./dist/stores/cloudflare-kv.cjs"
38
+ }
39
+ },
40
+ "./stores/cloudflare-d1": {
41
+ "import": {
42
+ "types": "./dist/stores/cloudflare-d1.d.ts",
43
+ "default": "./dist/stores/cloudflare-d1.js"
44
+ },
45
+ "require": {
46
+ "types": "./dist/stores/cloudflare-d1.d.cts",
47
+ "default": "./dist/stores/cloudflare-d1.cjs"
48
+ }
49
+ }
50
+ },
51
+ "files": [
52
+ "dist"
53
+ ],
54
+ "keywords": [
55
+ "hono",
56
+ "idempotency",
57
+ "idempotency-key",
58
+ "middleware",
59
+ "cloudflare-workers",
60
+ "edge"
61
+ ],
62
+ "author": "Ryota Ikezawa",
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "https://github.com/paveg/hono-idempotency.git"
66
+ },
67
+ "homepage": "https://github.com/paveg/hono-idempotency#readme",
68
+ "bugs": {
69
+ "url": "https://github.com/paveg/hono-idempotency/issues"
70
+ },
71
+ "license": "MIT",
72
+ "publishConfig": {
73
+ "access": "public",
74
+ "provenance": true
75
+ },
76
+ "engines": {
77
+ "node": ">=20"
78
+ },
79
+ "peerDependencies": {
80
+ "hono": ">=4.0.0"
81
+ },
82
+ "devDependencies": {
83
+ "@biomejs/biome": "^1.9.0",
84
+ "@changesets/changelog-github": "0.5.2",
85
+ "@changesets/cli": "2.29.8",
86
+ "@vitest/coverage-v8": "3.2.4",
87
+ "hono": "^4.7.0",
88
+ "lefthook": "2.1.1",
89
+ "tsup": "^8.0.0",
90
+ "typescript": "^5.7.0",
91
+ "vitest": "^3.0.0"
92
+ },
93
+ "scripts": {
94
+ "build": "tsup",
95
+ "test": "vitest run",
96
+ "test:watch": "vitest",
97
+ "lint": "biome check .",
98
+ "lint:fix": "biome check --write .",
99
+ "format": "biome format --write .",
100
+ "typecheck": "tsc --noEmit",
101
+ "release": "pnpm build && changeset publish"
102
+ }
103
+ }