@classytic/arc 2.41.1 → 2.43.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 (230) hide show
  1. package/dist/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
  2. package/dist/{EventTransport-DSqD_Xsg.mjs → EventTransport-tzA7CAB4.mjs} +22 -17
  3. package/dist/{HookSystem-CMF94SEA.mjs → HookSystem-B6UaKRbF.mjs} +2 -2
  4. package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
  5. package/dist/{ResourceRegistry-MGpXoxHZ.mjs → ResourceRegistry-CuCARu2B.mjs} +44 -13
  6. package/dist/actionPermissions-BotX1YOa.mjs +776 -0
  7. package/dist/audit/index.d.mts +43 -5
  8. package/dist/audit/index.mjs +48 -26
  9. package/dist/auth/audit.mjs +1 -1
  10. package/dist/auth/index.d.mts +43 -13
  11. package/dist/auth/index.mjs +130 -155
  12. package/dist/auth/redis-session.mjs +1 -1
  13. package/dist/{authorizationDecision-DawKGoXb.mjs → authorizationDecision-DQYrkl5f.mjs} +7 -102
  14. package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
  15. package/dist/boundedMap-BULzfXYU.mjs +57 -0
  16. package/dist/{buildHandler-CvxKOc0m.mjs → buildHandler-CRQ8EcX5.mjs} +4 -4
  17. package/dist/cache/index.d.mts +22 -34
  18. package/dist/cache/index.mjs +4 -4
  19. package/dist/{caching-DbCBxlgw.mjs → caching-Cc2M1DwO.mjs} +3 -3
  20. package/dist/claimed-B10RTfkM.mjs +8 -0
  21. package/dist/cleanup/index.d.mts +6 -10
  22. package/dist/cleanup/index.mjs +27 -73
  23. package/dist/cli/commands/describe.d.mts +4 -2
  24. package/dist/cli/commands/describe.mjs +7 -4
  25. package/dist/cli/commands/docs.mjs +3 -3
  26. package/dist/cli/commands/doctor.mjs +1 -1
  27. package/dist/cli/commands/generate.mjs +2 -2
  28. package/dist/cli/commands/init.mjs +2 -2
  29. package/dist/cli/commands/introspect.mjs +2 -2
  30. package/dist/cli/index.mjs +3 -3
  31. package/dist/{constants-D5Ubn1fe.mjs → constants-Cu7Mx7_U.mjs} +4 -2
  32. package/dist/context/index.d.mts +2 -5
  33. package/dist/context/index.mjs +18 -38
  34. package/dist/core/index.d.mts +3 -3
  35. package/dist/core/index.mjs +8 -8
  36. package/dist/{core-5lazhva9.mjs → core-Cb0LjccA.mjs} +5 -4
  37. package/dist/{createActionRouter-DNqoaK81.mjs → createActionRouter-xjqonMRO.mjs} +87 -39
  38. package/dist/{createAggregationRouter-BcbQJf10.mjs → createAggregationRouter-zRYWw_5D.mjs} +11 -8
  39. package/dist/{preloadResources-f3boenVZ.mjs → createApp-D-953ZAq.mjs} +299 -390
  40. package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-C9cxS8zt.d.mts} +1 -1
  41. package/dist/{defineAggregation-D_eJjm5A.mjs → defineAggregation-Bm8RjMbq.mjs} +1 -1
  42. package/dist/{defineResource-DxkMTjRn.mjs → defineResource-DIC7JZGU.mjs} +118 -1024
  43. package/dist/discovery/index.mjs +1 -1
  44. package/dist/docs/index.d.mts +10 -4
  45. package/dist/docs/index.mjs +4 -4
  46. package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
  47. package/dist/{elevation-CkvG0LG2.mjs → elevation-DG8SrLN-.mjs} +41 -12
  48. package/dist/encryption/index.mjs +3 -3
  49. package/dist/{errorHandler-D1GHKGlV.mjs → errorHandler-BnfDviL1.mjs} +26 -5
  50. package/dist/{errors-BH7YU_9O.mjs → errors-CtrywMHP.mjs} +58 -23
  51. package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
  52. package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
  53. package/dist/{eventPlugin-zZY-unhr.mjs → eventPlugin-Q6BhzHzs.mjs} +82 -21
  54. package/dist/{subscribe-helpers-DatDkcuG.mjs → eventTypes-Cdhm6ZlV.mjs} +151 -17
  55. package/dist/events/index.d.mts +33 -8
  56. package/dist/events/index.mjs +49 -53
  57. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  58. package/dist/events/transports/redis-stream-entry.mjs +139 -157
  59. package/dist/events/transports/redis.d.mts +1 -1
  60. package/dist/events/transports/redis.mjs +2 -2
  61. package/dist/factory/index.d.mts +32 -17
  62. package/dist/factory/index.mjs +213 -32
  63. package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DhUJ-mvo.d.mts} +11 -25
  64. package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
  65. package/dist/{fields-BHrkoF5y.mjs → fields-ToPYpMqS.mjs} +1 -1
  66. package/dist/filter-merge-IpGywvym.mjs +99 -0
  67. package/dist/{generate-BLRf2m3y.mjs → generate-DRTkLlIl.mjs} +2 -2
  68. package/dist/{headers-BIXWKrzQ.mjs → headers-BbkIF6vA.mjs} +2 -2
  69. package/dist/health-D-Ft6JM6.mjs +22 -0
  70. package/dist/{health-ZRHeDHXH.mjs → health-DqbsG-_w.mjs} +115 -55
  71. package/dist/hooks/index.d.mts +2 -2
  72. package/dist/hooks/index.mjs +2 -2
  73. package/dist/idempotency/index.d.mts +31 -7
  74. package/dist/idempotency/index.mjs +155 -98
  75. package/dist/idempotency/redis.d.mts +1 -1
  76. package/dist/idempotency/redis.mjs +48 -11
  77. package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
  78. package/dist/{preloadResources-CjS2OfpU.d.mts → index-C8zVGYsk.d.mts} +37 -38
  79. package/dist/{index-D4u76eQW.d.mts → index-CRoPtISf.d.mts} +23 -73
  80. package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
  81. package/dist/index-JdbZAIsm.d.mts +510 -0
  82. package/dist/{index-DHbyPo5j.d.mts → index-mqaqZNpe.d.mts} +253 -254
  83. package/dist/index.d.mts +8 -8
  84. package/dist/index.mjs +11 -11
  85. package/dist/{init-CpLkjU18.mjs → init-B_oUYDXI.mjs} +1 -1
  86. package/dist/integrations/event-gateway.d.mts +8 -3
  87. package/dist/integrations/event-gateway.mjs +4 -4
  88. package/dist/integrations/index.d.mts +3 -3
  89. package/dist/integrations/index.mjs +1 -1
  90. package/dist/integrations/jobs/index.d.mts +1 -1
  91. package/dist/integrations/jobs/index.mjs +18 -5
  92. package/dist/integrations/mcp/index.d.mts +72 -59
  93. package/dist/integrations/mcp/index.mjs +19 -4
  94. package/dist/integrations/mcp/testing.d.mts +2 -2
  95. package/dist/integrations/mcp/testing.mjs +2 -2
  96. package/dist/integrations/streamline.d.mts +78 -136
  97. package/dist/integrations/streamline.mjs +116 -54
  98. package/dist/integrations/webhooks.d.mts +60 -120
  99. package/dist/integrations/webhooks.mjs +36 -80
  100. package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
  101. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  102. package/dist/integrations/websocket-redis.d.mts +1 -1
  103. package/dist/integrations/websocket-redis.mjs +1 -1
  104. package/dist/integrations/websocket.d.mts +3 -3
  105. package/dist/integrations/websocket.mjs +2 -2
  106. package/dist/{interface-DEWNOMza.d.mts → interface-C0395yUN.d.mts} +13 -1
  107. package/dist/{jsonBody-ELR9lfeU.mjs → jsonBody-B4YVGcql.mjs} +2 -2
  108. package/dist/{keys-cpZCtcIE.mjs → keys-B2KtgMLJ.mjs} +7 -18
  109. package/dist/{loadResources-BPfWiI5s.mjs → loadResources-Dn4lalw2.mjs} +3 -3
  110. package/dist/{loadResourcesFromEntry-ixtfL3J2.mjs → loadResourcesFromEntry-3sLYbuse.mjs} +2 -2
  111. package/dist/logger/index.d.mts +1 -8
  112. package/dist/logger/index.mjs +3 -153
  113. package/dist/logger-DXjfIqjV.mjs +160 -0
  114. package/dist/{mcpPlugin-BX5PujI0.mjs → mcpPlugin-C08HalN5.mjs} +433 -170
  115. package/dist/{memory-Iotz0cCx.mjs → memory-9Cp7611C.mjs} +36 -34
  116. package/dist/{metrics-NMsmpRmY.mjs → metrics-CWI11eRu.mjs} +39 -72
  117. package/dist/middleware/index.d.mts +2 -2
  118. package/dist/middleware/index.mjs +3 -3
  119. package/dist/migrations/index.mjs +2 -2
  120. package/dist/{module-DpLJG2_N.mjs → module-BSvOQC7E.mjs} +8 -8
  121. package/dist/multiTenant-cY0FkDiA.d.mts +104 -0
  122. package/dist/{multipartBody-DtRWM6_U.mjs → multipartBody-CXM2a-Dy.mjs} +46 -7
  123. package/dist/{openapi-CmFf3yq_.mjs → openapi-DT_G4VhY.mjs} +48 -10
  124. package/dist/outbox-admin/index.d.mts +3 -3
  125. package/dist/outbox-admin/index.mjs +6 -18
  126. package/dist/{outbox-module-D2z5WkDY.mjs → outbox-module-Bjgzl6tg.mjs} +203 -33
  127. package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
  128. package/dist/permissions/index.d.mts +5 -4
  129. package/dist/permissions/index.mjs +8 -6
  130. package/dist/{permissions-D8TBH44E.mjs → permissions-Cm0uYHvd.mjs} +149 -33
  131. package/dist/{pipe-FMWNheSO.mjs → pipe-BqeGT6UO.mjs} +2 -2
  132. package/dist/pipeline/index.d.mts +2 -2
  133. package/dist/pipeline/index.mjs +2 -2
  134. package/dist/plan-FVR6LP59.mjs +565 -0
  135. package/dist/{BaseController-CYEwjyjN.mjs → plugin-BccET1tL.mjs} +1256 -345
  136. package/dist/plugins/index.d.mts +14 -11
  137. package/dist/plugins/index.mjs +44 -22
  138. package/dist/plugins/response-cache.mjs +8 -14
  139. package/dist/plugins/tracing-entry.mjs +2 -2
  140. package/dist/{pluralize-CXWZIzTv.mjs → pluralize-CVIOcgMF.mjs} +1 -1
  141. package/dist/presets/filesUpload.d.mts +2 -2
  142. package/dist/presets/filesUpload.mjs +5 -5
  143. package/dist/presets/index.d.mts +26 -31
  144. package/dist/presets/index.mjs +4 -4
  145. package/dist/presets/multiTenant.d.mts +2 -107
  146. package/dist/presets/multiTenant.mjs +22 -162
  147. package/dist/presets/search.d.mts +2 -2
  148. package/dist/presets/search.mjs +2 -2
  149. package/dist/{presets-ZJSPgmT2.mjs → presets-Cpvj6fGO.mjs} +99 -107
  150. package/dist/{projection-BzIgzoKZ.mjs → projection-Ce4aQhQk.mjs} +2 -2
  151. package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
  152. package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
  153. package/dist/{queryCachePlugin-DZHPOm9p.mjs → queryCachePlugin-CFWNH8aT.mjs} +12 -14
  154. package/dist/{rateLimitKey-BvOvcBDi.mjs → rateLimitKey-C4O-kPm0.mjs} +1 -1
  155. package/dist/{realtime-8tIQWZz4.mjs → realtime-DmPE9S54.mjs} +25 -21
  156. package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
  157. package/dist/{redis-Bzi1y3F9.d.mts → redis-vLvtcGy_.d.mts} +13 -24
  158. package/dist/redisShapes-CQksY67q.d.mts +55 -0
  159. package/dist/registry/index.d.mts +9 -4
  160. package/dist/registry/index.mjs +4 -4
  161. package/dist/{registry-DNkgGQcK.mjs → registry-Bcz9G1bh.mjs} +10 -8
  162. package/dist/{renewingLease-yF8BdW4b.mjs → renewingLease-qODa7KJQ.mjs} +1 -1
  163. package/dist/{replyHelpers-CNdvMfM4.mjs → replyHelpers-CG_VtVgS.mjs} +2 -2
  164. package/dist/{requestContext-cDkAUr-P.mjs → requestContext-Dsu9P1bV.mjs} +1 -1
  165. package/dist/requirement-CbiLdQ1n.d.mts +22 -0
  166. package/dist/requirement-KDdoRcOP.mjs +35 -0
  167. package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
  168. package/dist/resourceRateLimit-dSjVr1pk.mjs +180 -0
  169. package/dist/{rolldown-runtime-eysyzFAF.mjs → rolldown-runtime-D1w9Y1VJ.mjs} +1 -1
  170. package/dist/routeValidator-D36SW8qt.mjs +70 -0
  171. package/dist/{runtimeCapabilities-CkhS6jn6.mjs → runtimeCapabilities-0tccjRlj.mjs} +1 -1
  172. package/dist/{schedules-j6uXXAeJ.mjs → schedules-C-OpmHBq.mjs} +46 -37
  173. package/dist/{schemaConverter-BVA7cFRG.mjs → schemaConverter-BORek3O-.mjs} +2 -2
  174. package/dist/{schemaIR-CTvPDoPB.mjs → schemaIR-C7Vbp71I.mjs} +2 -2
  175. package/dist/schemas/index.d.mts +10 -0
  176. package/dist/schemas/index.mjs +8 -3
  177. package/dist/scim/index.mjs +2 -2
  178. package/dist/scope/index.d.mts +2 -2
  179. package/dist/scope/index.mjs +6 -6
  180. package/dist/{scope-BUi1xV1I.mjs → scope-DQthe88b.mjs} +4 -4
  181. package/dist/shutdown-MqA81z_P.mjs +59 -0
  182. package/dist/{simpleEqualityMatcher-C9bzq2gO.mjs → simpleEqualityMatcher-auW8QjXN.mjs} +1 -1
  183. package/dist/{sse-BeqVKQKu.mjs → sse-CYzQBVFe.mjs} +11 -15
  184. package/dist/{sseStream-Ppzo-WUH.mjs → sseStream-B5_Afiqn.mjs} +2 -2
  185. package/dist/{store-helpers-dW71XVWL.mjs → store-helpers-BOzk-CuX.mjs} +1 -1
  186. package/dist/{streaming-IcRFjWQU.mjs → streaming-DjTQhZrN.mjs} +1 -1
  187. package/dist/subscriberIdentity-CW39uA8G.mjs +28 -0
  188. package/dist/tenancy--ed9eH_z.mjs +43 -0
  189. package/dist/tenantPreparation-BKon6Yox.mjs +67 -0
  190. package/dist/testing/cleanupStoreContract.mjs +1 -1
  191. package/dist/testing/index.d.mts +193 -175
  192. package/dist/testing/index.mjs +286 -212
  193. package/dist/testing/outboxStoreContract.mjs +137 -2
  194. package/dist/testing/requiredPorts.d.mts +17 -0
  195. package/dist/testing/requiredPorts.mjs +65 -0
  196. package/dist/testing/storageContract.mjs +1 -1
  197. package/dist/timing-qczZ0YC-.mjs +46 -0
  198. package/dist/{transactionContext-CPbdlnVw.mjs → transactionContext-DjFkrNG6.mjs} +1 -1
  199. package/dist/{typeGuards-BLjPgVBm.mjs → typeGuards-CySF12G-.mjs} +1 -1
  200. package/dist/types/index.d.mts +4 -4
  201. package/dist/types/index.mjs +1 -1
  202. package/dist/types/storage.mjs +1 -1
  203. package/dist/{types-CWQn9-IC.d.mts → types-B4Aq4Sdj.d.mts} +22 -9
  204. package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
  205. package/dist/{types-CgenB1gv.mjs → types-Bk9NOgOC.mjs} +4 -4
  206. package/dist/usage/index.d.mts +2 -3
  207. package/dist/usage/index.mjs +5 -5
  208. package/dist/{userHelpers-ChRvkuSZ.mjs → userHelpers-aklawRaK.mjs} +1 -1
  209. package/dist/utils/index.d.mts +84 -223
  210. package/dist/utils/index.mjs +13 -12
  211. package/dist/{utils-CxcctrAh.mjs → utils-ec1oTo1n.mjs} +315 -404
  212. package/dist/{validate-DrYWnOzI.mjs → validate-9yzHC1e2.mjs} +6 -14
  213. package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
  214. package/dist/{versioning-DKkT4hNJ.mjs → versioning-fhhWHV74.mjs} +4 -4
  215. package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
  216. package/dist/{workScope-CxKqoIFn.mjs → workScope-zpPyge4t.mjs} +2 -2
  217. package/package.json +16 -12
  218. package/skills/arc/SKILL.md +13 -4
  219. package/skills/arc/references/auth.md +8 -5
  220. package/skills/arc/references/integrations.md +1 -1
  221. package/skills/arc/references/mcp.md +8 -6
  222. package/skills/arc/references/multi-tenancy.md +19 -14
  223. package/skills/arc/references/production.md +27 -10
  224. package/dist/actionPermissions-BoPGQ5-4.mjs +0 -350
  225. package/dist/index-CFfLKMH4.d.mts +0 -290
  226. package/dist/interface-YzIbF7Ii.mjs +0 -17
  227. package/dist/redis-stream-BPKscsTB.d.mts +0 -260
  228. package/dist/registerRoute-Bsnk6NiZ.mjs +0 -364
  229. package/dist/sync/changelog.d.mts +0 -3
  230. package/dist/sync/changelog.mjs +0 -3
@@ -1,5 +1,6 @@
1
- /*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
2
- import { t as __exportAll } from "./rolldown-runtime-eysyzFAF.mjs";
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ import { t as __exportAll } from "./rolldown-runtime-D1w9Y1VJ.mjs";
3
+ import { _ as pathTouchesBlocked } from "./plugin-BccET1tL.mjs";
3
4
  //#region src/core/aggregation/normalize.ts
4
5
  /**
5
6
  * Framework cap on URL-driven aggregation limits when the host opts
@@ -180,11 +181,7 @@ function buildCacheOptions(config) {
180
181
  function buildExecutionHints(config) {
181
182
  const hints = {};
182
183
  if (typeof config.timeout === "number" && config.timeout > 0) hints.maxTimeMs = config.timeout;
183
- if (config.indexHint && config.indexHint.leadingKeys.length > 0) {
184
- const hintObj = {};
185
- for (const key of config.indexHint.leadingKeys) hintObj[key] = 1;
186
- hints.indexHint = hintObj;
187
- }
184
+ if (config.indexHint && config.indexHint.leadingKeys.length > 0) hints.indexHint = { leadingKeys: [...config.indexHint.leadingKeys] };
188
185
  return Object.keys(hints).length > 0 ? hints : void 0;
189
186
  }
190
187
  //#endregion
@@ -445,13 +442,8 @@ function validateFieldReferences(input) {
445
442
  function assertFieldAllowed(context, ref, input) {
446
443
  const { resourceName, aggregationName, lookupAliases, blockedFields } = input;
447
444
  const dot = ref.indexOf(".");
448
- if (dot > 0) {
449
- const alias = ref.slice(0, dot);
450
- if (lookupAliases.has(alias)) return;
451
- if (blockedFields.has(alias)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" references field "${ref}" in ${context} whose root "${alias}" is blocked from aggregation (\`hidden: true\` or \`aggregable: false\` in schemaOptions.fieldRules). Aggregating hidden fields would leak cardinality information.`);
452
- return;
453
- }
454
- if (blockedFields.has(ref)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" references field "${ref}" in ${context}, but the field is blocked from aggregation (\`hidden: true\` or \`aggregable: false\` in schemaOptions.fieldRules). Aggregating hidden fields would leak cardinality information.`);
445
+ if (dot > 0 && lookupAliases.has(ref.slice(0, dot))) return;
446
+ if (pathTouchesBlocked(ref, blockedFields, { descendants: true })) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" references field "${ref}" in ${context}, but the field is blocked from aggregation (\`hidden: true\` or \`aggregable: false\` in schemaOptions.fieldRules). Aggregating hidden fields would leak cardinality information.`);
455
447
  }
456
448
  //#endregion
457
449
  export { compileAggRequest as i, validateAggregations as n, validate_exports as r, adapterSupportsAggregate as t };
@@ -1,5 +1,5 @@
1
1
 
2
- import { n as DomainEvent } from "./EventTransport-CbUKDzZZ.mjs";
2
+ import { n as DomainEvent } from "./EventTransport-CiicKz2Z.mjs";
3
3
  import { FastifyPluginAsync, FastifyRequest } from "fastify";
4
4
  //#region src/plugins/gracefulShutdown.d.ts
5
5
  interface GracefulShutdownOptions {
@@ -46,7 +46,6 @@ interface ShutdownState {
46
46
  /** When draining began. */
47
47
  readonly since?: Date;
48
48
  }
49
- declare const gracefulShutdownPlugin: FastifyPluginAsync<GracefulShutdownOptions>;
50
49
  declare module "fastify" {
51
50
  interface FastifyInstance {
52
51
  /** Trigger graceful shutdown manually */
@@ -82,7 +81,6 @@ interface CachingOptions {
82
81
  /** Custom cache rules per path prefix */
83
82
  rules?: CachingRule[];
84
83
  }
85
- declare const cachingPlugin: FastifyPluginAsync<CachingOptions>;
86
84
  declare const _default$4: FastifyPluginAsync<CachingOptions>;
87
85
  //#endregion
88
86
  //#region src/plugins/sse.d.ts
@@ -95,7 +93,22 @@ interface SSEOptions {
95
93
  patterns?: string[];
96
94
  /** Heartbeat interval in ms (default: 30000) */
97
95
  heartbeat?: number;
98
- /** Filter events by organizationId from request.scope (default: false) */
96
+ /**
97
+ * Deliver an event carrying `meta.organizationId` only to callers in that
98
+ * organization (default: **true** since 2.41.1).
99
+ *
100
+ * It defaulted to `false`, and with `patterns: ['*']` that meant a signed-in
101
+ * member of one tenant received every other tenant's domain events — CRUD
102
+ * events carry the full document as `data`. In a framework whose other
103
+ * defaults fail closed, the stream was the one door left open.
104
+ *
105
+ * Flipping it costs single-tenant apps nothing: events with no
106
+ * `organizationId` are delivered to every caller either way. An elevated
107
+ * platform admin with NO active organization still sees every tenant — the
108
+ * same rule `checkOrgScope` applies to elevated reads. Set `false` only for a
109
+ * stream that is deliberately cross-tenant (an internal ops console), and
110
+ * put `requireAuth` plus a `filter` in front of it.
111
+ */
99
112
  orgScoped?: boolean;
100
113
  /** Custom event filter function */
101
114
  filter?: (event: DomainEvent<unknown>, request: FastifyRequest) => boolean;
@@ -107,7 +120,6 @@ interface SSEOptions {
107
120
  */
108
121
  tokenQueryParam?: string | null;
109
122
  }
110
- declare const ssePlugin: FastifyPluginAsync<SSEOptions>;
111
123
  declare const _default$3: FastifyPluginAsync<SSEOptions>;
112
124
  //#endregion
113
125
  //#region src/plugins/realtime.d.ts
@@ -148,13 +160,21 @@ interface RealtimeOptions {
148
160
  */
149
161
  maxConnectionMs?: number;
150
162
  }
151
- declare const realtimePlugin: FastifyPluginAsync<RealtimeOptions>;
152
163
  declare const _default$2: FastifyPluginAsync<RealtimeOptions>;
153
164
  //#endregion
165
+ //#region src/utils/metricSeries.d.ts
166
+ interface MetricSample {
167
+ labels: Record<string, string>;
168
+ value: number;
169
+ suffix?: "_bucket" | "_sum" | "_count";
170
+ }
171
+ //#endregion
154
172
  //#region src/plugins/metrics.d.ts
155
173
  interface MetricsOptions {
156
174
  /** Endpoint path (default: '/_metrics') */
157
175
  path?: string;
176
+ /** Maximum label sets per metric, plus one overflow series (default: 1000). */
177
+ maxSeries?: number;
158
178
  /** Prefix for all metric names (default: 'arc') */
159
179
  prefix?: string;
160
180
  /** Called after metrics are collected (for OTLP push, etc.) */
@@ -164,10 +184,7 @@ interface MetricEntry {
164
184
  name: string;
165
185
  type: "counter" | "histogram" | "gauge";
166
186
  help: string;
167
- values: Array<{
168
- labels: Record<string, string>;
169
- value: number;
170
- }>;
187
+ values: MetricSample[];
171
188
  }
172
189
  interface MetricsCollector {
173
190
  /** Get all metrics as structured data */
@@ -192,7 +209,6 @@ declare module "fastify" {
192
209
  metrics: MetricsCollector;
193
210
  }
194
211
  }
195
- declare const metricsPlugin: FastifyPluginAsync<MetricsOptions>;
196
212
  declare const _default$1: FastifyPluginAsync<MetricsOptions>;
197
213
  //#endregion
198
214
  //#region src/plugins/versioning.d.ts
@@ -225,7 +241,6 @@ declare module "fastify" {
225
241
  apiVersion: string;
226
242
  }
227
243
  }
228
- declare const versioningPlugin: FastifyPluginAsync<VersioningOptions>;
229
244
  declare const _default: FastifyPluginAsync<VersioningOptions>;
230
245
  //#endregion
231
- export { gracefulShutdownPlugin as C, _default$5 as S, CachingRule as _, MetricsCollector as a, GracefulShutdownOptions as b, metricsPlugin as c, _default$2 as d, realtimePlugin as f, CachingOptions as g, ssePlugin as h, MetricEntry as i, ChangeOperation as l, _default$3 as m, _default as n, MetricsOptions as o, SSEOptions as p, versioningPlugin as r, _default$1 as s, VersioningOptions as t, RealtimeOptions as u, _default$4 as v, ShutdownState as x, cachingPlugin as y };
246
+ export { _default$5 as _, MetricsOptions as a, RealtimeOptions as c, _default$3 as d, CachingOptions as f, ShutdownState as g, GracefulShutdownOptions as h, MetricsCollector as i, _default$2 as l, _default$4 as m, _default as n, _default$1 as o, CachingRule as p, MetricEntry as r, ChangeOperation as s, VersioningOptions as t, SSEOptions as u };
@@ -1,6 +1,6 @@
1
- /*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
2
- import { t as __exportAll } from "./rolldown-runtime-eysyzFAF.mjs";
3
- import { d as createDomainError } from "./errors-BH7YU_9O.mjs";
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ import { t as __exportAll } from "./rolldown-runtime-D1w9Y1VJ.mjs";
3
+ import { d as createDomainError } from "./errors-CtrywMHP.mjs";
4
4
  import fp from "fastify-plugin";
5
5
  //#region src/plugins/versioning.ts
6
6
  var versioning_exports = /* @__PURE__ */ __exportAll({
@@ -40,4 +40,4 @@ var versioning_default = fp(versioningPlugin, {
40
40
  fastify: "5.x"
41
41
  });
42
42
  //#endregion
43
- export { versioning_default as n, versioning_exports as r, versioningPlugin as t };
43
+ export { versioning_exports as n, versioning_default as t };
@@ -1,5 +1,5 @@
1
1
 
2
- import { a as PushRefRegistry, l as EnvelopeWriter, s as PushRefStore } from "./pushref-registry-Dov9l4qE.mjs";
2
+ import { a as PushRefRegistry, l as EnvelopeWriter, s as PushRefStore } from "./pushref-registry-UxCkoA4C.mjs";
3
3
  import { FastifyBaseLogger, FastifyPluginAsync } from "fastify";
4
4
  //#region src/integrations/websocket/adapter.d.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
- /*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
2
- import { n as requestContext } from "./requestContext-cDkAUr-P.mjs";
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ import { n as requestContext } from "./requestContext-Dsu9P1bV.mjs";
3
3
  //#region src/context/workScope.ts
4
4
  /** Enter a scope for `fn`, or run it inside the active one. */
5
5
  function runWorkScope(seed, fn) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.41.1",
3
+ "version": "2.43.0",
4
4
  "description": "Resource-oriented backend framework for Fastify - clean, minimal, powerful, tree-shakable",
5
5
  "type": "module",
6
6
  "exports": {
@@ -208,10 +208,6 @@
208
208
  "types": "./dist/auth/audit.d.mts",
209
209
  "default": "./dist/auth/audit.mjs"
210
210
  },
211
- "./sync": {
212
- "types": "./dist/sync/changelog.d.mts",
213
- "default": "./dist/sync/changelog.mjs"
214
- },
215
211
  "./usage": {
216
212
  "types": "./dist/usage/index.d.mts",
217
213
  "default": "./dist/usage/index.mjs"
@@ -227,6 +223,10 @@
227
223
  "./testing/outbox": {
228
224
  "types": "./dist/testing/outboxStoreContract.d.mts",
229
225
  "default": "./dist/testing/outboxStoreContract.mjs"
226
+ },
227
+ "./testing/ports": {
228
+ "types": "./dist/testing/requiredPorts.d.mts",
229
+ "default": "./dist/testing/requiredPorts.mjs"
230
230
  }
231
231
  },
232
232
  "main": "./dist/index.mjs",
@@ -292,9 +292,9 @@
292
292
  "./mcp/testing": "stable",
293
293
  "./scim": "stable",
294
294
  "./auth/audit": "stable",
295
- "./sync": "experimental",
296
295
  "./usage": "experimental",
297
296
  "./testing/outbox": "stable",
297
+ "./testing/ports": "stable",
298
298
  "./outbox-admin": "experimental"
299
299
  }
300
300
  },
@@ -308,7 +308,7 @@
308
308
  "scripts": {
309
309
  "build": "tsdown",
310
310
  "dev": "tsdown --watch",
311
- "typecheck": "npm run typecheck:src && npm run typecheck:types && npm run check:type-lane",
311
+ "typecheck": "npm run typecheck:src && npm run typecheck:types && npm run typecheck:types-strict && npm run check:type-lane",
312
312
  "typecheck:src": "tsc --noEmit",
313
313
  "check:type-lane": "node scripts/check-type-lane.mjs",
314
314
  "lint": "biome check src/",
@@ -322,6 +322,7 @@
322
322
  "test:watch": "vitest",
323
323
  "test:ui": "vitest --ui",
324
324
  "typecheck:types": "tsc -p tsconfig.types.json",
325
+ "typecheck:types-strict": "tsc -p tsconfig.types-strict.json",
325
326
  "api:surface": "node scripts/check-api-surface.mjs --update",
326
327
  "check:api-surface": "node scripts/check-api-surface.mjs",
327
328
  "check:package": "publint && attw --pack . --profile esm-only",
@@ -337,6 +338,8 @@
337
338
  "smoke": "node scripts/smoke-test.mjs",
338
339
  "smoke:consumer": "node scripts/smoke-test.mjs --consumer",
339
340
  "smoke:mcp": "node scripts/mcp-e2e.mjs",
341
+ "bench:memory": "node --expose-gc scripts/bench-memory.mjs",
342
+ "bench:http": "node --expose-gc scripts/bench-http.mjs",
340
343
  "push": "classytic-push",
341
344
  "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
342
345
  "release": "npm run push -- main && npm run release:tag && npm publish",
@@ -353,8 +356,8 @@
353
356
  "node": ">=22"
354
357
  },
355
358
  "peerDependencies": {
356
- "@classytic/primitives": ">=0.28.0",
357
- "@classytic/repo-core": ">=0.28.0",
359
+ "@classytic/primitives": ">=0.31.0",
360
+ "@classytic/repo-core": ">=0.31.0",
358
361
  "@classytic/streamline": ">=2.8.0",
359
362
  "@fastify/cors": ">=11.0.0",
360
363
  "@fastify/helmet": ">=13.0.0",
@@ -476,6 +479,7 @@
476
479
  }
477
480
  },
478
481
  "dependencies": {
482
+ "@fastify/ajv-compiler": "^4.0.0",
479
483
  "fastify-plugin": "^6.0.0",
480
484
  "qs": "^6.16.0",
481
485
  "secure-json-parse": "^4.1.0"
@@ -487,10 +491,10 @@
487
491
  "@biomejs/biome": "^2.5.7",
488
492
  "@classytic/dev-tools": "^0.2.0",
489
493
  "@classytic/gitkit": ">=0.1.0",
490
- "@classytic/mongokit": "^3.33.0",
494
+ "@classytic/mongokit": "^3.40.1",
491
495
  "@classytic/pgkit": "^0.1.0",
492
- "@classytic/primitives": "^0.28.0",
493
- "@classytic/repo-core": "^0.28.0",
496
+ "@classytic/primitives": "^0.31.0",
497
+ "@classytic/repo-core": "^0.31.0",
494
498
  "@classytic/sqlitekit": "^0.9.0",
495
499
  "@classytic/streamline": "^2.8.2",
496
500
  "@electric-sql/pglite": "^0.5.4",
@@ -77,7 +77,7 @@ createApp({ role: process.env.ARC_ROLE as 'api' | 'worker' | 'relay' | 'schedule
77
77
 
78
78
  `api` and `worker` mount NO module schedule arms; `relay` mounts only `kind: 'relay'` arms (the outbox relay, durable webhook delivery); `scheduler` mounts all of them. Declaring `relay`/`scheduler` together with `resources`/`resourceDir` is BOOT-FATAL — a relay process must not serve routes. Default `all` is the pre-2.33 behaviour, unchanged. `createWorker()` forwards `role`; without one it still runs everything non-HTTP.
79
79
 
80
- `beforeBoot()` (2.21) runs before Fastify exists and before module thunks / resource files import — connect the DB there instead of dynamic-import ordering hacks.
80
+ `beforeBoot()` (2.21) runs before Fastify exists and before module thunks / resource files import — connect the DB there instead of dynamic-import ordering hacks. Return a disposer (`() => mongoose.disconnect()`) and arc runs it on close and on a failed boot. A bridge declared as `{ kind: 'bridge', name, dependsOn, load }` is never imported when inactive.
81
81
 
82
82
  For async-booted engines, pass `resources` as a factory so it runs after `bootstrap[]`:
83
83
 
@@ -210,7 +210,14 @@ presets: ['softDelete', { name: 'multiTenant', tenantField: 'organizationId' }]
210
210
 
211
211
  ## Permissions
212
212
 
213
- A `PermissionCheck` returns `boolean | { granted, reason?, filters?, scope? }`. `filters` propagate into the repo query (row-level ABAC). `scope` stamps attributes downstream.
213
+ A `PermissionCheck` returns `boolean | AuthorizationDecision` — build one with `allow({ policy?, scope? })` / `deny(reason?)`. `policy` is ANDed into the repo query (row-level ABAC); `scope` stamps attributes downstream. The pre-2.30 `{ granted, filters }` shape THROWS — never emit it.
214
+
215
+ **Public but row-filtered** — use `allowPublic({ policy })`, not a hand-written `allow({ policy })`: a hand-written check lacks the `_isPublic` flag, so anonymous callers get 401.
216
+
217
+ ```typescript
218
+ list: allowPublic({ policy: { status: 'published' } }), // strangers: published only
219
+ list: allowPublic({ policy: (ctx) => isElevated(scopeOf(ctx)) ? {} : { status: 'published' } }), // {} = unrestricted
220
+ ```
214
221
 
215
222
  ```typescript
216
223
  import {
@@ -233,9 +240,11 @@ permissions: {
233
240
  **Custom check:**
234
241
 
235
242
  ```typescript
243
+ import { allow, deny, type PermissionCheck } from '@classytic/arc/permissions';
244
+
236
245
  const requirePro = (): PermissionCheck => async (ctx) => {
237
- if (!ctx.user) return { granted: false, reason: 'Auth required' };
238
- return { granted: ctx.user.plan === 'pro' };
246
+ if (!ctx.user) return deny('Auth required');
247
+ return ctx.user.plan === 'pro' ? allow() : deny('Pro plan required');
239
248
  };
240
249
  ```
241
250
 
@@ -497,6 +497,7 @@ import {
497
497
  | Function | Description |
498
498
  |----------|-------------|
499
499
  | `allowPublic()` | No authentication |
500
+ | `allowPublic({ policy })` | No authentication, rows narrowed by a filter (or a per-caller resolver; `{}` = unrestricted) |
500
501
  | `requireAuth()` | Any authenticated user |
501
502
  | `requireRoles(['admin'])` | At least one role matches |
502
503
  | `requireOwnership('userId')` | Resource owner only (elevated bypasses) |
@@ -510,9 +511,11 @@ import {
510
511
  **Custom permission:**
511
512
 
512
513
  ```typescript
514
+ import { allow, deny, type PermissionCheck } from '@classytic/arc/permissions';
515
+
513
516
  const requirePro = (): PermissionCheck => async (ctx) => {
514
- if (!ctx.user) return { granted: false, reason: 'Auth required' };
515
- return { granted: ctx.user.plan === 'pro' };
517
+ if (!ctx.user) return deny('Auth required');
518
+ return ctx.user.plan === 'pro' ? allow() : deny('Pro plan required');
516
519
  };
517
520
  ```
518
521
 
@@ -646,12 +649,12 @@ Gate routes on service identity using the scope helpers + a permission builder:
646
649
 
647
650
  ```typescript
648
651
  import { isService, getClientId, getServiceScopes } from '@classytic/arc/scope';
649
- import type { PermissionCheck } from '@classytic/arc/permissions';
652
+ import { allow, deny, type PermissionCheck } from '@classytic/arc/permissions';
650
653
 
651
654
  const requireScope = (scope: string): PermissionCheck => (ctx) => {
652
- if (!isService(ctx.request.scope)) return { granted: false, reason: 'service only' };
655
+ if (!isService(ctx.request.scope)) return deny('service only');
653
656
  const has = getServiceScopes(ctx.request.scope).includes(scope);
654
- return has ? { granted: true } : { granted: false, reason: `missing scope: ${scope}` };
657
+ return has ? allow() : deny(`missing scope: ${scope}`);
655
658
  };
656
659
 
657
660
  defineResource({
@@ -238,7 +238,7 @@ Single configuration point for both SSE and WebSocket with shared auth, org-scop
238
238
  ```typescript
239
239
  await fastify.register(eventGatewayPlugin, {
240
240
  auth: true, // Fail-closed for both SSE and WebSocket
241
- orgScoped: true, // Filter events by org
241
+ // orgScoped: omitted → SSE's default (true). Set false only for a cross-tenant ops stream.
242
242
  roomPolicy: (client, room) => {
243
243
  return ['product', 'order', 'invoice'].includes(room);
244
244
  },
@@ -101,18 +101,20 @@ await app.register(mcpPlugin, {
101
101
 
102
102
  ### Permission Filters (v2.4.2)
103
103
 
104
- Resource permissions with `filters` are automatically enforced in MCP tools — same as REST:
104
+ Resource permissions that return a row `policy` are automatically enforced in MCP tools — same as REST:
105
105
 
106
106
  ```typescript
107
+ import { allow, deny } from '@classytic/arc/permissions';
108
+
107
109
  defineResource({
108
110
  name: 'task',
109
111
  permissions: {
110
- list: (ctx) => ({
111
- granted: !!ctx.user,
112
- filters: { orgId: ctx.user?.orgId, branchId: ctx.user?.branchId },
113
- }),
112
+ list: (ctx) =>
113
+ ctx.user
114
+ ? allow({ policy: { orgId: ctx.user.orgId, branchId: ctx.user.branchId } })
115
+ : deny('Auth required'),
114
116
  create: (ctx) => !!ctx.user, // boolean works too
115
- delete: (ctx) => ({ granted: false, reason: 'Read-only' }), // deny
117
+ delete: () => deny('Read-only'),
116
118
  },
117
119
  });
118
120
 
@@ -38,15 +38,21 @@ Every request goes through this ladder — if any step is missed, tenant isolati
38
38
 
39
39
  ## Where `tenantField` is read
40
40
 
41
- Every resource defines `tenantField` (default: `'organizationId'`, or `false` to disable). It is read in **exactly three places** and ALL of them derive the org ID from `metadata._scope` via `getOrgId()`:
41
+ Every resource defines `tenantField` (default: `'organizationId'`, or `false` to disable). **One function decides tenancy — `resolveTenancy()` in `src/core/tenancy.ts`** — and every layer switches on its result:
42
42
 
43
- | File | Method | Purpose |
44
- |--------------------------------|-------------------------|------------------------------------------------|
45
- | `src/core/QueryResolver.ts` | `resolve()` | `list` — prepends `{ [tenantField]: orgId }` to the filter |
46
- | `src/core/AccessControl.ts` | `buildIdFilter()` | `get`, `update`, `delete` by ID — compound filter so cross-tenant IDs 404 |
47
- | `src/core/BaseController.ts` | `buildBulkFilter()` | `bulkUpdate`, `bulkDelete` — narrows bulk ops to the caller's org |
43
+ | Result | Meaning |
44
+ |---|---|
45
+ | `{ mode: 'scoped', field, orgId }` | filter by `field = orgId` (a member, a service key, or an elevated admin who SELECTED an org) |
46
+ | `unscoped: 'platform'` | elevated admin with NO org — sees and writes across tenants |
47
+ | `unscoped: 'cross-tenant'` | the route declared `multiTenantPreset({ crossTenant: [...] })` — reads only; the row policy still applies |
48
+ | `unscoped: 'no-org'` | anonymous, or signed in with no org selected — reads get no tenant filter (the preset refuses earlier where one is required); **writes are refused** |
49
+ | `unscoped: 'untenanted-resource'` | `tenantField: false` |
48
50
 
49
- **Everything else** (`BodySanitizer`, `MongoKit QueryParser`, presets) already composes cleanly with these three read points. If `getOrgId(scope)` returns the right value, you have tenant isolation — end of story.
51
+ Consumers: `QueryResolver` (list), `AccessControl.buildIdFilter` (get/update/delete by id) and `checkOrgScope` (post-fetch), `buildTenantRepoOptions` (what the kit is told, `bypassTenant`), the bulk mixin (`bulkCreate` stamping + `buildBulkFilter`), and — via `eventAudienceOf` / `isEventVisible` — the `sse` and `realtime` streams.
52
+
53
+ **Never re-derive the org from the scope in a new layer.** Each layer used to, with its own condition, and they drifted: `crossTenant` was taught to three of four read layers (`get` silently ignored it for signed-in members), bulk writes skipped the org for ANY elevated admin (an admin acting as org-9 deleted across every tenant), and `realtime` withheld the platform view that reads grant. The union is exhaustive, so a new mode is a compile error in every consumer that has not handled it.
54
+
55
+ The cache key (`deriveCacheScope`) deliberately does NOT use it: it answers "whose cache partition", which on a cross-tenant read is still the caller's org — dropping it could let one org's cached page answer another's.
50
56
 
51
57
  ## Filter composition
52
58
 
@@ -101,19 +107,18 @@ Return the scope directly from your permission check. This is the cleanest integ
101
107
 
102
108
  ```ts
103
109
  // src/permissions/apiKey.ts
104
- import type { PermissionCheck } from '@classytic/arc/permissions';
110
+ import { allow, deny, type PermissionCheck } from '@classytic/arc/permissions';
105
111
  import { ClientModel } from '../models/Client.js';
106
112
 
107
113
  export function requireApiKey(): PermissionCheck {
108
114
  return async ({ request }) => {
109
115
  const apiKey = request.headers['x-api-key'] as string | undefined;
110
- if (!apiKey) return { granted: false, reason: 'Missing API key' };
116
+ if (!apiKey) return deny('Missing API key');
111
117
 
112
118
  const client = await ClientModel.findOne({ apiKey });
113
- if (!client) return { granted: false, reason: 'Invalid API key' };
119
+ if (!client) return deny('Invalid API key');
114
120
 
115
- return {
116
- granted: true,
121
+ return allow({
117
122
  scope: {
118
123
  kind: 'service',
119
124
  clientId: String(client._id),
@@ -121,8 +126,8 @@ export function requireApiKey(): PermissionCheck {
121
126
  scopes: client.allowedScopes, // optional OAuth-style scope strings
122
127
  },
123
128
  // Optional additional row-level narrowing
124
- filters: client.projectId ? { projectId: client.projectId } : undefined,
125
- };
129
+ ...(client.projectId ? { policy: { projectId: client.projectId } } : {}),
130
+ });
126
131
  };
127
132
  }
128
133
  ```
@@ -76,8 +76,8 @@ Default `0`, which is the pre-2.41 behaviour: close the moment SIGTERM arrives.
76
76
 
77
77
  `drainDelayMs` is the window between "start failing readiness" and "stop accepting":
78
78
 
79
- - `/health/ready` → `503 { status: 'draining' }` immediately on signal
80
- - `/health/live` → stays `200` (a draining process is alive — that's the point; a 503 here gets the pod **killed**, not drained)
79
+ - `/_health/ready` → `503 { status: 'draining' }` immediately on signal
80
+ - `/_health/live` → stays `200` (a draining process is alive — that's the point; a 503 here gets the pod **killed**, not drained)
81
81
  - everything already routed here is still served for the whole window
82
82
 
83
83
  Set it **above** your LB's deregistration window — typically 5000–15000ms. On Kubernetes, also keep `terminationGracePeriodSeconds` > `drainDelayMs + timeout`, or the kubelet SIGKILLs you mid-drain.
@@ -299,7 +299,7 @@ const app = await createApp({
299
299
  path: '/events/stream', // SSE endpoint (default: '/events/stream')
300
300
  requireAuth: true, // Fail-closed auth (default: true)
301
301
  patterns: ['order.*', 'product.*'], // Event patterns to stream (default: ['*'])
302
- orgScoped: false, // Filter events by org from request.scope (default: false)
302
+ // orgScoped defaults to TRUE (2.41.1) — a caller only sees its own org's events.
303
303
  heartbeat: 30000, // Heartbeat interval in ms (default: 30000)
304
304
  },
305
305
  },
@@ -307,7 +307,7 @@ const app = await createApp({
307
307
 
308
308
  // Manual registration
309
309
  import { ssePlugin } from '@classytic/arc/plugins';
310
- await fastify.register(ssePlugin, { requireAuth: true, orgScoped: true });
310
+ await fastify.register(ssePlugin, { requireAuth: true });
311
311
  ```
312
312
 
313
313
  ### Realtime resource change feed (2.22)
@@ -322,12 +322,9 @@ row-permissioned resources through it); event firehose → sse. Massive fan-out
322
322
  (viral feeds, live scores) → publish once to a dedicated edge fan-out layer
323
323
  via `app.events.subscribe(...)`; full tiering guide: docs/production-ops/realtime.mdx.
324
324
 
325
- ```typescript
326
- ```
327
-
328
325
  **Fail-closed auth:** When `requireAuth: true` (default), throws at registration if `fastify.authenticate` is missing — prevents exposing SSE without auth.
329
326
 
330
- **Org-scoped:** When `orgScoped: true`, events with `organizationId` are only sent to clients whose `request.scope` matches. Prevents cross-tenant leakage.
327
+ **Org-scoped by default (2.41.1):** an event carrying `organizationId` reaches only callers in that org. It defaulted to `false` — with `patterns: ['*']`, one tenant's member received every tenant's events, full documents included. Events with no `organizationId` still reach everyone, so single-tenant apps are unaffected; an elevated admin with no active org sees all tenants (the platform view). `orgScoped: false` is for a deliberately cross-tenant stream only — keep `requireAuth` and add a `filter`.
331
328
 
332
329
  ## Error Handler Plugin
333
330
 
@@ -383,7 +380,7 @@ return reply.header('content-type', 'text/csv').send(readableStream);
383
380
 
384
381
  ### BigInt Serialization
385
382
 
386
- Opt-in: `createApp({ serializeBigInt: true })` — auto-converts BigInt → Number in all JSON responses.
383
+ Opt-in: `createApp({ serializeBigInt: 'string' })` — BigInt → decimal string in JSON responses (lossless); `'number'` is lossy above 2^53-1.
387
384
 
388
385
  ## Multipart Body Middleware (v2.7.3)
389
386
 
@@ -678,7 +675,27 @@ await outbox.store(event, { session });
678
675
  const relayed = await outbox.relay(); // publishes pending → transport
679
676
  ```
680
677
 
681
- **OutboxStore interface**: `save(event)`, `getPending(limit)`, `acknowledge(eventId)` (+ optional `claimPending`, `fail`, `getDeadLettered`, `purge`). When you pass `repository`, arc adapts it internally.
678
+ **Prefer the module form** — it owns the relay schedule, shutdown, and a readiness check, so none of that is hand-wired:
679
+
680
+ ```typescript
681
+ import { createOutboxModule, exponentialBackoff, repositoryAsOutboxStore } from '@classytic/arc/events';
682
+
683
+ const outbox = createOutboxModule({
684
+ store: repositoryAsOutboxStore(new Repository(OutboxModel)),
685
+ // Bounds retries AND log volume. `attempts` is the DURABLE count (it survives relay
686
+ // restarts and is shared across relays), so a poison event really does stop at 5.
687
+ failurePolicy: ({ attempts }) =>
688
+ attempts >= 5 ? { deadLetter: true } : { retryAt: exponentialBackoff({ attempt: attempts }) },
689
+ health: { relayLagUnhealthyMs: 5 * 60_000, probeCacheMs: 5_000 },
690
+ });
691
+ // createApp({ modules: [outbox] }); consumers declare dependsOn: ['outbox'].
692
+ ```
693
+
694
+ - `requestDrain()` — nudge a pass after commit (milliseconds, not `relayEveryMs`); runs outside the caller's request scope.
695
+ - `drain()` — await delivery, cascades included. For tests and a shutdown that should empty the queue.
696
+ - `health()` + readiness check `<name>.relay` — **non-critical** (a stalled relay is a cluster problem; pulling one pod would not drain it). Unhealthy on any dead letter or pending lag past the threshold. The probe reuses a reading for `probeCacheMs` so N replicas × probe rate do not become N count queries a second on the outbox table.
697
+
698
+ **OutboxStore contract** (`@classytic/primitives/outbox`): required `save`, `getPending`, `acknowledge`; optional `claimPending` / `claimPendingFenced` (lease + fencing token, and the durable `attempts`), `fail`, `getDeadLettered`, `requeue`, `purge`, `countByStatus`, `oldestPendingAgeMs`. Prove a custom store with `runOutboxStoreContract` from `@classytic/arc/testing/outbox`. When you pass `repository`, arc adapts it internally.
682
699
 
683
700
  ## Bulk Operations Preset
684
701