@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
@@ -1,15 +1,22 @@
1
- /*! @classytic/arc v2.41.1 | MIT | https://classytic.com */
2
- import { t as __exportAll } from "./rolldown-runtime-eysyzFAF.mjs";
3
- import { arcLog, configureArcLogger, createPinoWriter, enterArcLoggerScope, runWithArcLogger } from "./logger/index.mjs";
4
- import { n as PUBLIC_SCOPE } from "./types-CgenB1gv.mjs";
5
- import { r as initRuntimeCapabilityRegistry, t as auditRuntimeCapabilities } from "./runtimeCapabilities-CkhS6jn6.mjs";
6
- import { n as isScopeAwareKeyGenerator, t as createTenantKeyGenerator } from "./rateLimitKey-BvOvcBDi.mjs";
7
- import { i as createRequestIdGenerator, r as mergeHealthChecks } from "./health-ZRHeDHXH.mjs";
8
- import { t as parseJsonBody } from "./jsonBody-ELR9lfeU.mjs";
9
- import { r as wrapWithBoundary } from "./subscribe-helpers-DatDkcuG.mjs";
10
- import { isProductionEnv } from "@classytic/primitives/environment";
1
+ /*! @classytic/arc v2.44.0 | MIT | https://classytic.com */
2
+ import { r as __exportAll } from "./rolldown-runtime-MYrLNGnq.mjs";
3
+ import { a as enterArcLoggerScope, i as runWithArcLogger, n as configureArcLogger, r as createPinoWriter, t as arcLog } from "./logger-D4V1zGLI.mjs";
4
+ import { n as PUBLIC_SCOPE } from "./types-fvO6Ovzx.mjs";
5
+ import { r as initRuntimeCapabilityRegistry, t as auditRuntimeCapabilities } from "./runtimeCapabilities-DIMKMV2G.mjs";
6
+ import { a as runShutdown, t as initShutdownRegistry } from "./shutdown-LpLMH8T1.mjs";
7
+ import { n as isScopeAwareKeyGenerator, t as createTenantKeyGenerator } from "./rateLimitKey-DVEZ_UpV.mjs";
8
+ import { i as resolveResourceRateLimiter, n as markRateLimitingDisabled } from "./resourceRateLimit-urWPJxrt.mjs";
9
+ import { r as markArcRootValidator, t as arcAjvCustomOptions } from "./routeValidator-DHtpzrzk.mjs";
10
+ import { c as createRequestIdGenerator, r as mergeHealthChecks } from "./health-CrqUp3sz.mjs";
11
+ import { t as parseJsonBody } from "./jsonBody-YIq-vGLv.mjs";
12
+ import { a as portsForModule, b as verifyEventDurability, f as resolveModuleArm, g as crudEventsOf, h as buildModuleGraph, m as analyzeModuleGraph, n as composeModules, x as verifyModuleGraph } from "./plan-CJGMKoui.mjs";
13
+ import { c as wrapWithAttribution, f as defineEvent, l as wrapWithBoundary, r as CRUD_EVENT_SCHEMA } from "./eventTypes-N4-omQFR.mjs";
14
+ import { r as withSubscriberId } from "./subscriberIdentity-DeyppIif.mjs";
15
+ import { r as getOptionalModuleExports } from "./module--L0iBIZz.mjs";
16
+ import { isProductionEnv, readEnv } from "@classytic/primitives/environment";
11
17
  import Fastify from "fastify";
12
18
  import qs from "qs";
19
+ import { createEventRouter } from "@classytic/primitives/events";
13
20
  //#region src/factory/security/pluginLoader.ts
14
21
  const PLUGIN_REGISTRY = {
15
22
  cors: {
@@ -205,47 +212,6 @@ async function registerAssetRoots(fastify, roots) {
205
212
  }
206
213
  }
207
214
  //#endregion
208
- //#region src/factory/module/resolve.ts
209
- /**
210
- * Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
211
- * (this is where a dynamic `import()` fires); a promise (or plain module) is
212
- * awaited. Called once per module at the start of the bootstrap phase.
213
- */
214
- async function resolveModule(input) {
215
- const resolved = typeof input === "function" ? await input() : await input;
216
- if (!resolved || typeof resolved !== "object" || typeof resolved.name !== "string") throw new Error("[arc] a `modules` entry resolved to something that is not an ArcModule (expected an object with a string `name`). Check dynamic-import thunks return `createXModule(deps)` (or the module object), not the namespace.");
217
- if (resolved.name.trim().length === 0) throw new Error("[arc] a `modules` entry has an empty (or whitespace-only) `name`. A module's name is its graph key, its `arc.modules` export key, its `dependsOn` target, and its label in every boot diagnostic — give it a stable, non-empty identifier.");
218
- return resolved;
219
- }
220
- /** Resolve a `ModuleContribution` to a concrete array (array passthrough or factory call). */
221
- async function resolveContribution(contribution, fastify) {
222
- if (!contribution) return [];
223
- return typeof contribution === "function" ? await contribution(fastify) : contribution;
224
- }
225
- /**
226
- * `resolveContribution` with module attribution — the form every arm collector
227
- * should use.
228
- *
229
- * A contribution factory closes over booted engines, so it throws for exactly
230
- * the reasons a bootstrap does ("connection unavailable", "engine not ready").
231
- * Bare, that message names neither the module nor the arm, which in a graph of
232
- * twenty modules turns a one-line fix into a bisect. Every other module
233
- * boundary (`plugins`, `bootstrap`, `resources`, `afterResources`) already
234
- * wraps with the owning module's name; this keeps the arms consistent with
235
- * them. The original error is preserved via `{ cause }`.
236
- */
237
- async function resolveModuleArm(module, arm, contribution, fastify) {
238
- let resolved;
239
- try {
240
- resolved = await resolveContribution(contribution, fastify);
241
- } catch (err) {
242
- const msg = err instanceof Error ? err.message : String(err);
243
- throw new Error(`[arc] module "${module.name}" ${arm} factory threw: ${msg}`, { cause: err });
244
- }
245
- if (!Array.isArray(resolved)) throw new TypeError(`[arc] module "${module.name}" ${arm} factory must return an array — received ${resolved === null ? "null" : typeof resolved}.`);
246
- return resolved;
247
- }
248
- //#endregion
249
215
  //#region src/factory/module/contributions.ts
250
216
  /**
251
217
  * Collect every module's `healthChecks` in the caller's order. Fails at boot
@@ -321,7 +287,15 @@ function describeResolvedModule(input) {
321
287
  }
322
288
  //#endregion
323
289
  //#region src/factory/module/disposers.ts
324
- function createDisposerRegistry(fastify) {
290
+ const NO_PORTS = Object.freeze({});
291
+ /**
292
+ * The one typing boundary for ports: `assertModuleRequirements` has already proven every
293
+ * declared required port present, so the module's subset is read as its declared shape.
294
+ */
295
+ function asModulePorts(ports) {
296
+ return ports;
297
+ }
298
+ function createDisposerRegistry(fastify, portsOf = () => NO_PORTS) {
325
299
  const stacks = /* @__PURE__ */ new Map();
326
300
  const drained = /* @__PURE__ */ new Set();
327
301
  const contexts = /* @__PURE__ */ new Map();
@@ -329,25 +303,28 @@ function createDisposerRegistry(fastify) {
329
303
  contextFor(moduleName) {
330
304
  const existing = contexts.get(moduleName);
331
305
  if (existing) return existing;
332
- const ctx = { defer(disposer) {
333
- if (typeof disposer !== "function") throw new TypeError(`[arc] module "${moduleName}" called defer() with a ${typeof disposer} — expected a function.`);
334
- if (drained.has(moduleName)) {
335
- (async () => {
336
- try {
337
- await disposer();
338
- } catch (err) {
339
- fastify.log?.error({
340
- err,
341
- module: moduleName
342
- }, `[arc] module "${moduleName}" deferred a disposer after teardown had already run; it was invoked immediately and threw.`);
343
- }
344
- })();
345
- return;
306
+ const ctx = {
307
+ ports: asModulePorts(portsOf(moduleName)),
308
+ defer(disposer) {
309
+ if (typeof disposer !== "function") throw new TypeError(`[arc] module "${moduleName}" called defer() with a ${typeof disposer} — expected a function.`);
310
+ if (drained.has(moduleName)) {
311
+ (async () => {
312
+ try {
313
+ await disposer();
314
+ } catch (err) {
315
+ fastify.log?.error({
316
+ err,
317
+ module: moduleName
318
+ }, `[arc] module "${moduleName}" deferred a disposer after teardown had already run; it was invoked immediately and threw.`);
319
+ }
320
+ })();
321
+ return;
322
+ }
323
+ const stack = stacks.get(moduleName);
324
+ if (stack) stack.push(disposer);
325
+ else stacks.set(moduleName, [disposer]);
346
326
  }
347
- const stack = stacks.get(moduleName);
348
- if (stack) stack.push(disposer);
349
- else stacks.set(moduleName, [disposer]);
350
- } };
327
+ };
351
328
  contexts.set(moduleName, ctx);
352
329
  return ctx;
353
330
  },
@@ -372,7 +349,7 @@ function createDisposerRegistry(fastify) {
372
349
  * Resolves factory contributions (after bootstraps) so a handler can close over
373
350
  * booted engines rather than a global getter.
374
351
  */
375
- async function subscribeModuleEventHandlers(fastify, modules, countsByModule) {
352
+ async function subscribeModuleEventHandlers(fastify, modules, countsByModule, consumedByModule, handlersByModule) {
376
353
  const unsubscribes = [];
377
354
  const owner = /* @__PURE__ */ new Map();
378
355
  try {
@@ -382,14 +359,24 @@ async function subscribeModuleEventHandlers(fastify, modules, countsByModule) {
382
359
  if (handlers.length === 0) continue;
383
360
  const bus = fastify.events;
384
361
  if (!bus) throw new Error(`[arc] module "${m.name}" declares eventHandlers but the event subsystem is unavailable (fastify.events). Enable arcPlugins.events.`);
385
- for (const def of handlers) {
362
+ for (const [index, def] of handlers.entries()) {
386
363
  if (def.name !== void 0) {
387
364
  const prior = owner.get(def.name);
388
365
  if (prior !== void 0) throw new Error(`[arc] duplicate event-handler name "${def.name}" — declared by module "${prior}" and module "${m.name}". Named event handlers must be unique across the module graph; prefix names with the owning module.`);
389
366
  owner.set(def.name, m.name);
390
367
  }
391
- const handler = applyBoundary(def, m.name, fastify);
392
368
  const patterns = Array.isArray(def.event) ? def.event : [def.event];
369
+ if (def.durability === "retry" && def.boundary) throw new Error(`[arc] event handler "${def.name ?? patterns.join(",")}" in module "${m.name}" declares durability 'retry' and a boundary — a boundary swallows, so it is 'boundary'.`);
370
+ const label = handlerLabel(def, m.name, patterns, index);
371
+ const identity = def.name ?? [...patterns].sort().join(",");
372
+ if (!def.name && handlers.some((other) => other !== def && !other.name && (Array.isArray(other.event) ? [...other.event] : [other.event]).sort().join(",") === identity)) throw new Error(`[arc] module "${m.name}" has ambiguous unnamed event handlers for "${identity}" — give each handler a stable name.`);
373
+ const handler = withSubscriberId(`${m.name}:${identity}`, applyBoundary(def, m.name, label, fastify));
374
+ handlersByModule?.set(m.name, [...handlersByModule.get(m.name) ?? [], {
375
+ label,
376
+ patterns,
377
+ ...def.durability !== void 0 ? { durability: def.durability } : {}
378
+ }]);
379
+ consumedByModule?.set(m.name, [...consumedByModule.get(m.name) ?? [], ...patterns]);
393
380
  for (const pattern of patterns) unsubscribes.push(await bus.subscribe(pattern, handler));
394
381
  }
395
382
  }
@@ -401,18 +388,24 @@ async function subscribeModuleEventHandlers(fastify, modules, countsByModule) {
401
388
  }
402
389
  }
403
390
  /**
404
- * Apply the declaration's opt-in error boundary. Without `boundary`, the raw
405
- * handler is subscribed and a throw reaches the transport — which is what makes
406
- * the durable path work (unacked → redelivered → DLQ). With it, failures are
407
- * logged through `fastify.log` and swallowed, for handlers whose retry would
408
- * only delay the next event's resync.
409
- *
410
- * The boundary label falls back to `<module>.<pattern>` so an unnamed handler
411
- * still produces an attributable log line instead of "anonymous".
391
+ * The name a subscriber's errors and logs carry: its declared `name`, else
392
+ * `<module>:<patterns>#<index in the module's handlers>` — unique, so two unnamed
393
+ * subscribers of one event stay distinguishable in a dead-lettered row.
394
+ */
395
+ function handlerLabel(def, moduleName, patterns, index) {
396
+ return def.name ?? `${moduleName}:${patterns.join(",")}#${index}`;
397
+ }
398
+ /**
399
+ * Without a boundary, attribute the handler's errors to `label` and let them reach the transport —
400
+ * that is what makes the durable path work (unacked → redelivered → DLQ), and the label is what
401
+ * names the subscriber in the outbox's `lastError`. With the opt-in boundary, failures are logged
402
+ * under the handler's name (`<module>.<pattern>` when unnamed) and swallowed, so the original error
403
+ * reaches the boundary's `onError` untouched.
412
404
  */
413
- function applyBoundary(def, moduleName, fastify) {
414
- if (!def.boundary) return def.handler;
415
- const onError = typeof def.boundary === "object" ? def.boundary.onError : void 0;
405
+ function applyBoundary(def, moduleName, label, fastify) {
406
+ const boundary = def.boundary ?? def.durability === "boundary";
407
+ if (!boundary) return wrapWithAttribution(def.handler, label);
408
+ const onError = typeof boundary === "object" ? boundary.onError : void 0;
416
409
  return wrapWithBoundary(def.handler, {
417
410
  logger: fastify.log,
418
411
  name: def.name ?? `${moduleName}.${String(def.event)}`,
@@ -430,98 +423,6 @@ async function unsubscribeModuleEventHandlers(unsubscribes) {
430
423
  return errors;
431
424
  }
432
425
  //#endregion
433
- //#region src/factory/module/order.ts
434
- /**
435
- * Order modules for composition by their `dependsOn` edges — a STABLE
436
- * topological sort. Called once at the start of the bootstrap phase; every
437
- * subsequent phase (bootstrap, resources, afterResources, and reverse-order
438
- * onClose) iterates the returned list, so a module's declared dependencies are
439
- * always composed before it.
440
- *
441
- * "Stable" = modules with no edge between them keep their original list order,
442
- * so declaring `dependsOn` on one module never silently reorders an unrelated
443
- * one. Backward compatible: a `modules` array with NO `dependsOn` anywhere is
444
- * returned unchanged.
445
- *
446
- * Fail-fast — throws (never reorders past a broken contract) on:
447
- * - duplicate module names (the name is the graph key)
448
- * - a `dependsOn` name not present in the composed set
449
- * - a self-reference (`dependsOn` includes the module's own name)
450
- * - a dependency cycle (reports the concrete `a → b → … → a` path)
451
- */
452
- function orderModules(modules) {
453
- const byName = /* @__PURE__ */ new Map();
454
- for (const m of modules) {
455
- if (byName.has(m.name)) throw new Error(`[arc] Duplicate module name "${m.name}" — composed twice; check your modules array.`);
456
- byName.set(m.name, m);
457
- }
458
- for (const m of modules) for (const dep of m.dependsOn ?? []) {
459
- if (dep === m.name) throw new Error(`[arc] module "${m.name}" dependsOn itself — remove the self-reference.`);
460
- if (!byName.has(dep)) throw new Error(`[arc] module "${m.name}" dependsOn "${dep}", which is not composed. Add the "${dep}" module to createApp({ modules }) (before this one is fine — arc orders them), or drop the dependency. Composed modules: ${[...byName.keys()].join(", ") || "(none)"}.`);
461
- }
462
- if (modules.every((m) => !m.dependsOn || m.dependsOn.length === 0)) return [...modules];
463
- const originalIndex = /* @__PURE__ */ new Map();
464
- let nextIndex = 0;
465
- for (const m of modules) originalIndex.set(m.name, nextIndex++);
466
- const pendingDeps = new Map(modules.map((m) => [m.name, (m.dependsOn ?? []).length]));
467
- const dependents = /* @__PURE__ */ new Map();
468
- for (const m of modules) for (const dep of m.dependsOn ?? []) {
469
- const list = dependents.get(dep);
470
- if (list) list.push(m.name);
471
- else dependents.set(dep, [m.name]);
472
- }
473
- const ordered = [];
474
- const remaining = new Set(modules.map((m) => m.name));
475
- while (remaining.size > 0) {
476
- let pick;
477
- let pickIndex = Number.POSITIVE_INFINITY;
478
- for (const name of remaining) {
479
- if (pendingDeps.get(name) !== 0) continue;
480
- const index = originalIndex.get(name) ?? Number.POSITIVE_INFINITY;
481
- if (index < pickIndex) {
482
- pick = name;
483
- pickIndex = index;
484
- }
485
- }
486
- if (pick === void 0) throw new Error(describeModuleCycle(remaining, byName));
487
- const picked = byName.get(pick);
488
- if (!picked) throw new Error(`[arc] internal: ordered module "${pick}" is missing from the index`);
489
- ordered.push(picked);
490
- remaining.delete(pick);
491
- for (const dependent of dependents.get(pick) ?? []) pendingDeps.set(dependent, (pendingDeps.get(dependent) ?? 1) - 1);
492
- }
493
- return ordered;
494
- }
495
- /** Walk the still-unordered subgraph for one concrete cycle path. */
496
- function describeModuleCycle(remaining, byName) {
497
- const stack = [];
498
- const onStack = /* @__PURE__ */ new Set();
499
- const done = /* @__PURE__ */ new Set();
500
- let cycle = null;
501
- const walk = (name) => {
502
- if (cycle) return;
503
- stack.push(name);
504
- onStack.add(name);
505
- for (const dep of byName.get(name)?.dependsOn ?? []) {
506
- if (!remaining.has(dep)) continue;
507
- if (onStack.has(dep)) {
508
- cycle = [...stack.slice(stack.indexOf(dep)), dep];
509
- return;
510
- }
511
- if (!done.has(dep)) walk(dep);
512
- if (cycle) return;
513
- }
514
- onStack.delete(name);
515
- stack.pop();
516
- done.add(name);
517
- };
518
- for (const name of remaining) {
519
- if (!done.has(name)) walk(name);
520
- if (cycle) break;
521
- }
522
- return `[arc] module dependency cycle: ${cycle ? cycle.join(" → ") : [...remaining].join(", ")}. Modules cannot dependsOn each other circularly — break it with a shared module both point at, or wire the softer direction through an event/port instead of a hard dependsOn.`;
523
- }
524
- //#endregion
525
426
  //#region src/factory/module/state.ts
526
427
  /**
527
428
  * The honest read of the state map. `fastify.arc` is optional (see
@@ -578,6 +479,28 @@ function getModuleState(fastify, name) {
578
479
  const states = readStates(fastify);
579
480
  return states !== void 0 && Object.hasOwn(states, name) ? states[name] : void 0;
580
481
  }
482
+ /**
483
+ * The `ResourceDefinition`s module `name` mounted, in its declared order — for building that
484
+ * module's MCP tools or docs after boot. Throws when `name` is not a composed module (a typo would
485
+ * otherwise read as "no resources"), and when called before `registerResources` has published the
486
+ * descriptors. A resource declaring `skipRegistry` is absent, as from every introspection surface.
487
+ */
488
+ function getModuleResources(fastify, name) {
489
+ const descriptors = fastify.arc?.moduleDescriptors;
490
+ const registry = fastify.arc?.registry;
491
+ if (!descriptors || !registry) throw new Error(`[arc] getModuleResources("${name}"): no composed module graph on this instance yet — call it after boot (afterResources, onReady, or after app.ready()).`);
492
+ const descriptor = descriptors.find((d) => d.name === name);
493
+ if (!descriptor) {
494
+ const known = descriptors.map((d) => d.name).join(", ") || "(none)";
495
+ throw new Error(`[arc] getModuleResources: "${name}" is not a composed module. Composed: ${known}.`);
496
+ }
497
+ const out = [];
498
+ for (const resource of descriptor.resources) {
499
+ const definition = resource.name === void 0 ? void 0 : registry.getDefinition(resource.name);
500
+ if (definition) out.push(definition);
501
+ }
502
+ return out;
503
+ }
581
504
  //#endregion
582
505
  //#region src/factory/module/teardown.ts
583
506
  function createModuleTeardown(fastify, modules, disposers) {
@@ -851,18 +774,13 @@ async function registerArcCore(fastify, config, trackPlugin) {
851
774
  await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
852
775
  trackPlugin("arc-core");
853
776
  if (config.arcPlugins?.events !== false) {
854
- const { default: eventPlugin } = await import("./eventPlugin-zZY-unhr.mjs").then((n) => n.n);
777
+ const { default: eventPlugin } = await import("./eventPlugin-BweHAVws.mjs").then((n) => n.r);
855
778
  const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
856
779
  await fastify.register(eventPlugin, {
857
780
  /**
858
- * An EXPLICIT `runtime: 'memory'` is the host's single-instance
859
- * declaration (its JSDoc says exactly that), so it flows down as the
860
- * event plugin's `singleProcess` flag — one topology axis, declared
861
- * once, no second knob to keep in sync. The comparison is against the
862
- * literal, not the resolved default: `runtime` UNSET also means memory
863
- * stores, but an unstated topology must keep the forgotten-default
864
- * warn. `eventOpts` spreads after, so a direct
865
- * `arcPlugins.events.singleProcess` still wins either way.
781
+ * Only an EXPLICIT `runtime: 'memory'` declares single-instance — an
782
+ * UNSET runtime must keep the forgotten-default warn. `eventOpts`
783
+ * spreads after, so `arcPlugins.events.singleProcess` still wins.
866
784
  */
867
785
  ...config.runtime === "memory" ? { singleProcess: true } : {},
868
786
  ...eventOpts,
@@ -911,15 +829,15 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules, moduleH
911
829
  trackPlugin("arc-graceful-shutdown", shutdownOpts);
912
830
  }
913
831
  if (config.arcPlugins?.caching) {
914
- const { default: cachingPlugin } = await import("./caching-DbCBxlgw.mjs").then((n) => n.r);
832
+ const { default: cachingPlugin } = await import("./caching-Drp68DIS.mjs").then((n) => n.n);
915
833
  const opts = config.arcPlugins.caching === true ? {} : config.arcPlugins.caching;
916
834
  await fastify.register(cachingPlugin, opts);
917
835
  trackPlugin("arc-caching", opts);
918
836
  }
919
837
  if (config.arcPlugins?.queryCache) {
920
- const { queryCachePlugin } = await import("./queryCachePlugin-DZHPOm9p.mjs").then((n) => n.n);
838
+ const { queryCachePlugin } = await import("./queryCachePlugin-BATECpyh.mjs").then((n) => n.n);
921
839
  const opts = config.arcPlugins.queryCache === true ? {} : config.arcPlugins.queryCache;
922
- const store = config.stores?.queryCache ?? new (await (import("./memory-Iotz0cCx.mjs").then((n) => n.n))).MemoryCacheStore();
840
+ const store = config.stores?.queryCache ?? new (await (import("./memory-CTa5pOCW.mjs").then((n) => n.n))).MemoryCacheStore();
923
841
  await fastify.register(queryCachePlugin, {
924
842
  store,
925
843
  ...opts
@@ -929,7 +847,7 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules, moduleH
929
847
  if (config.arcPlugins?.sse) {
930
848
  if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
931
849
  else {
932
- const { default: ssePlugin } = await import("./sse-BeqVKQKu.mjs").then((n) => n.r);
850
+ const { default: ssePlugin } = await import("./sse-BzJFPX_1.mjs").then((n) => n.n);
933
851
  const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
934
852
  await fastify.register(ssePlugin, opts);
935
853
  trackPlugin("arc-sse", opts);
@@ -938,20 +856,20 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules, moduleH
938
856
  if (config.arcPlugins?.realtime) {
939
857
  if (config.arcPlugins?.events === false) fastify.log.warn("Realtime plugin requires events plugin (arcPlugins.events). Realtime disabled.");
940
858
  else {
941
- const { default: realtimePlugin } = await import("./realtime-8tIQWZz4.mjs").then((n) => n.r);
859
+ const { default: realtimePlugin } = await import("./realtime-xyubMKSB.mjs").then((n) => n.n);
942
860
  const opts = config.arcPlugins.realtime === true ? {} : config.arcPlugins.realtime;
943
861
  await fastify.register(realtimePlugin, opts);
944
862
  trackPlugin("arc-realtime", opts);
945
863
  }
946
864
  }
947
865
  if (config.arcPlugins?.metrics) {
948
- const { default: metricsPlugin } = await import("./metrics-NMsmpRmY.mjs").then((n) => n.r);
866
+ const { default: metricsPlugin } = await import("./metrics-DsZrvnpB.mjs").then((n) => n.n);
949
867
  const opts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
950
868
  await fastify.register(metricsPlugin, opts);
951
869
  trackPlugin("arc-metrics", opts);
952
870
  }
953
871
  if (config.arcPlugins?.versioning) {
954
- const { default: versioningPlugin } = await import("./versioning-DKkT4hNJ.mjs").then((n) => n.r);
872
+ const { default: versioningPlugin } = await import("./versioning-CHhL9P3o.mjs").then((n) => n.n);
955
873
  await fastify.register(versioningPlugin, config.arcPlugins.versioning);
956
874
  trackPlugin("arc-versioning", config.arcPlugins.versioning);
957
875
  }
@@ -1028,7 +946,7 @@ async function registerAuth(fastify, config, trackPlugin) {
1028
946
  */
1029
947
  async function registerElevation(fastify, config, trackPlugin) {
1030
948
  if (!config.elevation) return;
1031
- const { elevationPlugin } = await import("./elevation-CkvG0LG2.mjs").then((n) => n.r);
949
+ const { elevationPlugin } = await import("./elevation-BG4gnMsJ.mjs").then((n) => n.n);
1032
950
  await fastify.register(elevationPlugin, config.elevation);
1033
951
  trackPlugin("arc-elevation", config.elevation);
1034
952
  fastify.log.debug("Elevation plugin enabled");
@@ -1043,7 +961,7 @@ async function registerElevation(fastify, config, trackPlugin) {
1043
961
  */
1044
962
  async function registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers) {
1045
963
  if (config.errorHandler === false) return;
1046
- const { errorHandlerPlugin } = await import("./errorHandler-D1GHKGlV.mjs").then((n) => n.r);
964
+ const { errorHandlerPlugin } = await import("./errorHandler-COHw3N1b.mjs").then((n) => n.r);
1047
965
  /**
1048
966
  * DISCLOSURE DEFAULTS MERGE — they used to be REPLACED, and that was two bugs.
1049
967
  *
@@ -1128,6 +1046,7 @@ function createOptionalAuthenticate(authenticate) {
1128
1046
  }
1129
1047
  //#endregion
1130
1048
  //#region src/factory/registerResources.ts
1049
+ /** Resource registration and the module/app lifecycle for createApp. */
1131
1050
  function isResourcesFactory(value) {
1132
1051
  return typeof value === "function";
1133
1052
  }
@@ -1150,8 +1069,8 @@ async function registerOne(parent, resource, mountRoutes) {
1150
1069
  * infra. Every phase below iterates that order; each module phase runs BEFORE
1151
1070
  * its app-level counterpart:
1152
1071
  *
1153
- * 1. resolve + orderModules — validate the graph (dup names, missing/self
1154
- * deps, cycles fail fast) BEFORE side effects
1072
+ * 1. composeModules — validate the graph (dup names, missing/self
1073
+ * deps, cycles, unmet `requires`) BEFORE side effects
1155
1074
  * 2. plugins() — app infra (DB, data, webhooks)
1156
1075
  * 3. module.plugins — module infra, dependsOn order, before bootstraps
1157
1076
  * 4a. module.bootstrap — engine init (return value → arc.modules[name])
@@ -1170,33 +1089,27 @@ async function registerOne(parent, resource, mountRoutes) {
1170
1089
  * THEN app onClose, in ONE hook so module
1171
1090
  * teardown runs before shared infra closes.
1172
1091
  *
1173
- * Boot is TRANSACTIONAL for module-held resources: phases 2–6 run inside one
1174
- * try/catch. On any failure, arc unsubscribes already-active module event
1175
- * handlers, then closes every module that ENTERED an init phase — reverse
1176
- * composition order, best-effort (see module/teardown.ts) — and rethrows the
1177
- * ORIGINAL boot error. Without this, a failure after module A's bootstrap but
1178
- * before the §7b hook registration leaked A's clients/timers/engines with no
1179
- * teardown path.
1180
- *
1181
- * "Entered", not "completed": a module is eligible for teardown from the
1182
- * moment it enters `plugins()`/`bootstrap()`, so the callback that allocates a
1183
- * client and THEN throws — the commonest partial-init shape — gets its closer
1184
- * called too.
1092
+ * Boot is TRANSACTIONAL for module-held resources: on any failure in phases
1093
+ * 2–6, arc unsubscribes live module event handlers, closes every module that
1094
+ * ENTERED an init phase (see module/teardown.ts), and rethrows the ORIGINAL
1095
+ * boot error.
1185
1096
  */
1186
- async function registerResources(fastify, config, preResolvedModules) {
1097
+ async function registerResources(fastify, config, preResolvedModules, skippedBridges = []) {
1098
+ if (config.strictResources === void 0) config = {
1099
+ ...config,
1100
+ strictResources: true
1101
+ };
1187
1102
  if (config.preset === "production") {
1188
- if (config.strictResources === void 0) config = {
1189
- ...config,
1190
- strictResources: true
1191
- };
1192
1103
  if (config.strictResourceDir === void 0) config = {
1193
1104
  ...config,
1194
1105
  strictResourceDir: true
1195
1106
  };
1196
1107
  }
1197
- const modules = preResolvedModules ?? orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
1108
+ if (config.strictResources === false) fastify.arc?.registry?.tolerateDuplicateNames();
1109
+ const modules = preResolvedModules ?? (await composeModules(config.modules ?? [], config.ports)).modules;
1198
1110
  initModuleStates(fastify, modules.map((m) => m.name));
1199
- const disposers = createDisposerRegistry(fastify);
1111
+ const portsByModule = new Map(modules.map((m) => [m.name, portsForModule(m, config.ports)]));
1112
+ const disposers = createDisposerRegistry(fastify, (name) => portsByModule.get(name) ?? {});
1200
1113
  const teardown = createModuleTeardown(fastify, modules, disposers);
1201
1114
  let eventUnsubscribes = [];
1202
1115
  try {
@@ -1267,7 +1180,7 @@ async function registerResources(fastify, config, preResolvedModules) {
1267
1180
  let discoveryPath;
1268
1181
  let discoveryYieldedZero = false;
1269
1182
  if (resolvedResources === void 0 && config.resourceDir) {
1270
- const { loadResources } = await import("./loadResources-BPfWiI5s.mjs").then((n) => n.n);
1183
+ const { loadResources } = await import("./loadResources-6jO52-RR.mjs").then((n) => n.n);
1271
1184
  const { resolve, dirname } = await import("node:path");
1272
1185
  const { fileURLToPath } = await import("node:url");
1273
1186
  const rawDir = config.resourceDir;
@@ -1276,7 +1189,7 @@ async function registerResources(fastify, config, preResolvedModules) {
1276
1189
  discoveryPath = dir;
1277
1190
  const discovered = await loadResources(dir, { logger: fastify.log });
1278
1191
  if (discovered.length === 0) {
1279
- if (config.strictResourceDir) throw new Error(`[arc] loadResources: resourceDir "${rawDir}" resolved to "${dir}" but yielded 0 resources. Check the path, file naming (*.resource.{ts,js,mts,mjs}), and runtime layout (src/ vs dist/). Use \`strictResourceDir: true\` to fail boot.`);
1192
+ if (config.strictResourceDir) throw new Error(`[arc] loadResources: resourceDir "${rawDir}" resolved to "${dir}" but yielded 0 resources. Check the path, file naming (*.resource.{ts,js,mts,mjs}), and runtime layout (src/ vs dist/).`);
1280
1193
  discoveryYieldedZero = true;
1281
1194
  }
1282
1195
  resolvedResources = discovered;
@@ -1336,9 +1249,9 @@ Resources supplied by "${m.name}": ${provided}.`);
1336
1249
  const seen = /* @__PURE__ */ new Set();
1337
1250
  for (const resource of resolvedResources) if (resource.name) {
1338
1251
  if (seen.has(resource.name)) {
1339
- const msg = `Duplicate resource name "${resource.name}" detected. This will cause route conflicts. Check your resources array and loadResources() output. Common cause: stale compiled files in dist/ alongside src/. Use \`strictResources: true\` to fail boot.`;
1252
+ const msg = `Duplicate resource name "${resource.name}" detected. This will cause route conflicts. Check your resources array and loadResources() output. Common cause: stale compiled files in dist/ alongside src/.`;
1340
1253
  if (config.strictResources) throw new Error(msg);
1341
- fastify.log.warn(msg);
1254
+ fastify.log.warn(`${msg} Boot continues because strictResources is false.`);
1342
1255
  }
1343
1256
  seen.add(resource.name);
1344
1257
  }
@@ -1348,10 +1261,12 @@ Resources supplied by "${m.name}": ${provided}.`);
1348
1261
  else prefixed.push(resource);
1349
1262
  for (const resource of root) await registerOne(fastify, resource, config.mountRoutes);
1350
1263
  if (prefixed.length) {
1351
- if (config.resourcePrefix) await fastify.register(async (scoped) => {
1352
- for (const resource of prefixed) await registerOne(scoped, resource, config.mountRoutes);
1353
- }, { prefix: config.resourcePrefix });
1354
- else for (const resource of prefixed) await registerOne(fastify, resource, config.mountRoutes);
1264
+ if (config.resourcePrefix) {
1265
+ const registerPrefixed = async (scoped) => {
1266
+ for (const resource of prefixed) await registerOne(scoped, resource, config.mountRoutes);
1267
+ };
1268
+ await fastify.register(registerPrefixed, { prefix: config.resourcePrefix });
1269
+ } else for (const resource of prefixed) await registerOne(fastify, resource, config.mountRoutes);
1355
1270
  }
1356
1271
  const names = resolvedResources.map((r) => r.name ?? "?").join(", ");
1357
1272
  const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
@@ -1375,11 +1290,8 @@ Resources supplied by "${m.name}": ${provided}.`);
1375
1290
  const scheduleCounts = /* @__PURE__ */ new Map();
1376
1291
  const collectedJobs = await collectModuleScheduledJobs(fastify, modules, scheduleCounts);
1377
1292
  /**
1378
- * ROLE gate (CreateAppOptions.role). Arms a role does not run are SKIPPED
1379
- * and logged by name, never fatal: a module legitimately declares arms
1380
- * that a different deployment of the same codebase executes. 'relay' runs
1381
- * only kind:'relay' arms (the outbox relays); 'api'/'worker' run none;
1382
- * 'scheduler'/'all' run everything.
1293
+ * ROLE gate: arms this role does not run are skipped and logged, never
1294
+ * fatal — another deployment of the same codebase runs them.
1383
1295
  */
1384
1296
  const role = config.role ?? "all";
1385
1297
  const scheduledJobs = role === "scheduler" || role === "all" ? collectedJobs : role === "relay" ? collectedJobs.filter((j) => j.kind === "relay") : [];
@@ -1393,7 +1305,7 @@ Resources supplied by "${m.name}": ${provided}.`);
1393
1305
  if (fastify.hasDecorator("getScheduleStats")) throw new Error("[arc] modules declare scheduledJobs but schedulesPlugin was already registered manually. Configure their runner through arcPlugins.schedules so Arc can compose one schedule table.");
1394
1306
  const frozenJobs = Object.freeze(scheduledJobs.map((job) => Object.freeze({ ...job })));
1395
1307
  const scheduleOptions = typeof config.arcPlugins?.schedules === "object" ? config.arcPlugins.schedules : {};
1396
- const { default: schedulesPlugin } = await import("./schedules-j6uXXAeJ.mjs").then((n) => n.r);
1308
+ const { default: schedulesPlugin } = await import("./schedules-CKTs5ys8.mjs").then((n) => n.n);
1397
1309
  await fastify.register(schedulesPlugin, {
1398
1310
  ...scheduleOptions,
1399
1311
  schedules: frozenJobs
@@ -1408,7 +1320,64 @@ Resources supplied by "${m.name}": ${provided}.`);
1408
1320
  }
1409
1321
  }
1410
1322
  const subscriptionCounts = /* @__PURE__ */ new Map();
1411
- eventUnsubscribes = await subscribeModuleEventHandlers(fastify, modules, subscriptionCounts);
1323
+ const consumedByModule = /* @__PURE__ */ new Map();
1324
+ const handlersByModule = /* @__PURE__ */ new Map();
1325
+ eventUnsubscribes = await subscribeModuleEventHandlers(fastify, modules, subscriptionCounts, consumedByModule, handlersByModule);
1326
+ const registry = fastify.arc?.registry;
1327
+ const emitsCrud = config.arcPlugins?.emitEvents !== false && registry !== void 0;
1328
+ const derived = /* @__PURE__ */ new Map();
1329
+ const moduleOwned = /* @__PURE__ */ new Set();
1330
+ if (emitsCrud) for (const [moduleName, names] of moduleResourceNames) {
1331
+ const publishes = [];
1332
+ for (const name of names) {
1333
+ moduleOwned.add(name);
1334
+ const entry = registry.get(name);
1335
+ if (entry) publishes.push(...crudEventsOf(entry));
1336
+ }
1337
+ derived.set(moduleName, {
1338
+ publishes,
1339
+ namespaces: [...names]
1340
+ });
1341
+ }
1342
+ const appCrudEvents = emitsCrud ? registry.getAll().filter((e) => !moduleOwned.has(e.name)).flatMap((e) => crudEventsOf(e)) : [];
1343
+ const eventRegistry = fastify.events?.registry;
1344
+ if (emitsCrud && eventRegistry) for (const entry of registry.getAll()) for (const name of crudEventsOf(entry)) {
1345
+ if (eventRegistry.get(name)) continue;
1346
+ eventRegistry.register(defineEvent({
1347
+ name,
1348
+ description: `arc CRUD event — a ${entry.name} was ${name.slice(entry.name.length + 1)}.`,
1349
+ schema: CRUD_EVENT_SCHEMA
1350
+ }));
1351
+ }
1352
+ const moduleGraph = buildModuleGraph(modules, consumedByModule, skippedBridges, [
1353
+ ...config.moduleGraph?.externalEvents ?? [],
1354
+ ...ARC_OWNED_EVENTS,
1355
+ ...appCrudEvents
1356
+ ], derived);
1357
+ for (const bridge of moduleGraph.skippedBridges) fastify.log.info({
1358
+ bridge: bridge.name,
1359
+ missing: bridge.missing
1360
+ }, "[arc] bridge module inactive — a module it joins is not composed");
1361
+ const graphMode = config.moduleGraph?.events ?? (config.preset === "production" ? "warn" : "off");
1362
+ if (graphMode !== "off") {
1363
+ const analysis = analyzeModuleGraph(moduleGraph);
1364
+ const issues = verifyModuleGraph(moduleGraph, analysis);
1365
+ if (issues.length > 0 && graphMode === "strict") throw new Error(`[arc] module graph has ${issues.length} issue(s):\n${issues.map((i) => ` - ${i.message}`).join("\n")}`);
1366
+ for (const issue of issues) fastify.log.warn({ issue }, `[arc] module graph: ${issue.message}`);
1367
+ const outboxModules = new Set(modules.filter((m) => isTransactionalOutbox(getOptionalModuleExports(fastify, m.name))).map((m) => m.name));
1368
+ const durabilityIssues = verifyEventDurability(modules, {
1369
+ outboxModules,
1370
+ handlers: handlersByModule
1371
+ });
1372
+ if (durabilityIssues.length && graphMode === "strict") throw new Error(`[arc] module graph durability has ${durabilityIssues.length} issue(s):\n${durabilityIssues.map((i) => ` - ${i.message}`).join("\n")}`);
1373
+ for (const issue of durabilityIssues) fastify.log.warn({ issue }, `[arc] module graph (${issue.code}): ${issue.message}`);
1374
+ }
1375
+ if (fastify.arc) fastify.arc.moduleGraph = moduleGraph;
1376
+ const events = fastify.events;
1377
+ if (graphMode !== "off" && typeof events?.observe === "function" && fastify.arc) {
1378
+ const undeclared = watchUndeclaredEvents(fastify, moduleGraph, graphMode);
1379
+ fastify.arc.undeclaredEvents = undeclared;
1380
+ }
1412
1381
  if (fastify.arc) fastify.arc.moduleDescriptors = Object.freeze(modules.map((m) => describeResolvedModule({
1413
1382
  name: m.name,
1414
1383
  dependsOn: m.dependsOn,
@@ -1427,6 +1396,8 @@ Resources supplied by "${m.name}": ${provided}.`);
1427
1396
  fastify.log.debug("afterResources hook executed");
1428
1397
  }
1429
1398
  } catch (err) {
1399
+ const shutdownErrors = await runShutdown(fastify);
1400
+ for (const shutdownError of shutdownErrors) fastify.log.error({ err: shutdownError }, "[arc] runtime rollback failed");
1430
1401
  const unsubscribeErrors = await unsubscribeModuleEventHandlers(eventUnsubscribes);
1431
1402
  for (const unsubscribeError of unsubscribeErrors) fastify.log.error({ err: unsubscribeError }, "[arc] module event-handler rollback after boot failure failed");
1432
1403
  const closeErrors = await teardown.rollback();
@@ -1441,30 +1412,53 @@ Resources supplied by "${m.name}": ${provided}.`);
1441
1412
  }
1442
1413
  const appOnClose = config.onClose;
1443
1414
  if (teardown.hasComposedModules() || appOnClose || eventUnsubscribes.length) fastify.addHook("onClose", async () => {
1415
+ const shutdownErrors = await runShutdown(fastify);
1444
1416
  const unsubscribeErrors = await unsubscribeModuleEventHandlers(eventUnsubscribes);
1445
1417
  for (const err of unsubscribeErrors) fastify.log.error({ err }, "[arc] module event-handler shutdown unsubscribe failed");
1446
1418
  const closeErrors = await teardown.closeAll();
1447
1419
  for (const err of closeErrors) fastify.log.error({ err }, "[arc] module onClose failed during shutdown");
1448
1420
  if (appOnClose) await appOnClose(fastify);
1449
1421
  if (closeErrors.length > 0) throw closeErrors[0];
1422
+ if (shutdownErrors.length > 0) throw shutdownErrors[0];
1450
1423
  });
1451
1424
  }
1452
1425
  /**
1453
- * `plugins()` is CALLED by arc, never handed to `fastify.register()` — so a
1454
- * module author who returns a Fastify plugin expecting arc to register it
1455
- * gets silence: nothing is registered, and the "disposer" shorthand then
1456
- * invokes their plugin at shutdown with no arguments.
1457
- *
1458
- * The `fastify-plugin` case is unambiguous (its marker symbols are only ever
1459
- * set by `fp()`), so it throws. A bare `(fastify, opts)` function is a strong
1460
- * but not certain signal — arc's disposer contract calls it with ZERO
1461
- * arguments, so a 2-parameter disposer is meaningless either way — and warns.
1426
+ * `plugins()` is CALLED by arc, never `register()`ed — a returned Fastify
1427
+ * plugin would be silently treated as a disposer. `fp()` markers are
1428
+ * unambiguous, so that throws; a bare 2+-arg function only warns.
1462
1429
  */
1463
1430
  function assertNotAReturnedPlugin(returned, moduleName, fastify) {
1464
1431
  const fn = returned;
1465
1432
  if (fn[Symbol.for("skip-override")] !== void 0 || fn[Symbol.for("plugin-meta")] !== void 0 || fn[Symbol.for("fastify.display-name")] !== void 0) throw new Error(`[arc] module "${moduleName}" returned a fastify-plugin from plugins(). That slot is a SETUP function arc calls directly — it is never passed to fastify.register(), so the returned value is read as a disposer and your plugin would be invoked at shutdown instead of registered at boot. Register it yourself: \`plugins: async (fastify) => { await fastify.register(myPlugin) }\`.`);
1466
1433
  if (returned.length >= 2) fastify.log?.warn?.(`[arc] module "${moduleName}" returned a ${returned.length}-argument function from plugins(). Arc calls disposers with NO arguments, so this looks like a Fastify plugin that was meant to be registered (\`await fastify.register(fn)\`) rather than a teardown callback. If it really is a disposer, declare it with no parameters.`);
1467
1434
  }
1435
+ /** Namespaces arc itself publishes — lifecycle (`arc.*`) and its streamline integration (`workflow.*`). */
1436
+ const ARC_OWNED_EVENTS = ["arc.*", "workflow.*"];
1437
+ /**
1438
+ * Record every published event type the graph has no owner for. Logs each name once; never
1439
+ * touches delivery.
1440
+ */
1441
+ function watchUndeclaredEvents(fastify, graph, mode) {
1442
+ const owned = new Set(graph.modules.flatMap((m) => m.publishes));
1443
+ const external = createEventRouter();
1444
+ for (const pattern of graph.externalEvents) external.add(pattern, true);
1445
+ const undeclared = /* @__PURE__ */ new Set();
1446
+ const stop = fastify.events.observe((event) => {
1447
+ const type = event.type;
1448
+ if (owned.has(type) || undeclared.has(type) || type.startsWith("$")) return;
1449
+ if (external.route(type).length > 0) return;
1450
+ undeclared.add(type);
1451
+ fastify.log[mode === "strict" ? "error" : "warn"]({ event: type }, `[arc] module graph: "${type}" was published but no module declares it — add it to its owner's \`publishes\` (or \`moduleGraph.externalEvents\`).`);
1452
+ });
1453
+ fastify.addHook("onClose", async () => stop());
1454
+ return undeclared;
1455
+ }
1456
+ /** A module export that is an outbox whose `save` enlists the caller's session. */
1457
+ function isTransactionalOutbox(exports) {
1458
+ if (typeof exports !== "object" || exports === null || !("store" in exports)) return false;
1459
+ const store = exports.store;
1460
+ return typeof store === "object" && store !== null && "transactionalSave" in store && store.transactionalSave === true;
1461
+ }
1468
1462
  //#endregion
1469
1463
  //#region src/factory/security/cors.ts
1470
1464
  /**
@@ -1650,12 +1644,17 @@ async function registerSecurityPlugins(fastify, config) {
1650
1644
  });
1651
1645
  await fastify.register(rateLimit, rateLimitOpts);
1652
1646
  const rateLimitRecord = typeof rateLimitOpts === "object" && rateLimitOpts !== null ? rateLimitOpts : void 0;
1653
- if (!(rateLimitRecord !== void 0 && (rateLimitRecord.store != null || rateLimitRecord.redis != null))) {
1647
+ const hasStore = rateLimitRecord !== void 0 && (rateLimitRecord.store != null || rateLimitRecord.redis != null);
1648
+ resolveResourceRateLimiter(fastify, hasStore ? "shared" : "memory");
1649
+ if (!hasStore) {
1654
1650
  if (config.runtime === "distributed") throw new Error("[Arc] runtime: 'distributed' with rate limiting requires a shared store.\nProvide rateLimit: { redis: ioredisClient } (or { store: CustomStore }) or disable rate limiting: rateLimit: false");
1655
1651
  else if (config.preset === "production") fastify.log.warn("Rate limiting is using in-memory store. In multi-instance deployments, each instance tracks limits independently. Configure a Redis store for distributed rate limiting.");
1656
1652
  }
1657
1653
  fastify.log.debug("Rate limiting enabled");
1658
- } else fastify.log.warn("Rate limiting disabled");
1654
+ } else {
1655
+ fastify.log.warn("Rate limiting disabled");
1656
+ markRateLimitingDisabled(fastify);
1657
+ }
1659
1658
  }
1660
1659
  /**
1661
1660
  * Register performance and utility plugins (Under Pressure, Sensible, Multipart, Raw Body).
@@ -1721,40 +1720,18 @@ async function registerUtilityPlugins(fastify, config) {
1721
1720
  //#endregion
1722
1721
  //#region src/factory/createApp.ts
1723
1722
  /**
1724
- * ArcFactory - Production-ready Fastify application factory
1725
- *
1726
- * Enforces security best practices by making plugins opt-out instead of opt-in.
1727
- * A developer must explicitly disable security features rather than forget to enable them.
1723
+ * ArcFactory - Production-ready Fastify application factory.
1728
1724
  *
1729
- * Note: Arc is database-agnostic. Connect your database separately and provide
1730
- * adapters when defining resources. This allows multiple databases, custom
1731
- * connection pooling, and full control over your data layer.
1725
+ * Security plugins are opt-out, not opt-in. Arc is database-agnostic: connect
1726
+ * your database separately and pass adapters to `defineResource`.
1732
1727
  *
1733
1728
  * @example
1734
- * // 1. Connect your database(s) separately
1735
- * import mongoose from 'mongoose';
1736
- * await mongoose.connect(process.env.MONGO_URI);
1737
- *
1738
- * // 2. Create Arc app with resources
1739
1729
  * const app = await createApp({
1740
1730
  * preset: 'production',
1731
+ * runtime: 'memory',
1741
1732
  * auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
1742
- * cors: { origin: ['https://example.com'] },
1743
1733
  * resources: [productResource, orderResource],
1744
1734
  * });
1745
- *
1746
- * @example
1747
- * // Multiple databases example
1748
- * const primaryDb = await mongoose.connect(process.env.PRIMARY_DB);
1749
- * const analyticsDb = mongoose.createConnection(process.env.ANALYTICS_DB);
1750
- *
1751
- * const orderResource = defineResource({
1752
- * adapter: createMongooseAdapter({ model: OrderModel, repository: orderRepo }),
1753
- * });
1754
- *
1755
- * const analyticsResource = defineResource({
1756
- * adapter: createMongooseAdapter({ model: AnalyticsModel, repository: analyticsRepo }),
1757
- * });
1758
1735
  */
1759
1736
  var createApp_exports = /* @__PURE__ */ __exportAll({
1760
1737
  ArcFactory: () => ArcFactory,
@@ -1763,10 +1740,8 @@ var createApp_exports = /* @__PURE__ */ __exportAll({
1763
1740
  resolveLoggerConfig: () => resolveLoggerConfig
1764
1741
  });
1765
1742
  /**
1766
- * The fail-loud switches, and what each one costs while OFF.
1767
- *
1768
- * Every one is a SPECIFIC instruction defeated by a GENERAL default, which is
1769
- * why they are reported together rather than one at a time.
1743
+ * The fail-loud switches — ON by default — and what each one costs when a deployment turns it OFF.
1744
+ * Reported together at production boot, so an opt-out is never silent.
1770
1745
  */
1771
1746
  const STRICT_ENV_FLAGS = [
1772
1747
  ["ARC_STRICT_PERMISSIONS", "An ungated write route warns instead of failing boot."],
@@ -1775,18 +1750,10 @@ const STRICT_ENV_FLAGS = [
1775
1750
  ];
1776
1751
  const MEMORY_STORE_NAMES = /* @__PURE__ */ new Set(["memory", "memory-cache"]);
1777
1752
  /**
1778
- * Default redact paths layered into Fastify's pino logger when the host
1779
- * doesn't supply a `logger.redact` of their own. Covers the common token /
1780
- * cookie / password leak surfaces — `Authorization` and `Cookie` headers
1781
- * (and their case-variants because Node lower-cases incoming headers but
1782
- * outgoing logs may carry either), API-key headers, and any nested
1783
- * `password` / `token` / `secret` / `apiKey` fields anywhere in the log
1784
- * tree.
1785
- *
1786
- * Hosts that need NO redaction (test fixtures, security audits) opt out
1787
- * with `logger: { redact: [] }`. Hosts that need MORE redaction supply
1788
- * their own `redact` and arc steps aside — this default never overrides
1789
- * an explicit setting.
1753
+ * Default pino redact paths (auth/cookie/API-key headers, nested
1754
+ * password/token/secret fields), applied only when the host supplies no
1755
+ * `logger.redact`. Opt out with `logger: { redact: [] }`; an explicit
1756
+ * `redact` always wins.
1790
1757
  */
1791
1758
  const DEFAULT_LOGGER_REDACT_PATHS = [
1792
1759
  "req.headers.authorization",
@@ -1805,16 +1772,8 @@ const DEFAULT_LOGGER_REDACT_PATHS = [
1805
1772
  "*.apiKey"
1806
1773
  ];
1807
1774
  /**
1808
- * Resolve the host's `logger` option, layering safe redact defaults
1809
- * when the host hasn't supplied any. Returns the value Fastify expects
1810
- * for its `logger` server option.
1811
- *
1812
- * Three branches:
1813
- * - `logger === false` → pass through (no logger).
1814
- * - `logger === undefined` → enable with default redact paths.
1815
- * - `logger === true` → enable with default redact paths.
1816
- * - `logger` is an object → respect explicit `redact` (host wins);
1817
- * otherwise inject defaults.
1775
+ * Resolve the host's `logger` option for Fastify, layering the default redact
1776
+ * paths unless it is `false` or an object with its own `redact`.
1818
1777
  */
1819
1778
  function resolveLoggerConfig(logger) {
1820
1779
  if (logger === false) return false;
@@ -1900,28 +1859,31 @@ Missing:\n${lines}\n\nFix: pass durable adapters under \`stores\`, or use runtim
1900
1859
  */
1901
1860
  async function createApp(options) {
1902
1861
  /**
1903
- * ROLE contradiction check — before anything constructs. A relay/scheduler
1904
- * process declaring HTTP resources is a wiring error, not a preference:
1905
- * those roles exist to NOT serve traffic, and mounting routes anyway would
1906
- * hide the misdeployment behind a working endpoint.
1862
+ * TOPOLOGY is declared, never guessed: a second replica on the in-memory runtime silently
1863
+ * loses cross-instance events and caches.
1864
+ */
1865
+ if (options.preset === "production" && options.runtime === void 0) throw new Error("createApp: preset 'production' requires an explicit `runtime` — 'memory' for a single instance, 'distributed' before running a second replica (it requires shared stores and is audited at boot).");
1866
+ /**
1867
+ * A relay/scheduler process declaring HTTP resources is a wiring error —
1868
+ * mounting routes anyway would hide the misdeployment behind a working endpoint.
1907
1869
  */
1908
1870
  const declaredRole = options.role ?? "all";
1909
1871
  if ((declaredRole === "relay" || declaredRole === "scheduler") && (options.resources !== void 0 || options.resourceDir !== void 0)) throw new Error(`createApp: role '${declaredRole}' declares HTTP resources — a ${declaredRole} process must not serve routes. Remove \`resources\`/\`resourceDir\`, or change the role.`);
1910
1872
  /**
1911
- * This app's own `arcLog` config, entered for the WHOLE boot.
1912
- *
1913
- * Held by reference and mutated in `buildApp` once `fastify.log` exists:
1914
- * boot starts before Fastify does, so a copied snapshot would send every
1915
- * post-construction boot warning to the console instead of this app's
1916
- * logger. Scoping it per app is what stops two apps in one process from
1917
- * overwriting each other's writer — see `src/logger/index.ts`.
1873
+ * This app's own `arcLog` config, entered for the WHOLE boot. Held by
1874
+ * reference and mutated in `buildApp` once `fastify.log` exists — a copy
1875
+ * would send post-construction boot warnings to the console. Per-app so two
1876
+ * apps in one process keep separate writers.
1918
1877
  */
1919
1878
  const loggerScope = {};
1920
1879
  if (options.debug !== void 0 && options.debug !== false) loggerScope.debug = options.debug;
1921
1880
  let partial;
1881
+ let disposeBoot;
1922
1882
  try {
1923
1883
  return await runWithArcLogger(loggerScope, () => buildApp(options, loggerScope, (instance) => {
1924
1884
  partial = instance;
1885
+ }, (dispose) => {
1886
+ disposeBoot = dispose;
1925
1887
  }));
1926
1888
  } catch (err) {
1927
1889
  if (partial) try {
@@ -1929,38 +1891,42 @@ async function createApp(options) {
1929
1891
  } catch (closeErr) {
1930
1892
  partial.log?.error?.({ err: closeErr }, "[arc] cleanup of a partially-booted app failed; original boot error follows");
1931
1893
  }
1894
+ if (disposeBoot) try {
1895
+ await disposeBoot();
1896
+ } catch (disposeErr) {
1897
+ arcLog("createApp").error("beforeBoot disposer failed during failed-boot cleanup; original boot error follows", disposeErr);
1898
+ }
1932
1899
  throw err;
1933
1900
  }
1934
1901
  }
1935
- async function buildApp(options, loggerScope, onInstance) {
1902
+ async function buildApp(options, loggerScope, onInstance, onBootDisposer) {
1936
1903
  validateAuthOptions(options);
1937
1904
  const deferredWarnings = validateDistributedRuntime(options);
1938
- if (options.beforeBoot) await options.beforeBoot();
1905
+ let disposeBoot;
1906
+ if (options.beforeBoot) {
1907
+ const dispose = await options.beforeBoot();
1908
+ if (typeof dispose === "function") {
1909
+ let once;
1910
+ disposeBoot = () => {
1911
+ once ??= Promise.resolve().then(dispose);
1912
+ return once;
1913
+ };
1914
+ onBootDisposer(disposeBoot);
1915
+ }
1916
+ }
1939
1917
  const config = {
1940
1918
  ...options.preset ? getPreset(options.preset) : {},
1941
- ...options
1919
+ ...options,
1920
+ ...options.role === "relay" || options.role === "scheduler" ? { mountRoutes: false } : {}
1942
1921
  };
1943
1922
  const inheritedTrustProxyDefault = options.preset === "production" && options.trustProxy === void 0;
1944
1923
  /**
1945
- * A NUMERIC `trustProxy` is a no-op on Fastify >= 5.12.1.
1946
- *
1947
- * Fastify made hop-count trust fail-closed on purpose — the comment in
1948
- * `lib/request.js` is explicit: "Hop-count-only trust cannot validate the
1949
- * immediate peer. Fail closed so direct clients cannot spoof X-Forwarded-*
1950
- * values by supplying enough hops." So `trustProxy: 1` now trusts NOTHING.
1951
- *
1952
- * That is the right call upstream and the wrong thing to inherit silently:
1953
- * `request.ip` becomes the proxy's address, which is the exact failure the
1954
- * production-preset warning below describes — rate-limit keys collapse onto
1955
- * one bucket and audit rows record the wrong client. The host asked for
1956
- * proxy trust and got none, with no error.
1957
- *
1958
- * Warn rather than refuse: the app still serves, and a hard boot failure on
1959
- * a transitive `fastify` bump would be a harsher surprise than the one being
1960
- * reported. The replacement is named so the fix is a copy-paste.
1924
+ * A NUMERIC `trustProxy` trusts NOTHING on Fastify >= 5.12.1 (hop-count
1925
+ * trust fails closed upstream), so the host silently gets the proxy's IP.
1926
+ * Warn rather than refuse — a boot failure on a transitive bump is harsher.
1961
1927
  */
1962
1928
  const numericTrustProxy = typeof config.trustProxy === "number";
1963
- const resolvedModules = orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
1929
+ const { modules: resolvedModules, skipped: skippedBridges } = await composeModules(config.modules ?? [], config.ports);
1964
1930
  const moduleErrorMappers = resolvedModules.flatMap((m) => m.errorMappers ?? []);
1965
1931
  const moduleHealthChecks = collectModuleHealthChecks(resolvedModules);
1966
1932
  const resolvedLogger = resolveLoggerConfig(config.logger);
@@ -1983,16 +1949,13 @@ async function buildApp(options, loggerScope, onInstance) {
1983
1949
  }),
1984
1950
  maxParamLength: config.maxParamLength ?? 400
1985
1951
  },
1986
- ajv: { customOptions: {
1987
- coerceTypes: true,
1988
- useDefaults: true,
1989
- removeAdditional: false,
1990
- strictTypes: false,
1991
- keywords: ["example", ...config.ajv?.keywords ?? []]
1992
- } }
1952
+ ajv: { customOptions: arcAjvCustomOptions(config.ajv?.keywords) }
1993
1953
  });
1954
+ markArcRootValidator(fastify);
1994
1955
  onInstance(fastify);
1956
+ if (disposeBoot) fastify.addHook("onClose", disposeBoot);
1995
1957
  initRuntimeCapabilityRegistry(fastify);
1958
+ initShutdownRegistry(fastify);
1996
1959
  if (resolvedLogger !== false) {
1997
1960
  const writer = createPinoWriter(fastify.log);
1998
1961
  loggerScope.writer = writer;
@@ -2006,22 +1969,15 @@ async function buildApp(options, loggerScope, onInstance) {
2006
1969
  });
2007
1970
  for (const warning of deferredWarnings) fastify.log.warn(warning);
2008
1971
  if (numericTrustProxy) fastify.log.warn(`[arc] trustProxy: ${String(config.trustProxy)} (a hop COUNT) trusts nothing on Fastify >= 5.12.1 — hop-count trust cannot validate the immediate peer, so it fails closed upstream to stop clients spoofing X-Forwarded-* with padded hops. \`request.ip\` is therefore the PROXY's address: rate-limit keys collapse onto one bucket and audit rows record the wrong client. Name the proxies instead — trustProxy: 'loopback', '10.0.0.0/8', a comma-separated list, or \`true\` if every upstream hop is genuinely trusted.`);
2009
- if (inheritedTrustProxyDefault) fastify.log.warn("[arc] preset: 'production' now defaults to trustProxy: false (fail-closed; was true before 2.24). If this app runs behind a load balancer or reverse proxy, request.ip is currently the PROXY's address — rate-limit keys collapse onto one bucket and audit logs record the wrong client. Set trustProxy explicitly: a hop count (trustProxy: 1), a CIDR allow-list ('10.0.0.0/8'), a resolver function, or false if clients truly connect directly.");
1972
+ if (inheritedTrustProxyDefault) fastify.log.warn("[arc] preset: 'production' defaults to trustProxy: false (fail-closed). If this app runs behind a load balancer or reverse proxy, request.ip is currently the PROXY's address — rate-limit keys collapse onto one bucket and audit logs record the wrong client. Set trustProxy explicitly: the proxies' addresses ('loopback', '10.0.0.0/8', a comma-separated list), a resolver function, or false if clients truly connect directly. A hop count trusts nothing on Fastify >= 5.12.1.");
2010
1973
  /**
2011
- * `preset: 'production'` REPORTS the fail-loud switches it cannot set.
2012
- *
2013
- * All three are read where the object is CONSTRUCTED — `defineResource` at
2014
- * define time, `QueryParser` and `BodySanitizer` in their constructors — and a
2015
- * host imports its resources before it calls `createApp`. So setting
2016
- * `process.env` here would be too late for everything already built: wired to
2017
- * read, doing nothing. Reporting cannot fail that way.
2018
- *
2019
- * Set them in the process entrypoint, BEFORE the first resource import, with
2020
- * `??=` so an operator's explicit `false` still wins.
1974
+ * Production names every fail-loud switch turned OFF. They are read before `createApp`, so this
1975
+ * reports rather than sets.
2021
1976
  */
2022
1977
  if (config.preset === "production") {
2023
- const off = STRICT_ENV_FLAGS.filter(([name]) => process.env[name] !== "true");
2024
- if (off.length > 0) fastify.log.warn(`[arc] preset: 'production' with fail-loud OFF: ${off.map(([n]) => n).join(", ")}. ${off.map(([, what]) => what).join(" ")} Set them in the entrypoint BEFORE any resource import — they are read when resources, parsers and sanitizers are CONSTRUCTED, so setting them later is silently too late. Per-resource options still override.`);
1978
+ const env = readEnv(process.env);
1979
+ const off = STRICT_ENV_FLAGS.filter(([name]) => !env.bool(name, true));
1980
+ if (off.length > 0) fastify.log.warn(`[arc] preset: 'production' with fail-loud turned OFF: ${off.map(([n]) => n).join(", ")}. ${off.map(([, what]) => what).join(" ")} They default ON; unset them to restore enforcement. Per-resource options still override.`);
2025
1981
  }
2026
1982
  fastify.removeContentTypeParser("application/json");
2027
1983
  fastify.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
@@ -2053,15 +2009,14 @@ async function buildApp(options, loggerScope, onInstance) {
2053
2009
  await registerAuth(fastify, config, trackPlugin);
2054
2010
  await registerElevation(fastify, config, trackPlugin);
2055
2011
  await registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers);
2056
- await registerResources(fastify, config, resolvedModules);
2012
+ await registerResources(fastify, config, resolvedModules, skippedBridges);
2057
2013
  if (config.replyHelpers) {
2058
- const { replyHelpersPlugin } = await import("./replyHelpers-CNdvMfM4.mjs").then((n) => n.n);
2014
+ const { replyHelpersPlugin } = await import("./replyHelpers-Bnc6lJyy.mjs").then((n) => n.n);
2059
2015
  await fastify.register(replyHelpersPlugin);
2060
2016
  }
2061
2017
  if (config.serializeBigInt) {
2062
- const mode = config.serializeBigInt === "string" ? "string" : "number";
2063
- if (config.serializeBigInt === true) arcLog("createApp").warn("serializeBigInt: true is a back-compat alias for 'number' (lossy above Number.MAX_SAFE_INTEGER = 9007199254740991). For IDs / money / counters / ledger values use serializeBigInt: 'string' (lossless). The boolean form will be removed in a future major.");
2064
- else if (mode === "number") arcLog("createApp").warn("serializeBigInt: 'number' loses precision above Number.MAX_SAFE_INTEGER (9007199254740991). Use 'string' instead unless you've audited that no bigint payload field can exceed the safe range.");
2018
+ const mode = config.serializeBigInt;
2019
+ if (mode === "number") arcLog("createApp").warn("serializeBigInt: 'number' loses precision above Number.MAX_SAFE_INTEGER (9007199254740991). Use 'string' instead unless you've audited that no bigint payload field can exceed the safe range.");
2065
2020
  fastify.addHook("preSerialization", async (_request, _reply, payload) => {
2066
2021
  if (payload === null || payload === void 0) return payload;
2067
2022
  try {
@@ -2107,48 +2062,4 @@ const ArcFactory = {
2107
2062
  }
2108
2063
  };
2109
2064
  //#endregion
2110
- //#region src/factory/preloadResources.ts
2111
- /**
2112
- * Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
2113
- *
2114
- * Accepts either:
2115
- * - `{ import: 'default' }` form: values are the resource directly
2116
- * - default form: values are the full module — picks first export with `toPlugin()`
2117
- *
2118
- * Throws if any module doesn't yield a valid `ResourceLike`.
2119
- */
2120
- function preloadResources(globResult) {
2121
- const resources = [];
2122
- for (const [path, value] of Object.entries(globResult)) {
2123
- const resource = pickResource(value);
2124
- if (!resource) throw new Error(`preloadResources: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
2125
- resources.push(resource);
2126
- }
2127
- return resources.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
2128
- }
2129
- /**
2130
- * Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
2131
- *
2132
- * Use this when resources depend on prior bootstrap (e.g., engine init) and
2133
- * cannot be evaluated at import time of the preload file.
2134
- */
2135
- async function preloadResourcesAsync(globResult) {
2136
- return (await Promise.all(Object.entries(globResult).map(async ([path, loader]) => {
2137
- const resource = pickResource(await loader());
2138
- if (!resource) throw new Error(`preloadResourcesAsync: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
2139
- return resource;
2140
- }))).sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
2141
- }
2142
- function pickResource(value) {
2143
- if (!value || typeof value !== "object") return void 0;
2144
- if (typeof value.toPlugin === "function") return value;
2145
- const mod = value;
2146
- const candidates = [
2147
- mod.default,
2148
- mod.resource,
2149
- ...Object.values(mod)
2150
- ];
2151
- for (const c of candidates) if (c && typeof c === "object" && typeof c.toPlugin === "function") return c;
2152
- }
2153
- //#endregion
2154
- export { resolveModule as S, describeResolvedModule as _, createApp as a, collectModuleWorkflows as b, edgePreset as c, testingPreset as d, workerPreset as f, subscribeModuleEventHandlers as g, orderModules as h, DEFAULT_LOGGER_REDACT_PATHS as i, getPreset as l, hasModule as m, preloadResourcesAsync as n, createApp_exports as o, getModuleState as p, ArcFactory as r, developmentPreset as s, preloadResources as t, productionPreset as u, collectModuleHealthChecks as v, resolveContribution as x, collectModuleScheduledJobs as y };
2065
+ export { developmentPreset as a, productionPreset as c, getModuleResources as d, getModuleState as f, collectModuleWorkflows as g, asModulePorts as h, createApp_exports as i, testingPreset as l, subscribeModuleEventHandlers as m, DEFAULT_LOGGER_REDACT_PATHS as n, edgePreset as o, hasModule as p, createApp as r, getPreset as s, ArcFactory as t, workerPreset as u };