@classytic/arc 2.31.0 → 2.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +1 -1
  2. package/bin/arc.js +58 -17
  3. package/dist/{BaseController-De7GqHck.mjs → BaseController-CaQC5EXX.mjs} +298 -35
  4. package/dist/{EventTransport-2uVV34WU.d.mts → EventTransport-BEVP50ka.d.mts} +16 -0
  5. package/dist/{EventTransport-9mlTgqzE.mjs → EventTransport-tq4CEB-q.mjs} +39 -2
  6. package/dist/{HookSystem-BcmPyq5I.mjs → HookSystem-DqXkMXkB.mjs} +2 -2
  7. package/dist/{ResourceRegistry-qq0mn2s9.mjs → ResourceRegistry-DoALUlOO.mjs} +18 -15
  8. package/dist/{actionPermissions-BE5NwaQk.mjs → actionPermissions-CUeCmpUd.mjs} +1 -1
  9. package/dist/audit/index.d.mts +2 -2
  10. package/dist/audit/index.mjs +2 -2
  11. package/dist/auth/audit.mjs +1 -1
  12. package/dist/auth/index.d.mts +2 -2
  13. package/dist/auth/index.mjs +44 -11
  14. package/dist/auth/redis-session.mjs +1 -1
  15. package/dist/{authorizationDecision-ChvUQGG1.mjs → authorizationDecision-B3mP_QTy.mjs} +2 -2
  16. package/dist/{types-B7XeLEgK.d.mts → base-Csjc2oF-.d.mts} +74 -74
  17. package/dist/{buildHandler-CDrCJzmB.mjs → buildHandler-DLE9xX-V.mjs} +6 -5
  18. package/dist/cache/index.mjs +4 -4
  19. package/dist/{caching-BFHA6Scf.mjs → caching-BcGmvnHh.mjs} +2 -2
  20. package/dist/cleanup/index.d.mts +52 -18
  21. package/dist/cleanup/index.mjs +281 -32
  22. package/dist/cli/commands/describe.d.mts +1 -1
  23. package/dist/cli/commands/describe.mjs +4 -6
  24. package/dist/cli/commands/docs.mjs +3 -3
  25. package/dist/cli/commands/doctor.mjs +1 -1
  26. package/dist/cli/commands/generate.mjs +2 -2
  27. package/dist/cli/commands/init.mjs +2 -2
  28. package/dist/cli/commands/introspect.mjs +2 -2
  29. package/dist/cli/index.mjs +3 -3
  30. package/dist/{constants-D3Dv4LAV.mjs → constants-BxCoxmiN.mjs} +1 -1
  31. package/dist/context/index.mjs +3 -3
  32. package/dist/core/index.d.mts +2 -2
  33. package/dist/core/index.mjs +7 -7
  34. package/dist/{core-BGpIxINN.mjs → core-dxhcHhUD.mjs} +3 -3
  35. package/dist/{createActionRouter-CDXEdasl.mjs → createActionRouter-DVpoA0do.mjs} +14 -11
  36. package/dist/{createAggregationRouter-KrQDzx-k.mjs → createAggregationRouter-B_B48JZ-.mjs} +10 -8
  37. package/dist/{createApp-Df89BEni.mjs → createApp-CLGegqiG.mjs} +287 -62
  38. package/dist/{defineAggregation-DbPPS8RI.mjs → defineAggregation-Nv-DwfOf.mjs} +1 -1
  39. package/dist/{defineResource-Bv6EtN1x.mjs → defineResource-DR9vsUYy.mjs} +318 -35
  40. package/dist/discovery/index.mjs +1 -1
  41. package/dist/docs/index.d.mts +1 -1
  42. package/dist/docs/index.mjs +3 -3
  43. package/dist/{elevation-D2l1mh4g.mjs → elevation-B0tlY8y1.mjs} +4 -4
  44. package/dist/encryption/index.mjs +5 -4
  45. package/dist/{errorHandler-C5NeD6bT.mjs → errorHandler-C7xBqHsI.mjs} +42 -5
  46. package/dist/{errorHandler-CpYUJmPA.d.mts → errorHandler-DI5AN2b5.d.mts} +14 -3
  47. package/dist/{errors-Cj42AntO.mjs → errors-Cd_yy7aW.mjs} +1 -1
  48. package/dist/{eventPlugin-GF5nnBIq.d.mts → eventPlugin-DSNEZWfV.d.mts} +29 -1
  49. package/dist/{eventPlugin-CkffEihm.mjs → eventPlugin-Dar8qRaj.mjs} +68 -11
  50. package/dist/events/index.d.mts +18 -297
  51. package/dist/events/index.mjs +7 -758
  52. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  53. package/dist/events/transports/redis-stream-entry.mjs +14 -10
  54. package/dist/events/transports/redis.d.mts +1 -1
  55. package/dist/events/transports/redis.mjs +1 -1
  56. package/dist/factory/index.d.mts +20 -5
  57. package/dist/factory/index.mjs +131 -8
  58. package/dist/fastify-augmentation-DVsTAy6X.d.mts +179 -0
  59. package/dist/{fields-CB5Cuzk9.mjs → fields-CojeUkS4.mjs} +6 -11
  60. package/dist/{generate-EcA6ui4i.mjs → generate-DiB0nHPR.mjs} +2 -2
  61. package/dist/{headers-3zvE0rM3.mjs → headers-CveNO9gR.mjs} +2 -2
  62. package/dist/{health-DjZYjM7H.mjs → health-CasG2A28.mjs} +1 -1
  63. package/dist/hooks/index.d.mts +1 -1
  64. package/dist/hooks/index.mjs +2 -2
  65. package/dist/idempotency/index.d.mts +45 -1
  66. package/dist/idempotency/index.mjs +95 -11
  67. package/dist/idempotency/redis.mjs +1 -1
  68. package/dist/{index-CEJNZFtX.d.mts → index-B-DvUzsr.d.mts} +82 -3
  69. package/dist/{index-CpzxqePp.d.mts → index-BcR2bFZQ.d.mts} +707 -373
  70. package/dist/{index-Bc2ox8_U.d.mts → index-Brin0ODU.d.mts} +4 -4
  71. package/dist/{index-CFThn1K9.d.mts → index-C5NnMEKF.d.mts} +70 -4
  72. package/dist/{index-C-481Zf2.d.mts → index-ugr1tQlW.d.mts} +17 -1
  73. package/dist/index.d.mts +6 -6
  74. package/dist/index.mjs +11 -11
  75. package/dist/{init-BEiNZBsJ.mjs → init-mdJ-v4Xp.mjs} +24 -16
  76. package/dist/integrations/event-gateway.d.mts +1 -1
  77. package/dist/integrations/event-gateway.mjs +2 -2
  78. package/dist/integrations/index.d.mts +2 -2
  79. package/dist/integrations/index.mjs +1 -1
  80. package/dist/integrations/jobs/index.d.mts +1 -1
  81. package/dist/integrations/jobs/index.mjs +100 -98
  82. package/dist/integrations/mcp/index.d.mts +12 -3
  83. package/dist/integrations/mcp/index.mjs +2 -2
  84. package/dist/integrations/mcp/testing.d.mts +1 -1
  85. package/dist/integrations/mcp/testing.mjs +2 -2
  86. package/dist/integrations/streamline.mjs +3 -3
  87. package/dist/integrations/webhooks.d.mts +124 -1
  88. package/dist/integrations/webhooks.mjs +180 -2
  89. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  90. package/dist/integrations/websocket-redis.mjs +1 -1
  91. package/dist/integrations/websocket.mjs +38 -35
  92. package/dist/{interface-DYzM0q-u.mjs → interface-DznJrm9R.mjs} +1 -1
  93. package/dist/{jsonBody-BYbLDi5D.mjs → jsonBody-CfGH-GxO.mjs} +2 -2
  94. package/dist/{keys-C0_pa1tY.mjs → keys-WeKvY1dE.mjs} +1 -1
  95. package/dist/{loadResources-yawi9tAE.mjs → loadResources-Bj-aSsXi.mjs} +4 -3
  96. package/dist/{loadResourcesFromEntry-BAjYZonx.mjs → loadResourcesFromEntry-DwaxNcE2.mjs} +2 -2
  97. package/dist/logger/index.mjs +1 -1
  98. package/dist/{mcpPlugin-B_es0hiF.mjs → mcpPlugin-aJg5TqHi.mjs} +76 -36
  99. package/dist/{memory-DR6wpa9F.mjs → memory-qYSB7rMQ.mjs} +5 -5
  100. package/dist/{metrics-CUdw_wLB.mjs → metrics-D7uchLjN.mjs} +2 -2
  101. package/dist/middleware/index.d.mts +1 -1
  102. package/dist/middleware/index.mjs +3 -3
  103. package/dist/migrations/index.mjs +3 -3
  104. package/dist/{module-0pFJA6YW.mjs → module-CwKsgZXp.mjs} +1 -1
  105. package/dist/{multipartBody-wOgj_fnj.mjs → multipartBody-4w-S3-PU.mjs} +3 -3
  106. package/dist/{openapi-B2L4js1v.mjs → openapi-BN5cQaO2.mjs} +6 -6
  107. package/dist/outbox-admin/index.d.mts +85 -0
  108. package/dist/outbox-admin/index.mjs +115 -0
  109. package/dist/outbox-module-CXB79bvt.mjs +839 -0
  110. package/dist/outbox-module-D-VFq37T.d.mts +368 -0
  111. package/dist/permissions/index.d.mts +3 -3
  112. package/dist/permissions/index.mjs +6 -6
  113. package/dist/{permissions-L--QDAir.mjs → permissions-BAfwgBxc.mjs} +120 -8
  114. package/dist/{pipe-DRDNsI2T.mjs → pipe-BzzoEhHj.mjs} +3 -5
  115. package/dist/pipeline/index.d.mts +1 -1
  116. package/dist/pipeline/index.mjs +2 -2
  117. package/dist/{pipeline-UPQFatit.mjs → pipeline-q69DZzgG.mjs} +17 -15
  118. package/dist/plugins/index.d.mts +6 -6
  119. package/dist/plugins/index.mjs +16 -16
  120. package/dist/plugins/response-cache.mjs +2 -2
  121. package/dist/plugins/tracing-entry.mjs +2 -2
  122. package/dist/{pluralize-O0FZgESl.mjs → pluralize-B7xf8qZF.mjs} +1 -1
  123. package/dist/{preloadResources-Dz6BjzM6.mjs → preloadResources-BkyqeTDz.mjs} +1 -1
  124. package/dist/{preloadResources-CyoJMSiO.d.mts → preloadResources-g07ldtzh.d.mts} +30 -7
  125. package/dist/presets/filesUpload.d.mts +2 -2
  126. package/dist/presets/filesUpload.mjs +6 -6
  127. package/dist/presets/index.d.mts +2 -2
  128. package/dist/presets/index.mjs +2 -2
  129. package/dist/presets/multiTenant.d.mts +1 -1
  130. package/dist/presets/multiTenant.mjs +3 -3
  131. package/dist/presets/search.d.mts +2 -2
  132. package/dist/presets/search.mjs +2 -2
  133. package/dist/{presets-C4-g4_4P.mjs → presets-D-h0uWtP.mjs} +6 -5
  134. package/dist/projection-BKM0Ui-s.mjs +42 -0
  135. package/dist/{queryCachePlugin-BZr8v46E.mjs → queryCachePlugin-DhsZ0ZIc.mjs} +50 -8
  136. package/dist/{rateLimit-CMPY5Qms.mjs → rateLimit-D4-ASwsI.mjs} +2 -2
  137. package/dist/{rateLimitKey-Dn5JNmkj.mjs → rateLimitKey-XngqVNEV.mjs} +1 -1
  138. package/dist/{realtime-C5NoxWFY.mjs → realtime-CTkv3O59.mjs} +10 -10
  139. package/dist/{redis-stream-qN6A-RFv.d.mts → redis-stream-mwLWlfHG.d.mts} +1 -1
  140. package/dist/{registerRoute-CBN84fc7.mjs → registerRoute-DNnd3vkz.mjs} +10 -8
  141. package/dist/registry/index.d.mts +3 -3
  142. package/dist/registry/index.mjs +3 -3
  143. package/dist/{registry-4213U4zb.mjs → registry-CLxCR5LF.mjs} +2 -2
  144. package/dist/{renewingLease-Dll8mGgc.mjs → renewingLease-BnWtbA2U.mjs} +1 -1
  145. package/dist/{replyHelpers-DIiZKGUt.mjs → replyHelpers-BSBVCwL3.mjs} +2 -2
  146. package/dist/{repositoryFilter-f9eAfq_l.mjs → repositoryFilter-DKny7PDn.mjs} +1 -1
  147. package/dist/{requestContext-2TxXOJ7T.mjs → requestContext-uYE35JxI.mjs} +1 -1
  148. package/dist/{rolldown-runtime-CUsjNX9R.mjs → rolldown-runtime-DNg-KNCk.mjs} +1 -1
  149. package/dist/runtimeCapabilities-Dm88DaTW.mjs +49 -0
  150. package/dist/{schedules-DdtlICTf.mjs → schedules-CAcZgia0.mjs} +3 -3
  151. package/dist/{schemaConverter-D--wdJM-.mjs → schemaConverter-XH1RZRyu.mjs} +1 -1
  152. package/dist/{schemaIR-Bm2enbnh.mjs → schemaIR-CDvclAew.mjs} +2 -2
  153. package/dist/schemas/index.mjs +1 -1
  154. package/dist/scim/index.mjs +2 -2
  155. package/dist/scope/index.mjs +5 -5
  156. package/dist/{scope-DHx5Fous.mjs → scope-BaWcqBvr.mjs} +4 -4
  157. package/dist/{simpleEqualityMatcher-DjQJ7FKA.mjs → simpleEqualityMatcher-Se8VrsaP.mjs} +1 -1
  158. package/dist/{sse-CUTo9y0X.mjs → sse-Cf0jUHsh.mjs} +7 -6
  159. package/dist/{sseStream-B_5XcVVd.mjs → sseStream-Bvpi-UXQ.mjs} +2 -2
  160. package/dist/{store-helpers-D1MP_gRG.mjs → store-helpers-B88p99Z2.mjs} +1 -1
  161. package/dist/{streaming-PSZwi8JV.mjs → streaming-DsT1B5zJ.mjs} +1 -1
  162. package/dist/{subscribe-helpers-C0Tb2aUp.mjs → subscribe-helpers-CLIOHsOK.mjs} +2 -2
  163. package/dist/sync/changelog.mjs +1 -1
  164. package/dist/testing/cleanupStoreContract.d.mts +1 -1
  165. package/dist/testing/cleanupStoreContract.mjs +12 -6
  166. package/dist/testing/index.d.mts +52 -13
  167. package/dist/testing/index.mjs +110 -40
  168. package/dist/testing/outboxStoreContract.d.mts +11 -2
  169. package/dist/testing/outboxStoreContract.mjs +110 -21
  170. package/dist/testing/storageContract.mjs +11 -6
  171. package/dist/{transactionContext-dAfBVUrk.mjs → transactionContext-BYtWnwAS.mjs} +1 -1
  172. package/dist/{typeGuards-B0ryPo3I.mjs → typeGuards-jjKJDN13.mjs} +1 -1
  173. package/dist/types/index.d.mts +3 -3
  174. package/dist/types/index.mjs +1 -1
  175. package/dist/types/storage.mjs +1 -1
  176. package/dist/{types-CBgvkp8O.d.mts → types-BcTumAz_.d.mts} +128 -18
  177. package/dist/{types-CwcUQ610.d.mts → types-BfC83KsP.d.mts} +15 -1
  178. package/dist/{types-Cys0ko9Z.mjs → types-CoqIRrkY.mjs} +4 -4
  179. package/dist/{types-C4QE8qnm.d.mts → types-DVLddu2q.d.mts} +144 -1
  180. package/dist/usage/index.d.mts +1 -1
  181. package/dist/usage/index.mjs +0 -0
  182. package/dist/{userHelpers-ChYJ6CdK.mjs → userHelpers-CWF5pbZD.mjs} +1 -1
  183. package/dist/utils/index.d.mts +57 -5
  184. package/dist/utils/index.mjs +12 -11
  185. package/dist/{utils-vL8BrDE2.mjs → utils-h1V_sT-t.mjs} +38 -7
  186. package/dist/{validate-_KbifpHF.mjs → validate-CdQlz52F.mjs} +2 -2
  187. package/dist/{versioning-D_rhHfZm.d.mts → versioning-DEs-_hFi.d.mts} +1 -1
  188. package/dist/{versioning-CHDTor3P.mjs → versioning-rZbyLVD-.mjs} +4 -4
  189. package/package.json +31 -20
  190. package/skills/arc/SKILL.md +96 -4
  191. package/skills/arc/references/api-reference.md +19 -0
  192. package/skills/arc/references/production.md +2 -2
  193. package/dist/fastify-augmentation-zh0_AYHa.d.mts +0 -85
  194. package/dist/projection-BHw6xlOQ.mjs +0 -18
@@ -1,12 +1,43 @@
1
- /*! @classytic/arc v2.31.0 | MIT | https://classytic.com */
2
- import { n as requireSingleHeaderValue } from "../headers-3zvE0rM3.mjs";
3
- import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-D1MP_gRG.mjs";
4
- import { t as createIdempotencyResult } from "../interface-DYzM0q-u.mjs";
1
+ /*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
2
+ import { n as requireSingleHeaderValue } from "../headers-CveNO9gR.mjs";
3
+ import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-B88p99Z2.mjs";
4
+ import { t as createIdempotencyResult } from "../interface-DznJrm9R.mjs";
5
5
  import { and, eq, exists, gt, lt, or, startsWith } from "@classytic/repo-core/filter";
6
6
  import { createHash } from "node:crypto";
7
7
  import fp from "fastify-plugin";
8
8
  import { update } from "@classytic/repo-core/update";
9
9
  //#region src/idempotency/repository-idempotency-adapter.ts
10
+ /**
11
+ * The backing repository returned a document whose key does not match the
12
+ * key that was asked for — which means the repository is NOT applying the
13
+ * key filter. Concretely observed (2026-08-14, be-prod): a Mongoose schema
14
+ * with no declared paths (`new Schema({}, { _id: false })`) under a global
15
+ * `mongoose.set('strictQuery', true)` strips EVERY filter key as an
16
+ * "unknown path", so `getOne({ _id: key })` becomes `getOne({})` — one
17
+ * shared document then absorbs every idempotency key, and every request
18
+ * replays the most recent cached response ACROSS KEYS AND ACROSS USERS
19
+ * (a cancel response for order A was served as "success" for order B,
20
+ * which was never actually canceled).
21
+ *
22
+ * That failure mode is silent by construction — every read "succeeds" with
23
+ * a plausible document — so the adapter verifies the identity of every
24
+ * document it gets back and THROWS this instead of trusting it. Returning
25
+ * `undefined` (treat as miss) would be worse, not safer: the very next
26
+ * `tryLock`/`set` on the same broken repository would corrupt the shared
27
+ * document again.
28
+ *
29
+ * Fix on the host side: declare the key path on the schema (e.g.
30
+ * `_id: String`) and/or set `strictQuery: false` in the SCHEMA options so
31
+ * filters on it are never stripped — see `idempotencyPlugin`'s
32
+ * `repository` option docs.
33
+ */
34
+ var IdempotencyStoreMisconfiguredError = class extends Error {
35
+ code = "IDEMPOTENCY_STORE_MISCONFIGURED";
36
+ constructor(operation, expectedKey, actualKey) {
37
+ super(`idempotency repository store: ${operation} for key '${expectedKey}' returned a document keyed '${String(actualKey)}' — the repository is not applying the key filter (commonly: a pathless Mongoose schema under strictQuery:true strips the filter to {}). Declare the key path on the schema (e.g. _id: String) and set strictQuery: false in the schema options. Refusing to serve cross-key results.`);
38
+ this.name = "IdempotencyStoreMisconfiguredError";
39
+ }
40
+ };
10
41
  function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
11
42
  const missing = [];
12
43
  if (typeof repository.getOne !== "function") missing.push("getOne");
@@ -17,11 +48,23 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
17
48
  const idField = repository.idField ?? "_id";
18
49
  const isDuplicateKeyError = createIsDuplicateKeyError(repository);
19
50
  const safeGetOne = createSafeGetOne(repository);
51
+ /**
52
+ * Every document read back MUST carry the key it was fetched by. A
53
+ * mismatch means the repository dropped the filter (see
54
+ * {@link IdempotencyStoreMisconfiguredError}) — fail loud, never serve
55
+ * or mutate a cross-key document.
56
+ */
57
+ function assertKeyIdentity(operation, doc, expectedKey) {
58
+ const actual = doc[idField];
59
+ if (String(actual) !== expectedKey) throw new IdempotencyStoreMisconfiguredError(operation, expectedKey, actual);
60
+ }
20
61
  return {
21
62
  name: "repository",
22
63
  async get(key) {
23
64
  const doc = await safeGetOne(eq(idField, key));
24
- if (!doc?.result) return void 0;
65
+ if (!doc) return void 0;
66
+ assertKeyIdentity("get", doc, key);
67
+ if (!doc.result) return void 0;
25
68
  if (new Date(doc.expiresAt) < /* @__PURE__ */ new Date()) return void 0;
26
69
  return {
27
70
  key,
@@ -33,7 +76,7 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
33
76
  };
34
77
  },
35
78
  async set(key, result) {
36
- await r.findOneAndUpdate(eq(idField, key), update({
79
+ const doc = await r.findOneAndUpdate(eq(idField, key), update({
37
80
  set: {
38
81
  result: {
39
82
  statusCode: result.statusCode,
@@ -43,11 +86,13 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
43
86
  createdAt: result.createdAt,
44
87
  expiresAt: result.expiresAt
45
88
  },
89
+ setOnInsert: { [idField]: key },
46
90
  unset: ["lock"]
47
91
  }), {
48
92
  upsert: true,
49
93
  returnDocument: "after"
50
94
  });
95
+ if (doc) assertKeyIdentity("set", doc, key);
51
96
  },
52
97
  async tryLock(key, requestId, ttlMs) {
53
98
  const now = /* @__PURE__ */ new Date();
@@ -60,6 +105,7 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
60
105
  expiresAt: lockExpiresAt
61
106
  } },
62
107
  setOnInsert: {
108
+ [idField]: key,
63
109
  createdAt: now,
64
110
  expiresAt: docExpiresAt
65
111
  }
@@ -67,6 +113,7 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
67
113
  upsert: true,
68
114
  returnDocument: "after"
69
115
  });
116
+ if (doc) assertKeyIdentity("tryLock", doc, key);
70
117
  return doc !== null && doc !== void 0;
71
118
  } catch (err) {
72
119
  if (isDuplicateKeyError(err)) return false;
@@ -90,6 +137,7 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
90
137
  async findByPrefix(prefix) {
91
138
  const doc = await safeGetOne(and(startsWith(idField, prefix, "sensitive"), exists("result", true), gt("expiresAt", /* @__PURE__ */ new Date())));
92
139
  if (!doc?.result) return void 0;
140
+ if (!String(doc[idField] ?? "").startsWith(prefix)) throw new IdempotencyStoreMisconfiguredError("findByPrefix", prefix, doc[idField]);
93
141
  return {
94
142
  key: String(doc[idField] ?? prefix),
95
143
  statusCode: doc.result.statusCode,
@@ -138,8 +186,10 @@ var MemoryIdempotencyStore = class {
138
186
  }
139
187
  async tryLock(key, requestId, ttlMs) {
140
188
  const existing = this.locks.get(key);
141
- if (existing) if (/* @__PURE__ */ new Date() > existing.expiresAt) this.locks.delete(key);
142
- else return false;
189
+ if (existing) {
190
+ if (/* @__PURE__ */ new Date() > existing.expiresAt) this.locks.delete(key);
191
+ else return false;
192
+ }
143
193
  this.locks.set(key, {
144
194
  key,
145
195
  requestId,
@@ -285,8 +335,42 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
285
335
  "POST",
286
336
  "PUT",
287
337
  "PATCH"
288
- ], include, exclude, repository, store: explicitStore, retryAfterSeconds = 1, namespace } = opts;
338
+ ], include, exclude, repository, store: explicitStore, retryAfterSeconds = 1, namespace, selfCheck = true } = opts;
289
339
  const store = repository ? repositoryAsIdempotencyStore(repository, ttlMs) : explicitStore ?? new MemoryIdempotencyStore({ ttlMs });
340
+ /**
341
+ * Who owns the store's lifecycle — arc closes ONLY what arc built.
342
+ *
343
+ * A host-supplied `store` may outlive this app: two arc apps in one process
344
+ * (a documented topology) can share one, and `MemoryIdempotencyStore.close()`
345
+ * CLEARS its maps. Closing on the first app's shutdown therefore wiped the
346
+ * second app's live idempotency records — replayable requests silently
347
+ * became re-executable. A store handed in from outside is the host's to
348
+ * close. The same rule now applies to the events transport and the
349
+ * query-cache store, which had the identical defect.
350
+ *
351
+ * The `repository` path builds its own adapter here, so arc owns that
352
+ * wrapper — but the adapter has no `close`, and the underlying repository
353
+ * (host-owned) is never touched.
354
+ */
355
+ const ownsStore = !explicitStore;
356
+ if (enabled && selfCheck) {
357
+ const probeSuffix = createHash("sha256").update(`${process.pid}:${Date.now()}:${Math.random()}`).digest("hex").slice(0, 12);
358
+ const probeKeyA = `__arc_idempotency_selfcheck__:${probeSuffix}:a`;
359
+ const probeKeyB = `__arc_idempotency_selfcheck__:${probeSuffix}:b`;
360
+ /** Correctness verdicts rethrow; everything else degrades to a warning. */
361
+ class SelfCheckViolation extends Error {}
362
+ try {
363
+ await store.set(probeKeyA, createIdempotencyResult(299, { probe: true }, {}, 6e4));
364
+ const own = await store.get(probeKeyA);
365
+ if (!own || own.statusCode !== 299) fastify.log?.warn?.({ probe: "own-key" }, "idempotencyPlugin self-check: probe entry did not read back under its own key. This is usually the store being unreachable at boot (connection not ready), not a misconfiguration — continuing, with the adapter's per-read key-identity checks as the runtime backstop. If keyed requests later fail with IDEMPOTENCY_STORE_MISCONFIGURED, the store's key filter is being stripped (see IdempotencyStoreMisconfiguredError).");
366
+ else if (await store.get(probeKeyB)) throw new SelfCheckViolation("idempotencyPlugin self-check: a probe read under key B returned key A's entry — the store is ignoring the key filter, which in production replays cached responses across keys and users (observed cause: a pathless Mongoose schema under strictQuery:true strips every filter to {}). Declare the key path on the schema (e.g. _id: String) and set strictQuery: false in the schema options.");
367
+ } catch (err) {
368
+ if (err instanceof SelfCheckViolation || err?.code === "IDEMPOTENCY_STORE_MISCONFIGURED") throw err;
369
+ fastify.log?.warn?.({ err }, "idempotencyPlugin self-check could not complete (store unreachable at boot?) — continuing. The adapter's per-read key-identity checks remain the runtime backstop.");
370
+ } finally {
371
+ await store.deleteByPrefix(`__arc_idempotency_selfcheck__:${probeSuffix}:`).catch(() => {});
372
+ }
373
+ }
290
374
  if (!enabled) {
291
375
  fastify.decorate("idempotency", {
292
376
  invalidate: async () => {},
@@ -429,7 +513,7 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
429
513
  await store.unlock(fullKey, request.id);
430
514
  });
431
515
  fastify.addHook("onClose", async () => {
432
- await store.close?.();
516
+ if (ownsStore) await store.close?.();
433
517
  });
434
518
  fastify.log?.debug?.({
435
519
  headerName,
@@ -442,4 +526,4 @@ var idempotencyPlugin_default = fp(idempotencyPlugin, {
442
526
  fastify: "5.x"
443
527
  });
444
528
  //#endregion
445
- export { MemoryIdempotencyStore, createIdempotencyResult, idempotencyPlugin_default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn, repositoryAsIdempotencyStore };
529
+ export { IdempotencyStoreMisconfiguredError, MemoryIdempotencyStore, createIdempotencyResult, idempotencyPlugin_default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn, repositoryAsIdempotencyStore };
@@ -1,4 +1,4 @@
1
- /*! @classytic/arc v2.31.0 | MIT | https://classytic.com */
1
+ /*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
2
2
  //#region src/idempotency/stores/redis.ts
3
3
  var RedisIdempotencyStore = class {
4
4
  name = "redis";
@@ -1,7 +1,7 @@
1
1
 
2
- import { r as CacheStore, t as CacheLogger } from "./interface-BOk3YnTz.mjs";
3
2
  import { i as RequestScope, n as Mandate } from "./types-ChuShpCP.mjs";
4
- import { i as PermissionContext, r as PermissionCheck, t as AuthorizationDecision } from "./types-B7XeLEgK.mjs";
3
+ import { l as PermissionCheck, s as AuthorizationDecision, u as PermissionContext } from "./base-Csjc2oF-.mjs";
4
+ import { r as CacheStore, t as CacheLogger } from "./interface-BOk3YnTz.mjs";
5
5
  import { FastifyReply, FastifyRequest } from "fastify";
6
6
  //#region src/permissions/agent.d.ts
7
7
  /**
@@ -990,4 +990,83 @@ declare function fullPublic<TDoc = Record<string, unknown>>(overrides?: Permissi
990
990
  */
991
991
  declare function readOnly<TDoc = Record<string, unknown>>(overrides?: PermissionOverrides<TDoc>): ResourcePermissions<TDoc>;
992
992
  //#endregion
993
- export { evaluatePermissionDecision as $, explainAccess as A, anyOf as B, requireGrant as C, collectPublicSurface as D, PermissionRequirement as E, createDynamicPermissionMatrix as F, requireOwnership as G, denyAll as H, createOrgPermissions as I, roles as J, requirePlatformRole as K, allOf as L, DynamicPermissionMatrix as M, DynamicPermissionMatrixConfig as N, describePermission as O, PermissionEventBus as P, applyAuthorizationDecision as Q, allow as R, modeSatisfies as S, AccessExplanation as T, not as U, deny as V, requireAuth as W, scopeOf as X, when as Y, PermissionCheckReturn as Z, createRoleHierarchy as _, permissionMatrix as a, requireMandate as at, GrantResolution as b, publicReadAdminWrite as c, requireOrgMembership as d, normalizeToDecision as et, requireOrgRole as f, RoleHierarchy as g, requireTeamMembership as h, ownerWithAdminBypass as i, requireDPoP as it, ConnectEventsOptions as j, describePermissionMap as k, readOnly as l, requireServiceScope as m, authenticated as n, RequireMandateOptions as nt, presets_d_exports as o, requireScopeContext as p, requireRoles as q, fullPublic as r, requireAgentScope as rt, publicRead as s, adminOnly as t, RequireAgentScopeOptions as tt, requireOrgInScope as u, GRANT_MODES as v, conjoinPolicyFilters as w, RequireGrantOptions as x, GrantMode as y, allowPublic as z };
993
+ //#region src/permissions/matrix.d.ts
994
+ type PermissionEntryType = "public" | "authenticated" | "roles" | "scoped";
995
+ interface PermissionEntry {
996
+ type: PermissionEntryType;
997
+ roles: string[];
998
+ /**
999
+ * Present only for `scoped` — the caller-scope dimensions the gate requires
1000
+ * (e.g. `{ branchRole: "head_office" }`), for the client to resolve against
1001
+ * the viewer's active context.
1002
+ */
1003
+ scope?: Record<string, unknown>;
1004
+ }
1005
+ /**
1006
+ * The slice of arc's `ResourceRegistry` this reads.
1007
+ *
1008
+ * Structural on purpose: coupling to the full registry type would drag the
1009
+ * resource layer into the permissions layer, and this only ever reads names
1010
+ * and gates.
1011
+ */
1012
+ interface PermissionMatrixRegistry {
1013
+ getAll(): ReadonlyArray<{
1014
+ name: string;
1015
+ permissions?: Record<string, unknown>;
1016
+ actions?: ReadonlyArray<{
1017
+ name: string;
1018
+ permissions?: unknown;
1019
+ }>;
1020
+ aggregations?: ReadonlyArray<{
1021
+ name: string;
1022
+ permissions?: unknown;
1023
+ }>;
1024
+ /**
1025
+ * Custom routes. Only those declaring `capability` are published — see the
1026
+ * loop in `introspectRegistry` for why a route needs an explicit name.
1027
+ */
1028
+ customRoutes?: ReadonlyArray<{
1029
+ method: string;
1030
+ path: string;
1031
+ capability?: string;
1032
+ permissions?: unknown;
1033
+ }>;
1034
+ }>;
1035
+ }
1036
+ interface PermissionMatrixOptions {
1037
+ /** The live registry. Omit and the matrix carries only curated modules. */
1038
+ registry?: PermissionMatrixRegistry;
1039
+ /**
1040
+ * Curated, human-named groupings the client already organises around
1041
+ * (`products`, `finance`, …). Overlaid FIRST — the live registry wins on an
1042
+ * exact name collision, because the enforced gate beats an approximation.
1043
+ */
1044
+ curatedModules?: Record<string, unknown>;
1045
+ /**
1046
+ * Second curated layer, applied only where `curatedModules` has no entry —
1047
+ * typically per-resource policies behind the domain groupings.
1048
+ */
1049
+ fallbackModules?: Record<string, unknown>;
1050
+ /** Platform-level role names. */
1051
+ platformRoles?: readonly string[];
1052
+ /** Organization/branch-level role names. */
1053
+ orgRoles?: readonly string[];
1054
+ }
1055
+ interface PermissionMatrix {
1056
+ /** Every role name, both namespaces — so a role picker has one source. */
1057
+ roles: string[];
1058
+ roleGroups: {
1059
+ platform: string[];
1060
+ branch: string[];
1061
+ };
1062
+ modules: Record<string, Record<string, PermissionEntry>>;
1063
+ }
1064
+ /** Re-shape arc's typed requirement into the wire entry. */
1065
+ declare function introspectCheck(check: PermissionCheck): PermissionEntry;
1066
+ /** Introspect a permission module, flattening nested groups to `a.b`. */
1067
+ declare function introspectModule(mod: Record<string, unknown>): Record<string, PermissionEntry>;
1068
+ /** Every live resource's ACTUAL gates, keyed by arc resource name. */
1069
+ declare function introspectRegistry(registry: PermissionMatrixRegistry): Record<string, Record<string, PermissionEntry>>;
1070
+ declare function buildPermissionMatrix(options?: PermissionMatrixOptions): PermissionMatrix;
1071
+ //#endregion
1072
+ export { requireAuth as $, RequireGrantOptions as A, ConnectEventsOptions as B, requireServiceScope as C, GRANT_MODES as D, createRoleHierarchy as E, PermissionRequirement as F, createOrgPermissions as G, DynamicPermissionMatrixConfig as H, collectPublicSurface as I, allowPublic as J, allOf as K, describePermission as L, requireGrant as M, conjoinPolicyFilters as N, GrantMode as O, AccessExplanation as P, not as Q, describePermissionMap as R, requireScopeContext as S, RoleHierarchy as T, PermissionEventBus as U, DynamicPermissionMatrix as V, createDynamicPermissionMatrix as W, deny as X, anyOf as Y, denyAll as Z, publicReadAdminWrite as _, PermissionMatrixRegistry as a, scopeOf as at, requireOrgMembership as b, introspectModule as c, evaluatePermissionDecision as ct, authenticated as d, RequireMandateOptions as dt, requireOwnership as et, fullPublic as f, requireAgentScope as ft, publicRead as g, presets_d_exports as h, PermissionMatrixOptions as i, when as it, modeSatisfies as j, GrantResolution as k, introspectRegistry as l, normalizeToDecision as lt, permissionMatrix as m, requireMandate as mt, PermissionEntryType as n, requireRoles as nt, buildPermissionMatrix as o, PermissionCheckReturn as ot, ownerWithAdminBypass as p, requireDPoP as pt, allow as q, PermissionMatrix as r, roles as rt, introspectCheck as s, applyAuthorizationDecision as st, PermissionEntry as t, requirePlatformRole as tt, adminOnly as u, RequireAgentScopeOptions as ut, readOnly as v, requireTeamMembership as w, requireOrgRole as x, requireOrgInScope as y, explainAccess as z };