@classytic/arc 1.1.0 → 2.1.3
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 +247 -794
- package/bin/arc.js +91 -52
- package/dist/EventTransport-BkUDYZEb.d.mts +99 -0
- package/dist/HookSystem-BsGV-j2l.mjs +404 -0
- package/dist/ResourceRegistry-7Ic20ZMw.mjs +249 -0
- package/dist/adapters/index.d.mts +5 -0
- package/dist/adapters/index.mjs +3 -0
- package/dist/audit/index.d.mts +81 -0
- package/dist/audit/index.mjs +275 -0
- package/dist/audit/mongodb.d.mts +5 -0
- package/dist/audit/mongodb.mjs +3 -0
- package/dist/audited-CGdLiSlE.mjs +140 -0
- package/dist/auth/index.d.mts +188 -0
- package/dist/auth/index.mjs +1096 -0
- package/dist/auth/redis-session.d.mts +43 -0
- package/dist/auth/redis-session.mjs +75 -0
- package/dist/betterAuthOpenApi-DjWDddNc.mjs +249 -0
- package/dist/cache/index.d.mts +145 -0
- package/dist/cache/index.mjs +91 -0
- package/dist/caching-GSDJcA6-.mjs +93 -0
- package/dist/chunk-C7Uep-_p.mjs +20 -0
- package/dist/circuitBreaker-DYhWBW_D.mjs +1096 -0
- package/dist/cli/commands/describe.d.mts +18 -0
- package/dist/cli/commands/describe.mjs +238 -0
- package/dist/cli/commands/docs.d.mts +13 -0
- package/dist/cli/commands/docs.mjs +52 -0
- package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -2
- package/dist/cli/commands/generate.mjs +357 -0
- package/dist/cli/commands/{init.d.ts → init.d.mts} +11 -8
- package/dist/cli/commands/{init.js → init.mjs} +807 -617
- package/dist/cli/commands/introspect.d.mts +10 -0
- package/dist/cli/commands/introspect.mjs +75 -0
- package/dist/cli/index.d.mts +16 -0
- package/dist/cli/index.mjs +156 -0
- package/dist/constants-DdXFXQtN.mjs +84 -0
- package/dist/core/index.d.mts +5 -0
- package/dist/core/index.mjs +4 -0
- package/dist/createApp-D2D5XXaV.mjs +559 -0
- package/dist/defineResource-PXzSJ15_.mjs +2197 -0
- package/dist/discovery/index.d.mts +46 -0
- package/dist/discovery/index.mjs +109 -0
- package/dist/docs/index.d.mts +162 -0
- package/dist/docs/index.mjs +74 -0
- package/dist/elevation-DGo5shaX.d.mts +87 -0
- package/dist/elevation-DSTbVvYj.mjs +113 -0
- package/dist/errorHandler-C3GY3_ow.mjs +108 -0
- package/dist/errorHandler-CW3OOeYq.d.mts +72 -0
- package/dist/errors-DAWRdiYP.d.mts +124 -0
- package/dist/errors-DBANPbGr.mjs +211 -0
- package/dist/eventPlugin-BEOvaDqo.mjs +229 -0
- package/dist/eventPlugin-H6wDDjGO.d.mts +124 -0
- package/dist/events/index.d.mts +53 -0
- package/dist/events/index.mjs +51 -0
- package/dist/events/transports/redis-stream-entry.d.mts +2 -0
- package/dist/events/transports/redis-stream-entry.mjs +177 -0
- package/dist/events/transports/redis.d.mts +76 -0
- package/dist/events/transports/redis.mjs +124 -0
- package/dist/externalPaths-SyPF2tgK.d.mts +50 -0
- package/dist/factory/index.d.mts +63 -0
- package/dist/factory/index.mjs +3 -0
- package/dist/fastifyAdapter-C8DlE0YH.d.mts +216 -0
- package/dist/fields-Bi_AVKSo.d.mts +109 -0
- package/dist/fields-CTd_CrKr.mjs +114 -0
- package/dist/hooks/index.d.mts +4 -0
- package/dist/hooks/index.mjs +3 -0
- package/dist/idempotency/index.d.mts +96 -0
- package/dist/idempotency/index.mjs +319 -0
- package/dist/idempotency/mongodb.d.mts +2 -0
- package/dist/idempotency/mongodb.mjs +114 -0
- package/dist/idempotency/redis.d.mts +2 -0
- package/dist/idempotency/redis.mjs +103 -0
- package/dist/index.d.mts +260 -0
- package/dist/index.mjs +104 -0
- package/dist/integrations/event-gateway.d.mts +46 -0
- package/dist/integrations/event-gateway.mjs +43 -0
- package/dist/integrations/index.d.mts +5 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs.d.mts +103 -0
- package/dist/integrations/jobs.mjs +123 -0
- package/dist/integrations/streamline.d.mts +60 -0
- package/dist/integrations/streamline.mjs +125 -0
- package/dist/integrations/websocket.d.mts +82 -0
- package/dist/integrations/websocket.mjs +288 -0
- package/dist/interface-CSNjltAc.d.mts +77 -0
- package/dist/interface-DTbsvIWe.d.mts +54 -0
- package/dist/interface-e9XfSsUV.d.mts +1097 -0
- package/dist/introspectionPlugin-B3JkrjwU.mjs +53 -0
- package/dist/keys-DhqDRxv3.mjs +42 -0
- package/dist/logger-ByrvQWZO.mjs +78 -0
- package/dist/memory-B2v7KrCB.mjs +143 -0
- package/dist/migrations/index.d.mts +156 -0
- package/dist/migrations/index.mjs +260 -0
- package/dist/mongodb-ClykrfGo.d.mts +118 -0
- package/dist/mongodb-DNKEExbf.mjs +93 -0
- package/dist/mongodb-Dg8O_gvd.d.mts +71 -0
- package/dist/openapi-9nB_kiuR.mjs +525 -0
- package/dist/org/index.d.mts +68 -0
- package/dist/org/index.mjs +513 -0
- package/dist/org/types.d.mts +82 -0
- package/dist/org/types.mjs +1 -0
- package/dist/permissions/index.d.mts +278 -0
- package/dist/permissions/index.mjs +579 -0
- package/dist/plugins/index.d.mts +172 -0
- package/dist/plugins/index.mjs +522 -0
- package/dist/plugins/response-cache.d.mts +87 -0
- package/dist/plugins/response-cache.mjs +283 -0
- package/dist/plugins/tracing-entry.d.mts +2 -0
- package/dist/plugins/tracing-entry.mjs +185 -0
- package/dist/pluralize-CM-jZg7p.mjs +86 -0
- package/dist/policies/{index.d.ts → index.d.mts} +204 -170
- package/dist/policies/index.mjs +321 -0
- package/dist/presets/{index.d.ts → index.d.mts} +62 -131
- package/dist/presets/index.mjs +143 -0
- package/dist/presets/multiTenant.d.mts +24 -0
- package/dist/presets/multiTenant.mjs +113 -0
- package/dist/presets-BTeYbw7h.d.mts +57 -0
- package/dist/presets-CeFtfDR8.mjs +119 -0
- package/dist/prisma-C3iornoK.d.mts +274 -0
- package/dist/prisma-DJbMt3yf.mjs +627 -0
- package/dist/queryCachePlugin-B6R0d4av.mjs +138 -0
- package/dist/queryCachePlugin-Q6SYuHZ6.d.mts +71 -0
- package/dist/redis-UwjEp8Ea.d.mts +49 -0
- package/dist/redis-stream-CBg0upHI.d.mts +103 -0
- package/dist/registry/index.d.mts +11 -0
- package/dist/registry/index.mjs +4 -0
- package/dist/requestContext-xi6OKBL-.mjs +55 -0
- package/dist/schemaConverter-Dtg0Kt9T.mjs +98 -0
- package/dist/schemas/index.d.mts +63 -0
- package/dist/schemas/index.mjs +82 -0
- package/dist/scope/index.d.mts +21 -0
- package/dist/scope/index.mjs +65 -0
- package/dist/sessionManager-D_iEHjQl.d.mts +186 -0
- package/dist/sse-DkqQ1uxb.mjs +123 -0
- package/dist/testing/index.d.mts +907 -0
- package/dist/testing/index.mjs +1976 -0
- package/dist/tracing-8CEbhF0w.d.mts +70 -0
- package/dist/typeGuards-DwxA1t_L.mjs +9 -0
- package/dist/types/index.d.mts +946 -0
- package/dist/types/index.mjs +14 -0
- package/dist/types-B0dhNrnd.d.mts +445 -0
- package/dist/types-Beqn1Un7.mjs +38 -0
- package/dist/types-DelU6kln.mjs +25 -0
- package/dist/types-RLkFVgaw.d.mts +101 -0
- package/dist/utils/index.d.mts +747 -0
- package/dist/utils/index.mjs +6 -0
- package/package.json +194 -68
- package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
- package/dist/adapters/index.d.ts +0 -237
- package/dist/adapters/index.js +0 -668
- package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
- package/dist/audit/index.d.ts +0 -195
- package/dist/audit/index.js +0 -319
- package/dist/auth/index.d.ts +0 -47
- package/dist/auth/index.js +0 -174
- package/dist/cli/commands/docs.d.ts +0 -11
- package/dist/cli/commands/docs.js +0 -474
- package/dist/cli/commands/generate.js +0 -334
- package/dist/cli/commands/introspect.d.ts +0 -8
- package/dist/cli/commands/introspect.js +0 -338
- package/dist/cli/index.d.ts +0 -4
- package/dist/cli/index.js +0 -3269
- package/dist/core/index.d.ts +0 -220
- package/dist/core/index.js +0 -2786
- package/dist/createApp-Ce9wl8W9.d.ts +0 -77
- package/dist/docs/index.d.ts +0 -166
- package/dist/docs/index.js +0 -658
- package/dist/errors-8WIxGS_6.d.ts +0 -122
- package/dist/events/index.d.ts +0 -117
- package/dist/events/index.js +0 -89
- package/dist/factory/index.d.ts +0 -38
- package/dist/factory/index.js +0 -1652
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -199
- package/dist/idempotency/index.d.ts +0 -323
- package/dist/idempotency/index.js +0 -500
- package/dist/index-B4t03KQ0.d.ts +0 -1366
- package/dist/index.d.ts +0 -135
- package/dist/index.js +0 -4756
- package/dist/migrations/index.d.ts +0 -185
- package/dist/migrations/index.js +0 -274
- package/dist/org/index.d.ts +0 -129
- package/dist/org/index.js +0 -220
- package/dist/permissions/index.d.ts +0 -144
- package/dist/permissions/index.js +0 -103
- package/dist/plugins/index.d.ts +0 -46
- package/dist/plugins/index.js +0 -1069
- package/dist/policies/index.js +0 -196
- package/dist/presets/index.js +0 -384
- package/dist/presets/multiTenant.d.ts +0 -39
- package/dist/presets/multiTenant.js +0 -112
- package/dist/registry/index.d.ts +0 -16
- package/dist/registry/index.js +0 -253
- package/dist/testing/index.d.ts +0 -618
- package/dist/testing/index.js +0 -48020
- package/dist/types/index.d.ts +0 -4
- package/dist/types/index.js +0 -8
- package/dist/types-B99TBmFV.d.ts +0 -76
- package/dist/types-BvckRbs2.d.ts +0 -143
- package/dist/utils/index.d.ts +0 -679
- package/dist/utils/index.js +0 -931
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
//#region src/permissions/fields.ts
|
|
2
|
+
/**
|
|
3
|
+
* Field-Level Permissions
|
|
4
|
+
*
|
|
5
|
+
* Control field visibility and writability per role.
|
|
6
|
+
* Integrated into the response path (read) and sanitization path (write).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { fields, defineResource } from '@classytic/arc';
|
|
11
|
+
*
|
|
12
|
+
* const userResource = defineResource({
|
|
13
|
+
* name: 'user',
|
|
14
|
+
* adapter: userAdapter,
|
|
15
|
+
* fields: {
|
|
16
|
+
* salary: fields.visibleTo(['admin', 'hr']),
|
|
17
|
+
* internalNotes: fields.writableBy(['admin']),
|
|
18
|
+
* email: fields.redactFor(['viewer']),
|
|
19
|
+
* password: fields.hidden(),
|
|
20
|
+
* },
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
/** Type guard for Mongoose-like documents with toObject() */
|
|
25
|
+
function isMongooseDoc(obj) {
|
|
26
|
+
return !!obj && typeof obj === "object" && "toObject" in obj && typeof obj.toObject === "function";
|
|
27
|
+
}
|
|
28
|
+
const fields = {
|
|
29
|
+
hidden() {
|
|
30
|
+
return { _type: "hidden" };
|
|
31
|
+
},
|
|
32
|
+
visibleTo(roles) {
|
|
33
|
+
return {
|
|
34
|
+
_type: "visibleTo",
|
|
35
|
+
roles
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
writableBy(roles) {
|
|
39
|
+
return {
|
|
40
|
+
_type: "writableBy",
|
|
41
|
+
roles
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
redactFor(roles, redactValue = "***") {
|
|
45
|
+
return {
|
|
46
|
+
_type: "redactFor",
|
|
47
|
+
roles,
|
|
48
|
+
redactValue
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Apply field-level READ permissions to a response object.
|
|
54
|
+
* Strips hidden fields, enforces visibility, and applies redaction.
|
|
55
|
+
*
|
|
56
|
+
* @param data - The response object (mutated in place for performance)
|
|
57
|
+
* @param fieldPermissions - Field permission map from resource config
|
|
58
|
+
* @param userRoles - Current user's roles (empty array for unauthenticated)
|
|
59
|
+
* @returns The filtered object
|
|
60
|
+
*/
|
|
61
|
+
function applyFieldReadPermissions(data, fieldPermissions, userRoles) {
|
|
62
|
+
if (!data || typeof data !== "object") return data;
|
|
63
|
+
const result = { ...isMongooseDoc(data) ? data.toObject() : data };
|
|
64
|
+
for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
|
|
65
|
+
case "hidden":
|
|
66
|
+
delete result[field];
|
|
67
|
+
break;
|
|
68
|
+
case "visibleTo":
|
|
69
|
+
if (!perm.roles?.some((r) => userRoles.includes(r))) delete result[field];
|
|
70
|
+
break;
|
|
71
|
+
case "redactFor":
|
|
72
|
+
if (perm.roles?.some((r) => userRoles.includes(r))) result[field] = perm.redactValue ?? "***";
|
|
73
|
+
break;
|
|
74
|
+
case "writableBy": break;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Apply field-level WRITE permissions to request body.
|
|
80
|
+
* Strips fields that the user doesn't have permission to write.
|
|
81
|
+
*
|
|
82
|
+
* @param body - The request body (returns a new filtered copy)
|
|
83
|
+
* @param fieldPermissions - Field permission map from resource config
|
|
84
|
+
* @param userRoles - Current user's roles
|
|
85
|
+
* @returns Filtered body
|
|
86
|
+
*/
|
|
87
|
+
function applyFieldWritePermissions(body, fieldPermissions, userRoles) {
|
|
88
|
+
const result = { ...body };
|
|
89
|
+
for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
|
|
90
|
+
case "hidden":
|
|
91
|
+
delete result[field];
|
|
92
|
+
break;
|
|
93
|
+
case "writableBy":
|
|
94
|
+
if (field in result && !perm.roles?.some((r) => userRoles.includes(r))) delete result[field];
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve effective roles by merging global user roles with org-level roles.
|
|
101
|
+
*
|
|
102
|
+
* Global roles come from `req.user.role` (normalized via getUserRoles()).
|
|
103
|
+
* Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).
|
|
104
|
+
*
|
|
105
|
+
* When no org context exists, returns global roles only — backward compatible.
|
|
106
|
+
*/
|
|
107
|
+
function resolveEffectiveRoles(userRoles, orgRoles) {
|
|
108
|
+
if (orgRoles.length === 0) return [...userRoles];
|
|
109
|
+
if (userRoles.length === 0) return [...orgRoles];
|
|
110
|
+
return [...new Set([...userRoles, ...orgRoles])];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
export { resolveEffectiveRoles as i, applyFieldWritePermissions as n, fields as r, applyFieldReadPermissions as t };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import "../elevation-DGo5shaX.mjs";
|
|
2
|
+
import { $ as beforeUpdate, B as DefineHookOptions, G as HookRegistration, H as HookHandler, J as afterCreate, K as HookSystem, Q as beforeDelete, U as HookOperation, V as HookContext, W as HookPhase, X as afterUpdate, Y as afterDelete, Z as beforeCreate, et as createHookSystem, q as HookSystemOptions, tt as defineHook } from "../interface-e9XfSsUV.mjs";
|
|
3
|
+
import "../types-RLkFVgaw.mjs";
|
|
4
|
+
export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { a as beforeCreate, c as createHookSystem, i as afterUpdate, l as defineHook, n as afterCreate, o as beforeDelete, r as afterDelete, s as beforeUpdate, t as HookSystem } from "../HookSystem-BsGV-j2l.mjs";
|
|
2
|
+
|
|
3
|
+
export { HookSystem, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-CSNjltAc.mjs";
|
|
2
|
+
import { r as RedisIdempotencyStoreOptions, t as RedisClient } from "../redis-UwjEp8Ea.mjs";
|
|
3
|
+
import { n as MongoIdempotencyStoreOptions } from "../mongodb-Dg8O_gvd.mjs";
|
|
4
|
+
import { FastifyPluginAsync } from "fastify";
|
|
5
|
+
|
|
6
|
+
//#region src/idempotency/idempotencyPlugin.d.ts
|
|
7
|
+
interface IdempotencyPluginOptions {
|
|
8
|
+
/** Enable idempotency (default: false) */
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/** Header name for idempotency key (default: 'idempotency-key') */
|
|
11
|
+
headerName?: string;
|
|
12
|
+
/** TTL for cached responses in ms (default: 86400000 = 24h) */
|
|
13
|
+
ttlMs?: number;
|
|
14
|
+
/** Lock timeout in ms (default: 30000 = 30s) */
|
|
15
|
+
lockTimeoutMs?: number;
|
|
16
|
+
/** HTTP methods to apply idempotency to (default: ['POST', 'PUT', 'PATCH']) */
|
|
17
|
+
methods?: string[];
|
|
18
|
+
/** URL patterns to include (regex). If set, only matching URLs use idempotency */
|
|
19
|
+
include?: RegExp[];
|
|
20
|
+
/** URL patterns to exclude (regex). Excluded patterns take precedence */
|
|
21
|
+
exclude?: RegExp[];
|
|
22
|
+
/** Custom store (default: MemoryIdempotencyStore) */
|
|
23
|
+
store?: IdempotencyStore;
|
|
24
|
+
/** Retry-After header value in seconds when request is in-flight (default: 1) */
|
|
25
|
+
retryAfterSeconds?: number;
|
|
26
|
+
}
|
|
27
|
+
declare module 'fastify' {
|
|
28
|
+
interface FastifyRequest {
|
|
29
|
+
/** The idempotency key for this request (if present) */
|
|
30
|
+
idempotencyKey?: string;
|
|
31
|
+
/** Whether this response was replayed from cache */
|
|
32
|
+
idempotencyReplayed?: boolean;
|
|
33
|
+
/** @internal Full key with fingerprint for store lookups */
|
|
34
|
+
_idempotencyFullKey?: string;
|
|
35
|
+
}
|
|
36
|
+
interface FastifyInstance {
|
|
37
|
+
/** Idempotency utilities */
|
|
38
|
+
idempotency: {
|
|
39
|
+
/** Manually invalidate an idempotency key */invalidate: (key: string) => Promise<void>; /** Check if a key has a cached response */
|
|
40
|
+
has: (key: string) => Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Route-level preHandler for idempotency check + lock.
|
|
43
|
+
* Wire AFTER authenticate in the preHandler chain so that
|
|
44
|
+
* `request.user` is populated before the fingerprint is computed.
|
|
45
|
+
*
|
|
46
|
+
* `createCrudRouter` injects this automatically for mutation routes.
|
|
47
|
+
* For custom routes, add it manually:
|
|
48
|
+
* ```typescript
|
|
49
|
+
* fastify.post('/orders', {
|
|
50
|
+
* preHandler: [fastify.authenticate, fastify.idempotency.middleware],
|
|
51
|
+
* }, handler);
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
middleware: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
declare const idempotencyPlugin: FastifyPluginAsync<IdempotencyPluginOptions>;
|
|
59
|
+
declare const _default: FastifyPluginAsync<IdempotencyPluginOptions>;
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/idempotency/stores/memory.d.ts
|
|
62
|
+
interface MemoryIdempotencyStoreOptions {
|
|
63
|
+
/** Default TTL in milliseconds (default: 86400000 = 24h) */
|
|
64
|
+
ttlMs?: number;
|
|
65
|
+
/** Cleanup interval in milliseconds (default: 60000 = 1 min) */
|
|
66
|
+
cleanupIntervalMs?: number;
|
|
67
|
+
/** Maximum entries before oldest are evicted (default: 10000) */
|
|
68
|
+
maxEntries?: number;
|
|
69
|
+
}
|
|
70
|
+
declare class MemoryIdempotencyStore implements IdempotencyStore {
|
|
71
|
+
readonly name = "memory";
|
|
72
|
+
private results;
|
|
73
|
+
private locks;
|
|
74
|
+
private ttlMs;
|
|
75
|
+
private maxEntries;
|
|
76
|
+
private cleanupInterval;
|
|
77
|
+
constructor(options?: MemoryIdempotencyStoreOptions);
|
|
78
|
+
get(key: string): Promise<IdempotencyResult | undefined>;
|
|
79
|
+
set(key: string, result: Omit<IdempotencyResult, 'key'>): Promise<void>;
|
|
80
|
+
tryLock(key: string, requestId: string, ttlMs: number): Promise<boolean>;
|
|
81
|
+
unlock(key: string, requestId: string): Promise<void>;
|
|
82
|
+
isLocked(key: string): Promise<boolean>;
|
|
83
|
+
delete(key: string): Promise<void>;
|
|
84
|
+
deleteByPrefix(prefix: string): Promise<number>;
|
|
85
|
+
findByPrefix(prefix: string): Promise<IdempotencyResult | undefined>;
|
|
86
|
+
close(): Promise<void>;
|
|
87
|
+
/** Get current stats (for debugging/monitoring) */
|
|
88
|
+
getStats(): {
|
|
89
|
+
results: number;
|
|
90
|
+
locks: number;
|
|
91
|
+
};
|
|
92
|
+
private cleanup;
|
|
93
|
+
private evictOldest;
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { type IdempotencyLock, type IdempotencyPluginOptions, type IdempotencyResult, type IdempotencyStore, MemoryIdempotencyStore, type MemoryIdempotencyStoreOptions, type MongoIdempotencyStoreOptions, type RedisClient, type RedisIdempotencyStoreOptions, createIdempotencyResult, _default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn };
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
|
|
4
|
+
//#region src/idempotency/stores/interface.ts
|
|
5
|
+
/**
|
|
6
|
+
* Helper to create a result object
|
|
7
|
+
*/
|
|
8
|
+
function createIdempotencyResult(statusCode, body, headers, ttlMs) {
|
|
9
|
+
const now = /* @__PURE__ */ new Date();
|
|
10
|
+
return {
|
|
11
|
+
statusCode,
|
|
12
|
+
headers,
|
|
13
|
+
body,
|
|
14
|
+
createdAt: now,
|
|
15
|
+
expiresAt: new Date(now.getTime() + ttlMs)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/idempotency/stores/memory.ts
|
|
21
|
+
var MemoryIdempotencyStore = class {
|
|
22
|
+
name = "memory";
|
|
23
|
+
results = /* @__PURE__ */ new Map();
|
|
24
|
+
locks = /* @__PURE__ */ new Map();
|
|
25
|
+
ttlMs;
|
|
26
|
+
maxEntries;
|
|
27
|
+
cleanupInterval = null;
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
this.ttlMs = options.ttlMs ?? 864e5;
|
|
30
|
+
this.maxEntries = options.maxEntries ?? 1e4;
|
|
31
|
+
const cleanupIntervalMs = options.cleanupIntervalMs ?? 6e4;
|
|
32
|
+
this.cleanupInterval = setInterval(() => {
|
|
33
|
+
this.cleanup();
|
|
34
|
+
}, cleanupIntervalMs);
|
|
35
|
+
if (this.cleanupInterval.unref) this.cleanupInterval.unref();
|
|
36
|
+
}
|
|
37
|
+
async get(key) {
|
|
38
|
+
const result = this.results.get(key);
|
|
39
|
+
if (!result) return void 0;
|
|
40
|
+
if (/* @__PURE__ */ new Date() > result.expiresAt) {
|
|
41
|
+
this.results.delete(key);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
async set(key, result) {
|
|
47
|
+
if (this.results.size >= this.maxEntries) this.evictOldest();
|
|
48
|
+
this.results.set(key, {
|
|
49
|
+
...result,
|
|
50
|
+
key
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async tryLock(key, requestId, ttlMs) {
|
|
54
|
+
const existing = this.locks.get(key);
|
|
55
|
+
if (existing) if (/* @__PURE__ */ new Date() > existing.expiresAt) this.locks.delete(key);
|
|
56
|
+
else return false;
|
|
57
|
+
this.locks.set(key, {
|
|
58
|
+
key,
|
|
59
|
+
requestId,
|
|
60
|
+
lockedAt: /* @__PURE__ */ new Date(),
|
|
61
|
+
expiresAt: new Date(Date.now() + ttlMs)
|
|
62
|
+
});
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
async unlock(key, requestId) {
|
|
66
|
+
const lock = this.locks.get(key);
|
|
67
|
+
if (lock && lock.requestId === requestId) this.locks.delete(key);
|
|
68
|
+
}
|
|
69
|
+
async isLocked(key) {
|
|
70
|
+
const lock = this.locks.get(key);
|
|
71
|
+
if (!lock) return false;
|
|
72
|
+
if (/* @__PURE__ */ new Date() > lock.expiresAt) {
|
|
73
|
+
this.locks.delete(key);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
async delete(key) {
|
|
79
|
+
this.results.delete(key);
|
|
80
|
+
this.locks.delete(key);
|
|
81
|
+
}
|
|
82
|
+
async deleteByPrefix(prefix) {
|
|
83
|
+
let count = 0;
|
|
84
|
+
for (const key of this.results.keys()) if (key.startsWith(prefix)) {
|
|
85
|
+
this.results.delete(key);
|
|
86
|
+
count++;
|
|
87
|
+
}
|
|
88
|
+
for (const key of this.locks.keys()) if (key.startsWith(prefix)) this.locks.delete(key);
|
|
89
|
+
return count;
|
|
90
|
+
}
|
|
91
|
+
async findByPrefix(prefix) {
|
|
92
|
+
const now = /* @__PURE__ */ new Date();
|
|
93
|
+
for (const [key, result] of this.results) if (key.startsWith(prefix)) {
|
|
94
|
+
if (now > result.expiresAt) {
|
|
95
|
+
this.results.delete(key);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async close() {
|
|
102
|
+
if (this.cleanupInterval) {
|
|
103
|
+
clearInterval(this.cleanupInterval);
|
|
104
|
+
this.cleanupInterval = null;
|
|
105
|
+
}
|
|
106
|
+
this.results.clear();
|
|
107
|
+
this.locks.clear();
|
|
108
|
+
}
|
|
109
|
+
/** Get current stats (for debugging/monitoring) */
|
|
110
|
+
getStats() {
|
|
111
|
+
return {
|
|
112
|
+
results: this.results.size,
|
|
113
|
+
locks: this.locks.size
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
cleanup() {
|
|
117
|
+
const now = /* @__PURE__ */ new Date();
|
|
118
|
+
for (const [key, result] of this.results) if (now > result.expiresAt) this.results.delete(key);
|
|
119
|
+
for (const [key, lock] of this.locks) if (now > lock.expiresAt) this.locks.delete(key);
|
|
120
|
+
}
|
|
121
|
+
evictOldest() {
|
|
122
|
+
const entries = Array.from(this.results.entries()).sort((a, b) => a[1].createdAt.getTime() - b[1].createdAt.getTime());
|
|
123
|
+
const toRemove = Math.max(1, Math.floor(entries.length * .1));
|
|
124
|
+
for (let i = 0; i < toRemove; i++) {
|
|
125
|
+
const entry = entries[i];
|
|
126
|
+
if (entry) this.results.delete(entry[0]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/idempotency/idempotencyPlugin.ts
|
|
133
|
+
/**
|
|
134
|
+
* Idempotency Plugin
|
|
135
|
+
*
|
|
136
|
+
* Duplicate request protection for mutating operations.
|
|
137
|
+
* Uses idempotency keys to ensure safe retries.
|
|
138
|
+
*
|
|
139
|
+
* ## Auth Safety
|
|
140
|
+
*
|
|
141
|
+
* The idempotency check runs as a **route-level middleware**
|
|
142
|
+
* (`idempotency.middleware`) that must be wired AFTER authentication in the
|
|
143
|
+
* preHandler chain. This ensures the fingerprint includes the real caller
|
|
144
|
+
* identity, preventing cross-user replay attacks.
|
|
145
|
+
*
|
|
146
|
+
* Arc's `createCrudRouter` does this automatically for mutation routes.
|
|
147
|
+
* For custom routes, wire it manually:
|
|
148
|
+
*
|
|
149
|
+
* ```typescript
|
|
150
|
+
* fastify.post('/orders', {
|
|
151
|
+
* preHandler: [fastify.authenticate, fastify.idempotency.middleware],
|
|
152
|
+
* }, handler);
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* import { idempotencyPlugin } from '@classytic/arc/idempotency';
|
|
157
|
+
*
|
|
158
|
+
* await fastify.register(idempotencyPlugin, {
|
|
159
|
+
* enabled: true,
|
|
160
|
+
* headerName: 'idempotency-key',
|
|
161
|
+
* ttlMs: 86400000, // 24 hours
|
|
162
|
+
* });
|
|
163
|
+
*
|
|
164
|
+
* // Client sends:
|
|
165
|
+
* // POST /api/orders
|
|
166
|
+
* // Idempotency-Key: order-123-abc
|
|
167
|
+
*
|
|
168
|
+
* // If same key sent again within TTL, returns cached response
|
|
169
|
+
*/
|
|
170
|
+
const HEADER_IDEMPOTENCY_REPLAYED = "x-idempotency-replayed";
|
|
171
|
+
const HEADER_IDEMPOTENCY_KEY = "x-idempotency-key";
|
|
172
|
+
const idempotencyPlugin = async (fastify, opts = {}) => {
|
|
173
|
+
const { enabled = false, headerName = "idempotency-key", ttlMs = 864e5, lockTimeoutMs = 3e4, methods = [
|
|
174
|
+
"POST",
|
|
175
|
+
"PUT",
|
|
176
|
+
"PATCH"
|
|
177
|
+
], include, exclude, store = new MemoryIdempotencyStore({ ttlMs }), retryAfterSeconds = 1 } = opts;
|
|
178
|
+
if (!enabled) {
|
|
179
|
+
fastify.decorate("idempotency", {
|
|
180
|
+
invalidate: async () => {},
|
|
181
|
+
has: async () => false,
|
|
182
|
+
middleware: async () => {}
|
|
183
|
+
});
|
|
184
|
+
fastify.decorateRequest("idempotencyKey", void 0);
|
|
185
|
+
fastify.decorateRequest("idempotencyReplayed", false);
|
|
186
|
+
fastify.log?.debug?.("Idempotency plugin disabled");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const methodSet = new Set(methods.map((m) => m.toUpperCase()));
|
|
190
|
+
fastify.decorateRequest("idempotencyKey", void 0);
|
|
191
|
+
fastify.decorateRequest("idempotencyReplayed", false);
|
|
192
|
+
/**
|
|
193
|
+
* Check if this request should use idempotency
|
|
194
|
+
*/
|
|
195
|
+
function shouldApplyIdempotency(request) {
|
|
196
|
+
if (!methodSet.has(request.method)) return false;
|
|
197
|
+
const url = request.url;
|
|
198
|
+
if (exclude?.some((pattern) => pattern.test(url))) return false;
|
|
199
|
+
if (include && !include.some((pattern) => pattern.test(url))) return false;
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Normalize body for consistent hashing (sort keys recursively)
|
|
204
|
+
*/
|
|
205
|
+
function normalizeBody(obj) {
|
|
206
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
207
|
+
if (Array.isArray(obj)) return obj.map(normalizeBody);
|
|
208
|
+
const sorted = {};
|
|
209
|
+
const keys = Object.keys(obj).sort();
|
|
210
|
+
for (const key of keys) sorted[key] = normalizeBody(obj[key]);
|
|
211
|
+
return sorted;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Generate a fingerprint for the request (for key generation).
|
|
215
|
+
* Includes caller identity so the same idempotency key from different
|
|
216
|
+
* users doesn't replay one user's response to another.
|
|
217
|
+
*
|
|
218
|
+
* IMPORTANT: This must be called AFTER auth has populated request.user,
|
|
219
|
+
* otherwise userId falls back to 'anon' and cross-user replay is possible.
|
|
220
|
+
*/
|
|
221
|
+
function getRequestFingerprint(request) {
|
|
222
|
+
let bodyHash = "nobody";
|
|
223
|
+
if (request.body && typeof request.body === "object") {
|
|
224
|
+
const normalized = normalizeBody(request.body);
|
|
225
|
+
const bodyString = JSON.stringify(normalized);
|
|
226
|
+
bodyHash = createHash("sha256").update(bodyString).digest("hex").substring(0, 16);
|
|
227
|
+
if (request.log && request.log.debug) request.log.debug({ bodyHash }, "Generated body hash");
|
|
228
|
+
}
|
|
229
|
+
const user = request.user;
|
|
230
|
+
const userId = user?.id ?? user?._id ?? "anon";
|
|
231
|
+
return `${request.method}:${request.url}:${bodyHash}:u=${userId}`;
|
|
232
|
+
}
|
|
233
|
+
const idempotencyMiddleware = async (request, reply) => {
|
|
234
|
+
if (!shouldApplyIdempotency(request)) return;
|
|
235
|
+
const keyHeader = request.headers[headerName.toLowerCase()];
|
|
236
|
+
const idempotencyKey = typeof keyHeader === "string" ? keyHeader.trim() : void 0;
|
|
237
|
+
if (!idempotencyKey) return;
|
|
238
|
+
request.idempotencyKey = idempotencyKey;
|
|
239
|
+
const fullKey = `${idempotencyKey}:${getRequestFingerprint(request)}`;
|
|
240
|
+
const cached = await store.get(fullKey);
|
|
241
|
+
if (cached) {
|
|
242
|
+
request.idempotencyReplayed = true;
|
|
243
|
+
reply.header(HEADER_IDEMPOTENCY_REPLAYED, "true");
|
|
244
|
+
reply.header(HEADER_IDEMPOTENCY_KEY, idempotencyKey);
|
|
245
|
+
for (const [key, value] of Object.entries(cached.headers)) if (!key.startsWith("x-idempotency")) reply.header(key, value);
|
|
246
|
+
reply.code(cached.statusCode).send(cached.body);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (!await store.tryLock(fullKey, request.id, lockTimeoutMs)) {
|
|
250
|
+
reply.code(409).header("Retry-After", retryAfterSeconds.toString()).send({
|
|
251
|
+
error: "Request with this idempotency key is already in progress",
|
|
252
|
+
code: "IDEMPOTENCY_CONFLICT",
|
|
253
|
+
retryAfter: retryAfterSeconds
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
request._idempotencyFullKey = fullKey;
|
|
258
|
+
};
|
|
259
|
+
fastify.decorate("idempotency", {
|
|
260
|
+
invalidate: async (key) => {
|
|
261
|
+
await store.deleteByPrefix(`${key}:`);
|
|
262
|
+
},
|
|
263
|
+
has: async (key) => {
|
|
264
|
+
return !!await store.findByPrefix(`${key}:`);
|
|
265
|
+
},
|
|
266
|
+
middleware: idempotencyMiddleware
|
|
267
|
+
});
|
|
268
|
+
fastify.addHook("onSend", async (request, reply, payload) => {
|
|
269
|
+
if (request.idempotencyReplayed) return payload;
|
|
270
|
+
const fullKey = request._idempotencyFullKey;
|
|
271
|
+
if (!fullKey) return payload;
|
|
272
|
+
const statusCode = reply.statusCode;
|
|
273
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
274
|
+
await store.unlock(fullKey, request.id);
|
|
275
|
+
return payload;
|
|
276
|
+
}
|
|
277
|
+
const headersToCache = {};
|
|
278
|
+
const excludeHeaders = new Set([
|
|
279
|
+
"content-length",
|
|
280
|
+
"transfer-encoding",
|
|
281
|
+
"connection",
|
|
282
|
+
"keep-alive",
|
|
283
|
+
"date",
|
|
284
|
+
"set-cookie"
|
|
285
|
+
]);
|
|
286
|
+
const rawHeaders = reply.getHeaders();
|
|
287
|
+
for (const [key, value] of Object.entries(rawHeaders)) if (!excludeHeaders.has(key.toLowerCase()) && typeof value === "string") headersToCache[key] = value;
|
|
288
|
+
let body;
|
|
289
|
+
try {
|
|
290
|
+
body = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
291
|
+
} catch {
|
|
292
|
+
body = payload;
|
|
293
|
+
}
|
|
294
|
+
const result = createIdempotencyResult(statusCode, body, headersToCache, ttlMs);
|
|
295
|
+
await store.set(fullKey, result);
|
|
296
|
+
await store.unlock(fullKey, request.id);
|
|
297
|
+
reply.header(HEADER_IDEMPOTENCY_KEY, request.idempotencyKey);
|
|
298
|
+
return payload;
|
|
299
|
+
});
|
|
300
|
+
fastify.addHook("onError", async (request) => {
|
|
301
|
+
const fullKey = request._idempotencyFullKey;
|
|
302
|
+
if (fullKey) await store.unlock(fullKey, request.id);
|
|
303
|
+
});
|
|
304
|
+
fastify.addHook("onClose", async () => {
|
|
305
|
+
await store.close?.();
|
|
306
|
+
});
|
|
307
|
+
fastify.log?.debug?.({
|
|
308
|
+
headerName,
|
|
309
|
+
ttlMs,
|
|
310
|
+
methods
|
|
311
|
+
}, "Idempotency plugin enabled");
|
|
312
|
+
};
|
|
313
|
+
var idempotencyPlugin_default = fp(idempotencyPlugin, {
|
|
314
|
+
name: "arc-idempotency",
|
|
315
|
+
fastify: "5.x"
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
//#endregion
|
|
319
|
+
export { MemoryIdempotencyStore, createIdempotencyResult, idempotencyPlugin_default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
//#region src/idempotency/stores/mongodb.ts
|
|
2
|
+
var MongoIdempotencyStore = class {
|
|
3
|
+
name = "mongodb";
|
|
4
|
+
connection;
|
|
5
|
+
collectionName;
|
|
6
|
+
ttlMs;
|
|
7
|
+
indexCreated = false;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.connection = options.connection;
|
|
10
|
+
this.collectionName = options.collection ?? "arc_idempotency";
|
|
11
|
+
this.ttlMs = options.ttlMs ?? 864e5;
|
|
12
|
+
if (options.createIndex !== false) this.ensureIndex().catch((err) => {
|
|
13
|
+
console.warn("[MongoIdempotencyStore] Failed to create index:", err);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
get collection() {
|
|
17
|
+
return this.connection.db.collection(this.collectionName);
|
|
18
|
+
}
|
|
19
|
+
async ensureIndex() {
|
|
20
|
+
if (this.indexCreated) return;
|
|
21
|
+
try {
|
|
22
|
+
await this.collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
|
23
|
+
this.indexCreated = true;
|
|
24
|
+
} catch {
|
|
25
|
+
this.indexCreated = true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async get(key) {
|
|
29
|
+
const doc = await this.collection.findOne({ _id: key });
|
|
30
|
+
if (!doc || !doc.result) return void 0;
|
|
31
|
+
if (new Date(doc.expiresAt) < /* @__PURE__ */ new Date()) return;
|
|
32
|
+
return {
|
|
33
|
+
key,
|
|
34
|
+
statusCode: doc.result.statusCode,
|
|
35
|
+
headers: doc.result.headers,
|
|
36
|
+
body: doc.result.body,
|
|
37
|
+
createdAt: new Date(doc.createdAt),
|
|
38
|
+
expiresAt: new Date(doc.expiresAt)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async set(key, result) {
|
|
42
|
+
await this.collection.updateOne({ _id: key }, {
|
|
43
|
+
$set: {
|
|
44
|
+
result: {
|
|
45
|
+
statusCode: result.statusCode,
|
|
46
|
+
headers: result.headers,
|
|
47
|
+
body: result.body
|
|
48
|
+
},
|
|
49
|
+
createdAt: result.createdAt,
|
|
50
|
+
expiresAt: result.expiresAt
|
|
51
|
+
},
|
|
52
|
+
$unset: { lock: "" }
|
|
53
|
+
}, { upsert: true });
|
|
54
|
+
}
|
|
55
|
+
async tryLock(key, requestId, ttlMs) {
|
|
56
|
+
const now = /* @__PURE__ */ new Date();
|
|
57
|
+
const expiresAt = new Date(now.getTime() + ttlMs);
|
|
58
|
+
try {
|
|
59
|
+
const result = await this.collection.updateOne({
|
|
60
|
+
_id: key,
|
|
61
|
+
$or: [{ lock: { $exists: false } }, { "lock.expiresAt": { $lt: now } }]
|
|
62
|
+
}, {
|
|
63
|
+
$set: { lock: {
|
|
64
|
+
requestId,
|
|
65
|
+
expiresAt
|
|
66
|
+
} },
|
|
67
|
+
$setOnInsert: {
|
|
68
|
+
createdAt: now,
|
|
69
|
+
expiresAt: new Date(now.getTime() + this.ttlMs)
|
|
70
|
+
}
|
|
71
|
+
}, { upsert: true });
|
|
72
|
+
return result.matchedCount === 1 || result.modifiedCount === 1;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async unlock(key, requestId) {
|
|
78
|
+
await this.collection.updateOne({
|
|
79
|
+
_id: key,
|
|
80
|
+
"lock.requestId": requestId
|
|
81
|
+
}, { $unset: { lock: "" } });
|
|
82
|
+
}
|
|
83
|
+
async isLocked(key) {
|
|
84
|
+
const doc = await this.collection.findOne({ _id: key });
|
|
85
|
+
if (!doc || !doc.lock) return false;
|
|
86
|
+
return new Date(doc.lock.expiresAt) > /* @__PURE__ */ new Date();
|
|
87
|
+
}
|
|
88
|
+
async delete(key) {
|
|
89
|
+
await this.collection.deleteOne({ _id: key });
|
|
90
|
+
}
|
|
91
|
+
async deleteByPrefix(prefix) {
|
|
92
|
+
return (await this.collection.deleteMany({ _id: { $regex: `^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}` } })).deletedCount;
|
|
93
|
+
}
|
|
94
|
+
async findByPrefix(prefix) {
|
|
95
|
+
const doc = await this.collection.findOne({
|
|
96
|
+
_id: { $regex: `^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}` },
|
|
97
|
+
result: { $exists: true },
|
|
98
|
+
expiresAt: { $gt: /* @__PURE__ */ new Date() }
|
|
99
|
+
});
|
|
100
|
+
if (!doc || !doc.result) return void 0;
|
|
101
|
+
return {
|
|
102
|
+
key: doc._id,
|
|
103
|
+
statusCode: doc.result.statusCode,
|
|
104
|
+
headers: doc.result.headers,
|
|
105
|
+
body: doc.result.body,
|
|
106
|
+
createdAt: new Date(doc.createdAt),
|
|
107
|
+
expiresAt: new Date(doc.expiresAt)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async close() {}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
export { MongoIdempotencyStore };
|