@classytic/arc 2.41.0 → 2.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/dist/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
  2. package/dist/{EventTransport-n7qO5Cz3.mjs → EventTransport-tzA7CAB4.mjs} +22 -17
  3. package/dist/{HookSystem-CxvZBo77.mjs → HookSystem-B6UaKRbF.mjs} +2 -2
  4. package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
  5. package/dist/{ResourceRegistry-Cu7uTJp9.mjs → ResourceRegistry-CuCARu2B.mjs} +44 -13
  6. package/dist/actionPermissions-BotX1YOa.mjs +776 -0
  7. package/dist/audit/index.d.mts +43 -5
  8. package/dist/audit/index.mjs +48 -26
  9. package/dist/auth/audit.mjs +1 -1
  10. package/dist/auth/index.d.mts +43 -13
  11. package/dist/auth/index.mjs +130 -155
  12. package/dist/auth/redis-session.mjs +1 -1
  13. package/dist/{authorizationDecision-2YY_Kaj1.mjs → authorizationDecision-DQYrkl5f.mjs} +7 -102
  14. package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
  15. package/dist/boundedMap-BULzfXYU.mjs +57 -0
  16. package/dist/{buildHandler-CUhjGHGZ.mjs → buildHandler-CRQ8EcX5.mjs} +4 -4
  17. package/dist/cache/index.d.mts +22 -34
  18. package/dist/cache/index.mjs +4 -4
  19. package/dist/{caching-DsWDjIxx.mjs → caching-Cc2M1DwO.mjs} +3 -3
  20. package/dist/claimed-B10RTfkM.mjs +8 -0
  21. package/dist/cleanup/index.d.mts +6 -10
  22. package/dist/cleanup/index.mjs +27 -73
  23. package/dist/cli/commands/describe.d.mts +4 -2
  24. package/dist/cli/commands/describe.mjs +7 -4
  25. package/dist/cli/commands/docs.mjs +3 -3
  26. package/dist/cli/commands/doctor.mjs +1 -1
  27. package/dist/cli/commands/generate.mjs +2 -2
  28. package/dist/cli/commands/init.mjs +2 -2
  29. package/dist/cli/commands/introspect.mjs +2 -2
  30. package/dist/cli/index.mjs +3 -3
  31. package/dist/{constants-BxLgNFF7.mjs → constants-Cu7Mx7_U.mjs} +4 -2
  32. package/dist/context/index.d.mts +2 -5
  33. package/dist/context/index.mjs +18 -38
  34. package/dist/core/index.d.mts +3 -3
  35. package/dist/core/index.mjs +8 -8
  36. package/dist/{core-iNGqYerO.mjs → core-Cb0LjccA.mjs} +5 -4
  37. package/dist/{createActionRouter-BBlbPs3k.mjs → createActionRouter-xjqonMRO.mjs} +87 -39
  38. package/dist/{createAggregationRouter-Dv5SuK2x.mjs → createAggregationRouter-zRYWw_5D.mjs} +11 -8
  39. package/dist/{preloadResources-Bbp4QJ8V.mjs → createApp-D-953ZAq.mjs} +299 -390
  40. package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-C9cxS8zt.d.mts} +1 -1
  41. package/dist/{defineAggregation-UyeurGkp.mjs → defineAggregation-Bm8RjMbq.mjs} +1 -1
  42. package/dist/{defineResource-DzGT-2LP.mjs → defineResource-DIC7JZGU.mjs} +118 -1024
  43. package/dist/discovery/index.mjs +1 -1
  44. package/dist/docs/index.d.mts +10 -4
  45. package/dist/docs/index.mjs +4 -4
  46. package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
  47. package/dist/{elevation-BF-vhGxY.mjs → elevation-DG8SrLN-.mjs} +41 -12
  48. package/dist/encryption/index.mjs +3 -3
  49. package/dist/{errorHandler-CIMkCOWm.mjs → errorHandler-BnfDviL1.mjs} +26 -5
  50. package/dist/{errors-C_YhELhe.mjs → errors-CtrywMHP.mjs} +58 -23
  51. package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
  52. package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
  53. package/dist/{eventPlugin-CmXaPWLv.mjs → eventPlugin-Q6BhzHzs.mjs} +82 -21
  54. package/dist/{subscribe-helpers-AVYPgqTl.mjs → eventTypes-Cdhm6ZlV.mjs} +151 -17
  55. package/dist/events/index.d.mts +33 -8
  56. package/dist/events/index.mjs +49 -53
  57. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  58. package/dist/events/transports/redis-stream-entry.mjs +139 -157
  59. package/dist/events/transports/redis.d.mts +1 -1
  60. package/dist/events/transports/redis.mjs +2 -2
  61. package/dist/factory/index.d.mts +32 -17
  62. package/dist/factory/index.mjs +213 -32
  63. package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DhUJ-mvo.d.mts} +11 -25
  64. package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
  65. package/dist/{fields-BTyPLHK0.mjs → fields-ToPYpMqS.mjs} +1 -1
  66. package/dist/filter-merge-IpGywvym.mjs +99 -0
  67. package/dist/{generate-D_YmiCNf.mjs → generate-DRTkLlIl.mjs} +2 -2
  68. package/dist/{headers-CNECDE6q.mjs → headers-BbkIF6vA.mjs} +2 -2
  69. package/dist/health-D-Ft6JM6.mjs +22 -0
  70. package/dist/{health-CxQzbJmr.mjs → health-DqbsG-_w.mjs} +115 -55
  71. package/dist/hooks/index.d.mts +2 -2
  72. package/dist/hooks/index.mjs +2 -2
  73. package/dist/idempotency/index.d.mts +31 -7
  74. package/dist/idempotency/index.mjs +155 -98
  75. package/dist/idempotency/redis.d.mts +1 -1
  76. package/dist/idempotency/redis.mjs +48 -11
  77. package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
  78. package/dist/{preloadResources-CjS2OfpU.d.mts → index-C8zVGYsk.d.mts} +37 -38
  79. package/dist/{index-D4u76eQW.d.mts → index-CRoPtISf.d.mts} +23 -73
  80. package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
  81. package/dist/index-JdbZAIsm.d.mts +510 -0
  82. package/dist/{index-DHbyPo5j.d.mts → index-mqaqZNpe.d.mts} +253 -254
  83. package/dist/index.d.mts +8 -8
  84. package/dist/index.mjs +11 -11
  85. package/dist/{init-Y2MlSN9n.mjs → init-B_oUYDXI.mjs} +1 -1
  86. package/dist/integrations/event-gateway.d.mts +8 -3
  87. package/dist/integrations/event-gateway.mjs +4 -4
  88. package/dist/integrations/index.d.mts +3 -3
  89. package/dist/integrations/index.mjs +1 -1
  90. package/dist/integrations/jobs/index.d.mts +1 -1
  91. package/dist/integrations/jobs/index.mjs +18 -5
  92. package/dist/integrations/mcp/index.d.mts +72 -59
  93. package/dist/integrations/mcp/index.mjs +19 -4
  94. package/dist/integrations/mcp/testing.d.mts +2 -2
  95. package/dist/integrations/mcp/testing.mjs +2 -2
  96. package/dist/integrations/streamline.d.mts +78 -136
  97. package/dist/integrations/streamline.mjs +116 -54
  98. package/dist/integrations/webhooks.d.mts +60 -120
  99. package/dist/integrations/webhooks.mjs +36 -80
  100. package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
  101. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  102. package/dist/integrations/websocket-redis.d.mts +1 -1
  103. package/dist/integrations/websocket-redis.mjs +1 -1
  104. package/dist/integrations/websocket.d.mts +3 -3
  105. package/dist/integrations/websocket.mjs +2 -2
  106. package/dist/{interface-DEWNOMza.d.mts → interface-C0395yUN.d.mts} +13 -1
  107. package/dist/{jsonBody-Cq9KYl2Z.mjs → jsonBody-B4YVGcql.mjs} +2 -2
  108. package/dist/{keys-Cmv4cBRa.mjs → keys-B2KtgMLJ.mjs} +7 -18
  109. package/dist/{loadResources-CblF7xt2.mjs → loadResources-Dn4lalw2.mjs} +3 -3
  110. package/dist/{loadResourcesFromEntry-99fIMC3p.mjs → loadResourcesFromEntry-3sLYbuse.mjs} +2 -2
  111. package/dist/logger/index.d.mts +1 -8
  112. package/dist/logger/index.mjs +3 -153
  113. package/dist/logger-DXjfIqjV.mjs +160 -0
  114. package/dist/{mcpPlugin-xF3Gw-HW.mjs → mcpPlugin-C08HalN5.mjs} +433 -170
  115. package/dist/{memory-CpDZ9NPw.mjs → memory-9Cp7611C.mjs} +36 -34
  116. package/dist/{metrics-BaKnq8ag.mjs → metrics-CWI11eRu.mjs} +39 -72
  117. package/dist/middleware/index.d.mts +2 -2
  118. package/dist/middleware/index.mjs +3 -3
  119. package/dist/migrations/index.mjs +2 -2
  120. package/dist/{module-BkQEesNQ.mjs → module-BSvOQC7E.mjs} +8 -8
  121. package/dist/multiTenant-cY0FkDiA.d.mts +104 -0
  122. package/dist/{multipartBody-C92aJ8am.mjs → multipartBody-CXM2a-Dy.mjs} +46 -7
  123. package/dist/{openapi-C3LFRzPq.mjs → openapi-DT_G4VhY.mjs} +48 -10
  124. package/dist/outbox-admin/index.d.mts +3 -3
  125. package/dist/outbox-admin/index.mjs +6 -18
  126. package/dist/{outbox-module-dsCyLxXH.mjs → outbox-module-Bjgzl6tg.mjs} +203 -33
  127. package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
  128. package/dist/permissions/index.d.mts +5 -4
  129. package/dist/permissions/index.mjs +8 -6
  130. package/dist/{permissions-DiHXy8Ck.mjs → permissions-Cm0uYHvd.mjs} +149 -33
  131. package/dist/{pipe-CBVmUJ-j.mjs → pipe-BqeGT6UO.mjs} +2 -2
  132. package/dist/pipeline/index.d.mts +2 -2
  133. package/dist/pipeline/index.mjs +2 -2
  134. package/dist/plan-FVR6LP59.mjs +565 -0
  135. package/dist/{BaseController-DYJRZ0EJ.mjs → plugin-BccET1tL.mjs} +1256 -344
  136. package/dist/plugins/index.d.mts +14 -11
  137. package/dist/plugins/index.mjs +44 -22
  138. package/dist/plugins/response-cache.mjs +8 -14
  139. package/dist/plugins/tracing-entry.mjs +2 -2
  140. package/dist/{pluralize-2_U6QZQg.mjs → pluralize-CVIOcgMF.mjs} +1 -1
  141. package/dist/presets/filesUpload.d.mts +2 -2
  142. package/dist/presets/filesUpload.mjs +5 -5
  143. package/dist/presets/index.d.mts +26 -31
  144. package/dist/presets/index.mjs +4 -4
  145. package/dist/presets/multiTenant.d.mts +2 -107
  146. package/dist/presets/multiTenant.mjs +22 -162
  147. package/dist/presets/search.d.mts +2 -2
  148. package/dist/presets/search.mjs +2 -2
  149. package/dist/{presets-BdlugYTc.mjs → presets-Cpvj6fGO.mjs} +99 -107
  150. package/dist/{projection-Uvgp-EEj.mjs → projection-Ce4aQhQk.mjs} +2 -2
  151. package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
  152. package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
  153. package/dist/{queryCachePlugin-B5F09bYr.mjs → queryCachePlugin-CFWNH8aT.mjs} +12 -14
  154. package/dist/{rateLimitKey-CK-EU2L8.mjs → rateLimitKey-C4O-kPm0.mjs} +1 -1
  155. package/dist/{realtime-FnnD0n3Z.mjs → realtime-DmPE9S54.mjs} +25 -21
  156. package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
  157. package/dist/{redis-Bzi1y3F9.d.mts → redis-vLvtcGy_.d.mts} +13 -24
  158. package/dist/redisShapes-CQksY67q.d.mts +55 -0
  159. package/dist/registry/index.d.mts +9 -4
  160. package/dist/registry/index.mjs +4 -4
  161. package/dist/{registry-D3y8VEJ1.mjs → registry-Bcz9G1bh.mjs} +10 -8
  162. package/dist/{renewingLease-B3VYh5aU.mjs → renewingLease-qODa7KJQ.mjs} +1 -1
  163. package/dist/{replyHelpers-DPHsdw-5.mjs → replyHelpers-CG_VtVgS.mjs} +2 -2
  164. package/dist/{requestContext-cSVKnYPk.mjs → requestContext-Dsu9P1bV.mjs} +1 -1
  165. package/dist/requirement-CbiLdQ1n.d.mts +22 -0
  166. package/dist/requirement-KDdoRcOP.mjs +35 -0
  167. package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
  168. package/dist/resourceRateLimit-dSjVr1pk.mjs +180 -0
  169. package/dist/{rolldown-runtime-DjDuyglE.mjs → rolldown-runtime-D1w9Y1VJ.mjs} +1 -1
  170. package/dist/routeValidator-D36SW8qt.mjs +70 -0
  171. package/dist/{runtimeCapabilities-DSA_dH1_.mjs → runtimeCapabilities-0tccjRlj.mjs} +1 -1
  172. package/dist/{schedules-BktJb3Y3.mjs → schedules-C-OpmHBq.mjs} +46 -37
  173. package/dist/{schemaConverter-BLCSmNbr.mjs → schemaConverter-BORek3O-.mjs} +2 -2
  174. package/dist/{schemaIR-K16Bb5BX.mjs → schemaIR-C7Vbp71I.mjs} +2 -2
  175. package/dist/schemas/index.d.mts +10 -0
  176. package/dist/schemas/index.mjs +8 -3
  177. package/dist/scim/index.mjs +2 -2
  178. package/dist/scope/index.d.mts +2 -2
  179. package/dist/scope/index.mjs +6 -6
  180. package/dist/{scope-jSxuBTFM.mjs → scope-DQthe88b.mjs} +4 -4
  181. package/dist/shutdown-MqA81z_P.mjs +59 -0
  182. package/dist/{simpleEqualityMatcher-BiOnyflP.mjs → simpleEqualityMatcher-auW8QjXN.mjs} +1 -1
  183. package/dist/{sse-CH7Slxc1.mjs → sse-CYzQBVFe.mjs} +11 -15
  184. package/dist/{sseStream-CN-Lh1Tu.mjs → sseStream-B5_Afiqn.mjs} +2 -2
  185. package/dist/{store-helpers-9DX9-1j_.mjs → store-helpers-BOzk-CuX.mjs} +1 -1
  186. package/dist/{streaming-dvppnxsr.mjs → streaming-DjTQhZrN.mjs} +1 -1
  187. package/dist/subscriberIdentity-CW39uA8G.mjs +28 -0
  188. package/dist/tenancy--ed9eH_z.mjs +43 -0
  189. package/dist/tenantPreparation-BKon6Yox.mjs +67 -0
  190. package/dist/testing/cleanupStoreContract.mjs +1 -1
  191. package/dist/testing/index.d.mts +193 -175
  192. package/dist/testing/index.mjs +286 -212
  193. package/dist/testing/outboxStoreContract.mjs +137 -2
  194. package/dist/testing/requiredPorts.d.mts +17 -0
  195. package/dist/testing/requiredPorts.mjs +65 -0
  196. package/dist/testing/storageContract.mjs +1 -1
  197. package/dist/timing-qczZ0YC-.mjs +46 -0
  198. package/dist/{transactionContext-DCWMI6hV.mjs → transactionContext-DjFkrNG6.mjs} +1 -1
  199. package/dist/{typeGuards-D7tHFdH6.mjs → typeGuards-CySF12G-.mjs} +1 -1
  200. package/dist/types/index.d.mts +4 -4
  201. package/dist/types/index.mjs +1 -1
  202. package/dist/types/storage.mjs +1 -1
  203. package/dist/{types-CWQn9-IC.d.mts → types-B4Aq4Sdj.d.mts} +22 -9
  204. package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
  205. package/dist/{types-CDy3-zw1.mjs → types-Bk9NOgOC.mjs} +4 -4
  206. package/dist/usage/index.d.mts +2 -3
  207. package/dist/usage/index.mjs +5 -5
  208. package/dist/{userHelpers-DDMOZ_Pm.mjs → userHelpers-aklawRaK.mjs} +1 -1
  209. package/dist/utils/index.d.mts +84 -223
  210. package/dist/utils/index.mjs +13 -12
  211. package/dist/{utils-B-zj_b__.mjs → utils-ec1oTo1n.mjs} +315 -404
  212. package/dist/{validate-R0G_JrnK.mjs → validate-9yzHC1e2.mjs} +6 -14
  213. package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
  214. package/dist/{versioning-DuVWcu-F.mjs → versioning-fhhWHV74.mjs} +4 -4
  215. package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
  216. package/dist/{workScope-BhsWde4k.mjs → workScope-zpPyge4t.mjs} +2 -2
  217. package/package.json +578 -574
  218. package/skills/arc/SKILL.md +13 -4
  219. package/skills/arc/references/auth.md +8 -5
  220. package/skills/arc/references/integrations.md +1 -1
  221. package/skills/arc/references/mcp.md +8 -6
  222. package/skills/arc/references/multi-tenancy.md +19 -14
  223. package/skills/arc/references/production.md +27 -10
  224. package/dist/actionPermissions-DPwPWPOV.mjs +0 -350
  225. package/dist/index-CFfLKMH4.d.mts +0 -290
  226. package/dist/interface-BIpgzbZl.mjs +0 -17
  227. package/dist/redis-stream-BPKscsTB.d.mts +0 -260
  228. package/dist/registerRoute-DRsSHPSM.mjs +0 -364
  229. package/dist/sync/changelog.d.mts +0 -3
  230. package/dist/sync/changelog.mjs +0 -3
@@ -1,15 +1,22 @@
1
- /*! @classytic/arc v2.41.0 | MIT | https://classytic.com */
2
- import { t as __exportAll } from "./rolldown-runtime-DjDuyglE.mjs";
3
- import { arcLog, configureArcLogger, createPinoWriter, enterArcLoggerScope, runWithArcLogger } from "./logger/index.mjs";
4
- import { n as PUBLIC_SCOPE } from "./types-CDy3-zw1.mjs";
5
- import { r as initRuntimeCapabilityRegistry, t as auditRuntimeCapabilities } from "./runtimeCapabilities-DSA_dH1_.mjs";
6
- import { n as isScopeAwareKeyGenerator, t as createTenantKeyGenerator } from "./rateLimitKey-CK-EU2L8.mjs";
7
- import { i as createRequestIdGenerator, r as mergeHealthChecks } from "./health-CxQzbJmr.mjs";
8
- import { t as parseJsonBody } from "./jsonBody-Cq9KYl2Z.mjs";
9
- import { r as wrapWithBoundary } from "./subscribe-helpers-AVYPgqTl.mjs";
10
- import { isProductionEnv } from "@classytic/primitives/environment";
1
+ /*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
2
+ import { t as __exportAll } from "./rolldown-runtime-D1w9Y1VJ.mjs";
3
+ import { a as enterArcLoggerScope, i as runWithArcLogger, n as configureArcLogger, r as createPinoWriter, t as arcLog } from "./logger-DXjfIqjV.mjs";
4
+ import { n as PUBLIC_SCOPE } from "./types-Bk9NOgOC.mjs";
5
+ import { r as initRuntimeCapabilityRegistry, t as auditRuntimeCapabilities } from "./runtimeCapabilities-0tccjRlj.mjs";
6
+ import { a as runShutdown, t as initShutdownRegistry } from "./shutdown-MqA81z_P.mjs";
7
+ import { n as isScopeAwareKeyGenerator, t as createTenantKeyGenerator } from "./rateLimitKey-C4O-kPm0.mjs";
8
+ import { i as resolveResourceRateLimiter, n as markRateLimitingDisabled } from "./resourceRateLimit-dSjVr1pk.mjs";
9
+ import { r as markArcRootValidator, t as arcAjvCustomOptions } from "./routeValidator-D36SW8qt.mjs";
10
+ import { c as createRequestIdGenerator, r as mergeHealthChecks } from "./health-DqbsG-_w.mjs";
11
+ import { t as parseJsonBody } from "./jsonBody-B4YVGcql.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-FVR6LP59.mjs";
13
+ import { c as wrapWithAttribution, f as defineEvent, l as wrapWithBoundary, r as CRUD_EVENT_SCHEMA } from "./eventTypes-Cdhm6ZlV.mjs";
14
+ import { r as withSubscriberId } from "./subscriberIdentity-CW39uA8G.mjs";
15
+ import { r as getOptionalModuleExports } from "./module-BSvOQC7E.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-CmXaPWLv.mjs").then((n) => n.n);
777
+ const { default: eventPlugin } = await import("./eventPlugin-Q6BhzHzs.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-DsWDjIxx.mjs").then((n) => n.r);
832
+ const { default: cachingPlugin } = await import("./caching-Cc2M1DwO.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-B5F09bYr.mjs").then((n) => n.n);
838
+ const { queryCachePlugin } = await import("./queryCachePlugin-CFWNH8aT.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-CpDZ9NPw.mjs").then((n) => n.n))).MemoryCacheStore();
840
+ const store = config.stores?.queryCache ?? new (await (import("./memory-9Cp7611C.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-CH7Slxc1.mjs").then((n) => n.r);
850
+ const { default: ssePlugin } = await import("./sse-CYzQBVFe.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-FnnD0n3Z.mjs").then((n) => n.r);
859
+ const { default: realtimePlugin } = await import("./realtime-DmPE9S54.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-BaKnq8ag.mjs").then((n) => n.r);
866
+ const { default: metricsPlugin } = await import("./metrics-CWI11eRu.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-DuVWcu-F.mjs").then((n) => n.r);
872
+ const { default: versioningPlugin } = await import("./versioning-fhhWHV74.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-BF-vhGxY.mjs").then((n) => n.r);
949
+ const { elevationPlugin } = await import("./elevation-DG8SrLN-.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-CIMkCOWm.mjs").then((n) => n.r);
964
+ const { errorHandlerPlugin } = await import("./errorHandler-BnfDviL1.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-CblF7xt2.mjs").then((n) => n.n);
1183
+ const { loadResources } = await import("./loadResources-Dn4lalw2.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
  }
@@ -1375,11 +1288,8 @@ Resources supplied by "${m.name}": ${provided}.`);
1375
1288
  const scheduleCounts = /* @__PURE__ */ new Map();
1376
1289
  const collectedJobs = await collectModuleScheduledJobs(fastify, modules, scheduleCounts);
1377
1290
  /**
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.
1291
+ * ROLE gate: arms this role does not run are skipped and logged, never
1292
+ * fatal — another deployment of the same codebase runs them.
1383
1293
  */
1384
1294
  const role = config.role ?? "all";
1385
1295
  const scheduledJobs = role === "scheduler" || role === "all" ? collectedJobs : role === "relay" ? collectedJobs.filter((j) => j.kind === "relay") : [];
@@ -1393,7 +1303,7 @@ Resources supplied by "${m.name}": ${provided}.`);
1393
1303
  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
1304
  const frozenJobs = Object.freeze(scheduledJobs.map((job) => Object.freeze({ ...job })));
1395
1305
  const scheduleOptions = typeof config.arcPlugins?.schedules === "object" ? config.arcPlugins.schedules : {};
1396
- const { default: schedulesPlugin } = await import("./schedules-BktJb3Y3.mjs").then((n) => n.r);
1306
+ const { default: schedulesPlugin } = await import("./schedules-C-OpmHBq.mjs").then((n) => n.n);
1397
1307
  await fastify.register(schedulesPlugin, {
1398
1308
  ...scheduleOptions,
1399
1309
  schedules: frozenJobs
@@ -1408,7 +1318,64 @@ Resources supplied by "${m.name}": ${provided}.`);
1408
1318
  }
1409
1319
  }
1410
1320
  const subscriptionCounts = /* @__PURE__ */ new Map();
1411
- eventUnsubscribes = await subscribeModuleEventHandlers(fastify, modules, subscriptionCounts);
1321
+ const consumedByModule = /* @__PURE__ */ new Map();
1322
+ const handlersByModule = /* @__PURE__ */ new Map();
1323
+ eventUnsubscribes = await subscribeModuleEventHandlers(fastify, modules, subscriptionCounts, consumedByModule, handlersByModule);
1324
+ const registry = fastify.arc?.registry;
1325
+ const emitsCrud = config.arcPlugins?.emitEvents !== false && registry !== void 0;
1326
+ const derived = /* @__PURE__ */ new Map();
1327
+ const moduleOwned = /* @__PURE__ */ new Set();
1328
+ if (emitsCrud) for (const [moduleName, names] of moduleResourceNames) {
1329
+ const publishes = [];
1330
+ for (const name of names) {
1331
+ moduleOwned.add(name);
1332
+ const entry = registry.get(name);
1333
+ if (entry) publishes.push(...crudEventsOf(entry));
1334
+ }
1335
+ derived.set(moduleName, {
1336
+ publishes,
1337
+ namespaces: [...names]
1338
+ });
1339
+ }
1340
+ const appCrudEvents = emitsCrud ? registry.getAll().filter((e) => !moduleOwned.has(e.name)).flatMap((e) => crudEventsOf(e)) : [];
1341
+ const eventRegistry = fastify.events?.registry;
1342
+ if (emitsCrud && eventRegistry) for (const entry of registry.getAll()) for (const name of crudEventsOf(entry)) {
1343
+ if (eventRegistry.get(name)) continue;
1344
+ eventRegistry.register(defineEvent({
1345
+ name,
1346
+ description: `arc CRUD event — a ${entry.name} was ${name.slice(entry.name.length + 1)}.`,
1347
+ schema: CRUD_EVENT_SCHEMA
1348
+ }));
1349
+ }
1350
+ const moduleGraph = buildModuleGraph(modules, consumedByModule, skippedBridges, [
1351
+ ...config.moduleGraph?.externalEvents ?? [],
1352
+ ...ARC_OWNED_EVENTS,
1353
+ ...appCrudEvents
1354
+ ], derived);
1355
+ for (const bridge of moduleGraph.skippedBridges) fastify.log.info({
1356
+ bridge: bridge.name,
1357
+ missing: bridge.missing
1358
+ }, "[arc] bridge module inactive — a module it joins is not composed");
1359
+ const graphMode = config.moduleGraph?.events ?? (config.preset === "production" ? "warn" : "off");
1360
+ if (graphMode !== "off") {
1361
+ const analysis = analyzeModuleGraph(moduleGraph);
1362
+ const issues = verifyModuleGraph(moduleGraph, analysis);
1363
+ 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")}`);
1364
+ for (const issue of issues) fastify.log.warn({ issue }, `[arc] module graph: ${issue.message}`);
1365
+ const outboxModules = new Set(modules.filter((m) => isTransactionalOutbox(getOptionalModuleExports(fastify, m.name))).map((m) => m.name));
1366
+ const durabilityIssues = verifyEventDurability(modules, {
1367
+ outboxModules,
1368
+ handlers: handlersByModule
1369
+ });
1370
+ 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")}`);
1371
+ for (const issue of durabilityIssues) fastify.log.warn({ issue }, `[arc] module graph (${issue.code}): ${issue.message}`);
1372
+ }
1373
+ if (fastify.arc) fastify.arc.moduleGraph = moduleGraph;
1374
+ const events = fastify.events;
1375
+ if (graphMode !== "off" && typeof events?.observe === "function" && fastify.arc) {
1376
+ const undeclared = watchUndeclaredEvents(fastify, moduleGraph, graphMode);
1377
+ fastify.arc.undeclaredEvents = undeclared;
1378
+ }
1412
1379
  if (fastify.arc) fastify.arc.moduleDescriptors = Object.freeze(modules.map((m) => describeResolvedModule({
1413
1380
  name: m.name,
1414
1381
  dependsOn: m.dependsOn,
@@ -1427,6 +1394,8 @@ Resources supplied by "${m.name}": ${provided}.`);
1427
1394
  fastify.log.debug("afterResources hook executed");
1428
1395
  }
1429
1396
  } catch (err) {
1397
+ const shutdownErrors = await runShutdown(fastify);
1398
+ for (const shutdownError of shutdownErrors) fastify.log.error({ err: shutdownError }, "[arc] runtime rollback failed");
1430
1399
  const unsubscribeErrors = await unsubscribeModuleEventHandlers(eventUnsubscribes);
1431
1400
  for (const unsubscribeError of unsubscribeErrors) fastify.log.error({ err: unsubscribeError }, "[arc] module event-handler rollback after boot failure failed");
1432
1401
  const closeErrors = await teardown.rollback();
@@ -1441,30 +1410,53 @@ Resources supplied by "${m.name}": ${provided}.`);
1441
1410
  }
1442
1411
  const appOnClose = config.onClose;
1443
1412
  if (teardown.hasComposedModules() || appOnClose || eventUnsubscribes.length) fastify.addHook("onClose", async () => {
1413
+ const shutdownErrors = await runShutdown(fastify);
1444
1414
  const unsubscribeErrors = await unsubscribeModuleEventHandlers(eventUnsubscribes);
1445
1415
  for (const err of unsubscribeErrors) fastify.log.error({ err }, "[arc] module event-handler shutdown unsubscribe failed");
1446
1416
  const closeErrors = await teardown.closeAll();
1447
1417
  for (const err of closeErrors) fastify.log.error({ err }, "[arc] module onClose failed during shutdown");
1448
1418
  if (appOnClose) await appOnClose(fastify);
1449
1419
  if (closeErrors.length > 0) throw closeErrors[0];
1420
+ if (shutdownErrors.length > 0) throw shutdownErrors[0];
1450
1421
  });
1451
1422
  }
1452
1423
  /**
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.
1424
+ * `plugins()` is CALLED by arc, never `register()`ed — a returned Fastify
1425
+ * plugin would be silently treated as a disposer. `fp()` markers are
1426
+ * unambiguous, so that throws; a bare 2+-arg function only warns.
1462
1427
  */
1463
1428
  function assertNotAReturnedPlugin(returned, moduleName, fastify) {
1464
1429
  const fn = returned;
1465
1430
  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
1431
  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
1432
  }
1433
+ /** Namespaces arc itself publishes — lifecycle (`arc.*`) and its streamline integration (`workflow.*`). */
1434
+ const ARC_OWNED_EVENTS = ["arc.*", "workflow.*"];
1435
+ /**
1436
+ * Record every published event type the graph has no owner for. Logs each name once; never
1437
+ * touches delivery.
1438
+ */
1439
+ function watchUndeclaredEvents(fastify, graph, mode) {
1440
+ const owned = new Set(graph.modules.flatMap((m) => m.publishes));
1441
+ const external = createEventRouter();
1442
+ for (const pattern of graph.externalEvents) external.add(pattern, true);
1443
+ const undeclared = /* @__PURE__ */ new Set();
1444
+ const stop = fastify.events.observe((event) => {
1445
+ const type = event.type;
1446
+ if (owned.has(type) || undeclared.has(type) || type.startsWith("$")) return;
1447
+ if (external.route(type).length > 0) return;
1448
+ undeclared.add(type);
1449
+ 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\`).`);
1450
+ });
1451
+ fastify.addHook("onClose", async () => stop());
1452
+ return undeclared;
1453
+ }
1454
+ /** A module export that is an outbox whose `save` enlists the caller's session. */
1455
+ function isTransactionalOutbox(exports) {
1456
+ if (typeof exports !== "object" || exports === null || !("store" in exports)) return false;
1457
+ const store = exports.store;
1458
+ return typeof store === "object" && store !== null && "transactionalSave" in store && store.transactionalSave === true;
1459
+ }
1468
1460
  //#endregion
1469
1461
  //#region src/factory/security/cors.ts
1470
1462
  /**
@@ -1650,12 +1642,17 @@ async function registerSecurityPlugins(fastify, config) {
1650
1642
  });
1651
1643
  await fastify.register(rateLimit, rateLimitOpts);
1652
1644
  const rateLimitRecord = typeof rateLimitOpts === "object" && rateLimitOpts !== null ? rateLimitOpts : void 0;
1653
- if (!(rateLimitRecord !== void 0 && (rateLimitRecord.store != null || rateLimitRecord.redis != null))) {
1645
+ const hasStore = rateLimitRecord !== void 0 && (rateLimitRecord.store != null || rateLimitRecord.redis != null);
1646
+ resolveResourceRateLimiter(fastify, hasStore ? "shared" : "memory");
1647
+ if (!hasStore) {
1654
1648
  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
1649
  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
1650
  }
1657
1651
  fastify.log.debug("Rate limiting enabled");
1658
- } else fastify.log.warn("Rate limiting disabled");
1652
+ } else {
1653
+ fastify.log.warn("Rate limiting disabled");
1654
+ markRateLimitingDisabled(fastify);
1655
+ }
1659
1656
  }
1660
1657
  /**
1661
1658
  * Register performance and utility plugins (Under Pressure, Sensible, Multipart, Raw Body).
@@ -1721,40 +1718,18 @@ async function registerUtilityPlugins(fastify, config) {
1721
1718
  //#endregion
1722
1719
  //#region src/factory/createApp.ts
1723
1720
  /**
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.
1721
+ * ArcFactory - Production-ready Fastify application factory.
1728
1722
  *
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.
1723
+ * Security plugins are opt-out, not opt-in. Arc is database-agnostic: connect
1724
+ * your database separately and pass adapters to `defineResource`.
1732
1725
  *
1733
1726
  * @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
1727
  * const app = await createApp({
1740
1728
  * preset: 'production',
1729
+ * runtime: 'memory',
1741
1730
  * auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
1742
- * cors: { origin: ['https://example.com'] },
1743
1731
  * resources: [productResource, orderResource],
1744
1732
  * });
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
1733
  */
1759
1734
  var createApp_exports = /* @__PURE__ */ __exportAll({
1760
1735
  ArcFactory: () => ArcFactory,
@@ -1763,10 +1738,8 @@ var createApp_exports = /* @__PURE__ */ __exportAll({
1763
1738
  resolveLoggerConfig: () => resolveLoggerConfig
1764
1739
  });
1765
1740
  /**
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.
1741
+ * The fail-loud switches — ON by default — and what each one costs when a deployment turns it OFF.
1742
+ * Reported together at production boot, so an opt-out is never silent.
1770
1743
  */
1771
1744
  const STRICT_ENV_FLAGS = [
1772
1745
  ["ARC_STRICT_PERMISSIONS", "An ungated write route warns instead of failing boot."],
@@ -1775,18 +1748,10 @@ const STRICT_ENV_FLAGS = [
1775
1748
  ];
1776
1749
  const MEMORY_STORE_NAMES = /* @__PURE__ */ new Set(["memory", "memory-cache"]);
1777
1750
  /**
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.
1751
+ * Default pino redact paths (auth/cookie/API-key headers, nested
1752
+ * password/token/secret fields), applied only when the host supplies no
1753
+ * `logger.redact`. Opt out with `logger: { redact: [] }`; an explicit
1754
+ * `redact` always wins.
1790
1755
  */
1791
1756
  const DEFAULT_LOGGER_REDACT_PATHS = [
1792
1757
  "req.headers.authorization",
@@ -1805,16 +1770,8 @@ const DEFAULT_LOGGER_REDACT_PATHS = [
1805
1770
  "*.apiKey"
1806
1771
  ];
1807
1772
  /**
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.
1773
+ * Resolve the host's `logger` option for Fastify, layering the default redact
1774
+ * paths unless it is `false` or an object with its own `redact`.
1818
1775
  */
1819
1776
  function resolveLoggerConfig(logger) {
1820
1777
  if (logger === false) return false;
@@ -1900,28 +1857,31 @@ Missing:\n${lines}\n\nFix: pass durable adapters under \`stores\`, or use runtim
1900
1857
  */
1901
1858
  async function createApp(options) {
1902
1859
  /**
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.
1860
+ * TOPOLOGY is declared, never guessed: a second replica on the in-memory runtime silently
1861
+ * loses cross-instance events and caches.
1862
+ */
1863
+ 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).");
1864
+ /**
1865
+ * A relay/scheduler process declaring HTTP resources is a wiring error —
1866
+ * mounting routes anyway would hide the misdeployment behind a working endpoint.
1907
1867
  */
1908
1868
  const declaredRole = options.role ?? "all";
1909
1869
  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
1870
  /**
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`.
1871
+ * This app's own `arcLog` config, entered for the WHOLE boot. Held by
1872
+ * reference and mutated in `buildApp` once `fastify.log` exists — a copy
1873
+ * would send post-construction boot warnings to the console. Per-app so two
1874
+ * apps in one process keep separate writers.
1918
1875
  */
1919
1876
  const loggerScope = {};
1920
1877
  if (options.debug !== void 0 && options.debug !== false) loggerScope.debug = options.debug;
1921
1878
  let partial;
1879
+ let disposeBoot;
1922
1880
  try {
1923
1881
  return await runWithArcLogger(loggerScope, () => buildApp(options, loggerScope, (instance) => {
1924
1882
  partial = instance;
1883
+ }, (dispose) => {
1884
+ disposeBoot = dispose;
1925
1885
  }));
1926
1886
  } catch (err) {
1927
1887
  if (partial) try {
@@ -1929,38 +1889,42 @@ async function createApp(options) {
1929
1889
  } catch (closeErr) {
1930
1890
  partial.log?.error?.({ err: closeErr }, "[arc] cleanup of a partially-booted app failed; original boot error follows");
1931
1891
  }
1892
+ if (disposeBoot) try {
1893
+ await disposeBoot();
1894
+ } catch (disposeErr) {
1895
+ arcLog("createApp").error("beforeBoot disposer failed during failed-boot cleanup; original boot error follows", disposeErr);
1896
+ }
1932
1897
  throw err;
1933
1898
  }
1934
1899
  }
1935
- async function buildApp(options, loggerScope, onInstance) {
1900
+ async function buildApp(options, loggerScope, onInstance, onBootDisposer) {
1936
1901
  validateAuthOptions(options);
1937
1902
  const deferredWarnings = validateDistributedRuntime(options);
1938
- if (options.beforeBoot) await options.beforeBoot();
1903
+ let disposeBoot;
1904
+ if (options.beforeBoot) {
1905
+ const dispose = await options.beforeBoot();
1906
+ if (typeof dispose === "function") {
1907
+ let once;
1908
+ disposeBoot = () => {
1909
+ once ??= Promise.resolve().then(dispose);
1910
+ return once;
1911
+ };
1912
+ onBootDisposer(disposeBoot);
1913
+ }
1914
+ }
1939
1915
  const config = {
1940
1916
  ...options.preset ? getPreset(options.preset) : {},
1941
- ...options
1917
+ ...options,
1918
+ ...options.role === "relay" || options.role === "scheduler" ? { mountRoutes: false } : {}
1942
1919
  };
1943
1920
  const inheritedTrustProxyDefault = options.preset === "production" && options.trustProxy === void 0;
1944
1921
  /**
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.
1922
+ * A NUMERIC `trustProxy` trusts NOTHING on Fastify >= 5.12.1 (hop-count
1923
+ * trust fails closed upstream), so the host silently gets the proxy's IP.
1924
+ * Warn rather than refuse — a boot failure on a transitive bump is harsher.
1961
1925
  */
1962
1926
  const numericTrustProxy = typeof config.trustProxy === "number";
1963
- const resolvedModules = orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
1927
+ const { modules: resolvedModules, skipped: skippedBridges } = await composeModules(config.modules ?? [], config.ports);
1964
1928
  const moduleErrorMappers = resolvedModules.flatMap((m) => m.errorMappers ?? []);
1965
1929
  const moduleHealthChecks = collectModuleHealthChecks(resolvedModules);
1966
1930
  const resolvedLogger = resolveLoggerConfig(config.logger);
@@ -1983,16 +1947,13 @@ async function buildApp(options, loggerScope, onInstance) {
1983
1947
  }),
1984
1948
  maxParamLength: config.maxParamLength ?? 400
1985
1949
  },
1986
- ajv: { customOptions: {
1987
- coerceTypes: true,
1988
- useDefaults: true,
1989
- removeAdditional: false,
1990
- strictTypes: false,
1991
- keywords: ["example", ...config.ajv?.keywords ?? []]
1992
- } }
1950
+ ajv: { customOptions: arcAjvCustomOptions(config.ajv?.keywords) }
1993
1951
  });
1952
+ markArcRootValidator(fastify);
1994
1953
  onInstance(fastify);
1954
+ if (disposeBoot) fastify.addHook("onClose", disposeBoot);
1995
1955
  initRuntimeCapabilityRegistry(fastify);
1956
+ initShutdownRegistry(fastify);
1996
1957
  if (resolvedLogger !== false) {
1997
1958
  const writer = createPinoWriter(fastify.log);
1998
1959
  loggerScope.writer = writer;
@@ -2006,22 +1967,15 @@ async function buildApp(options, loggerScope, onInstance) {
2006
1967
  });
2007
1968
  for (const warning of deferredWarnings) fastify.log.warn(warning);
2008
1969
  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.");
1970
+ 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
1971
  /**
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.
1972
+ * Production names every fail-loud switch turned OFF. They are read before `createApp`, so this
1973
+ * reports rather than sets.
2021
1974
  */
2022
1975
  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.`);
1976
+ const env = readEnv(process.env);
1977
+ const off = STRICT_ENV_FLAGS.filter(([name]) => !env.bool(name, true));
1978
+ 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
1979
  }
2026
1980
  fastify.removeContentTypeParser("application/json");
2027
1981
  fastify.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
@@ -2053,15 +2007,14 @@ async function buildApp(options, loggerScope, onInstance) {
2053
2007
  await registerAuth(fastify, config, trackPlugin);
2054
2008
  await registerElevation(fastify, config, trackPlugin);
2055
2009
  await registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers);
2056
- await registerResources(fastify, config, resolvedModules);
2010
+ await registerResources(fastify, config, resolvedModules, skippedBridges);
2057
2011
  if (config.replyHelpers) {
2058
- const { replyHelpersPlugin } = await import("./replyHelpers-DPHsdw-5.mjs").then((n) => n.n);
2012
+ const { replyHelpersPlugin } = await import("./replyHelpers-CG_VtVgS.mjs").then((n) => n.n);
2059
2013
  await fastify.register(replyHelpersPlugin);
2060
2014
  }
2061
2015
  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.");
2016
+ const mode = config.serializeBigInt;
2017
+ 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
2018
  fastify.addHook("preSerialization", async (_request, _reply, payload) => {
2066
2019
  if (payload === null || payload === void 0) return payload;
2067
2020
  try {
@@ -2107,48 +2060,4 @@ const ArcFactory = {
2107
2060
  }
2108
2061
  };
2109
2062
  //#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 };
2063
+ 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 };