@classytic/arc 2.9.1 → 2.10.8

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.
Files changed (132) hide show
  1. package/README.md +20 -91
  2. package/dist/{BaseController-Vu2yc56T.mjs → BaseController-DVNKvoX4.mjs} +154 -170
  3. package/dist/{ResourceRegistry-Dq3_zBQP.mjs → ResourceRegistry-CcN2LVrc.mjs} +1 -1
  4. package/dist/actionPermissions-TUVR3uiZ.mjs +22 -0
  5. package/dist/adapters/index.d.mts +3 -3
  6. package/dist/adapters/index.mjs +2 -2
  7. package/dist/{adapters-BBqAVvPK.mjs → adapters-BXY4i-hw.mjs} +210 -41
  8. package/dist/audit/index.d.mts +38 -3
  9. package/dist/audit/index.mjs +54 -22
  10. package/dist/auth/index.d.mts +2 -2
  11. package/dist/auth/index.mjs +3 -3
  12. package/dist/cache/index.d.mts +17 -15
  13. package/dist/cache/index.mjs +16 -15
  14. package/dist/{caching-CjybdRwx.mjs → caching-3h93rkJM.mjs} +8 -3
  15. package/dist/cli/commands/describe.mjs +1 -1
  16. package/dist/cli/commands/docs.mjs +2 -2
  17. package/dist/cli/commands/init.mjs +1 -1
  18. package/dist/cli/commands/introspect.mjs +1 -1
  19. package/dist/context/index.d.mts +58 -0
  20. package/dist/context/index.mjs +2 -0
  21. package/dist/core/index.d.mts +2 -2
  22. package/dist/core/index.mjs +3 -4
  23. package/dist/{defineResource-C__jkwvs.mjs → core-3MWJosCH.mjs} +174 -94
  24. package/dist/{createActionRouter-DH1YFL9m.mjs → createActionRouter-C8UUB3Px.mjs} +1 -1
  25. package/dist/{createApp-CBJUJKGP.mjs → createApp-BwnEAO2h.mjs} +53 -19
  26. package/dist/docs/index.d.mts +1 -1
  27. package/dist/docs/index.mjs +2 -2
  28. package/dist/{elevation-DxQ6ACbt.mjs → elevation-Dci0AYLT.mjs} +2 -2
  29. package/dist/errorHandler-2ii4RIYr.d.mts +114 -0
  30. package/dist/{errorHandler-CZDW4EXS.mjs → errorHandler-CSxe7KIM.mjs} +1 -1
  31. package/dist/{eventPlugin-Dl7MoVWH.mjs → eventPlugin-ByU4Cv0e.mjs} +1 -1
  32. package/dist/{eventPlugin-BxvaCIZF.d.mts → eventPlugin-D1ThQ1Pp.d.mts} +1 -1
  33. package/dist/events/index.d.mts +8 -5
  34. package/dist/events/index.mjs +87 -52
  35. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  36. package/dist/events/transports/redis.d.mts +1 -1
  37. package/dist/factory/index.d.mts +1 -1
  38. package/dist/factory/index.mjs +1 -1
  39. package/dist/{types-DZi1aYhm.d.mts → fields-C8Y0XLAu.d.mts} +122 -2
  40. package/dist/hooks/index.d.mts +1 -1
  41. package/dist/idempotency/index.d.mts +5 -2
  42. package/dist/idempotency/index.mjs +46 -37
  43. package/dist/{interface-YrWsmKqE.d.mts → index-BGbpGVyM.d.mts} +2107 -2756
  44. package/dist/{index-CtGKT0lf.d.mts → index-BziRPS4H.d.mts} +81 -7
  45. package/dist/{index-C-xjcA6F.d.mts → index-C_Noptz-.d.mts} +284 -409
  46. package/dist/{index-Cibkchnx.d.mts → index-EqQN6p0W.d.mts} +3 -3
  47. package/dist/index.d.mts +6 -219
  48. package/dist/index.mjs +10 -131
  49. package/dist/integrations/event-gateway.d.mts +1 -1
  50. package/dist/integrations/event-gateway.mjs +1 -1
  51. package/dist/integrations/index.d.mts +1 -1
  52. package/dist/integrations/mcp/index.d.mts +2 -2
  53. package/dist/integrations/mcp/index.mjs +1 -1
  54. package/dist/integrations/mcp/testing.d.mts +1 -1
  55. package/dist/integrations/mcp/testing.mjs +1 -1
  56. package/dist/interface-yhyb_pLY.d.mts +77 -0
  57. package/dist/logger/index.d.mts +81 -0
  58. package/dist/{logger-CDjpjySd.mjs → logger/index.mjs} +1 -6
  59. package/dist/{memory-BFAYkf8H.mjs → memory-DqI-449b.mjs} +23 -8
  60. package/dist/middleware/index.d.mts +109 -0
  61. package/dist/middleware/index.mjs +70 -0
  62. package/dist/multipartBody-CUQGVlM_.mjs +123 -0
  63. package/dist/{openapi-CXuTG1M9.mjs → openapi-DpNpqBmo.mjs} +9 -7
  64. package/dist/org/index.d.mts +2 -2
  65. package/dist/permissions/index.d.mts +3 -4
  66. package/dist/permissions/index.mjs +5 -5
  67. package/dist/{permissions-oNZawnkR.mjs → permissions-wkqRwicB.mjs} +315 -397
  68. package/dist/pipe-CGJxqDGx.mjs +62 -0
  69. package/dist/pipeline/index.d.mts +62 -0
  70. package/dist/pipeline/index.mjs +53 -0
  71. package/dist/plugins/index.d.mts +23 -3
  72. package/dist/plugins/index.mjs +9 -11
  73. package/dist/plugins/response-cache.mjs +1 -1
  74. package/dist/plugins/tracing-entry.mjs +1 -1
  75. package/dist/presets/filesUpload.d.mts +3 -3
  76. package/dist/presets/filesUpload.mjs +255 -1
  77. package/dist/presets/index.d.mts +1 -1
  78. package/dist/presets/index.mjs +2 -2
  79. package/dist/presets/multiTenant.d.mts +1 -1
  80. package/dist/presets/multiTenant.mjs +43 -9
  81. package/dist/presets/search.d.mts +91 -4
  82. package/dist/presets/search.mjs +1 -1
  83. package/dist/{presets-hM4WhNWY.mjs → presets-CrwOvuXI.mjs} +1 -1
  84. package/dist/{queryCachePlugin-DbUVroUG.mjs → queryCachePlugin-ChLNZvFT.mjs} +9 -9
  85. package/dist/{queryCachePlugin-CnTZZTC5.d.mts → queryCachePlugin-Dumka73q.d.mts} +1 -1
  86. package/dist/{queryParser-Cs-6SHQK.mjs → queryParser-NR__Qiju.mjs} +69 -2
  87. package/dist/{redis-stream-Bz-4q96t.d.mts → redis-stream-bkO88VHx.d.mts} +1 -1
  88. package/dist/registry/index.d.mts +1 -1
  89. package/dist/registry/index.mjs +1 -1
  90. package/dist/{requestContext-DYtmNpm5.mjs → requestContext-C38GskNt.mjs} +1 -1
  91. package/dist/{resourceToTools-C3cWymnW.mjs → resourceToTools-BhF3JV5p.mjs} +8 -3
  92. package/dist/scope/index.d.mts +2 -2
  93. package/dist/scope/index.mjs +2 -2
  94. package/dist/{sse-CJpt7LGI.mjs → sse-D8UeDwis.mjs} +1 -1
  95. package/dist/{store-helpers-DFiZl5TL.mjs → store-helpers-DYYUQbQN.mjs} +4 -0
  96. package/dist/testing/index.d.mts +6 -5
  97. package/dist/testing/index.mjs +17 -10
  98. package/dist/types/index.d.mts +5 -5
  99. package/dist/types/index.mjs +1 -31
  100. package/dist/types-CDnTEpga.mjs +27 -0
  101. package/dist/{types-CoSzA-s-.d.mts → types-CVKBssX5.d.mts} +1 -1
  102. package/dist/{types-CunEX4UX.d.mts → types-CVdgPXBW.d.mts} +20 -7
  103. package/dist/utils/index.d.mts +277 -3
  104. package/dist/utils/index.mjs +4 -5
  105. package/dist/{utils-B7FuRr9w.mjs → utils-LMwVidKy.mjs} +303 -2
  106. package/dist/{versioning-Cm8qoFDg.mjs → versioning-B6mimogM.mjs} +3 -5
  107. package/dist/versioning-CeUXHfjw.d.mts +117 -0
  108. package/package.json +31 -18
  109. package/skills/arc/SKILL.md +8 -12
  110. package/skills/arc/references/production.md +0 -41
  111. package/dist/circuitBreaker-CvXkjfrW.d.mts +0 -206
  112. package/dist/circuitBreaker-l18oRgL5.mjs +0 -284
  113. package/dist/core-DNncu0xF.mjs +0 -34
  114. package/dist/dynamic/index.d.mts +0 -93
  115. package/dist/dynamic/index.mjs +0 -122
  116. package/dist/errorHandler-DixGcttC.d.mts +0 -218
  117. package/dist/fields-BC7zcmI9.d.mts +0 -121
  118. package/dist/filesUpload-q8oHt--L.mjs +0 -377
  119. package/dist/interface-DplgQO2e.d.mts +0 -54
  120. package/dist/policies/index.d.mts +0 -425
  121. package/dist/policies/index.mjs +0 -318
  122. package/dist/rpc/index.d.mts +0 -90
  123. package/dist/rpc/index.mjs +0 -248
  124. /package/dist/{EventTransport-CqZ8FyM_.d.mts → EventTransport-CfVEGaEl.d.mts} +0 -0
  125. /package/dist/{applyPermissionResult-bqGpo9ML.mjs → applyPermissionResult-QhV1Pa-g.mjs} +0 -0
  126. /package/dist/{constants-Cxde4rpC.mjs → constants-BhY1OHoH.mjs} +0 -0
  127. /package/dist/{elevation-B6S5csVA.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
  128. /package/dist/{errors-CqWnSqM-.mjs → errors-BqdUDja_.mjs} +0 -0
  129. /package/dist/{fields-CU6FlaDV.mjs → fields-CTMWOUDt.mjs} +0 -0
  130. /package/dist/{keys-qcD-TVJl.mjs → keys-nWQGUTu1.mjs} +0 -0
  131. /package/dist/{types-ZUu_h0jp.mjs → types-D57iXYb8.mjs} +0 -0
  132. /package/dist/{types-BD85MlEK.d.mts → types-tgR4Pt8F.d.mts} +0 -0
@@ -1,4 +1,4 @@
1
- import { r as RequestScope } from "./types-BD85MlEK.mjs";
1
+ import { r as RequestScope } from "./types-tgR4Pt8F.mjs";
2
2
  import { FastifyRequest } from "fastify";
3
3
 
4
4
  //#region src/permissions/types.d.ts
@@ -175,4 +175,124 @@ interface PermissionCheckMeta {
175
175
  _orgInScopeTarget?: string | ((ctx: PermissionContext) => string | undefined);
176
176
  }
177
177
  //#endregion
178
- export { getUserRoles as a, UserBase as i, PermissionContext as n, normalizeRoles as o, PermissionResult as r, PermissionCheck as t };
178
+ //#region src/permissions/fields.d.ts
179
+ /**
180
+ * Field-Level Permissions
181
+ *
182
+ * Control field visibility and writability per role.
183
+ * Integrated into the response path (read) and sanitization path (write).
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * import { fields, defineResource } from '@classytic/arc';
188
+ *
189
+ * const userResource = defineResource({
190
+ * name: 'user',
191
+ * adapter: userAdapter,
192
+ * fields: {
193
+ * salary: fields.visibleTo(['admin', 'hr']),
194
+ * internalNotes: fields.writableBy(['admin']),
195
+ * email: fields.redactFor(['viewer']),
196
+ * password: fields.hidden(),
197
+ * },
198
+ * });
199
+ * ```
200
+ */
201
+ type FieldPermissionType = "hidden" | "visibleTo" | "writableBy" | "redactFor";
202
+ interface FieldPermission {
203
+ readonly _type: FieldPermissionType;
204
+ readonly roles?: readonly string[];
205
+ readonly redactValue?: unknown;
206
+ }
207
+ type FieldPermissionMap = Record<string, FieldPermission>;
208
+ declare const fields: {
209
+ /**
210
+ * Field is never included in responses. Not writable via API.
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * fields: { password: fields.hidden() }
215
+ * ```
216
+ */
217
+ hidden(): FieldPermission;
218
+ /**
219
+ * Field is only visible to users with specified roles.
220
+ * Other users don't see the field at all.
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * fields: { salary: fields.visibleTo(['admin', 'hr']) }
225
+ * ```
226
+ */
227
+ visibleTo(roles: readonly string[]): FieldPermission;
228
+ /**
229
+ * Field is only writable by users with specified roles.
230
+ * All users can still read the field. Users without the role
231
+ * have the field silently stripped from write operations.
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * fields: { role: fields.writableBy(['admin']) }
236
+ * ```
237
+ */
238
+ writableBy(roles: readonly string[]): FieldPermission;
239
+ /**
240
+ * Field is redacted (replaced with a placeholder) for specified roles.
241
+ * Other users see the real value.
242
+ *
243
+ * @param roles - Roles that see the redacted value
244
+ * @param redactValue - Replacement value (default: '***')
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * fields: {
249
+ * email: fields.redactFor(['viewer']),
250
+ * ssn: fields.redactFor(['basic'], '***-**-****'),
251
+ * }
252
+ * ```
253
+ */
254
+ redactFor(roles: readonly string[], redactValue?: unknown): FieldPermission;
255
+ };
256
+ /**
257
+ * Apply field-level READ permissions to a response object.
258
+ * Strips hidden fields, enforces visibility, and applies redaction.
259
+ *
260
+ * @param data - The response object (mutated in place for performance)
261
+ * @param fieldPermissions - Field permission map from resource config
262
+ * @param userRoles - Current user's roles (empty array for unauthenticated)
263
+ * @returns The filtered object
264
+ */
265
+ declare function applyFieldReadPermissions<T extends Record<string, unknown>>(data: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): T;
266
+ /**
267
+ * Result of applying write permissions — includes both the filtered body
268
+ * and the list of fields that were stripped so callers can decide whether
269
+ * to reject the request (secure default) or silently strip (legacy).
270
+ */
271
+ interface FieldWritePermissionResult<T extends Record<string, unknown>> {
272
+ readonly body: T;
273
+ readonly deniedFields: readonly string[];
274
+ }
275
+ /**
276
+ * Apply field-level WRITE permissions to request body.
277
+ *
278
+ * Returns both the filtered body and the list of denied fields. Callers are
279
+ * expected to reject the request when `deniedFields.length > 0` — silently
280
+ * stripping fields hides misconfigurations and real attacks. See
281
+ * `BodySanitizer` for the default policy.
282
+ *
283
+ * @param body - The request body (returns a new filtered copy)
284
+ * @param fieldPermissions - Field permission map from resource config
285
+ * @param userRoles - Current user's roles
286
+ */
287
+ declare function applyFieldWritePermissions<T extends Record<string, unknown>>(body: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): FieldWritePermissionResult<T>;
288
+ /**
289
+ * Resolve effective roles by merging global user roles with org-level roles.
290
+ *
291
+ * Global roles come from `req.user.role` (normalized via getUserRoles()).
292
+ * Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).
293
+ *
294
+ * When no org context exists, returns global roles only — backward compatible.
295
+ */
296
+ declare function resolveEffectiveRoles(userRoles: readonly string[], orgRoles: readonly string[]): string[];
297
+ //#endregion
298
+ export { applyFieldWritePermissions as a, PermissionCheck as c, UserBase as d, getUserRoles as f, applyFieldReadPermissions as i, PermissionContext as l, FieldPermissionMap as n, fields as o, normalizeRoles as p, FieldPermissionType as r, resolveEffectiveRoles as s, FieldPermission as t, PermissionResult as u };
@@ -1,2 +1,2 @@
1
- import { An as beforeCreate, Cn as HookPhase, Dn as afterCreate, En as HookSystemOptions, Mn as beforeUpdate, Nn as createHookSystem, On as afterDelete, Pn as defineHook, Sn as HookOperation, Tn as HookSystem, bn as HookContext, jn as beforeDelete, kn as afterUpdate, wn as HookRegistration, xn as HookHandler, yn as DefineHookOptions } from "../interface-YrWsmKqE.mjs";
1
+ import { $ as beforeCreate, G as HookOperation, H as DefineHookOptions, J as HookSystem, K as HookPhase, Q as afterUpdate, U as HookContext, W as HookHandler, X as afterCreate, Y as HookSystemOptions, Z as afterDelete, et as beforeDelete, nt as createHookSystem, q as HookRegistration, rt as defineHook, tt as beforeUpdate } from "../index-BGbpGVyM.mjs";
2
2
  export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
@@ -1,4 +1,4 @@
1
- import { o as RepositoryLike } from "../interface-YrWsmKqE.mjs";
1
+ import { _n as RepositoryLike } from "../index-BGbpGVyM.mjs";
2
2
  import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-B-pe8fhj.mjs";
3
3
  import { i as RedisIdempotencyStoreOptions, n as RedisClient } from "../redis-MXLp1oOf.mjs";
4
4
  import { FastifyPluginAsync } from "fastify";
@@ -82,6 +82,9 @@ declare module "fastify" {
82
82
  declare const idempotencyPlugin: FastifyPluginAsync<IdempotencyPluginOptions>;
83
83
  declare const _default: FastifyPluginAsync<IdempotencyPluginOptions>;
84
84
  //#endregion
85
+ //#region src/idempotency/repository-idempotency-adapter.d.ts
86
+ declare function repositoryAsIdempotencyStore(repository: RepositoryLike, defaultTtlMs: number): IdempotencyStore;
87
+ //#endregion
85
88
  //#region src/idempotency/stores/memory.d.ts
86
89
  interface MemoryIdempotencyStoreOptions {
87
90
  /** Default TTL in milliseconds (default: 86400000 = 24h) */
@@ -117,4 +120,4 @@ declare class MemoryIdempotencyStore implements IdempotencyStore {
117
120
  private evictOldest;
118
121
  }
119
122
  //#endregion
120
- export { type IdempotencyLock, type IdempotencyPluginOptions, type IdempotencyResult, type IdempotencyStore, MemoryIdempotencyStore, type MemoryIdempotencyStoreOptions, type RedisClient, type RedisIdempotencyStoreOptions, createIdempotencyResult, _default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn };
123
+ export { type IdempotencyLock, type IdempotencyPluginOptions, type IdempotencyResult, type IdempotencyStore, MemoryIdempotencyStore, type MemoryIdempotencyStoreOptions, type RedisClient, type RedisIdempotencyStoreOptions, createIdempotencyResult, _default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn, repositoryAsIdempotencyStore };
@@ -1,7 +1,28 @@
1
- import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-DFiZl5TL.mjs";
1
+ import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-DYYUQbQN.mjs";
2
2
  import { createHash } from "node:crypto";
3
3
  import fp from "fastify-plugin";
4
+ import { and, eq, exists, gt, lt, or, startsWith } from "@classytic/repo-core/filter";
5
+ import { update } from "@classytic/repo-core/update";
4
6
  //#region src/idempotency/repository-idempotency-adapter.ts
7
+ /**
8
+ * RepositoryLike → IdempotencyStore adapter.
9
+ *
10
+ * Maps the idempotency store's verbs (get / set / tryLock / unlock / delete /
11
+ * deleteByPrefix / findByPrefix) onto arc's canonical repository primitives
12
+ * (`getOne` / `deleteMany` / `findOneAndUpdate`). `idempotencyPlugin` wraps
13
+ * a passed repository with this helper when you use the `{ repository }`
14
+ * option; the function is also re-exported from `@classytic/arc/idempotency`
15
+ * so consumers can build and decorate the store (metrics, tracing, key
16
+ * namespacing) before passing it via `store:`.
17
+ *
18
+ * Portability: filters compose via `@classytic/repo-core/filter` builders
19
+ * (`and` / `or` / `eq` / `gt` / `lt` / `exists` / `startsWith`) and updates
20
+ * via `@classytic/repo-core/update` (`update({ set, unset, setOnInsert })`).
21
+ * Both IRs compile to Mongo operators on mongokit, SQL predicates on
22
+ * sqlitekit / pgkit, and `WhereInput` / `update` on prismakit. The store
23
+ * therefore runs identically on every backend that implements the
24
+ * `StandardRepo.findOneAndUpdate` + `getOne` + `deleteMany` surface.
25
+ */
5
26
  function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
6
27
  const missing = [];
7
28
  if (typeof repository.getOne !== "function") missing.push("getOne");
@@ -9,13 +30,13 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
9
30
  if (typeof repository.findOneAndUpdate !== "function") missing.push("findOneAndUpdate");
10
31
  if (missing.length > 0) throw new Error(`idempotencyPlugin: repository is missing required methods: ${missing.join(", ")}. mongokit ≥3.8 satisfies these; other kits must implement them to back idempotency via a repository.`);
11
32
  const r = repository;
33
+ const idField = repository.idField ?? "_id";
12
34
  const isDuplicateKeyError = createIsDuplicateKeyError(repository);
13
35
  const safeGetOne = createSafeGetOne(repository);
14
- const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15
36
  return {
16
37
  name: "repository",
17
38
  async get(key) {
18
- const doc = await safeGetOne({ _id: key });
39
+ const doc = await safeGetOne(eq(idField, key));
19
40
  if (!doc?.result) return void 0;
20
41
  if (new Date(doc.expiresAt) < /* @__PURE__ */ new Date()) return void 0;
21
42
  return {
@@ -28,8 +49,8 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
28
49
  };
29
50
  },
30
51
  async set(key, result) {
31
- await r.findOneAndUpdate({ _id: key }, {
32
- $set: {
52
+ await r.findOneAndUpdate(eq(idField, key), update({
53
+ set: {
33
54
  result: {
34
55
  statusCode: result.statusCode,
35
56
  headers: result.headers,
@@ -38,8 +59,8 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
38
59
  createdAt: result.createdAt,
39
60
  expiresAt: result.expiresAt
40
61
  },
41
- $unset: { lock: "" }
42
- }, {
62
+ unset: ["lock"]
63
+ }), {
43
64
  upsert: true,
44
65
  returnDocument: "after"
45
66
  });
@@ -49,19 +70,16 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
49
70
  const lockExpiresAt = new Date(now.getTime() + ttlMs);
50
71
  const docExpiresAt = new Date(now.getTime() + defaultTtlMs);
51
72
  try {
52
- const doc = await r.findOneAndUpdate({
53
- _id: key,
54
- $or: [{ lock: { $exists: false } }, { "lock.expiresAt": { $lt: now } }]
55
- }, {
56
- $set: { lock: {
73
+ const doc = await r.findOneAndUpdate(and(eq(idField, key), or(exists("lock", false), lt("lock.expiresAt", now))), update({
74
+ set: { lock: {
57
75
  requestId,
58
76
  expiresAt: lockExpiresAt
59
77
  } },
60
- $setOnInsert: {
78
+ setOnInsert: {
61
79
  createdAt: now,
62
80
  expiresAt: docExpiresAt
63
81
  }
64
- }, {
82
+ }), {
65
83
  upsert: true,
66
84
  returnDocument: "after"
67
85
  });
@@ -72,31 +90,24 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
72
90
  }
73
91
  },
74
92
  async unlock(key, requestId) {
75
- await r.findOneAndUpdate({
76
- _id: key,
77
- "lock.requestId": requestId
78
- }, { $unset: { lock: "" } });
93
+ await r.findOneAndUpdate(and(eq(idField, key), eq("lock.requestId", requestId)), update({ unset: ["lock"] }));
79
94
  },
80
95
  async isLocked(key) {
81
- const doc = await safeGetOne({ _id: key });
96
+ const doc = await safeGetOne(eq(idField, key));
82
97
  if (!doc?.lock) return false;
83
98
  return new Date(doc.lock.expiresAt) > /* @__PURE__ */ new Date();
84
99
  },
85
100
  async delete(key) {
86
- await r.deleteMany({ _id: key });
101
+ await r.deleteMany(eq(idField, key));
87
102
  },
88
103
  async deleteByPrefix(prefix) {
89
- return (await r.deleteMany({ _id: { $regex: `^${escapeRegex(prefix)}` } })).deletedCount ?? 0;
104
+ return (await r.deleteMany(startsWith(idField, prefix, "sensitive"))).deletedCount ?? 0;
90
105
  },
91
106
  async findByPrefix(prefix) {
92
- const doc = await safeGetOne({
93
- _id: { $regex: `^${escapeRegex(prefix)}` },
94
- result: { $exists: true },
95
- expiresAt: { $gt: /* @__PURE__ */ new Date() }
96
- });
107
+ const doc = await safeGetOne(and(startsWith(idField, prefix, "sensitive"), exists("result", true), gt("expiresAt", /* @__PURE__ */ new Date())));
97
108
  if (!doc?.result) return void 0;
98
109
  return {
99
- key: doc._id,
110
+ key: String(doc[idField] ?? prefix),
100
111
  statusCode: doc.result.statusCode,
101
112
  headers: doc.result.headers,
102
113
  body: doc.result.body,
@@ -361,6 +372,7 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
361
372
  return;
362
373
  }
363
374
  request._idempotencyFullKey = fullKey;
375
+ reply.header(HEADER_IDEMPOTENCY_KEY, idempotencyKey);
364
376
  };
365
377
  fastify.decorate("idempotency", {
366
378
  invalidate: async (key) => {
@@ -371,15 +383,12 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
371
383
  },
372
384
  middleware: idempotencyMiddleware
373
385
  });
374
- fastify.addHook("onSend", async (request, reply, payload) => {
386
+ fastify.addHook("preSerialization", async (request, reply, payload) => {
375
387
  if (request.idempotencyReplayed) return payload;
376
388
  const fullKey = request._idempotencyFullKey;
377
389
  if (!fullKey) return payload;
378
390
  const statusCode = reply.statusCode;
379
- if (statusCode < 200 || statusCode >= 300) {
380
- await store.unlock(fullKey, request.id);
381
- return payload;
382
- }
391
+ if (statusCode < 200 || statusCode >= 300) return payload;
383
392
  const headersToCache = {};
384
393
  const excludeHeaders = new Set([
385
394
  "content-length",
@@ -399,13 +408,13 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
399
408
  }
400
409
  const result = createIdempotencyResult(statusCode, body, headersToCache, ttlMs);
401
410
  await store.set(fullKey, result);
402
- await store.unlock(fullKey, request.id);
403
- reply.header(HEADER_IDEMPOTENCY_KEY, request.idempotencyKey);
404
411
  return payload;
405
412
  });
406
- fastify.addHook("onError", async (request) => {
413
+ fastify.addHook("onResponse", async (request) => {
414
+ if (request.idempotencyReplayed) return;
407
415
  const fullKey = request._idempotencyFullKey;
408
- if (fullKey) await store.unlock(fullKey, request.id);
416
+ if (!fullKey) return;
417
+ await store.unlock(fullKey, request.id);
409
418
  });
410
419
  fastify.addHook("onClose", async () => {
411
420
  await store.close?.();
@@ -421,4 +430,4 @@ var idempotencyPlugin_default = fp(idempotencyPlugin, {
421
430
  fastify: "5.x"
422
431
  });
423
432
  //#endregion
424
- export { MemoryIdempotencyStore, createIdempotencyResult, idempotencyPlugin_default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn };
433
+ export { MemoryIdempotencyStore, createIdempotencyResult, idempotencyPlugin_default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn, repositoryAsIdempotencyStore };