hono-idempotency 0.7.0 → 0.7.1
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 +2 -0
- package/dist/index.cjs +25 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +25 -4
- package/dist/index.js.map +1 -1
- package/dist/stores/cloudflare-d1.d.cts +1 -1
- package/dist/stores/cloudflare-d1.d.ts +1 -1
- package/dist/stores/cloudflare-kv.d.cts +1 -1
- package/dist/stores/cloudflare-kv.d.ts +1 -1
- package/dist/stores/durable-objects.d.cts +1 -1
- package/dist/stores/durable-objects.d.ts +1 -1
- package/dist/stores/memory.d.cts +1 -1
- package/dist/stores/memory.d.ts +1 -1
- package/dist/stores/redis.d.cts +1 -1
- package/dist/stores/redis.d.ts +1 -1
- package/dist/{types-DEW0vNcS.d.cts → types-BJnmp4OE.d.cts} +3 -1
- package/dist/{types-DEW0vNcS.d.ts → types-BJnmp4OE.d.ts} +3 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -345,6 +345,8 @@ All errors follow [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9
|
|
|
345
345
|
| 409 | `CONFLICT` | `/errors/conflict` | Concurrent request with same key |
|
|
346
346
|
| 422 | `FINGERPRINT_MISMATCH` | `/errors/fingerprint-mismatch` | Same key, different request body |
|
|
347
347
|
|
|
348
|
+
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.
|
|
349
|
+
|
|
348
350
|
## Accessing the Key in Handlers
|
|
349
351
|
|
|
350
352
|
The middleware sets `idempotencyKey` on the Hono context:
|
package/dist/index.cjs
CHANGED
|
@@ -81,6 +81,15 @@ var IdempotencyErrors = {
|
|
|
81
81
|
code: "KEY_TOO_LONG"
|
|
82
82
|
};
|
|
83
83
|
},
|
|
84
|
+
bodyTooLarge(maxSize) {
|
|
85
|
+
return {
|
|
86
|
+
type: `${BASE_URL}/body-too-large`,
|
|
87
|
+
title: "Request body is too large",
|
|
88
|
+
status: 413,
|
|
89
|
+
detail: `Request body must be at most ${maxSize} bytes`,
|
|
90
|
+
code: "BODY_TOO_LARGE"
|
|
91
|
+
};
|
|
92
|
+
},
|
|
84
93
|
fingerprintMismatch() {
|
|
85
94
|
return {
|
|
86
95
|
type: `${BASE_URL}/fingerprint-mismatch`,
|
|
@@ -102,12 +111,17 @@ var IdempotencyErrors = {
|
|
|
102
111
|
};
|
|
103
112
|
|
|
104
113
|
// src/fingerprint.ts
|
|
114
|
+
var encoder = new TextEncoder();
|
|
115
|
+
var HEX_TABLE = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
105
116
|
async function generateFingerprint(method, path, body) {
|
|
106
117
|
const data = `${method}:${path}:${body}`;
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", encoder.encode(data));
|
|
119
|
+
const bytes = new Uint8Array(hashBuffer);
|
|
120
|
+
let hex = "";
|
|
121
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
122
|
+
hex += HEX_TABLE[bytes[i]];
|
|
123
|
+
}
|
|
124
|
+
return hex;
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
// src/middleware.ts
|
|
@@ -125,6 +139,7 @@ function idempotency(options) {
|
|
|
125
139
|
skipRequest,
|
|
126
140
|
onError,
|
|
127
141
|
cacheKeyPrefix,
|
|
142
|
+
maxBodySize,
|
|
128
143
|
onCacheHit,
|
|
129
144
|
onCacheMiss
|
|
130
145
|
} = options;
|
|
@@ -165,6 +180,12 @@ function idempotency(options) {
|
|
|
165
180
|
if (key.length > maxKeyLength) {
|
|
166
181
|
return errorResponse(IdempotencyErrors.keyTooLong(maxKeyLength));
|
|
167
182
|
}
|
|
183
|
+
if (maxBodySize != null) {
|
|
184
|
+
const cl = c.req.header("Content-Length");
|
|
185
|
+
if (cl && Number.parseInt(cl, 10) > maxBodySize) {
|
|
186
|
+
return errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
168
189
|
const body = await c.req.text();
|
|
169
190
|
const fp = customFingerprint ? await customFingerprint(c) : await generateFingerprint(c.req.method, c.req.path, body);
|
|
170
191
|
const rawPrefix = typeof cacheKeyPrefix === "function" ? await cacheKeyPrefix(c) : cacheKeyPrefix;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/middleware.ts","../src/compat.ts","../src/errors.ts","../src/fingerprint.ts"],"sourcesContent":["export { idempotency } from \"./middleware.js\";\nexport { problemResponse } from \"./errors.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 } from \"./stores/memory.js\";\n","import { createMiddleware } from \"hono/factory\";\nimport { getHonoProblemDetails } from \"./compat.js\";\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\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 (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\tawait safeHook(onCacheHit, key, c);\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\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(\"Idempotency-Replayed\", \"true\");\n\n\treturn new Response(stored.body, {\n\t\tstatus: 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| \"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\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\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\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","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":["key"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/middleware.ts","../src/compat.ts","../src/errors.ts","../src/fingerprint.ts"],"sourcesContent":["export { idempotency } from \"./middleware.js\";\nexport { problemResponse } from \"./errors.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 } from \"./stores/memory.js\";\n","import { createMiddleware } from \"hono/factory\";\nimport { getHonoProblemDetails } from \"./compat.js\";\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\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 (key.length > maxKeyLength) {\n\t\t\treturn errorResponse(IdempotencyErrors.keyTooLong(maxKeyLength));\n\t\t}\n\n\t\tif (maxBodySize != null) {\n\t\t\tconst cl = c.req.header(\"Content-Length\");\n\t\t\tif (cl && Number.parseInt(cl, 10) > maxBodySize) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));\n\t\t\t}\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\tawait safeHook(onCacheHit, key, c);\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\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(\"Idempotency-Replayed\", \"true\");\n\n\treturn new Response(stored.body, {\n\t\tstatus: 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\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\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"],"mappings":"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","names":["key"]}
|
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-
|
|
3
|
-
export { b as IdempotencyErrorCode, c as IdempotencyRecord, d as IdempotencyStore, P as ProblemDetail, S as StoredResponse, p as problemResponse } from './types-
|
|
2
|
+
import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-BJnmp4OE.cjs';
|
|
3
|
+
export { b as IdempotencyErrorCode, c as IdempotencyRecord, d as IdempotencyStore, P as ProblemDetail, S as StoredResponse, p as problemResponse } from './types-BJnmp4OE.cjs';
|
|
4
4
|
export { MemoryStore } from './stores/memory.cjs';
|
|
5
5
|
|
|
6
6
|
declare function idempotency(options: IdempotencyOptions): hono.MiddlewareHandler<IdempotencyEnv, string, {}, Response>;
|
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-
|
|
3
|
-
export { b as IdempotencyErrorCode, c as IdempotencyRecord, d as IdempotencyStore, P as ProblemDetail, S as StoredResponse, p as problemResponse } from './types-
|
|
2
|
+
import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-BJnmp4OE.js';
|
|
3
|
+
export { b as IdempotencyErrorCode, c as IdempotencyRecord, d as IdempotencyStore, P as ProblemDetail, S as StoredResponse, p as problemResponse } from './types-BJnmp4OE.js';
|
|
4
4
|
export { MemoryStore } from './stores/memory.js';
|
|
5
5
|
|
|
6
6
|
declare function idempotency(options: IdempotencyOptions): hono.MiddlewareHandler<IdempotencyEnv, string, {}, Response>;
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,15 @@ var IdempotencyErrors = {
|
|
|
44
44
|
code: "KEY_TOO_LONG"
|
|
45
45
|
};
|
|
46
46
|
},
|
|
47
|
+
bodyTooLarge(maxSize) {
|
|
48
|
+
return {
|
|
49
|
+
type: `${BASE_URL}/body-too-large`,
|
|
50
|
+
title: "Request body is too large",
|
|
51
|
+
status: 413,
|
|
52
|
+
detail: `Request body must be at most ${maxSize} bytes`,
|
|
53
|
+
code: "BODY_TOO_LARGE"
|
|
54
|
+
};
|
|
55
|
+
},
|
|
47
56
|
fingerprintMismatch() {
|
|
48
57
|
return {
|
|
49
58
|
type: `${BASE_URL}/fingerprint-mismatch`,
|
|
@@ -65,12 +74,17 @@ var IdempotencyErrors = {
|
|
|
65
74
|
};
|
|
66
75
|
|
|
67
76
|
// src/fingerprint.ts
|
|
77
|
+
var encoder = new TextEncoder();
|
|
78
|
+
var HEX_TABLE = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
68
79
|
async function generateFingerprint(method, path, body) {
|
|
69
80
|
const data = `${method}:${path}:${body}`;
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", encoder.encode(data));
|
|
82
|
+
const bytes = new Uint8Array(hashBuffer);
|
|
83
|
+
let hex = "";
|
|
84
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
85
|
+
hex += HEX_TABLE[bytes[i]];
|
|
86
|
+
}
|
|
87
|
+
return hex;
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
// src/middleware.ts
|
|
@@ -88,6 +102,7 @@ function idempotency(options) {
|
|
|
88
102
|
skipRequest,
|
|
89
103
|
onError,
|
|
90
104
|
cacheKeyPrefix,
|
|
105
|
+
maxBodySize,
|
|
91
106
|
onCacheHit,
|
|
92
107
|
onCacheMiss
|
|
93
108
|
} = options;
|
|
@@ -128,6 +143,12 @@ function idempotency(options) {
|
|
|
128
143
|
if (key.length > maxKeyLength) {
|
|
129
144
|
return errorResponse(IdempotencyErrors.keyTooLong(maxKeyLength));
|
|
130
145
|
}
|
|
146
|
+
if (maxBodySize != null) {
|
|
147
|
+
const cl = c.req.header("Content-Length");
|
|
148
|
+
if (cl && Number.parseInt(cl, 10) > maxBodySize) {
|
|
149
|
+
return errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
131
152
|
const body = await c.req.text();
|
|
132
153
|
const fp = customFingerprint ? await customFingerprint(c) : await generateFingerprint(c.req.method, c.req.path, body);
|
|
133
154
|
const rawPrefix = typeof cacheKeyPrefix === "function" ? await cacheKeyPrefix(c) : cacheKeyPrefix;
|
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 { 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\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 (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\tawait safeHook(onCacheHit, key, c);\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\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(\"Idempotency-Replayed\", \"true\");\n\n\treturn new Response(stored.body, {\n\t\tstatus: 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| \"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\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\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\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","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":["key"]}
|
|
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 { 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\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 (key.length > maxKeyLength) {\n\t\t\treturn errorResponse(IdempotencyErrors.keyTooLong(maxKeyLength));\n\t\t}\n\n\t\tif (maxBodySize != null) {\n\t\t\tconst cl = c.req.header(\"Content-Length\");\n\t\t\tif (cl && Number.parseInt(cl, 10) > maxBodySize) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.bodyTooLarge(maxBodySize));\n\t\t\t}\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\tawait safeHook(onCacheHit, key, c);\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\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(\"Idempotency-Replayed\", \"true\");\n\n\treturn new Response(stored.body, {\n\t\tstatus: 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\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\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"],"mappings":"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","names":["key"]}
|
package/dist/stores/memory.d.cts
CHANGED
package/dist/stores/memory.d.ts
CHANGED
package/dist/stores/redis.d.cts
CHANGED
package/dist/stores/redis.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Env, Context } from 'hono';
|
|
2
2
|
|
|
3
|
-
type IdempotencyErrorCode = "MISSING_KEY" | "KEY_TOO_LONG" | "FINGERPRINT_MISMATCH" | "CONFLICT";
|
|
3
|
+
type IdempotencyErrorCode = "MISSING_KEY" | "KEY_TOO_LONG" | "BODY_TOO_LARGE" | "FINGERPRINT_MISMATCH" | "CONFLICT";
|
|
4
4
|
interface ProblemDetail {
|
|
5
5
|
type: string;
|
|
6
6
|
title: string;
|
|
@@ -67,6 +67,8 @@ interface IdempotencyOptions {
|
|
|
67
67
|
required?: boolean;
|
|
68
68
|
methods?: string[];
|
|
69
69
|
maxKeyLength?: number;
|
|
70
|
+
/** Maximum request body size in bytes. Checked via Content-Length header before reading the body. */
|
|
71
|
+
maxBodySize?: number;
|
|
70
72
|
/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */
|
|
71
73
|
skipRequest?: (c: Context) => boolean | Promise<boolean>;
|
|
72
74
|
/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Env, Context } from 'hono';
|
|
2
2
|
|
|
3
|
-
type IdempotencyErrorCode = "MISSING_KEY" | "KEY_TOO_LONG" | "FINGERPRINT_MISMATCH" | "CONFLICT";
|
|
3
|
+
type IdempotencyErrorCode = "MISSING_KEY" | "KEY_TOO_LONG" | "BODY_TOO_LARGE" | "FINGERPRINT_MISMATCH" | "CONFLICT";
|
|
4
4
|
interface ProblemDetail {
|
|
5
5
|
type: string;
|
|
6
6
|
title: string;
|
|
@@ -67,6 +67,8 @@ interface IdempotencyOptions {
|
|
|
67
67
|
required?: boolean;
|
|
68
68
|
methods?: string[];
|
|
69
69
|
maxKeyLength?: number;
|
|
70
|
+
/** Maximum request body size in bytes. Checked via Content-Length header before reading the body. */
|
|
71
|
+
maxBodySize?: number;
|
|
70
72
|
/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */
|
|
71
73
|
skipRequest?: (c: Context) => boolean | Promise<boolean>;
|
|
72
74
|
/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hono-idempotency",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
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",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"@changesets/cli": "2.29.8",
|
|
113
113
|
"@vitest/coverage-v8": "3.2.4",
|
|
114
114
|
"hono": "^4.7.0",
|
|
115
|
-
"hono-problem-details": "0.1.
|
|
115
|
+
"hono-problem-details": "0.1.2",
|
|
116
116
|
"lefthook": "2.1.1",
|
|
117
117
|
"tsup": "^8.0.0",
|
|
118
118
|
"typescript": "^5.7.0",
|