@classytic/arc 2.38.0 → 2.41.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 (196) hide show
  1. package/dist/{BaseController-CYaDvmOB.mjs → BaseController-DYJRZ0EJ.mjs} +114 -24
  2. package/dist/{EventTransport-Cx60_s-6.d.mts → EventTransport-CbUKDzZZ.d.mts} +33 -0
  3. package/dist/{EventTransport-Gq7JWbWw.mjs → EventTransport-n7qO5Cz3.mjs} +22 -2
  4. package/dist/{HookSystem-C0ikzqQf.mjs → HookSystem-CxvZBo77.mjs} +2 -2
  5. package/dist/{ResourceRegistry-DPcX--zk.mjs → ResourceRegistry-Cu7uTJp9.mjs} +2 -2
  6. package/dist/{actionPermissions-jlFZ1it4.mjs → actionPermissions-DPwPWPOV.mjs} +7 -6
  7. package/dist/audit/index.d.mts +2 -2
  8. package/dist/audit/index.mjs +10 -3
  9. package/dist/auth/audit.mjs +1 -1
  10. package/dist/auth/index.d.mts +2 -2
  11. package/dist/auth/index.mjs +12 -6
  12. package/dist/auth/redis-session.mjs +1 -1
  13. package/dist/{authorizationDecision-BP8GHtjc.mjs → authorizationDecision-2YY_Kaj1.mjs} +2 -2
  14. package/dist/{base-C-NL2fTi.d.mts → base-CxrV1LmG.d.mts} +10 -0
  15. package/dist/{buildHandler-DjXOqIvX.mjs → buildHandler-CUhjGHGZ.mjs} +4 -4
  16. package/dist/cache/index.d.mts +4 -2
  17. package/dist/cache/index.mjs +4 -4
  18. package/dist/{caching-a3ZEcJJs.mjs → caching-DsWDjIxx.mjs} +2 -2
  19. package/dist/cleanup/index.d.mts +2 -2
  20. package/dist/cleanup/index.mjs +21 -5
  21. package/dist/cli/commands/describe.d.mts +1 -1
  22. package/dist/cli/commands/describe.mjs +3 -3
  23. package/dist/cli/commands/docs.mjs +3 -3
  24. package/dist/cli/commands/doctor.mjs +1 -1
  25. package/dist/cli/commands/generate.mjs +2 -2
  26. package/dist/cli/commands/init.mjs +2 -2
  27. package/dist/cli/commands/introspect.mjs +2 -2
  28. package/dist/cli/index.mjs +3 -3
  29. package/dist/{constants-BONghE1Y.mjs → constants-BxLgNFF7.mjs} +1 -1
  30. package/dist/context/index.d.mts +34 -6
  31. package/dist/context/index.mjs +42 -24
  32. package/dist/core/index.d.mts +2 -2
  33. package/dist/core/index.mjs +7 -7
  34. package/dist/{core-8She7FgF.mjs → core-iNGqYerO.mjs} +3 -3
  35. package/dist/{createActionRouter-Bqi6Bhhx.mjs → createActionRouter-BBlbPs3k.mjs} +7 -7
  36. package/dist/{createAggregationRouter-BsYO7ktd.mjs → createAggregationRouter-Dv5SuK2x.mjs} +7 -7
  37. package/dist/createMcpServer-CbDWkZ1T.d.mts +48 -0
  38. package/dist/{defineAggregation-0KBvqEEC.mjs → defineAggregation-UyeurGkp.mjs} +1 -1
  39. package/dist/{defineResource-BMdyRelW.mjs → defineResource-DzGT-2LP.mjs} +20 -22
  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-C6woysGC.mjs → elevation-BF-vhGxY.mjs} +4 -4
  44. package/dist/encryption/index.mjs +3 -3
  45. package/dist/{errorHandler-DI5AN2b5.d.mts → errorHandler--4rSMMvq.d.mts} +7 -1
  46. package/dist/{errorHandler-BdmIWPba.mjs → errorHandler-CIMkCOWm.mjs} +3 -3
  47. package/dist/{errors-2ExmOgIJ.mjs → errors-C_YhELhe.mjs} +1 -1
  48. package/dist/{eventPlugin-DFvHa4Nf.d.mts → eventPlugin-B52abJ8W.d.mts} +1 -1
  49. package/dist/{eventPlugin-DJwpGMzx.mjs → eventPlugin-CmXaPWLv.mjs} +5 -5
  50. package/dist/events/index.d.mts +4 -4
  51. package/dist/events/index.mjs +5 -5
  52. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  53. package/dist/events/transports/redis-stream-entry.mjs +34 -24
  54. package/dist/events/transports/redis.d.mts +1 -1
  55. package/dist/events/transports/redis.mjs +11 -8
  56. package/dist/factory/index.d.mts +5 -5
  57. package/dist/factory/index.mjs +5 -5
  58. package/dist/{fastify-augmentation-jYA2oHyL.d.mts → fastify-augmentation-Cq2nHVaW.d.mts} +3 -3
  59. package/dist/{fields-Cha2QCKH.mjs → fields-BTyPLHK0.mjs} +1 -1
  60. package/dist/{generate-BUP4Oydg.mjs → generate-D_YmiCNf.mjs} +4 -3
  61. package/dist/{headers-DeXNifVj.mjs → headers-CNECDE6q.mjs} +2 -2
  62. package/dist/{health-Bh0N_FLk.mjs → health-CxQzbJmr.mjs} +10 -2
  63. package/dist/hooks/index.d.mts +1 -1
  64. package/dist/hooks/index.mjs +2 -2
  65. package/dist/idempotency/index.d.mts +1 -1
  66. package/dist/idempotency/index.mjs +4 -4
  67. package/dist/idempotency/redis.d.mts +1 -1
  68. package/dist/idempotency/redis.mjs +1 -1
  69. package/dist/{index-BtYoMip1.d.mts → index-BHdZD8zK.d.mts} +10 -2
  70. package/dist/{index-D2uIUSQk.d.mts → index-CFfLKMH4.d.mts} +2 -2
  71. package/dist/{index-eyg5WEcy.d.mts → index-CbJiYrfv.d.mts} +1 -1
  72. package/dist/{index-BRoDtjek.d.mts → index-D4u76eQW.d.mts} +3 -3
  73. package/dist/{index-B78WZom8.d.mts → index-DHbyPo5j.d.mts} +52 -12
  74. package/dist/index.d.mts +5 -5
  75. package/dist/index.mjs +11 -11
  76. package/dist/{init-Dg_Am2o_.mjs → init-Y2MlSN9n.mjs} +9 -9
  77. package/dist/integrations/event-gateway.d.mts +1 -1
  78. package/dist/integrations/event-gateway.mjs +2 -2
  79. package/dist/integrations/index.d.mts +2 -2
  80. package/dist/integrations/index.mjs +1 -1
  81. package/dist/integrations/jobs/index.d.mts +1 -1
  82. package/dist/integrations/jobs/index.mjs +2 -2
  83. package/dist/integrations/mcp/index.d.mts +4 -47
  84. package/dist/integrations/mcp/index.mjs +2 -2
  85. package/dist/integrations/mcp/testing.d.mts +22 -2
  86. package/dist/integrations/mcp/testing.mjs +55 -33
  87. package/dist/integrations/streamline.mjs +3 -3
  88. package/dist/integrations/webhooks.d.mts +2 -2
  89. package/dist/integrations/webhooks.mjs +3 -3
  90. package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
  91. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  92. package/dist/integrations/websocket-redis.mjs +1 -1
  93. package/dist/integrations/websocket.mjs +24 -6
  94. package/dist/{interface-DYPVscCU.mjs → interface-BIpgzbZl.mjs} +1 -1
  95. package/dist/{jsonBody-DVui0uJl.mjs → jsonBody-Cq9KYl2Z.mjs} +2 -2
  96. package/dist/{keys-BSuxGS-B.mjs → keys-Cmv4cBRa.mjs} +1 -1
  97. package/dist/{loadResources-CNd1DWnK.mjs → loadResources-CblF7xt2.mjs} +2 -2
  98. package/dist/{loadResourcesFromEntry-DNj8cpiY.mjs → loadResourcesFromEntry-99fIMC3p.mjs} +2 -2
  99. package/dist/logger/index.mjs +1 -1
  100. package/dist/{mcpPlugin-KWsezMx7.mjs → mcpPlugin-xF3Gw-HW.mjs} +75 -31
  101. package/dist/{memory-BiPVaCPg.mjs → memory-CpDZ9NPw.mjs} +2 -2
  102. package/dist/{metrics-CsWaIVTa.mjs → metrics-BaKnq8ag.mjs} +2 -2
  103. package/dist/middleware/index.d.mts +1 -1
  104. package/dist/middleware/index.mjs +3 -3
  105. package/dist/migrations/index.mjs +2 -2
  106. package/dist/{module-BQXHxMEk.mjs → module-BkQEesNQ.mjs} +1 -1
  107. package/dist/{multipartBody-u6VrS_iH.mjs → multipartBody-C92aJ8am.mjs} +1 -1
  108. package/dist/{openapi-Dxc8RLy_.mjs → openapi-C3LFRzPq.mjs} +6 -6
  109. package/dist/outbox-admin/index.d.mts +2 -2
  110. package/dist/outbox-admin/index.mjs +4 -4
  111. package/dist/{outbox-module-CQXu_O6o.d.mts → outbox-module-BHdUnuuK.d.mts} +15 -2
  112. package/dist/{outbox-module-DyIqA5vo.mjs → outbox-module-dsCyLxXH.mjs} +43 -21
  113. package/dist/permissions/index.d.mts +2 -2
  114. package/dist/permissions/index.mjs +5 -5
  115. package/dist/{permissions-H7Zcb82Z.mjs → permissions-DiHXy8Ck.mjs} +12 -6
  116. package/dist/{pipe-SunhEaZ6.mjs → pipe-CBVmUJ-j.mjs} +2 -2
  117. package/dist/pipeline/index.d.mts +1 -1
  118. package/dist/pipeline/index.mjs +2 -2
  119. package/dist/plugins/index.d.mts +7 -37
  120. package/dist/plugins/index.mjs +34 -22
  121. package/dist/plugins/response-cache.mjs +9 -2
  122. package/dist/plugins/tracing-entry.mjs +2 -2
  123. package/dist/{pluralize-Da7IT_qJ.mjs → pluralize-2_U6QZQg.mjs} +1 -1
  124. package/dist/{preloadResources-DlhH81py.mjs → preloadResources-Bbp4QJ8V.mjs} +82 -26
  125. package/dist/{preloadResources-BbBkNkqK.d.mts → preloadResources-CjS2OfpU.d.mts} +14 -9
  126. package/dist/presets/filesUpload.d.mts +2 -2
  127. package/dist/presets/filesUpload.mjs +5 -5
  128. package/dist/presets/index.d.mts +2 -2
  129. package/dist/presets/index.mjs +2 -2
  130. package/dist/presets/multiTenant.d.mts +38 -1
  131. package/dist/presets/multiTenant.mjs +24 -6
  132. package/dist/presets/search.d.mts +2 -2
  133. package/dist/presets/search.mjs +2 -2
  134. package/dist/{presets-CNyfaxGL.mjs → presets-BdlugYTc.mjs} +4 -4
  135. package/dist/{projection-DSfEoooA.mjs → projection-Uvgp-EEj.mjs} +2 -2
  136. package/dist/{queryCachePlugin-oTsmVyEQ.mjs → queryCachePlugin-B5F09bYr.mjs} +11 -5
  137. package/dist/rateLimitKey-CK-EU2L8.mjs +41 -0
  138. package/dist/{realtime-CPmjjgqd.mjs → realtime-FnnD0n3Z.mjs} +9 -9
  139. package/dist/{redis-CduhKsJi.d.mts → redis-Bzi1y3F9.d.mts} +10 -2
  140. package/dist/{redis-stream-eh0eOtjh.d.mts → redis-stream-BPKscsTB.d.mts} +12 -7
  141. package/dist/{registerRoute-XvcI_3sh.mjs → registerRoute-DRsSHPSM.mjs} +6 -6
  142. package/dist/registry/index.d.mts +2 -2
  143. package/dist/registry/index.mjs +3 -3
  144. package/dist/{registry-UFEJQN8R.mjs → registry-D3y8VEJ1.mjs} +2 -2
  145. package/dist/{renewingLease-DuRGVOMe.mjs → renewingLease-B3VYh5aU.mjs} +1 -1
  146. package/dist/{replyHelpers-BHebarf0.mjs → replyHelpers-DPHsdw-5.mjs} +2 -2
  147. package/dist/{requestContext-DpmntRZy.mjs → requestContext-cSVKnYPk.mjs} +1 -1
  148. package/dist/{rolldown-runtime-Add2L50I.mjs → rolldown-runtime-DjDuyglE.mjs} +1 -1
  149. package/dist/{runtimeCapabilities-D793Pxci.mjs → runtimeCapabilities-DSA_dH1_.mjs} +31 -5
  150. package/dist/{schedules-DthM8Qag.mjs → schedules-BktJb3Y3.mjs} +8 -4
  151. package/dist/{schemaConverter-Db-MMsH6.mjs → schemaConverter-BLCSmNbr.mjs} +1 -1
  152. package/dist/{schemaIR-CIXShch7.mjs → schemaIR-K16Bb5BX.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-D03sk17n.mjs → scope-jSxuBTFM.mjs} +4 -4
  157. package/dist/{simpleEqualityMatcher-B7qVjQBv.mjs → simpleEqualityMatcher-BiOnyflP.mjs} +1 -1
  158. package/dist/{sse-D4MYqkic.mjs → sse-CH7Slxc1.mjs} +5 -5
  159. package/dist/{sseStream-BuMNPR6r.mjs → sseStream-CN-Lh1Tu.mjs} +2 -2
  160. package/dist/{store-helpers-CIF4RG8s.mjs → store-helpers-9DX9-1j_.mjs} +1 -1
  161. package/dist/{streaming-DkpW1HwJ.mjs → streaming-dvppnxsr.mjs} +1 -1
  162. package/dist/{subscribe-helpers-CBZOi6pk.mjs → subscribe-helpers-AVYPgqTl.mjs} +2 -2
  163. package/dist/sync/changelog.mjs +1 -1
  164. package/dist/testing/cleanupStoreContract.mjs +1 -1
  165. package/dist/testing/index.d.mts +5 -5
  166. package/dist/testing/index.mjs +10 -10
  167. package/dist/testing/outboxStoreContract.mjs +14 -3
  168. package/dist/testing/storageContract.mjs +1 -1
  169. package/dist/{transactionContext-DMmvcfdo.mjs → transactionContext-DCWMI6hV.mjs} +1 -1
  170. package/dist/{typeGuards-DHTnEs5X.mjs → typeGuards-D7tHFdH6.mjs} +1 -1
  171. package/dist/types/index.d.mts +2 -2
  172. package/dist/types/index.mjs +1 -1
  173. package/dist/types/storage.mjs +1 -1
  174. package/dist/{types-BqLyyH0o.mjs → types-CDy3-zw1.mjs} +4 -4
  175. package/dist/{types-LQFwC37d.d.mts → types-CWQn9-IC.d.mts} +46 -1
  176. package/dist/{types-BRU-Kd3t.d.mts → types-DfVDvkdP.d.mts} +20 -2
  177. package/dist/usage/index.d.mts +58 -2
  178. package/dist/usage/index.mjs +0 -0
  179. package/dist/{userHelpers-CXySQFRg.mjs → userHelpers-DDMOZ_Pm.mjs} +1 -1
  180. package/dist/utils/index.d.mts +110 -8
  181. package/dist/utils/index.mjs +12 -12
  182. package/dist/{utils-BbGG8XNT.mjs → utils-B-zj_b__.mjs} +61 -12
  183. package/dist/{validate-CidSeUrx.mjs → validate-R0G_JrnK.mjs} +2 -2
  184. package/dist/{versioning-B843jOYX.d.mts → versioning-D6JIK8Tk.d.mts} +58 -2
  185. package/dist/{versioning-C2r0eOZ4.mjs → versioning-DuVWcu-F.mjs} +3 -3
  186. package/dist/workScope-BhsWde4k.mjs +41 -0
  187. package/package.json +574 -565
  188. package/skills/arc/SKILL.md +8 -4
  189. package/skills/arc/references/mcp.md +20 -0
  190. package/skills/arc/references/production.md +94 -9
  191. package/skills/arc/references/testing.md +0 -12
  192. package/skills/arc-code-review/references/anti-patterns.md +1 -1
  193. package/skills/arc-code-review/references/migration-recipes.md +0 -60
  194. package/skills/arc-code-review/references/mongokit-migration.md +0 -27
  195. package/skills/arc-module-publishing/references/package-manifest.md +3 -1
  196. package/dist/rateLimitKey-CewdR-m6.mjs +0 -15
@@ -1,15 +1,15 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-BONghE1Y.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-BxLgNFF7.mjs";
3
3
  import { arcLog } from "./logger/index.mjs";
4
- import { a as conjoinPolicyFilters } from "./authorizationDecision-BP8GHtjc.mjs";
5
- import { d as createDomainError, f as createError, i as NotFoundError, p as isArcError, r as ForbiddenError } from "./errors-2ExmOgIJ.mjs";
6
- import { n as getUserRoles, t as getUserId } from "./userHelpers-CXySQFRg.mjs";
7
- import { b as isElevated, c as getOrgId, n as PUBLIC_SCOPE, x as isMember } from "./types-BqLyyH0o.mjs";
8
- import { t as simpleEqualityMatcher } from "./simpleEqualityMatcher-B7qVjQBv.mjs";
9
- import { t as buildQueryKey } from "./keys-BSuxGS-B.mjs";
10
- import { t as transactionContext } from "./transactionContext-DMmvcfdo.mjs";
11
- import { A as scheduleBackground, E as ArcQueryParser } from "./utils-BbGG8XNT.mjs";
12
- import { a as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-Cha2QCKH.mjs";
4
+ import { a as conjoinPolicyFilters } from "./authorizationDecision-2YY_Kaj1.mjs";
5
+ import { d as createDomainError, f as createError, i as NotFoundError, p as isArcError, r as ForbiddenError, u as ValidationError } from "./errors-C_YhELhe.mjs";
6
+ import { n as getUserRoles, t as getUserId } from "./userHelpers-DDMOZ_Pm.mjs";
7
+ import { b as isElevated, c as getOrgId, n as PUBLIC_SCOPE, x as isMember } from "./types-CDy3-zw1.mjs";
8
+ import { t as simpleEqualityMatcher } from "./simpleEqualityMatcher-BiOnyflP.mjs";
9
+ import { t as buildQueryKey } from "./keys-Cmv4cBRa.mjs";
10
+ import { t as transactionContext } from "./transactionContext-DCWMI6hV.mjs";
11
+ import { D as ArcQueryParser, j as scheduleBackground } from "./utils-B-zj_b__.mjs";
12
+ import { a as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-BTyPLHK0.mjs";
13
13
  import { policyRecordToFilter } from "@classytic/repo-core/filter";
14
14
  import { retryingTransaction } from "@classytic/repo-core/repository";
15
15
  import { isProductionEnv } from "@classytic/primitives/environment";
@@ -179,6 +179,7 @@ var AccessControl = class {
179
179
  */
180
180
  checkOrgScope(item, arcContext) {
181
181
  if (!this.tenantField) return true;
182
+ if (arcContext?._crossTenantRead === true) return true;
182
183
  const scope = arcContext?._scope;
183
184
  const orgId = scope ? getOrgId(scope) : void 0;
184
185
  if (!item || !orgId) return true;
@@ -423,6 +424,11 @@ function buildTenantRepoOptions(req, tenantField, arcContext) {
423
424
  if (cached) return cached;
424
425
  const out = {};
425
426
  const scope = arcContext?._scope;
427
+ if (arcContext?._crossTenantRead === true) {
428
+ out.bypassTenant = true;
429
+ req._tenantRepoOptions = out;
430
+ return out;
431
+ }
426
432
  if (tenantField) {
427
433
  const orgId = scope ? getOrgId(scope) : void 0;
428
434
  if (orgId) out[tenantField] = orgId;
@@ -650,7 +656,7 @@ async function executeAfterHook(ctx, op, data, meta) {
650
656
  * Every mixin used to spell out its own narrowing cast inline:
651
657
  *
652
658
  * ```ts
653
- * const repo = this.repository as unknown as {
659
+ * const repo = this.repository as {
654
660
  * getTree?: (options?: unknown) => Promise<AnyRecord[]>;
655
661
  * };
656
662
  * ```
@@ -717,7 +723,7 @@ async function runDistinctVerb(repository, field, resolveScope) {
717
723
  const repo = withRepoFeatures(repository);
718
724
  if (typeof repo.distinct !== "function") throw createError(501, "_distinct is not supported: the resource's storage adapter does not implement repo.distinct()");
719
725
  const { filter, options } = resolveScope();
720
- return await repo.distinct(field, filter, options);
726
+ return { values: await repo.distinct(field, filter, options) };
721
727
  }
722
728
  /**
723
729
  * `?_exists=true` → `repo.exists(filter)`. Returns the RAW repo result —
@@ -789,6 +795,27 @@ function noteDeniedJoins(kind) {
789
795
  notedDeniedJoins = true;
790
796
  log.warn(`client requested ${kind} but no allowlist is configured — DENIED (2.24 default flip; it was allow-all). Set schemaOptions.query.allowedPopulate / allowedLookups to enable client-driven joins. (dev-only note, shown once)`);
791
797
  }
798
+ /**
799
+ * A `$lookup` reaches ANOTHER collection, and the tenant filter arc conjoins applies to the
800
+ * BASE one only. The joined side carries whatever predicate the lookup itself names and
801
+ * nothing more — no tenant scope, no soft-delete filter — so a resource that opens
802
+ * `allowedLookups` can return rows from a collection the caller could not query directly.
803
+ *
804
+ * Unreachable by default (2.24 denies `lookup=` with no allowlist) and no resource in this
805
+ * fleet configures one, so this is a WARNING at the moment a deployment first opts in
806
+ * rather than machinery for a door nobody has opened. Warned once per collection set: the
807
+ * decision is made when the allowlist is written, not per request.
808
+ *
809
+ * Scope the join with the lookup's own `where`, which mongokit compiles into a `$match`
810
+ * inside the lookup pipeline.
811
+ */
812
+ const notedUnscopedJoins = /* @__PURE__ */ new Set();
813
+ function noteUnscopedJoin(lookups) {
814
+ const key = lookups.map((l) => l.from).filter((f) => typeof f === "string").sort().join(",");
815
+ if (key.length === 0 || notedUnscopedJoins.has(key)) return;
816
+ notedUnscopedJoins.add(key);
817
+ log.warn(`client-driven $lookup on [${key}] — the JOINED side carries no tenant or soft-delete filter (arc scopes the base collection only). Narrow each lookup with its own \`where\`, or the join can surface rows the caller cannot query directly. (shown once)`);
818
+ }
792
819
  var QueryResolver = class {
793
820
  queryParser;
794
821
  maxLimit;
@@ -912,6 +939,12 @@ var QueryResolver = class {
912
939
  query = rest;
913
940
  }
914
941
  const parsed = this.queryParser.parse(query);
942
+ /**
943
+ * Client-supplied keys only, and BEFORE the policy conjunction below: trusted policy
944
+ * legitimately filters on hidden fields (`requireOwnership('userId')` emits exactly that),
945
+ * so checking the conjoined result would reject the framework's own restrictions.
946
+ */
947
+ this.assertNoBlockedReadKeys(parsed.filters, parsed.sort);
915
948
  const arcContext = meta ?? req.metadata;
916
949
  const limit = Math.min(Math.max(1, parsed.limit || this.defaultLimit), this.maxLimit);
917
950
  const page = parsed.after ? void 0 : parsed.page ? Math.max(1, parsed.page) : 1;
@@ -921,7 +954,7 @@ var QueryResolver = class {
921
954
  let filters = conjoinPolicyFilters(parsed.filters, policyFilters);
922
955
  const scope = arcContext?._scope;
923
956
  const orgId = scope ? getOrgId(scope) : void 0;
924
- if (this.tenantField && orgId && !policyFilters?.[this.tenantField]) filters = conjoinPolicyFilters(filters, { [this.tenantField]: orgId });
957
+ if (this.tenantField && orgId && !arcContext?._crossTenantRead && !policyFilters?.[this.tenantField]) filters = conjoinPolicyFilters(filters, { [this.tenantField]: orgId });
925
958
  const portableFilters = toRepositoryFilter(filters);
926
959
  return {
927
960
  page,
@@ -950,20 +983,14 @@ var QueryResolver = class {
950
983
  if (!select) return exclusion();
951
984
  if (typeof select === "object" && !Array.isArray(select)) {
952
985
  const sanitized = {};
953
- for (const [field, val] of Object.entries(select)) if (!blockedFields.includes(field)) sanitized[field] = val;
986
+ for (const [field, val] of Object.entries(select)) if (!this.isBlockedProjection(field, blockedFields)) sanitized[field] = val;
954
987
  return Object.keys(sanitized).length > 0 ? sanitized : exclusion();
955
988
  }
956
989
  if (Array.isArray(select)) {
957
- const sanitized = select.filter((f) => {
958
- const fieldName = f.replace(/^-/, "");
959
- return !blockedFields.includes(fieldName);
960
- });
990
+ const sanitized = select.filter((f) => !this.isBlockedProjection(f, blockedFields));
961
991
  return sanitized.length > 0 ? sanitized : exclusion();
962
992
  }
963
- const sanitized = select.split(/[\s,]+/).filter(Boolean).filter((f) => {
964
- const fieldName = f.replace(/^-/, "");
965
- return !blockedFields.includes(fieldName);
966
- });
993
+ const sanitized = select.split(/[\s,]+/).filter(Boolean).filter((f) => !this.isBlockedProjection(f, blockedFields));
967
994
  return sanitized.length > 0 ? sanitized.join(" ") : exclusion();
968
995
  }
969
996
  /** Sanitize populate fields */
@@ -1011,6 +1038,7 @@ var QueryResolver = class {
1011
1038
  if (!allowedLookups.includes(lookup.from)) return false;
1012
1039
  return true;
1013
1040
  });
1041
+ if (sanitized.length > 0) noteUnscopedJoin(sanitized);
1014
1042
  return sanitized.length > 0 ? sanitized : void 0;
1015
1043
  }
1016
1044
  /**
@@ -1023,6 +1051,68 @@ var QueryResolver = class {
1023
1051
  const blocked = collectReadBlockedFields(schemaOptions);
1024
1052
  return blocked ? Array.from(blocked) : [];
1025
1053
  }
1054
+ /**
1055
+ * Is this projection path blocked — by its own name, or by a hidden ANCESTOR?
1056
+ *
1057
+ * `hidden: { secret: true }` hides the object, and an exact-name match left its children
1058
+ * reachable: `?select=secret.value` projected the very field the rule exists to withhold.
1059
+ * A rule on a parent covers everything under it, which is what "hidden" already means to
1060
+ * whoever wrote it.
1061
+ */
1062
+ isBlockedProjection(path, blockedFields) {
1063
+ const field = path.replace(/^-/, "");
1064
+ return blockedFields.some((blocked) => field === blocked || field.startsWith(`${blocked}.`));
1065
+ }
1066
+ /**
1067
+ * Refuse a read that FILTERS or SORTS on a `hidden` field.
1068
+ *
1069
+ * `hidden` was enforced on the surfaces that RETURN a value — the response, `select=`,
1070
+ * `_distinct`, aggregations — but a filter key never returns the field, it INTERROGATES it:
1071
+ * `?passwordHash[like]=^ab` narrows the result set, so the row count answers a question about
1072
+ * a value the caller may not read. A resource with no `allowedFilterFields` was a blind
1073
+ * existence oracle over every hidden field it declared.
1074
+ *
1075
+ * Rejected rather than dropped, unlike `select`. Dropping a projection narrows what comes
1076
+ * back; dropping a filter WIDENS the rows, which is the silent-permissive shape this
1077
+ * framework fails loudly on everywhere else.
1078
+ *
1079
+ * Compound keys (`$or`, `$and`) are walked, because the offending key can be nested inside
1080
+ * one. A dotted path is blocked by its own name or by a hidden ancestor, so a rule on
1081
+ * `secret` also covers `secret.value`.
1082
+ */
1083
+ assertNoBlockedReadKeys(filters, sort) {
1084
+ const blocked = collectReadBlockedFields(this.schemaOptions);
1085
+ if (!blocked) return;
1086
+ const offenders = /* @__PURE__ */ new Set();
1087
+ const isBlocked = (key) => {
1088
+ if (blocked.has(key)) return true;
1089
+ const dot = key.indexOf(".");
1090
+ return dot > 0 && blocked.has(key.slice(0, dot));
1091
+ };
1092
+ const walk = (node, depth) => {
1093
+ if (depth > 8 || !node || typeof node !== "object") return;
1094
+ if (Array.isArray(node)) {
1095
+ for (const item of node) walk(item, depth + 1);
1096
+ return;
1097
+ }
1098
+ for (const [key, value] of Object.entries(node)) {
1099
+ if (key.startsWith("$")) {
1100
+ walk(value, depth + 1);
1101
+ continue;
1102
+ }
1103
+ if (isBlocked(key)) offenders.add(key);
1104
+ }
1105
+ };
1106
+ walk(filters, 0);
1107
+ const sortKeys = typeof sort === "string" ? sort.split(",").map((s) => s.trim().replace(/^-/, "")).filter(Boolean) : Object.keys(sort ?? {});
1108
+ for (const key of sortKeys) if (isBlocked(key)) offenders.add(key);
1109
+ if (offenders.size === 0) return;
1110
+ const fields = [...offenders];
1111
+ throw new ValidationError(`Cannot filter or sort on hidden field(s): ${fields.join(", ")}`, fields.map((field) => ({
1112
+ field,
1113
+ message: "Field is not readable"
1114
+ })));
1115
+ }
1026
1116
  };
1027
1117
  //#endregion
1028
1118
  //#region src/core/BaseCrudController.ts
@@ -1550,7 +1640,7 @@ var BaseCrudController = class {
1550
1640
  }
1551
1641
  async list(req) {
1552
1642
  const dispatch = this.dispatchResourceVerb(req);
1553
- if (dispatch) return dispatch;
1643
+ if (dispatch !== null) return dispatch;
1554
1644
  const options = this.queryResolver.resolve(req, this.meta(req));
1555
1645
  const cached = await this.withListCache(req, options);
1556
1646
  if (cached) return cached;
@@ -42,6 +42,30 @@ interface MemoryEventTransportOptions {
42
42
  * QUEUE, not of the work.
43
43
  */
44
44
  handlerDispatch?: "sequential" | "parallel";
45
+ /**
46
+ * Called when a published event matched ZERO subscribers.
47
+ *
48
+ * Silence is the default because a fire-and-forget bus legitimately carries
49
+ * events nobody wants. It stops being legitimate the moment an OUTBOX is
50
+ * wired: the relay acknowledges a row it delivered to nobody, so the numbers
51
+ * report success and the instruction the event carried was never carried
52
+ * out. A host that expects its events to be consumed passes this and finds
53
+ * out.
54
+ *
55
+ * Runs inside the publish's work scope, after handler resolution and before
56
+ * the (empty) dispatch. It must not throw — a reporter that fails must not
57
+ * decide whether an event was published.
58
+ *
59
+ * MEMORY TRANSPORT ONLY, and deliberately so — this is not an oversight to be
60
+ * fixed by adding it to the Redis transports. In-process, "no handler
61
+ * matched" IS "nobody consumed it": this process is the whole bus. On a
62
+ * broker, no LOCAL handler matched says nothing about consumption — another
63
+ * replica, another consumer group, or a consumer not yet started may own the
64
+ * event, and firing there would report a healthy fan-out as a fault every
65
+ * time work is routed away from this node. The broker's own pending/lag
66
+ * metrics answer that question; this one cannot.
67
+ */
68
+ onUnroutedEvent?: (event: DomainEvent) => void;
45
69
  /**
46
70
  * Cap on handlers running at once under `'parallel'`. Default: unlimited.
47
71
  *
@@ -69,8 +93,17 @@ declare class MemoryEventTransport implements EventTransport {
69
93
  private onHandlerError;
70
94
  private handlerDispatch;
71
95
  private handlerConcurrency;
96
+ private onUnroutedEvent;
72
97
  constructor(options?: MemoryEventTransportOptions);
98
+ /**
99
+ * One work scope per publish, around the WHOLE handler set — so every
100
+ * handler of this event, parallel or not, shares one `requestScopedCache()`
101
+ * and one correlation id. Inherits an active request scope (a live
102
+ * in-request dispatch keeps the request's reads); a relay- or job-driven
103
+ * publish gets its own.
104
+ */
73
105
  publish(event: DomainEvent): Promise<void>;
106
+ private dispatch;
74
107
  /**
75
108
  * Reference `publishMany` implementation — delegates to `publish()` in order.
76
109
  *
@@ -1,4 +1,5 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { r as workSeedFromEvent, t as runWorkScope } from "./workScope-BhsWde4k.mjs";
2
3
  import { createEvent, matchEventPattern } from "@classytic/primitives/events";
3
4
  //#region src/events/EventTransport.ts
4
5
  /**
@@ -81,17 +82,36 @@ var MemoryEventTransport = class {
81
82
  onHandlerError;
82
83
  handlerDispatch;
83
84
  handlerConcurrency;
85
+ onUnroutedEvent;
84
86
  constructor(options) {
85
87
  this.logger = options?.logger ?? console;
86
88
  this.onHandlerError = options?.onHandlerError ?? "log";
87
89
  this.handlerDispatch = options?.handlerDispatch ?? "sequential";
90
+ this.onUnroutedEvent = options?.onUnroutedEvent;
88
91
  const cap = options?.handlerConcurrency;
89
92
  if (cap !== void 0 && (!Number.isInteger(cap) || cap < 1)) throw new TypeError(`MemoryEventTransport: handlerConcurrency must be a positive integer, received ${String(cap)}`);
90
93
  this.handlerConcurrency = cap ?? Number.POSITIVE_INFINITY;
91
94
  }
92
- async publish(event) {
95
+ /**
96
+ * One work scope per publish, around the WHOLE handler set — so every
97
+ * handler of this event, parallel or not, shares one `requestScopedCache()`
98
+ * and one correlation id. Inherits an active request scope (a live
99
+ * in-request dispatch keeps the request's reads); a relay- or job-driven
100
+ * publish gets its own.
101
+ */
102
+ publish(event) {
103
+ return runWorkScope(workSeedFromEvent(event), () => this.dispatch(event));
104
+ }
105
+ async dispatch(event) {
93
106
  const allHandlers = /* @__PURE__ */ new Set();
94
107
  for (const [pattern, handlers] of this.handlers) if (matchEventPattern(pattern, event.type)) for (const h of handlers) allHandlers.add(h);
108
+ if (allHandlers.size === 0 && this.onUnroutedEvent) try {
109
+ this.onUnroutedEvent(event);
110
+ } catch (err) {
111
+ try {
112
+ this.logger.error(`[EventTransport] onUnroutedEvent threw for ${event.type}:`, err);
113
+ } catch {}
114
+ }
95
115
  const failures = [];
96
116
  const run = async (handler) => {
97
117
  try {
@@ -1,5 +1,5 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { t as buildRequestScopeProjection } from "./projection-DSfEoooA.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { t as buildRequestScopeProjection } from "./projection-Uvgp-EEj.mjs";
3
3
  //#region src/hooks/HookSystem.ts
4
4
  /**
5
5
  * Hook System
@@ -1,5 +1,5 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import "./constants-BONghE1Y.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import "./constants-BxLgNFF7.mjs";
3
3
  //#region src/registry/ResourceRegistry.ts
4
4
  /**
5
5
  * Resource Registry
@@ -1,9 +1,9 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { n as getUserRoles } from "./userHelpers-CXySQFRg.mjs";
3
- import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-BqLyyH0o.mjs";
4
- import { a as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-Cha2QCKH.mjs";
5
- import { t as buildRequestScopeProjection } from "./projection-DSfEoooA.mjs";
6
- import { t as executePipeline } from "./pipe-SunhEaZ6.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { n as getUserRoles } from "./userHelpers-DDMOZ_Pm.mjs";
3
+ import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-CDy3-zw1.mjs";
4
+ import { a as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-BTyPLHK0.mjs";
5
+ import { t as buildRequestScopeProjection } from "./projection-Uvgp-EEj.mjs";
6
+ import { t as executePipeline } from "./pipe-CBVmUJ-j.mjs";
7
7
  import { isPaginatedResult, toCanonicalList } from "@classytic/repo-core/pagination";
8
8
  //#region src/core/fastifyAdapter.ts
9
9
  /** Type guard for Mongoose-like documents with toObject() */
@@ -82,6 +82,7 @@ function createRequestContext(req) {
82
82
  arc: reqWithExtras.arc,
83
83
  _scope: rawScope,
84
84
  _ownershipCheck: reqWithExtras._ownershipCheck,
85
+ _crossTenantRead: reqWithExtras._crossTenantRead,
85
86
  _policyFilters: reqWithExtras._policyFilters ?? {},
86
87
  log: reqWithExtras.log
87
88
  },
@@ -1,6 +1,6 @@
1
1
 
2
- import { d as UserBase } from "../base-C-NL2fTi.mjs";
3
- import "../index-B78WZom8.mjs";
2
+ import { d as UserBase } from "../base-CxrV1LmG.mjs";
3
+ import "../index-DHbyPo5j.mjs";
4
4
  import { FastifyPluginAsync } from "fastify";
5
5
  import { RepositoryLike } from "@classytic/repo-core/adapter";
6
6
  //#region src/audit/stores/interface.d.ts
@@ -1,5 +1,6 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { t as transactionContext } from "../transactionContext-DMmvcfdo.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { t as transactionContext } from "../transactionContext-DCWMI6hV.mjs";
3
+ import { n as declareRuntimeCapability } from "../runtimeCapabilities-DSA_dH1_.mjs";
3
4
  import { and, anyOf, eq, gte, lt, lte } from "@classytic/repo-core/filter";
4
5
  import fp from "fastify-plugin";
5
6
  //#region src/audit/repository-audit-adapter.ts
@@ -187,7 +188,13 @@ const auditPlugin = async (fastify, opts = {}) => {
187
188
  const stores = [];
188
189
  if (repository) stores.push(repositoryAsAuditStore(repository, { sessionProvider: sessionProvider === false ? void 0 : sessionProvider }));
189
190
  stores.push(...customStores);
190
- if (stores.length === 0) stores.push(new MemoryAuditStore());
191
+ const memoryFallback = stores.length === 0;
192
+ if (memoryFallback) stores.push(new MemoryAuditStore());
193
+ declareRuntimeCapability(fastify, {
194
+ subsystem: "audit.store",
195
+ durability: memoryFallback ? "memory" : "shared",
196
+ detail: "default MemoryAuditStore: the trail is replica-local and lost on restart"
197
+ });
191
198
  async function logToStores(entry) {
192
199
  await Promise.all(stores.map((store) => store.log(entry)));
193
200
  }
@@ -1,4 +1,4 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
2
  //#region src/auth/audit.ts
3
3
  function compileGlobs(patterns) {
4
4
  if (patterns.length === 0) return () => false;
@@ -1,6 +1,6 @@
1
1
 
2
- import { l as PermissionCheck } from "../base-C-NL2fTi.mjs";
3
- import { Ft as AuthPluginOptions, Pt as AuthHelpers } from "../index-B78WZom8.mjs";
2
+ import { l as PermissionCheck } from "../base-CxrV1LmG.mjs";
3
+ import { Ft as AuthPluginOptions, Pt as AuthHelpers } from "../index-DHbyPo5j.mjs";
4
4
  import { t as ExternalOpenApiPaths } from "../externalPaths-CmGOxOtD.mjs";
5
5
  import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-qimlKV0P.mjs";
6
6
  import { FastifyPluginAsync, FastifyReply as FastifyReply$1, FastifyRequest } from "fastify";
@@ -1,9 +1,10 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { d as createDomainError, l as UnauthorizedError, p as isArcError, t as ArcError } from "../errors-2ExmOgIJ.mjs";
3
- import { n as requireSingleHeaderValue } from "../headers-DeXNifVj.mjs";
4
- import { n as getUserRoles, r as normalizeRoles } from "../userHelpers-CXySQFRg.mjs";
5
- import { o as toJsonSchema } from "../schemaConverter-Db-MMsH6.mjs";
6
- import { E as requireOrgRole, T as requireOrgMembership, k as requireTeamMembership } from "../permissions-H7Zcb82Z.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { d as createDomainError, l as UnauthorizedError, p as isArcError, t as ArcError } from "../errors-C_YhELhe.mjs";
3
+ import { n as requireSingleHeaderValue } from "../headers-CNECDE6q.mjs";
4
+ import { n as getUserRoles, r as normalizeRoles } from "../userHelpers-DDMOZ_Pm.mjs";
5
+ import { o as toJsonSchema } from "../schemaConverter-BLCSmNbr.mjs";
6
+ import { n as declareRuntimeCapability } from "../runtimeCapabilities-DSA_dH1_.mjs";
7
+ import { E as requireOrgRole, T as requireOrgMembership, k as requireTeamMembership } from "../permissions-DiHXy8Ck.mjs";
7
8
  import { isProductionEnv } from "@classytic/primitives/environment";
8
9
  import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
9
10
  import fp from "fastify-plugin";
@@ -1298,6 +1299,11 @@ function createSessionManager(options) {
1298
1299
  }
1299
1300
  };
1300
1301
  const sessionPlugin = async (fastify) => {
1302
+ declareRuntimeCapability(fastify, {
1303
+ subsystem: "auth.sessions",
1304
+ durability: store instanceof MemorySessionStore ? "memory" : "shared",
1305
+ detail: "MemorySessionStore: sessions exist on the replica that created them"
1306
+ });
1301
1307
  const authenticate = async (request, reply) => {
1302
1308
  const cookieHeader = request.headers.cookie;
1303
1309
  const signedValue = parseCookies(typeof cookieHeader === "string" ? cookieHeader : void 0).get(cookieName);
@@ -1,4 +1,4 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
2
  import sjson from "secure-json-parse";
3
3
  //#region src/auth/redis-session.ts
4
4
  /**
@@ -1,6 +1,6 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
2
  import { arcLog } from "./logger/index.mjs";
3
- import { t as ArcError } from "./errors-2ExmOgIJ.mjs";
3
+ import { t as ArcError } from "./errors-C_YhELhe.mjs";
4
4
  //#region src/permissions/filter-merge.ts
5
5
  /**
6
6
  * Policy-filter composition — logical AND, never last-writer-wins.
@@ -294,6 +294,16 @@ declare module "fastify" {
294
294
  document?: unknown;
295
295
  /** Ownership check context (field name + user field) */
296
296
  _ownershipCheck?: Record<string, unknown>;
297
+ /**
298
+ * This read is deliberately NOT scoped to a tenant — a marketplace listing.
299
+ *
300
+ * Set by `multiTenantPreset({ crossTenant: [...] })`. Tenancy is applied in four separate
301
+ * places for a read (the preset middleware, `QueryResolver`, `buildTenantRepoOptions` and the
302
+ * post-fetch `checkOrgScope`), each deriving the org from the scope on its own, so a decision
303
+ * made in one of them is silently re-made by the other three. This flag is how that decision
304
+ * travels. Never set from user input.
305
+ */
306
+ _crossTenantRead?: boolean;
297
307
  }
298
308
  }
299
309
  type AnyRecord = Record<string, unknown>;
@@ -1,7 +1,7 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { a as conjoinPolicyFilters } from "./authorizationDecision-BP8GHtjc.mjs";
3
- import { g as toRepositoryFilter } from "./BaseController-CYaDvmOB.mjs";
4
- import { i as compileAggRequest, t as adapterSupportsAggregate } from "./validate-CidSeUrx.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { a as conjoinPolicyFilters } from "./authorizationDecision-2YY_Kaj1.mjs";
3
+ import { g as toRepositoryFilter } from "./BaseController-DYJRZ0EJ.mjs";
4
+ import { i as compileAggRequest, t as adapterSupportsAggregate } from "./validate-R0G_JrnK.mjs";
5
5
  //#region src/core/aggregation/buildHandler.ts
6
6
  /**
7
7
  * Framework-agnostic aggregation execution. Runs safety guards,
@@ -180,13 +180,15 @@ declare class RedisCacheStore<TValue = unknown> implements CacheStore<TValue> {
180
180
  }
181
181
  /**
182
182
  * Minimal ioredis shape we depend on. We don't import ioredis itself so the
183
- * cache subpath stays peer-dep-free.
183
+ * cache subpath stays peer-dep-free. The `scan` tail is `unknown[]` so a real
184
+ * ioredis `Redis` (overloads with literal tokens + optional callback) is
185
+ * assignable — see the same note on the idempotency `IoredisLike`.
184
186
  */
185
187
  interface IoredisLike {
186
188
  get(key: string): Promise<string | null>;
187
189
  set(...args: unknown[]): Promise<string | null>;
188
190
  del(...keys: string[]): Promise<number>;
189
- scan(cursor: string | number, ...args: (string | number)[]): Promise<[string, string[]]>;
191
+ scan(cursor: string | number, ...args: unknown[]): Promise<[string, string[]]>;
190
192
  pipeline?(): {
191
193
  del(key: string): unknown;
192
194
  exec(): Promise<unknown>;
@@ -1,7 +1,7 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { i as versionKey, n as hashParams, r as tagVersionKey, t as buildQueryKey } from "../keys-BSuxGS-B.mjs";
3
- import { t as MemoryCacheStore } from "../memory-BiPVaCPg.mjs";
4
- import { r as QueryCache, t as queryCachePlugin } from "../queryCachePlugin-oTsmVyEQ.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { i as versionKey, n as hashParams, r as tagVersionKey, t as buildQueryKey } from "../keys-Cmv4cBRa.mjs";
3
+ import { t as MemoryCacheStore } from "../memory-CpDZ9NPw.mjs";
4
+ import { r as QueryCache, t as queryCachePlugin } from "../queryCachePlugin-B5F09bYr.mjs";
5
5
  //#region src/cache/redis.ts
6
6
  /**
7
7
  * Redis-backed cache store.
@@ -1,5 +1,5 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { t as __exportAll } from "./rolldown-runtime-Add2L50I.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { t as __exportAll } from "./rolldown-runtime-DjDuyglE.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/plugins/caching.ts
5
5
  var caching_exports = /* @__PURE__ */ __exportAll({
@@ -1,7 +1,7 @@
1
1
 
2
- import { L as RequestWithExtras } from "../index-B78WZom8.mjs";
2
+ import { L as RequestWithExtras } from "../index-DHbyPo5j.mjs";
3
3
  import { A as CleanupStepResult, B as VerificationResult, C as CleanupResult, D as CleanupRunStore, E as CleanupRunStatus, F as PurgeEvidence, I as PurgeResourceResult, L as PurgeStrategyKind, M as CleanupWriteFence, N as DEFAULT_CLEANUP_LIMITS, O as CleanupRunTransitionPatch, P as PurgeActor, R as PurgeVerificationSummary, S as CleanupRecipe, T as CleanupRunCreateResult, _ as CleanupPermissions, a as CleanupEvidenceStore, b as CleanupPlanItem, c as CleanupInput, d as CleanupLease, f as CleanupLimits, g as CleanupPermissionCheck, h as CleanupOutcomeStatus, i as CleanupContext, j as CleanupStepRunStatus, k as CleanupStepProgress, l as CleanupJob, m as CleanupManifest, n as CLEANUP_TERMINAL_STATUSES, o as CleanupExecutionContext, p as CleanupLogger, r as CleanupCancelRequest, s as CleanupFinalizationPayload, t as Availability, u as CleanupJobQueue, v as CleanupPlan, w as CleanupRun, x as CleanupProgressSummary, y as CleanupPlanDraft, z as VerificationCheck } from "../types-DVLddu2q.mjs";
4
- import { t as ArcModule } from "../types-BRU-Kd3t.mjs";
4
+ import { t as ArcModule } from "../types-DfVDvkdP.mjs";
5
5
  import { PurgeStrategyKind as PurgeStrategyKind$1 } from "@classytic/primitives/retention";
6
6
  import { canonicalJson } from "@classytic/primitives/canonical";
7
7
  import { CleanupStep } from "@classytic/repo-core/cleanup";
@@ -1,7 +1,7 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { g as getUserId } from "../types-BqLyyH0o.mjs";
3
- import { t as defineResource } from "../defineResource-BMdyRelW.mjs";
4
- import { t as defineModule } from "../module-BQXHxMEk.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { g as getUserId } from "../types-CDy3-zw1.mjs";
3
+ import { t as defineResource } from "../defineResource-DzGT-2LP.mjs";
4
+ import { t as defineModule } from "../module-BkQEesNQ.mjs";
5
5
  import { createPurgeEvidence } from "@classytic/primitives/retention";
6
6
  import { canonicalJson, sha256Hex } from "@classytic/primitives/canonical";
7
7
  //#region src/cleanup/errors.ts
@@ -723,9 +723,25 @@ function createCleanupService(deps) {
723
723
  const logger = deps.logger;
724
724
  const generateId = deps.generateId ?? (() => globalThis.crypto.randomUUID());
725
725
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
726
+ /**
727
+ * Undefined-valued keys are DROPPED, not spread.
728
+ *
729
+ * `limits` is `Partial<CleanupLimits>`, which permits an EXPLICIT `undefined`
730
+ * — and a plain spread lets that overwrite the default rather than fall back
731
+ * to it. Every limit is then read in a comparison, and `n > undefined` is
732
+ * always `false`, so `{ limits: { maxPlanItems: undefined } }` did not raise
733
+ * the cap: it silently REMOVED it, on a framework whose whole job is deleting
734
+ * data. Same for `maxReasonLength` / `maxParamDepth`, and `maxResults` turned
735
+ * `slice(0, undefined)` into "keep everything".
736
+ *
737
+ * A host reaches this by forwarding an optional config through
738
+ * (`limits: { maxPlanItems: cfg.planCap }`) — no cast, no obvious mistake.
739
+ * `permissions/presets.ts` already filters for exactly this reason; this is
740
+ * the same rule applied to the destructive path.
741
+ */
726
742
  const limits = {
727
743
  ...DEFAULT_CLEANUP_LIMITS,
728
- ...deps.limits
744
+ ...Object.fromEntries(Object.entries(deps.limits ?? {}).filter(([, v]) => v !== void 0))
729
745
  };
730
746
  const leaseMs = deps.leaseMs ?? 3e5;
731
747
  const progressThrottleMs = deps.progressThrottleMs ?? 0;
@@ -1,5 +1,5 @@
1
1
 
2
- import { Nt as RateLimitConfig, V as ResourceDefinition, et as RouteSchemaOptions } from "../../index-B78WZom8.mjs";
2
+ import { Nt as RateLimitConfig, V as ResourceDefinition, et as RouteSchemaOptions } from "../../index-DHbyPo5j.mjs";
3
3
  //#region src/cli/commands/describe.d.ts
4
4
  interface DescribedResource {
5
5
  name: string;
@@ -1,6 +1,6 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { t as CRUD_OPERATIONS } from "../../constants-BONghE1Y.mjs";
3
- import { n as stringifyMeasureMap } from "../../ResourceRegistry-DPcX--zk.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { t as CRUD_OPERATIONS } from "../../constants-BxLgNFF7.mjs";
3
+ import { n as stringifyMeasureMap } from "../../ResourceRegistry-Cu7uTJp9.mjs";
4
4
  import { resolve } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  //#region src/cli/commands/describe.ts
@@ -1,6 +1,6 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { t as buildOpenApiSpec } from "../../openapi-Dxc8RLy_.mjs";
3
- import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-DNj8cpiY.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { t as buildOpenApiSpec } from "../../openapi-C3LFRzPq.mjs";
3
+ import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-99fIMC3p.mjs";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { mkdirSync, writeFileSync } from "node:fs";
6
6
  //#region src/cli/commands/docs.ts
@@ -1,4 +1,4 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
2
  import { dirname, join, parse, resolve } from "node:path";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  //#region src/cli/commands/doctor.ts
@@ -1,3 +1,3 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { t as generate } from "../../generate-BUP4Oydg.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { t as generate } from "../../generate-D_YmiCNf.mjs";
3
3
  export { generate };
@@ -1,3 +1,3 @@
1
- /*! @classytic/arc v2.38.0 | MIT | https://classytic.com */
2
- import { t as init } from "../../init-Dg_Am2o_.mjs";
1
+ /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
+ import { t as init } from "../../init-Y2MlSN9n.mjs";
3
3
  export { init };