@classytic/arc 2.41.1 → 2.44.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 (248) hide show
  1. package/dist/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
  2. package/dist/{EventTransport-DSqD_Xsg.mjs → EventTransport-DZWDsaPa.mjs} +22 -17
  3. package/dist/{HookSystem-CMF94SEA.mjs → HookSystem-yjEgn7Cm.mjs} +2 -2
  4. package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
  5. package/dist/{ResourceRegistry-MGpXoxHZ.mjs → ResourceRegistry-DJxHUpE4.mjs} +44 -13
  6. package/dist/acquire-BozJ-9yt.mjs +35 -0
  7. package/dist/actionPermissions-5N04zBL4.mjs +772 -0
  8. package/dist/audit/index.d.mts +46 -8
  9. package/dist/audit/index.mjs +48 -26
  10. package/dist/auth/audit.d.mts +8 -8
  11. package/dist/auth/audit.mjs +1 -1
  12. package/dist/auth/index.d.mts +46 -16
  13. package/dist/auth/index.mjs +134 -156
  14. package/dist/auth/redis-session.d.mts +4 -5
  15. package/dist/auth/redis-session.mjs +1 -1
  16. package/dist/{authorizationDecision-DawKGoXb.mjs → authorizationDecision-wX3SA9w_.mjs} +7 -102
  17. package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
  18. package/dist/boundedMap-DWiKW7lP.mjs +57 -0
  19. package/dist/{buildHandler-CvxKOc0m.mjs → buildHandler-Bswu_vNb.mjs} +4 -4
  20. package/dist/cache/index.d.mts +31 -43
  21. package/dist/cache/index.mjs +4 -4
  22. package/dist/{caching-DbCBxlgw.mjs → caching-Drp68DIS.mjs} +3 -3
  23. package/dist/claimed-DYvQoZLv.mjs +8 -0
  24. package/dist/cleanup/index.d.mts +18 -22
  25. package/dist/cleanup/index.mjs +27 -73
  26. package/dist/cli/commands/describe.d.mts +7 -5
  27. package/dist/cli/commands/describe.mjs +7 -4
  28. package/dist/cli/commands/docs.d.mts +2 -3
  29. package/dist/cli/commands/docs.mjs +3 -3
  30. package/dist/cli/commands/doctor.d.mts +2 -3
  31. package/dist/cli/commands/doctor.mjs +1 -1
  32. package/dist/cli/commands/generate.mjs +2 -3
  33. package/dist/cli/commands/init.mjs +2 -3
  34. package/dist/cli/commands/introspect.d.mts +2 -3
  35. package/dist/cli/commands/introspect.mjs +2 -2
  36. package/dist/cli/index.mjs +3 -3
  37. package/dist/{constants-D5Ubn1fe.mjs → constants-B2QHb6u6.mjs} +4 -2
  38. package/dist/context/index.d.mts +11 -14
  39. package/dist/context/index.mjs +18 -38
  40. package/dist/core/index.d.mts +3 -3
  41. package/dist/core/index.mjs +8 -8
  42. package/dist/{core-5lazhva9.mjs → core-COcqo0Cp.mjs} +15 -5
  43. package/dist/{createActionRouter-DNqoaK81.mjs → createActionRouter-zG13rpPJ.mjs} +97 -39
  44. package/dist/{createAggregationRouter-BcbQJf10.mjs → createAggregationRouter-CWRz96Vr.mjs} +11 -8
  45. package/dist/{preloadResources-f3boenVZ.mjs → createApp-BGLYtErt.mjs} +305 -394
  46. package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-DSztofa9.d.mts} +1 -1
  47. package/dist/{defineAggregation-D_eJjm5A.mjs → defineAggregation--3xguLkk.mjs} +1 -1
  48. package/dist/{defineResource-DxkMTjRn.mjs → defineResource-gUp9ug66.mjs} +118 -1024
  49. package/dist/discovery/index.d.mts +6 -7
  50. package/dist/discovery/index.mjs +1 -1
  51. package/dist/docs/index.d.mts +11 -5
  52. package/dist/docs/index.mjs +4 -4
  53. package/dist/{elevation-CkvG0LG2.mjs → elevation-BG4gnMsJ.mjs} +41 -12
  54. package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
  55. package/dist/encryption/index.d.mts +5 -5
  56. package/dist/encryption/index.mjs +3 -3
  57. package/dist/{errorHandler-D1GHKGlV.mjs → errorHandler-COHw3N1b.mjs} +26 -5
  58. package/dist/{errors-BH7YU_9O.mjs → errors-CW4anJBy.mjs} +58 -23
  59. package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
  60. package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
  61. package/dist/{eventPlugin-zZY-unhr.mjs → eventPlugin-BweHAVws.mjs} +82 -21
  62. package/dist/{subscribe-helpers-DatDkcuG.mjs → eventTypes-N4-omQFR.mjs} +151 -17
  63. package/dist/events/index.d.mts +43 -18
  64. package/dist/events/index.mjs +49 -53
  65. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  66. package/dist/events/transports/redis-stream-entry.mjs +139 -157
  67. package/dist/events/transports/redis.d.mts +5 -6
  68. package/dist/events/transports/redis.mjs +2 -2
  69. package/dist/factory/index.d.mts +42 -27
  70. package/dist/factory/index.mjs +213 -32
  71. package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DRlCIWE9.d.mts} +11 -25
  72. package/dist/{fields-BHrkoF5y.mjs → fields-A5crhsJb.mjs} +1 -1
  73. package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
  74. package/dist/filter-merge-DGM-GQ1J.mjs +99 -0
  75. package/dist/{generate-BLRf2m3y.mjs → generate-BXKo2Gc8.mjs} +2 -2
  76. package/dist/{headers-BIXWKrzQ.mjs → headers-DKZQKL-Z.mjs} +2 -2
  77. package/dist/health-BYAwA6YS.mjs +22 -0
  78. package/dist/{health-ZRHeDHXH.mjs → health-CrqUp3sz.mjs} +115 -55
  79. package/dist/hooks/index.d.mts +2 -2
  80. package/dist/hooks/index.mjs +2 -3
  81. package/dist/idempotency/index.d.mts +36 -10
  82. package/dist/idempotency/index.mjs +230 -111
  83. package/dist/idempotency/redis.d.mts +1 -1
  84. package/dist/idempotency/redis.mjs +77 -12
  85. package/dist/{index-D4u76eQW.d.mts → index-Beg3aNnK.d.mts} +23 -101
  86. package/dist/{preloadResources-CjS2OfpU.d.mts → index-BkV55WlV.d.mts} +37 -38
  87. package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
  88. package/dist/index-CUJvoyw3.d.mts +510 -0
  89. package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
  90. package/dist/{index-DHbyPo5j.d.mts → index-WH7xgvxS.d.mts} +304 -254
  91. package/dist/index.d.mts +9 -9
  92. package/dist/index.mjs +11 -11
  93. package/dist/{init-CpLkjU18.mjs → init-BPzK5Lnr.mjs} +1 -1
  94. package/dist/integrations/event-gateway.d.mts +11 -7
  95. package/dist/integrations/event-gateway.mjs +4 -4
  96. package/dist/integrations/index.d.mts +3 -3
  97. package/dist/integrations/index.mjs +1 -1
  98. package/dist/integrations/jobs/index.d.mts +1 -1
  99. package/dist/integrations/jobs/index.mjs +18 -5
  100. package/dist/integrations/mcp/index.d.mts +102 -89
  101. package/dist/integrations/mcp/index.mjs +19 -4
  102. package/dist/integrations/mcp/testing.d.mts +8 -9
  103. package/dist/integrations/mcp/testing.mjs +2 -2
  104. package/dist/integrations/streamline.d.mts +85 -144
  105. package/dist/integrations/streamline.mjs +131 -55
  106. package/dist/integrations/webhooks.d.mts +83 -143
  107. package/dist/integrations/webhooks.mjs +36 -80
  108. package/dist/integrations/websocket-pushref-redis.d.mts +5 -6
  109. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  110. package/dist/integrations/websocket-redis.d.mts +5 -6
  111. package/dist/integrations/websocket-redis.mjs +1 -1
  112. package/dist/integrations/websocket.d.mts +3 -3
  113. package/dist/integrations/websocket.mjs +2 -2
  114. package/dist/{interface-DEWNOMza.d.mts → interface-JJzwH4Ro.d.mts} +34 -2
  115. package/dist/{jsonBody-ELR9lfeU.mjs → jsonBody-YIq-vGLv.mjs} +2 -2
  116. package/dist/{keys-cpZCtcIE.mjs → keys-CpoiZ8hU.mjs} +7 -18
  117. package/dist/{loadResources-BPfWiI5s.mjs → loadResources-6jO52-RR.mjs} +3 -3
  118. package/dist/{loadResourcesFromEntry-ixtfL3J2.mjs → loadResourcesFromEntry-MP8xEppu.mjs} +2 -2
  119. package/dist/logger/index.d.mts +9 -17
  120. package/dist/logger/index.mjs +2 -153
  121. package/dist/logger-D4V1zGLI.mjs +160 -0
  122. package/dist/{mcpPlugin-BX5PujI0.mjs → mcpPlugin-srPc7ItN.mjs} +446 -170
  123. package/dist/{memory-Iotz0cCx.mjs → memory-CTa5pOCW.mjs} +36 -34
  124. package/dist/{metrics-NMsmpRmY.mjs → metrics-DsZrvnpB.mjs} +39 -72
  125. package/dist/middleware/index.d.mts +5 -5
  126. package/dist/middleware/index.mjs +3 -3
  127. package/dist/migrations/index.d.mts +13 -13
  128. package/dist/migrations/index.mjs +2 -2
  129. package/dist/{module-DpLJG2_N.mjs → module--L0iBIZz.mjs} +8 -8
  130. package/dist/mongodb-memory-server-CHhYSq5y.mjs +35539 -0
  131. package/dist/mongoose-CYJCLyi-.mjs +67451 -0
  132. package/dist/multiTenant-CAtYlW6H.d.mts +104 -0
  133. package/dist/{multipartBody-DtRWM6_U.mjs → multipartBody-Cc7eZ9cq.mjs} +46 -7
  134. package/dist/node-D8UZv_8c.mjs +41236 -0
  135. package/dist/{openapi-CmFf3yq_.mjs → openapi-CMegfsm8.mjs} +48 -10
  136. package/dist/outbox-admin/index.d.mts +5 -5
  137. package/dist/outbox-admin/index.mjs +6 -18
  138. package/dist/{outbox-module-D2z5WkDY.mjs → outbox-module-Ryk270jd.mjs} +203 -33
  139. package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
  140. package/dist/permissions/index.d.mts +5 -4
  141. package/dist/permissions/index.mjs +8 -6
  142. package/dist/{permissions-D8TBH44E.mjs → permissions-C5ijIz5Q.mjs} +149 -33
  143. package/dist/{pipe-FMWNheSO.mjs → pipe-D81zMtWs.mjs} +2 -2
  144. package/dist/pipeline/index.d.mts +7 -7
  145. package/dist/pipeline/index.mjs +2 -2
  146. package/dist/plan-CJGMKoui.mjs +565 -0
  147. package/dist/{BaseController-CYEwjyjN.mjs → plugin-DALd9dcF.mjs} +1269 -346
  148. package/dist/plugins/index.d.mts +17 -14
  149. package/dist/plugins/index.mjs +44 -22
  150. package/dist/plugins/response-cache.d.mts +5 -6
  151. package/dist/plugins/response-cache.mjs +8 -14
  152. package/dist/plugins/tracing-entry.mjs +2 -2
  153. package/dist/{pluralize-CXWZIzTv.mjs → pluralize-COUNYo3d.mjs} +1 -1
  154. package/dist/presets/filesUpload.d.mts +8 -8
  155. package/dist/presets/filesUpload.mjs +5 -5
  156. package/dist/presets/index.d.mts +35 -40
  157. package/dist/presets/index.mjs +4 -4
  158. package/dist/presets/multiTenant.d.mts +2 -107
  159. package/dist/presets/multiTenant.mjs +22 -162
  160. package/dist/presets/search.d.mts +9 -10
  161. package/dist/presets/search.mjs +2 -2
  162. package/dist/{presets-ZJSPgmT2.mjs → presets-CONf2Qem.mjs} +99 -107
  163. package/dist/{projection-BzIgzoKZ.mjs → projection-wFoLqdss.mjs} +2 -2
  164. package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
  165. package/dist/{queryCachePlugin-DZHPOm9p.mjs → queryCachePlugin-BATECpyh.mjs} +12 -14
  166. package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
  167. package/dist/{rateLimitKey-BvOvcBDi.mjs → rateLimitKey-DVEZ_UpV.mjs} +1 -1
  168. package/dist/{realtime-8tIQWZz4.mjs → realtime-xyubMKSB.mjs} +25 -21
  169. package/dist/{redis-Bzi1y3F9.d.mts → redis-BDgv9Glw.d.mts} +17 -24
  170. package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
  171. package/dist/redisShapes-CQksY67q.d.mts +55 -0
  172. package/dist/registry/index.d.mts +15 -10
  173. package/dist/registry/index.mjs +4 -4
  174. package/dist/{registry-DNkgGQcK.mjs → registry-Bdzg-hdm.mjs} +13 -10
  175. package/dist/{renewingLease-yF8BdW4b.mjs → renewingLease-BFJxGPuh.mjs} +1 -1
  176. package/dist/{replyHelpers-CNdvMfM4.mjs → replyHelpers-Bnc6lJyy.mjs} +2 -2
  177. package/dist/{requestContext-cDkAUr-P.mjs → requestContext-C10UeFe5.mjs} +1 -1
  178. package/dist/requirement-Bu50QALH.mjs +35 -0
  179. package/dist/requirement-CbiLdQ1n.d.mts +22 -0
  180. package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
  181. package/dist/resourceRateLimit-urWPJxrt.mjs +180 -0
  182. package/dist/rolldown-runtime-MYrLNGnq.mjs +45 -0
  183. package/dist/routeValidator-DHtpzrzk.mjs +70 -0
  184. package/dist/{runtimeCapabilities-CkhS6jn6.mjs → runtimeCapabilities-DIMKMV2G.mjs} +1 -1
  185. package/dist/{schedules-j6uXXAeJ.mjs → schedules-CKTs5ys8.mjs} +46 -37
  186. package/dist/{schemaConverter-BVA7cFRG.mjs → schemaConverter-BVjkQ5DD.mjs} +2 -2
  187. package/dist/{schemaIR-CTvPDoPB.mjs → schemaIR-BA0Hb21Y.mjs} +2 -2
  188. package/dist/schemas/index.d.mts +20 -10
  189. package/dist/schemas/index.mjs +8 -3
  190. package/dist/scim/index.d.mts +13 -13
  191. package/dist/scim/index.mjs +2 -2
  192. package/dist/scope/index.d.mts +4 -4
  193. package/dist/scope/index.mjs +6 -6
  194. package/dist/{scope-BUi1xV1I.mjs → scope-DINaj7Ax.mjs} +4 -4
  195. package/dist/shutdown-LpLMH8T1.mjs +59 -0
  196. package/dist/{simpleEqualityMatcher-C9bzq2gO.mjs → simpleEqualityMatcher-DbwIM1f1.mjs} +1 -1
  197. package/dist/{sse-BeqVKQKu.mjs → sse-BzJFPX_1.mjs} +11 -15
  198. package/dist/{sseStream-Ppzo-WUH.mjs → sseStream-DQChzGTs.mjs} +2 -2
  199. package/dist/{store-helpers-dW71XVWL.mjs → store-helpers-Coj9RhOZ.mjs} +1 -1
  200. package/dist/{streaming-IcRFjWQU.mjs → streaming-JzB5CAga.mjs} +1 -1
  201. package/dist/subscriberIdentity-DeyppIif.mjs +28 -0
  202. package/dist/tenancy-DxV8OuX2.mjs +43 -0
  203. package/dist/tenantPreparation-CBvvAcAj.mjs +67 -0
  204. package/dist/testing/cleanupStoreContract.d.mts +4 -5
  205. package/dist/testing/cleanupStoreContract.mjs +1 -1
  206. package/dist/testing/index.d.mts +215 -197
  207. package/dist/testing/index.mjs +289 -214
  208. package/dist/testing/outboxStoreContract.d.mts +4 -5
  209. package/dist/testing/outboxStoreContract.mjs +137 -2
  210. package/dist/testing/requiredPorts.d.mts +16 -0
  211. package/dist/testing/requiredPorts.mjs +65 -0
  212. package/dist/testing/storageContract.d.mts +4 -5
  213. package/dist/testing/storageContract.mjs +1 -1
  214. package/dist/timing-CntavTlR.mjs +46 -0
  215. package/dist/{transactionContext-CPbdlnVw.mjs → transactionContext-orvnGu4F.mjs} +1 -1
  216. package/dist/{typeGuards-BLjPgVBm.mjs → typeGuards-kEaP_WmO.mjs} +1 -1
  217. package/dist/types/index.d.mts +4 -4
  218. package/dist/types/index.mjs +1 -1
  219. package/dist/types/storage.mjs +1 -1
  220. package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
  221. package/dist/{types-CWQn9-IC.d.mts → types-D3Ixsh00.d.mts} +22 -9
  222. package/dist/{types-CgenB1gv.mjs → types-fvO6Ovzx.mjs} +4 -4
  223. package/dist/usage/index.d.mts +5 -6
  224. package/dist/usage/index.mjs +5 -5
  225. package/dist/{userHelpers-ChRvkuSZ.mjs → userHelpers-C76zD5pa.mjs} +1 -1
  226. package/dist/utils/index.d.mts +125 -264
  227. package/dist/utils/index.mjs +13 -12
  228. package/dist/{utils-CxcctrAh.mjs → utils-BpkVZ8rB.mjs} +315 -404
  229. package/dist/{validate-DrYWnOzI.mjs → validate-CWp-lsrm.mjs} +6 -14
  230. package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
  231. package/dist/{versioning-DKkT4hNJ.mjs → versioning-CHhL9P3o.mjs} +4 -4
  232. package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
  233. package/dist/{workScope-CxKqoIFn.mjs → workScope-n9a7d_lZ.mjs} +2 -2
  234. package/package.json +57 -59
  235. package/skills/arc/SKILL.md +13 -4
  236. package/skills/arc/references/auth.md +8 -5
  237. package/skills/arc/references/integrations.md +1 -1
  238. package/skills/arc/references/mcp.md +8 -6
  239. package/skills/arc/references/multi-tenancy.md +19 -14
  240. package/skills/arc/references/production.md +27 -10
  241. package/dist/actionPermissions-BoPGQ5-4.mjs +0 -350
  242. package/dist/index-CFfLKMH4.d.mts +0 -290
  243. package/dist/interface-YzIbF7Ii.mjs +0 -17
  244. package/dist/redis-stream-BPKscsTB.d.mts +0 -260
  245. package/dist/registerRoute-Bsnk6NiZ.mjs +0 -364
  246. package/dist/rolldown-runtime-eysyzFAF.mjs +0 -15
  247. package/dist/sync/changelog.d.mts +0 -3
  248. package/dist/sync/changelog.mjs +0 -3
@@ -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
 
@@ -1,350 +0,0 @@
1
- /*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
2
- import { n as getUserRoles } from "./userHelpers-ChRvkuSZ.mjs";
3
- import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-CgenB1gv.mjs";
4
- import { a as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-BHrkoF5y.mjs";
5
- import { t as buildRequestScopeProjection } from "./projection-BzIgzoKZ.mjs";
6
- import { t as executePipeline } from "./pipe-FMWNheSO.mjs";
7
- import { isPaginatedResult, toCanonicalList } from "@classytic/repo-core/pagination";
8
- //#region src/core/fastifyAdapter.ts
9
- /** Type guard for Mongoose-like documents with toObject() */
10
- function isMongooseDoc(obj) {
11
- return !!obj && typeof obj === "object" && "toObject" in obj && typeof obj.toObject === "function";
12
- }
13
- /**
14
- * Apply field mask to a single object
15
- * Filters fields based on include/exclude rules
16
- */
17
- function applyFieldMaskToObject(obj, fieldMask) {
18
- if (!obj || typeof obj !== "object") return obj;
19
- const plain = isMongooseDoc(obj) ? obj.toObject() : obj;
20
- const { include, exclude } = fieldMask;
21
- if (include && include.length > 0) {
22
- const filtered = {};
23
- for (const field of include) if (field in plain) filtered[field] = plain[field];
24
- return filtered;
25
- }
26
- if (exclude && exclude.length > 0) {
27
- const filtered = { ...plain };
28
- for (const field of exclude) delete filtered[field];
29
- return filtered;
30
- }
31
- return plain;
32
- }
33
- /**
34
- * Apply field mask to response data (handles both objects and arrays)
35
- */
36
- function applyFieldMask(data, fieldMask) {
37
- if (!fieldMask) return data;
38
- if (Array.isArray(data)) return data.map((item) => applyFieldMaskToObject(item, fieldMask));
39
- if (data && typeof data === "object") return applyFieldMaskToObject(data, fieldMask);
40
- return data;
41
- }
42
- /**
43
- * Project `FastifyRequest` into the curated `IRequestContext` shape that
44
- * arc's controllers receive. This is a developer-facing facade, not a
45
- * portability boundary — arc is Fastify-only. The projection lifts the
46
- * fields tenant-scoped controllers reach for (`organizationId`,
47
- * `teamId`, `scope`, `context`, server accessor) onto a flat, typed
48
- * surface so handler code doesn't have to dig through Fastify's
49
- * decorator surface or `req.metadata._scope` internals.
50
- */
51
- function createRequestContext(req) {
52
- const reqWithExtras = req;
53
- const requestContext = reqWithExtras.context ?? {};
54
- const srv = req.server;
55
- const serverAccessor = {
56
- events: srv && "events" in srv ? srv.events : void 0,
57
- audit: srv && "audit" in srv ? srv.audit : void 0,
58
- queryCache: srv && "queryCache" in srv ? srv.queryCache : void 0,
59
- log: req.log
60
- };
61
- const rawScope = reqWithExtras.scope;
62
- const scopeProjection = buildRequestScopeProjection(rawScope);
63
- return {
64
- query: reqWithExtras.query ?? {},
65
- body: reqWithExtras.body ?? {},
66
- params: reqWithExtras.params ?? {},
67
- headers: reqWithExtras.headers,
68
- user: reqWithExtras.user ? (() => {
69
- const user = reqWithExtras.user;
70
- const rawId = user._id ?? user.id;
71
- const normalizedId = rawId ? String(rawId) : void 0;
72
- return {
73
- ...user,
74
- id: normalizedId,
75
- _id: normalizedId
76
- };
77
- })() : null,
78
- context: requestContext,
79
- scope: scopeProjection,
80
- metadata: {
81
- ...reqWithExtras.context,
82
- arc: reqWithExtras.arc,
83
- _scope: rawScope,
84
- _ownershipCheck: reqWithExtras._ownershipCheck,
85
- _crossTenantRead: reqWithExtras._crossTenantRead,
86
- _policyFilters: reqWithExtras._policyFilters ?? {},
87
- log: reqWithExtras.log
88
- },
89
- server: serverAccessor
90
- };
91
- }
92
- /**
93
- * Get typed auth context from an IRequestContext.
94
- * Use this in controller overrides to access request context.
95
- *
96
- * For org scope, use `getControllerScope(req)` instead.
97
- */
98
- function getControllerContext(req) {
99
- return req.context ?? req.metadata ?? {};
100
- }
101
- /**
102
- * Get request scope from an IRequestContext.
103
- * Returns the RequestScope set by auth adapters.
104
- */
105
- function getControllerScope(req) {
106
- return req.metadata?._scope ?? PUBLIC_SCOPE;
107
- }
108
- /**
109
- * Compute per-field capability metadata for the current user.
110
- * Only includes fields that have restrictions — unrestricted fields
111
- * are omitted (frontend defaults to { readable: true, writable: true }).
112
- */
113
- function computeFieldCapabilities(fieldPerms, effectiveRoles) {
114
- const caps = {};
115
- for (const [field, perm] of Object.entries(fieldPerms)) {
116
- let readable = true;
117
- let writable = true;
118
- switch (perm._type) {
119
- case "hidden":
120
- readable = false;
121
- writable = false;
122
- break;
123
- case "visibleTo":
124
- readable = perm.roles?.some((r) => effectiveRoles.includes(r)) ?? false;
125
- break;
126
- case "writableBy": writable = perm.roles?.some((r) => effectiveRoles.includes(r)) ?? false;
127
- }
128
- caps[field] = {
129
- readable,
130
- writable
131
- };
132
- }
133
- return caps;
134
- }
135
- /**
136
- * Send a controller's `IControllerResponse` through Fastify's reply.
137
- *
138
- * The `IControllerResponse<T>` envelope is arc's internal pipeline
139
- * contract — it's how custom `status`, `meta`, `headers`, and field
140
- * permissions thread from a handler through interceptors to the wire
141
- * without each step having to touch `reply` directly. Not a portability
142
- * abstraction (arc is Fastify-only); a pipeline-internal one.
143
- *
144
- * **Auto-envelope of bare returns.** Custom route handlers are
145
- * expected to return the envelope shape so this function can apply
146
- * field permissions, attach `fieldCaps`, and flatten paginated lists.
147
- * A handler that returns raw `T` (array, plain object, primitive) used
148
- * to silently produce a broken response — `response.data` was
149
- * `undefined`, the body sent was `undefined`, and any declared
150
- * `fields:` permission map was bypassed (the leak shape mentora
151
- * flagged). 2.17 detects bare returns and wraps them into
152
- * `{ data: response, status: 200 }` before the field-permission
153
- * pipeline runs, so `fields:` applies to ALL custom routes whether the
154
- * handler envelopes its return value or not. The envelope shape is
155
- * still the canonical contract — auto-wrapping is a safety net, not
156
- * an invitation to drop the envelope.
157
- */
158
- function sendControllerResponse(reply, response, request) {
159
- if (response === null || typeof response !== "object" || !Object.hasOwn(response, "data")) response = {
160
- data: response,
161
- status: 200
162
- };
163
- const reqWithExtras = request;
164
- const fieldMaskConfig = reqWithExtras?.fieldMask;
165
- const arcMeta = reqWithExtras?.arc;
166
- const scope = reqWithExtras?.scope ?? PUBLIC_SCOPE;
167
- const fieldPerms = isElevated(scope) ? void 0 : arcMeta?.fields;
168
- const effectiveRoles = fieldPerms ? resolveEffectiveRoles(getUserRoles(reqWithExtras?.user), isMember(scope) ? scope.orgRoles : []) : [];
169
- const fieldCaps = fieldPerms ? computeFieldCapabilities(fieldPerms, effectiveRoles) : void 0;
170
- const hasFieldRestrictions = !!(fieldMaskConfig || fieldPerms);
171
- /** Apply both field mask and field-level permissions to a data item */
172
- const applyPermissions = (data) => {
173
- let result = fieldMaskConfig ? applyFieldMask(data, fieldMaskConfig) : data;
174
- if (fieldPerms && result && typeof result === "object") {
175
- if (Array.isArray(result)) result = result.map((item) => applyFieldReadPermissions(item, fieldPerms, effectiveRoles));
176
- else result = applyFieldReadPermissions(result, fieldPerms, effectiveRoles);
177
- }
178
- return result;
179
- };
180
- if (response.headers) for (const [key, value] of Object.entries(response.headers)) reply.header(key, value);
181
- if (response.data && typeof response.data === "object" && Array.isArray(response.data.data)) {
182
- const payload = response.data;
183
- const filteredRows = hasFieldRestrictions ? applyPermissions(payload.data) : payload.data;
184
- let canonical;
185
- if (isPaginatedResult(payload)) canonical = toCanonicalList({
186
- ...payload,
187
- data: filteredRows
188
- });
189
- else if (typeof payload.page === "number" && typeof payload.limit === "number" && typeof payload.total === "number") canonical = toCanonicalList({
190
- method: "offset",
191
- data: filteredRows,
192
- page: payload.page,
193
- limit: payload.limit,
194
- total: payload.total,
195
- pages: payload.pages ?? Math.ceil(payload.total / Math.max(1, payload.limit)),
196
- hasNext: payload.hasNext ?? false,
197
- hasPrev: payload.hasPrev ?? false
198
- });
199
- else canonical = toCanonicalList(filteredRows);
200
- reply.code(response.status ?? 200).send({
201
- ...canonical,
202
- ...response.meta ?? {},
203
- ...fieldCaps ? { fieldPermissions: fieldCaps } : {}
204
- });
205
- return;
206
- }
207
- const rawData = hasFieldRestrictions ? applyPermissions(response.data) : response.data;
208
- const filteredData = rawData !== null && typeof rawData === "object" && typeof rawData.toJSON === "function" ? rawData.toJSON() : rawData;
209
- const status = response.status ?? 200;
210
- if (filteredData !== null && typeof filteredData === "object" && !Array.isArray(filteredData) && (response.meta || fieldCaps)) {
211
- reply.code(status).send({
212
- ...filteredData,
213
- ...response.meta ?? {},
214
- ...fieldCaps ? { fieldPermissions: fieldCaps } : {}
215
- });
216
- return;
217
- }
218
- reply.code(status).send(filteredData);
219
- }
220
- /**
221
- * Bridge an `IController` method (`(ctx: IRequestContext) => Promise<IControllerResponse<T>>`)
222
- * into a Fastify route handler. Translates between arc's curated request
223
- * context surface and Fastify's request/reply, and routes the envelope
224
- * through `sendControllerResponse` so the pipeline applies field
225
- * permissions, status, and meta uniformly. Internal plumbing, not a
226
- * portability layer — arc is Fastify-only.
227
- *
228
- * @example
229
- * ```typescript
230
- * const controller = new BaseController(repository);
231
- * const listHandler = createFastifyHandler(controller.list.bind(controller));
232
- * fastify.get('/products', listHandler);
233
- * ```
234
- */
235
- function createFastifyHandler(controllerMethod) {
236
- return async (req, reply) => {
237
- sendControllerResponse(reply, await controllerMethod(createRequestContext(req)), req);
238
- return reply;
239
- };
240
- }
241
- /**
242
- * Create Fastify adapters for all CRUD methods of an IController
243
- *
244
- * Returns Fastify-compatible handlers for each CRUD operation
245
- *
246
- * @example
247
- * ```typescript
248
- * const controller = new BaseController(repository);
249
- * const handlers = createCrudHandlers(controller);
250
- *
251
- * fastify.get('/', handlers.list);
252
- * fastify.get('/:id', handlers.get);
253
- * fastify.post('/', handlers.create);
254
- * fastify.patch('/:id', handlers.update);
255
- * fastify.delete('/:id', handlers.delete);
256
- * ```
257
- */
258
- function createCrudHandlers(controller) {
259
- return {
260
- list: createFastifyHandler(controller.list.bind(controller)),
261
- get: createFastifyHandler(controller.get.bind(controller)),
262
- create: createFastifyHandler(controller.create.bind(controller)),
263
- update: createFastifyHandler(controller.update.bind(controller)),
264
- delete: createFastifyHandler(controller.delete.bind(controller))
265
- };
266
- }
267
- //#endregion
268
- //#region src/core/middlewares/pipeline.ts
269
- /**
270
- * Resolve pipeline steps for a specific operation.
271
- * Flat-array config applies to every op; map config applies per-op.
272
- */
273
- function resolvePipelineSteps(pipeline, operation) {
274
- if (!pipeline) return [];
275
- if (Array.isArray(pipeline)) return pipeline;
276
- return pipeline[operation] ?? [];
277
- }
278
- /**
279
- * Wrap a controller method (one that takes `IRequestContext` and returns
280
- * `IControllerResponse<T>`) with pipeline execution. Used by CRUD ops and
281
- * string-handler custom routes.
282
- */
283
- function buildPipelineHandler(controllerMethod, steps, operation, resourceName) {
284
- return async (req, reply) => {
285
- const pipeCtx = {
286
- ...createRequestContext(req),
287
- resource: resourceName,
288
- operation
289
- };
290
- sendControllerResponse(reply, await executePipeline(steps, pipeCtx, (ctx) => controllerMethod(ctx), operation), req);
291
- };
292
- }
293
- /**
294
- * Wrap an action handler (one that takes `(id, data, req)` and returns a raw
295
- * result) with pipeline execution. Returns a function that produces a full
296
- * `IControllerResponse<unknown>` — the action router feeds this directly into
297
- * `sendControllerResponse`, so field masking, custom status codes, `meta`,
298
- * `details`, and structured error codes from pipeline interceptors flow
299
- * through to the client unchanged.
300
- *
301
- * CRUD and actions now share the same parity invariant: a pipeline that
302
- * returns `{ success: false, status: 422, error, details, meta }` reaches the
303
- * client with all four fields intact. Previously the action path stringified
304
- * failures into a generic `Error` and dropped everything except `statusCode`.
305
- *
306
- * Handler throws still bubble out — the caller's try/catch handles `onError`
307
- * shaping and the generic `ACTION_FAILED` fallback.
308
- */
309
- function buildActionPipelineHandler(handler, steps, operation, resourceName) {
310
- if (steps.length === 0) return async (id, data, req) => ({
311
- status: 200,
312
- data: await handler(id, data, req)
313
- });
314
- return async (id, data, req) => {
315
- const pipeCtx = {
316
- ...createRequestContext(req),
317
- resource: resourceName,
318
- operation
319
- };
320
- return executePipeline(steps, pipeCtx, async (_ctx) => ({
321
- status: 200,
322
- data: await handler(id, data, req)
323
- }), operation);
324
- };
325
- }
326
- //#endregion
327
- //#region src/core/actionPermissions.ts
328
- /**
329
- * Return the effective `PermissionCheck` for a single action, or `undefined`
330
- * when the resource declares no gate at any level.
331
- *
332
- * Callers decide what "no gate" means:
333
- * - HTTP: boot-time throw in `normalizeActionsToRouterConfig`.
334
- * - MCP: tool-generation throw in `resourceToTools` (mirrors HTTP — the
335
- * two surfaces fail closed identically so MCP can't expose an
336
- * unauthenticated mutating tool when the HTTP plugin lifecycle hasn't
337
- * run).
338
- * - OpenAPI: docs advertise the endpoint as unauthenticated.
339
- */
340
- function resolveActionPermission(input) {
341
- const { action, resourcePermissions, resourceActionPermissions, globalAuth } = input;
342
- const explicit = typeof action !== "function" && action.permissions ? action.permissions : void 0;
343
- if (explicit) return explicit;
344
- if (resourceActionPermissions) return resourceActionPermissions;
345
- if (globalAuth) return globalAuth;
346
- const updateFallback = resourcePermissions?.update;
347
- if (updateFallback) return updateFallback;
348
- }
349
- //#endregion
350
- export { createCrudHandlers as a, getControllerContext as c, resolvePipelineSteps as i, getControllerScope as l, buildActionPipelineHandler as n, createFastifyHandler as o, buildPipelineHandler as r, createRequestContext as s, resolveActionPermission as t, sendControllerResponse as u };