hono-idempotency 0.7.1 → 0.7.2
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 +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +1 -0
- package/dist/stores/cloudflare-d1.d.cts +1 -1
- package/dist/stores/cloudflare-d1.d.ts +1 -1
- package/dist/stores/cloudflare-kv.cjs +3 -4
- package/dist/stores/cloudflare-kv.cjs.map +1 -1
- package/dist/stores/cloudflare-kv.d.cts +6 -1
- package/dist/stores/cloudflare-kv.d.ts +6 -1
- package/dist/stores/cloudflare-kv.js +3 -4
- package/dist/stores/cloudflare-kv.js.map +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-BJnmp4OE.d.cts → types-C_OW_leh.d.cts} +8 -1
- package/dist/{types-BJnmp4OE.d.ts → types-C_OW_leh.d.ts} +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/hono-idempotency)
|
|
4
4
|
[](https://github.com/paveg/hono-idempotency/actions/workflows/ci.yml)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://coderabbit.ai)
|
|
6
7
|
[](https://app.devin.ai/org/ryota-ikezawa/wiki/paveg/hono-idempotency)
|
|
7
8
|
|
|
8
9
|
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.
|
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
+
IdempotencyErrors: () => IdempotencyErrors,
|
|
33
34
|
idempotency: () => idempotency,
|
|
34
35
|
problemResponse: () => problemResponse
|
|
35
36
|
});
|
|
@@ -264,6 +265,7 @@ function replayResponse(stored) {
|
|
|
264
265
|
}
|
|
265
266
|
// Annotate the CommonJS export names for ESM import in node:
|
|
266
267
|
0 && (module.exports = {
|
|
268
|
+
IdempotencyErrors,
|
|
267
269
|
idempotency,
|
|
268
270
|
problemResponse
|
|
269
271
|
});
|
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\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"]}
|
|
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 { IdempotencyErrors, 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, MemoryStoreOptions } from \"./stores/memory.js\";\nexport type { RedisClientLike, RedisStoreOptions } from \"./stores/redis.js\";\nexport type { KVNamespaceLike, KVStoreOptions } from \"./stores/cloudflare-kv.js\";\nexport type {\n\tD1DatabaseLike,\n\tD1PreparedStatementLike,\n\tD1StoreOptions,\n} from \"./stores/cloudflare-d1.js\";\nexport type {\n\tDurableObjectStorageLike,\n\tDurableObjectStoreOptions,\n} from \"./stores/durable-objects.js\";\n","import { createMiddleware } from \"hono/factory\";\nimport { getHonoProblemDetails } from \"./compat.js\";\nimport { 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,7 +1,11 @@
|
|
|
1
1
|
import * as hono from 'hono';
|
|
2
|
-
import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-
|
|
3
|
-
export { b as IdempotencyErrorCode, c as
|
|
4
|
-
export { MemoryStore } from './stores/memory.cjs';
|
|
2
|
+
import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-C_OW_leh.cjs';
|
|
3
|
+
export { b as IdempotencyErrorCode, c as IdempotencyErrors, d as IdempotencyRecord, e as IdempotencyStore, P as ProblemDetail, S as StoredResponse, p as problemResponse } from './types-C_OW_leh.cjs';
|
|
4
|
+
export { MemoryStore, MemoryStoreOptions } from './stores/memory.cjs';
|
|
5
|
+
export { RedisClientLike, RedisStoreOptions } from './stores/redis.cjs';
|
|
6
|
+
export { KVNamespaceLike, KVStoreOptions } from './stores/cloudflare-kv.cjs';
|
|
7
|
+
export { D1DatabaseLike, D1PreparedStatementLike, D1StoreOptions } from './stores/cloudflare-d1.cjs';
|
|
8
|
+
export { DurableObjectStorageLike, DurableObjectStoreOptions } from './stores/durable-objects.cjs';
|
|
5
9
|
|
|
6
10
|
declare function idempotency(options: IdempotencyOptions): hono.MiddlewareHandler<IdempotencyEnv, string, {}, Response>;
|
|
7
11
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as hono from 'hono';
|
|
2
|
-
import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-
|
|
3
|
-
export { b as IdempotencyErrorCode, c as
|
|
4
|
-
export { MemoryStore } from './stores/memory.js';
|
|
2
|
+
import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-C_OW_leh.js';
|
|
3
|
+
export { b as IdempotencyErrorCode, c as IdempotencyErrors, d as IdempotencyRecord, e as IdempotencyStore, P as ProblemDetail, S as StoredResponse, p as problemResponse } from './types-C_OW_leh.js';
|
|
4
|
+
export { MemoryStore, MemoryStoreOptions } from './stores/memory.js';
|
|
5
|
+
export { RedisClientLike, RedisStoreOptions } from './stores/redis.js';
|
|
6
|
+
export { KVNamespaceLike, KVStoreOptions } from './stores/cloudflare-kv.js';
|
|
7
|
+
export { D1DatabaseLike, D1PreparedStatementLike, D1StoreOptions } from './stores/cloudflare-d1.js';
|
|
8
|
+
export { DurableObjectStorageLike, DurableObjectStoreOptions } from './stores/durable-objects.js';
|
|
5
9
|
|
|
6
10
|
declare function idempotency(options: IdempotencyOptions): hono.MiddlewareHandler<IdempotencyEnv, string, {}, Response>;
|
|
7
11
|
|
package/dist/index.js
CHANGED
|
@@ -33,11 +33,10 @@ function kvStore(options) {
|
|
|
33
33
|
},
|
|
34
34
|
async lock(key, record) {
|
|
35
35
|
const existing = await kv.get(key, { type: "json" });
|
|
36
|
-
if (existing)
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
36
|
+
if (existing) return false;
|
|
39
37
|
await kv.put(key, JSON.stringify(record), { expirationTtl: ttl });
|
|
40
|
-
|
|
38
|
+
const stored = await kv.get(key, { type: "json" });
|
|
39
|
+
return stored?.fingerprint === record.fingerprint;
|
|
41
40
|
},
|
|
42
41
|
async complete(key, response) {
|
|
43
42
|
const record = await kv.get(key, { type: "json" });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/stores/cloudflare-kv.ts"],"sourcesContent":["import type { IdempotencyRecord, StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */\nexport interface KVNamespaceLike {\n\tget(key: string, options: { type: \"json\" }): Promise<unknown>;\n\tput(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;\n\tdelete(key: string): Promise<void>;\n}\n\nexport interface KVStoreOptions {\n\t/** Cloudflare Workers KV namespace binding. */\n\tnamespace: KVNamespaceLike;\n\t/** TTL in seconds (default: 86400 = 24h). KV minimum is 60 seconds. */\n\tttl?: number;\n}\n\nexport function kvStore(options: KVStoreOptions): IdempotencyStore {\n\tconst { namespace: kv, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst record = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\treturn record ?? undefined;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\tif (existing)
|
|
1
|
+
{"version":3,"sources":["../../src/stores/cloudflare-kv.ts"],"sourcesContent":["import type { IdempotencyRecord, StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */\nexport interface KVNamespaceLike {\n\tget(key: string, options: { type: \"json\" }): Promise<unknown>;\n\tput(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;\n\tdelete(key: string): Promise<void>;\n}\n\nexport interface KVStoreOptions {\n\t/** Cloudflare Workers KV namespace binding. */\n\tnamespace: KVNamespaceLike;\n\t/** TTL in seconds (default: 86400 = 24h). KV minimum is 60 seconds. */\n\tttl?: number;\n}\n\n/**\n * KV is eventually consistent — lock() uses write-first with read-back\n * verification for best-effort race detection but cannot guarantee atomicity.\n * For strict concurrency guarantees, use d1Store or durableObjectStore.\n */\nexport function kvStore(options: KVStoreOptions): IdempotencyStore {\n\tconst { namespace: kv, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst record = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\treturn record ?? undefined;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\tif (existing) return false;\n\n\t\t\tawait kv.put(key, JSON.stringify(record), { expirationTtl: ttl });\n\n\t\t\t// Read-back verification: detect if a concurrent writer overwrote our record\n\t\t\tconst stored = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\treturn stored?.fingerprint === record.fingerprint;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\tif (!record) return;\n\t\t\trecord.status = \"completed\";\n\t\t\trecord.response = response;\n\t\t\tawait kv.put(key, JSON.stringify(record), { expirationTtl: ttl });\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait kv.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\t// KV handles expiration automatically via expirationTtl — no manual purge needed\n\t\t\treturn 0;\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,IAAM,cAAc;AAqBb,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,WAAW,IAAI,MAAM,YAAY,IAAI;AAE7C,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAClD,aAAO,UAAU;AAAA,IAClB;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,WAAY,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AACpD,UAAI,SAAU,QAAO;AAErB,YAAM,GAAG,IAAI,KAAK,KAAK,UAAU,MAAM,GAAG,EAAE,eAAe,IAAI,CAAC;AAGhE,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAClD,aAAO,QAAQ,gBAAgB,OAAO;AAAA,IACvC;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAClD,UAAI,CAAC,OAAQ;AACb,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,GAAG,IAAI,KAAK,KAAK,UAAU,MAAM,GAAG,EAAE,eAAe,IAAI,CAAC;AAAA,IACjE;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,GAAG,OAAO,GAAG;AAAA,IACpB;AAAA,IAEA,MAAM,QAAQ;AAEb,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { e as IdempotencyStore } from '../types-C_OW_leh.cjs';
|
|
2
2
|
import 'hono';
|
|
3
3
|
|
|
4
4
|
/** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */
|
|
@@ -17,6 +17,11 @@ interface KVStoreOptions {
|
|
|
17
17
|
/** TTL in seconds (default: 86400 = 24h). KV minimum is 60 seconds. */
|
|
18
18
|
ttl?: number;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* KV is eventually consistent — lock() uses write-first with read-back
|
|
22
|
+
* verification for best-effort race detection but cannot guarantee atomicity.
|
|
23
|
+
* For strict concurrency guarantees, use d1Store or durableObjectStore.
|
|
24
|
+
*/
|
|
20
25
|
declare function kvStore(options: KVStoreOptions): IdempotencyStore;
|
|
21
26
|
|
|
22
27
|
export { type KVNamespaceLike, type KVStoreOptions, kvStore };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { e as IdempotencyStore } from '../types-C_OW_leh.js';
|
|
2
2
|
import 'hono';
|
|
3
3
|
|
|
4
4
|
/** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */
|
|
@@ -17,6 +17,11 @@ interface KVStoreOptions {
|
|
|
17
17
|
/** TTL in seconds (default: 86400 = 24h). KV minimum is 60 seconds. */
|
|
18
18
|
ttl?: number;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* KV is eventually consistent — lock() uses write-first with read-back
|
|
22
|
+
* verification for best-effort race detection but cannot guarantee atomicity.
|
|
23
|
+
* For strict concurrency guarantees, use d1Store or durableObjectStore.
|
|
24
|
+
*/
|
|
20
25
|
declare function kvStore(options: KVStoreOptions): IdempotencyStore;
|
|
21
26
|
|
|
22
27
|
export { type KVNamespaceLike, type KVStoreOptions, kvStore };
|
|
@@ -9,11 +9,10 @@ function kvStore(options) {
|
|
|
9
9
|
},
|
|
10
10
|
async lock(key, record) {
|
|
11
11
|
const existing = await kv.get(key, { type: "json" });
|
|
12
|
-
if (existing)
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
12
|
+
if (existing) return false;
|
|
15
13
|
await kv.put(key, JSON.stringify(record), { expirationTtl: ttl });
|
|
16
|
-
|
|
14
|
+
const stored = await kv.get(key, { type: "json" });
|
|
15
|
+
return stored?.fingerprint === record.fingerprint;
|
|
17
16
|
},
|
|
18
17
|
async complete(key, response) {
|
|
19
18
|
const record = await kv.get(key, { type: "json" });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/stores/cloudflare-kv.ts"],"sourcesContent":["import type { IdempotencyRecord, StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */\nexport interface KVNamespaceLike {\n\tget(key: string, options: { type: \"json\" }): Promise<unknown>;\n\tput(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;\n\tdelete(key: string): Promise<void>;\n}\n\nexport interface KVStoreOptions {\n\t/** Cloudflare Workers KV namespace binding. */\n\tnamespace: KVNamespaceLike;\n\t/** TTL in seconds (default: 86400 = 24h). KV minimum is 60 seconds. */\n\tttl?: number;\n}\n\nexport function kvStore(options: KVStoreOptions): IdempotencyStore {\n\tconst { namespace: kv, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst record = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\treturn record ?? undefined;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\tif (existing)
|
|
1
|
+
{"version":3,"sources":["../../src/stores/cloudflare-kv.ts"],"sourcesContent":["import type { IdempotencyRecord, StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */\nexport interface KVNamespaceLike {\n\tget(key: string, options: { type: \"json\" }): Promise<unknown>;\n\tput(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;\n\tdelete(key: string): Promise<void>;\n}\n\nexport interface KVStoreOptions {\n\t/** Cloudflare Workers KV namespace binding. */\n\tnamespace: KVNamespaceLike;\n\t/** TTL in seconds (default: 86400 = 24h). KV minimum is 60 seconds. */\n\tttl?: number;\n}\n\n/**\n * KV is eventually consistent — lock() uses write-first with read-back\n * verification for best-effort race detection but cannot guarantee atomicity.\n * For strict concurrency guarantees, use d1Store or durableObjectStore.\n */\nexport function kvStore(options: KVStoreOptions): IdempotencyStore {\n\tconst { namespace: kv, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst record = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\treturn record ?? undefined;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\tif (existing) return false;\n\n\t\t\tawait kv.put(key, JSON.stringify(record), { expirationTtl: ttl });\n\n\t\t\t// Read-back verification: detect if a concurrent writer overwrote our record\n\t\t\tconst stored = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\treturn stored?.fingerprint === record.fingerprint;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\tif (!record) return;\n\t\t\trecord.status = \"completed\";\n\t\t\trecord.response = response;\n\t\t\tawait kv.put(key, JSON.stringify(record), { expirationTtl: ttl });\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait kv.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\t// KV handles expiration automatically via expirationTtl — no manual purge needed\n\t\t\treturn 0;\n\t\t},\n\t};\n}\n"],"mappings":";AAGA,IAAM,cAAc;AAqBb,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,WAAW,IAAI,MAAM,YAAY,IAAI;AAE7C,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAClD,aAAO,UAAU;AAAA,IAClB;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,WAAY,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AACpD,UAAI,SAAU,QAAO;AAErB,YAAM,GAAG,IAAI,KAAK,KAAK,UAAU,MAAM,GAAG,EAAE,eAAe,IAAI,CAAC;AAGhE,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAClD,aAAO,QAAQ,gBAAgB,OAAO;AAAA,IACvC;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAClD,UAAI,CAAC,OAAQ;AACb,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,GAAG,IAAI,KAAK,KAAK,UAAU,MAAM,GAAG,EAAE,eAAe,IAAI,CAAC;AAAA,IACjE;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,GAAG,OAAO,GAAG;AAAA,IACpB;AAAA,IAEA,MAAM,QAAQ;AAEb,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
|
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
|
@@ -9,6 +9,13 @@ interface ProblemDetail {
|
|
|
9
9
|
code: IdempotencyErrorCode;
|
|
10
10
|
}
|
|
11
11
|
declare function problemResponse(problem: ProblemDetail, extraHeaders?: Record<string, string>): Response;
|
|
12
|
+
declare const IdempotencyErrors: {
|
|
13
|
+
readonly missingKey: () => ProblemDetail;
|
|
14
|
+
readonly keyTooLong: (maxLength: number) => ProblemDetail;
|
|
15
|
+
readonly bodyTooLarge: (maxSize: number) => ProblemDetail;
|
|
16
|
+
readonly fingerprintMismatch: () => ProblemDetail;
|
|
17
|
+
readonly conflict: () => ProblemDetail;
|
|
18
|
+
};
|
|
12
19
|
|
|
13
20
|
interface IdempotencyStore {
|
|
14
21
|
/**
|
|
@@ -88,4 +95,4 @@ interface IdempotencyOptions {
|
|
|
88
95
|
onCacheMiss?: (key: string, c: Context) => void | Promise<void>;
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
export { type IdempotencyOptions as I, type ProblemDetail as P, type StoredResponse as S, type IdempotencyEnv as a, type IdempotencyErrorCode as b, type IdempotencyRecord as
|
|
98
|
+
export { type IdempotencyOptions as I, type ProblemDetail as P, type StoredResponse as S, type IdempotencyEnv as a, type IdempotencyErrorCode as b, IdempotencyErrors as c, type IdempotencyRecord as d, type IdempotencyStore as e, problemResponse as p };
|
|
@@ -9,6 +9,13 @@ interface ProblemDetail {
|
|
|
9
9
|
code: IdempotencyErrorCode;
|
|
10
10
|
}
|
|
11
11
|
declare function problemResponse(problem: ProblemDetail, extraHeaders?: Record<string, string>): Response;
|
|
12
|
+
declare const IdempotencyErrors: {
|
|
13
|
+
readonly missingKey: () => ProblemDetail;
|
|
14
|
+
readonly keyTooLong: (maxLength: number) => ProblemDetail;
|
|
15
|
+
readonly bodyTooLarge: (maxSize: number) => ProblemDetail;
|
|
16
|
+
readonly fingerprintMismatch: () => ProblemDetail;
|
|
17
|
+
readonly conflict: () => ProblemDetail;
|
|
18
|
+
};
|
|
12
19
|
|
|
13
20
|
interface IdempotencyStore {
|
|
14
21
|
/**
|
|
@@ -88,4 +95,4 @@ interface IdempotencyOptions {
|
|
|
88
95
|
onCacheMiss?: (key: string, c: Context) => void | Promise<void>;
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
export { type IdempotencyOptions as I, type ProblemDetail as P, type StoredResponse as S, type IdempotencyEnv as a, type IdempotencyErrorCode as b, type IdempotencyRecord as
|
|
98
|
+
export { type IdempotencyOptions as I, type ProblemDetail as P, type StoredResponse as S, type IdempotencyEnv as a, type IdempotencyErrorCode as b, IdempotencyErrors as c, type IdempotencyRecord as d, type IdempotencyStore as e, problemResponse as p };
|
package/package.json
CHANGED