@classytic/arc 2.31.0 → 2.33.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 (194) hide show
  1. package/README.md +1 -1
  2. package/bin/arc.js +58 -17
  3. package/dist/{BaseController-De7GqHck.mjs → BaseController-CaQC5EXX.mjs} +298 -35
  4. package/dist/{EventTransport-2uVV34WU.d.mts → EventTransport-BEVP50ka.d.mts} +16 -0
  5. package/dist/{EventTransport-9mlTgqzE.mjs → EventTransport-tq4CEB-q.mjs} +39 -2
  6. package/dist/{HookSystem-BcmPyq5I.mjs → HookSystem-DqXkMXkB.mjs} +2 -2
  7. package/dist/{ResourceRegistry-qq0mn2s9.mjs → ResourceRegistry-DoALUlOO.mjs} +18 -15
  8. package/dist/{actionPermissions-BE5NwaQk.mjs → actionPermissions-CUeCmpUd.mjs} +1 -1
  9. package/dist/audit/index.d.mts +2 -2
  10. package/dist/audit/index.mjs +2 -2
  11. package/dist/auth/audit.mjs +1 -1
  12. package/dist/auth/index.d.mts +2 -2
  13. package/dist/auth/index.mjs +44 -11
  14. package/dist/auth/redis-session.mjs +1 -1
  15. package/dist/{authorizationDecision-ChvUQGG1.mjs → authorizationDecision-B3mP_QTy.mjs} +2 -2
  16. package/dist/{types-B7XeLEgK.d.mts → base-Csjc2oF-.d.mts} +74 -74
  17. package/dist/{buildHandler-CDrCJzmB.mjs → buildHandler-DLE9xX-V.mjs} +6 -5
  18. package/dist/cache/index.mjs +4 -4
  19. package/dist/{caching-BFHA6Scf.mjs → caching-BcGmvnHh.mjs} +2 -2
  20. package/dist/cleanup/index.d.mts +52 -18
  21. package/dist/cleanup/index.mjs +281 -32
  22. package/dist/cli/commands/describe.d.mts +1 -1
  23. package/dist/cli/commands/describe.mjs +4 -6
  24. package/dist/cli/commands/docs.mjs +3 -3
  25. package/dist/cli/commands/doctor.mjs +1 -1
  26. package/dist/cli/commands/generate.mjs +2 -2
  27. package/dist/cli/commands/init.mjs +2 -2
  28. package/dist/cli/commands/introspect.mjs +2 -2
  29. package/dist/cli/index.mjs +3 -3
  30. package/dist/{constants-D3Dv4LAV.mjs → constants-BxCoxmiN.mjs} +1 -1
  31. package/dist/context/index.mjs +3 -3
  32. package/dist/core/index.d.mts +2 -2
  33. package/dist/core/index.mjs +7 -7
  34. package/dist/{core-BGpIxINN.mjs → core-dxhcHhUD.mjs} +3 -3
  35. package/dist/{createActionRouter-CDXEdasl.mjs → createActionRouter-DVpoA0do.mjs} +14 -11
  36. package/dist/{createAggregationRouter-KrQDzx-k.mjs → createAggregationRouter-B_B48JZ-.mjs} +10 -8
  37. package/dist/{createApp-Df89BEni.mjs → createApp-CLGegqiG.mjs} +287 -62
  38. package/dist/{defineAggregation-DbPPS8RI.mjs → defineAggregation-Nv-DwfOf.mjs} +1 -1
  39. package/dist/{defineResource-Bv6EtN1x.mjs → defineResource-DR9vsUYy.mjs} +318 -35
  40. package/dist/discovery/index.mjs +1 -1
  41. package/dist/docs/index.d.mts +1 -1
  42. package/dist/docs/index.mjs +3 -3
  43. package/dist/{elevation-D2l1mh4g.mjs → elevation-B0tlY8y1.mjs} +4 -4
  44. package/dist/encryption/index.mjs +5 -4
  45. package/dist/{errorHandler-C5NeD6bT.mjs → errorHandler-C7xBqHsI.mjs} +42 -5
  46. package/dist/{errorHandler-CpYUJmPA.d.mts → errorHandler-DI5AN2b5.d.mts} +14 -3
  47. package/dist/{errors-Cj42AntO.mjs → errors-Cd_yy7aW.mjs} +1 -1
  48. package/dist/{eventPlugin-GF5nnBIq.d.mts → eventPlugin-DSNEZWfV.d.mts} +29 -1
  49. package/dist/{eventPlugin-CkffEihm.mjs → eventPlugin-Dar8qRaj.mjs} +68 -11
  50. package/dist/events/index.d.mts +18 -297
  51. package/dist/events/index.mjs +7 -758
  52. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  53. package/dist/events/transports/redis-stream-entry.mjs +14 -10
  54. package/dist/events/transports/redis.d.mts +1 -1
  55. package/dist/events/transports/redis.mjs +1 -1
  56. package/dist/factory/index.d.mts +20 -5
  57. package/dist/factory/index.mjs +131 -8
  58. package/dist/fastify-augmentation-DVsTAy6X.d.mts +179 -0
  59. package/dist/{fields-CB5Cuzk9.mjs → fields-CojeUkS4.mjs} +6 -11
  60. package/dist/{generate-EcA6ui4i.mjs → generate-DiB0nHPR.mjs} +2 -2
  61. package/dist/{headers-3zvE0rM3.mjs → headers-CveNO9gR.mjs} +2 -2
  62. package/dist/{health-DjZYjM7H.mjs → health-CasG2A28.mjs} +1 -1
  63. package/dist/hooks/index.d.mts +1 -1
  64. package/dist/hooks/index.mjs +2 -2
  65. package/dist/idempotency/index.d.mts +45 -1
  66. package/dist/idempotency/index.mjs +95 -11
  67. package/dist/idempotency/redis.mjs +1 -1
  68. package/dist/{index-CEJNZFtX.d.mts → index-B-DvUzsr.d.mts} +82 -3
  69. package/dist/{index-CpzxqePp.d.mts → index-BcR2bFZQ.d.mts} +707 -373
  70. package/dist/{index-Bc2ox8_U.d.mts → index-Brin0ODU.d.mts} +4 -4
  71. package/dist/{index-CFThn1K9.d.mts → index-C5NnMEKF.d.mts} +70 -4
  72. package/dist/{index-C-481Zf2.d.mts → index-ugr1tQlW.d.mts} +17 -1
  73. package/dist/index.d.mts +6 -6
  74. package/dist/index.mjs +11 -11
  75. package/dist/{init-BEiNZBsJ.mjs → init-mdJ-v4Xp.mjs} +24 -16
  76. package/dist/integrations/event-gateway.d.mts +1 -1
  77. package/dist/integrations/event-gateway.mjs +2 -2
  78. package/dist/integrations/index.d.mts +2 -2
  79. package/dist/integrations/index.mjs +1 -1
  80. package/dist/integrations/jobs/index.d.mts +1 -1
  81. package/dist/integrations/jobs/index.mjs +100 -98
  82. package/dist/integrations/mcp/index.d.mts +12 -3
  83. package/dist/integrations/mcp/index.mjs +2 -2
  84. package/dist/integrations/mcp/testing.d.mts +1 -1
  85. package/dist/integrations/mcp/testing.mjs +2 -2
  86. package/dist/integrations/streamline.mjs +3 -3
  87. package/dist/integrations/webhooks.d.mts +124 -1
  88. package/dist/integrations/webhooks.mjs +180 -2
  89. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  90. package/dist/integrations/websocket-redis.mjs +1 -1
  91. package/dist/integrations/websocket.mjs +38 -35
  92. package/dist/{interface-DYzM0q-u.mjs → interface-DznJrm9R.mjs} +1 -1
  93. package/dist/{jsonBody-BYbLDi5D.mjs → jsonBody-CfGH-GxO.mjs} +2 -2
  94. package/dist/{keys-C0_pa1tY.mjs → keys-WeKvY1dE.mjs} +1 -1
  95. package/dist/{loadResources-yawi9tAE.mjs → loadResources-Bj-aSsXi.mjs} +4 -3
  96. package/dist/{loadResourcesFromEntry-BAjYZonx.mjs → loadResourcesFromEntry-DwaxNcE2.mjs} +2 -2
  97. package/dist/logger/index.mjs +1 -1
  98. package/dist/{mcpPlugin-B_es0hiF.mjs → mcpPlugin-aJg5TqHi.mjs} +76 -36
  99. package/dist/{memory-DR6wpa9F.mjs → memory-qYSB7rMQ.mjs} +5 -5
  100. package/dist/{metrics-CUdw_wLB.mjs → metrics-D7uchLjN.mjs} +2 -2
  101. package/dist/middleware/index.d.mts +1 -1
  102. package/dist/middleware/index.mjs +3 -3
  103. package/dist/migrations/index.mjs +3 -3
  104. package/dist/{module-0pFJA6YW.mjs → module-CwKsgZXp.mjs} +1 -1
  105. package/dist/{multipartBody-wOgj_fnj.mjs → multipartBody-4w-S3-PU.mjs} +3 -3
  106. package/dist/{openapi-B2L4js1v.mjs → openapi-BN5cQaO2.mjs} +6 -6
  107. package/dist/outbox-admin/index.d.mts +85 -0
  108. package/dist/outbox-admin/index.mjs +115 -0
  109. package/dist/outbox-module-CXB79bvt.mjs +839 -0
  110. package/dist/outbox-module-D-VFq37T.d.mts +368 -0
  111. package/dist/permissions/index.d.mts +3 -3
  112. package/dist/permissions/index.mjs +6 -6
  113. package/dist/{permissions-L--QDAir.mjs → permissions-BAfwgBxc.mjs} +120 -8
  114. package/dist/{pipe-DRDNsI2T.mjs → pipe-BzzoEhHj.mjs} +3 -5
  115. package/dist/pipeline/index.d.mts +1 -1
  116. package/dist/pipeline/index.mjs +2 -2
  117. package/dist/{pipeline-UPQFatit.mjs → pipeline-q69DZzgG.mjs} +17 -15
  118. package/dist/plugins/index.d.mts +6 -6
  119. package/dist/plugins/index.mjs +16 -16
  120. package/dist/plugins/response-cache.mjs +2 -2
  121. package/dist/plugins/tracing-entry.mjs +2 -2
  122. package/dist/{pluralize-O0FZgESl.mjs → pluralize-B7xf8qZF.mjs} +1 -1
  123. package/dist/{preloadResources-Dz6BjzM6.mjs → preloadResources-BkyqeTDz.mjs} +1 -1
  124. package/dist/{preloadResources-CyoJMSiO.d.mts → preloadResources-g07ldtzh.d.mts} +30 -7
  125. package/dist/presets/filesUpload.d.mts +2 -2
  126. package/dist/presets/filesUpload.mjs +6 -6
  127. package/dist/presets/index.d.mts +2 -2
  128. package/dist/presets/index.mjs +2 -2
  129. package/dist/presets/multiTenant.d.mts +1 -1
  130. package/dist/presets/multiTenant.mjs +3 -3
  131. package/dist/presets/search.d.mts +2 -2
  132. package/dist/presets/search.mjs +2 -2
  133. package/dist/{presets-C4-g4_4P.mjs → presets-D-h0uWtP.mjs} +6 -5
  134. package/dist/projection-BKM0Ui-s.mjs +42 -0
  135. package/dist/{queryCachePlugin-BZr8v46E.mjs → queryCachePlugin-DhsZ0ZIc.mjs} +50 -8
  136. package/dist/{rateLimit-CMPY5Qms.mjs → rateLimit-D4-ASwsI.mjs} +2 -2
  137. package/dist/{rateLimitKey-Dn5JNmkj.mjs → rateLimitKey-XngqVNEV.mjs} +1 -1
  138. package/dist/{realtime-C5NoxWFY.mjs → realtime-CTkv3O59.mjs} +10 -10
  139. package/dist/{redis-stream-qN6A-RFv.d.mts → redis-stream-mwLWlfHG.d.mts} +1 -1
  140. package/dist/{registerRoute-CBN84fc7.mjs → registerRoute-DNnd3vkz.mjs} +10 -8
  141. package/dist/registry/index.d.mts +3 -3
  142. package/dist/registry/index.mjs +3 -3
  143. package/dist/{registry-4213U4zb.mjs → registry-CLxCR5LF.mjs} +2 -2
  144. package/dist/{renewingLease-Dll8mGgc.mjs → renewingLease-BnWtbA2U.mjs} +1 -1
  145. package/dist/{replyHelpers-DIiZKGUt.mjs → replyHelpers-BSBVCwL3.mjs} +2 -2
  146. package/dist/{repositoryFilter-f9eAfq_l.mjs → repositoryFilter-DKny7PDn.mjs} +1 -1
  147. package/dist/{requestContext-2TxXOJ7T.mjs → requestContext-uYE35JxI.mjs} +1 -1
  148. package/dist/{rolldown-runtime-CUsjNX9R.mjs → rolldown-runtime-DNg-KNCk.mjs} +1 -1
  149. package/dist/runtimeCapabilities-Dm88DaTW.mjs +49 -0
  150. package/dist/{schedules-DdtlICTf.mjs → schedules-CAcZgia0.mjs} +3 -3
  151. package/dist/{schemaConverter-D--wdJM-.mjs → schemaConverter-XH1RZRyu.mjs} +1 -1
  152. package/dist/{schemaIR-Bm2enbnh.mjs → schemaIR-CDvclAew.mjs} +2 -2
  153. package/dist/schemas/index.mjs +1 -1
  154. package/dist/scim/index.mjs +2 -2
  155. package/dist/scope/index.mjs +5 -5
  156. package/dist/{scope-DHx5Fous.mjs → scope-BaWcqBvr.mjs} +4 -4
  157. package/dist/{simpleEqualityMatcher-DjQJ7FKA.mjs → simpleEqualityMatcher-Se8VrsaP.mjs} +1 -1
  158. package/dist/{sse-CUTo9y0X.mjs → sse-Cf0jUHsh.mjs} +7 -6
  159. package/dist/{sseStream-B_5XcVVd.mjs → sseStream-Bvpi-UXQ.mjs} +2 -2
  160. package/dist/{store-helpers-D1MP_gRG.mjs → store-helpers-B88p99Z2.mjs} +1 -1
  161. package/dist/{streaming-PSZwi8JV.mjs → streaming-DsT1B5zJ.mjs} +1 -1
  162. package/dist/{subscribe-helpers-C0Tb2aUp.mjs → subscribe-helpers-CLIOHsOK.mjs} +2 -2
  163. package/dist/sync/changelog.mjs +1 -1
  164. package/dist/testing/cleanupStoreContract.d.mts +1 -1
  165. package/dist/testing/cleanupStoreContract.mjs +12 -6
  166. package/dist/testing/index.d.mts +52 -13
  167. package/dist/testing/index.mjs +110 -40
  168. package/dist/testing/outboxStoreContract.d.mts +11 -2
  169. package/dist/testing/outboxStoreContract.mjs +110 -21
  170. package/dist/testing/storageContract.mjs +11 -6
  171. package/dist/{transactionContext-dAfBVUrk.mjs → transactionContext-BYtWnwAS.mjs} +1 -1
  172. package/dist/{typeGuards-B0ryPo3I.mjs → typeGuards-jjKJDN13.mjs} +1 -1
  173. package/dist/types/index.d.mts +3 -3
  174. package/dist/types/index.mjs +1 -1
  175. package/dist/types/storage.mjs +1 -1
  176. package/dist/{types-CBgvkp8O.d.mts → types-BcTumAz_.d.mts} +128 -18
  177. package/dist/{types-CwcUQ610.d.mts → types-BfC83KsP.d.mts} +15 -1
  178. package/dist/{types-Cys0ko9Z.mjs → types-CoqIRrkY.mjs} +4 -4
  179. package/dist/{types-C4QE8qnm.d.mts → types-DVLddu2q.d.mts} +144 -1
  180. package/dist/usage/index.d.mts +1 -1
  181. package/dist/usage/index.mjs +0 -0
  182. package/dist/{userHelpers-ChYJ6CdK.mjs → userHelpers-CWF5pbZD.mjs} +1 -1
  183. package/dist/utils/index.d.mts +57 -5
  184. package/dist/utils/index.mjs +12 -11
  185. package/dist/{utils-vL8BrDE2.mjs → utils-h1V_sT-t.mjs} +38 -7
  186. package/dist/{validate-_KbifpHF.mjs → validate-CdQlz52F.mjs} +2 -2
  187. package/dist/{versioning-D_rhHfZm.d.mts → versioning-DEs-_hFi.d.mts} +1 -1
  188. package/dist/{versioning-CHDTor3P.mjs → versioning-rZbyLVD-.mjs} +4 -4
  189. package/package.json +31 -20
  190. package/skills/arc/SKILL.md +96 -4
  191. package/skills/arc/references/api-reference.md +19 -0
  192. package/skills/arc/references/production.md +2 -2
  193. package/dist/fastify-augmentation-zh0_AYHa.d.mts +0 -85
  194. package/dist/projection-BHw6xlOQ.mjs +0 -18
@@ -1,11 +1,13 @@
1
- /*! @classytic/arc v2.31.0 | MIT | https://classytic.com */
2
- import { t as __exportAll } from "./rolldown-runtime-CUsjNX9R.mjs";
1
+ /*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
2
+ import { t as __exportAll } from "./rolldown-runtime-DNg-KNCk.mjs";
3
3
  import { arcLog, configureArcLogger, createPinoWriter } from "./logger/index.mjs";
4
- import { n as PUBLIC_SCOPE } from "./types-Cys0ko9Z.mjs";
5
- import { t as createTenantKeyGenerator } from "./rateLimitKey-Dn5JNmkj.mjs";
6
- import { i as createRequestIdGenerator, r as mergeHealthChecks } from "./health-DjZYjM7H.mjs";
7
- import { t as parseJsonBody } from "./jsonBody-BYbLDi5D.mjs";
8
- import { r as wrapWithBoundary } from "./subscribe-helpers-C0Tb2aUp.mjs";
4
+ import { n as PUBLIC_SCOPE } from "./types-CoqIRrkY.mjs";
5
+ import { t as auditRuntimeCapabilities } from "./runtimeCapabilities-Dm88DaTW.mjs";
6
+ import { t as createTenantKeyGenerator } from "./rateLimitKey-XngqVNEV.mjs";
7
+ import { i as createRequestIdGenerator, r as mergeHealthChecks } from "./health-CasG2A28.mjs";
8
+ import { t as parseJsonBody } from "./jsonBody-CfGH-GxO.mjs";
9
+ import { r as wrapWithBoundary } from "./subscribe-helpers-CLIOHsOK.mjs";
10
+ import { isProductionEnv } from "@classytic/primitives/environment";
9
11
  import Fastify from "fastify";
10
12
  import qs from "qs";
11
13
  //#region src/factory/security/pluginLoader.ts
@@ -183,12 +185,15 @@ async function resolveContribution(contribution, fastify) {
183
185
  * them. The original error is preserved via `{ cause }`.
184
186
  */
185
187
  async function resolveModuleArm(module, arm, contribution, fastify) {
188
+ let resolved;
186
189
  try {
187
- return await resolveContribution(contribution, fastify);
190
+ resolved = await resolveContribution(contribution, fastify);
188
191
  } catch (err) {
189
192
  const msg = err instanceof Error ? err.message : String(err);
190
193
  throw new Error(`[arc] module "${module.name}" ${arm} factory threw: ${msg}`, { cause: err });
191
194
  }
195
+ if (!Array.isArray(resolved)) throw new TypeError(`[arc] module "${module.name}" ${arm} factory must return an array — received ${resolved === null ? "null" : typeof resolved}.`);
196
+ return resolved;
192
197
  }
193
198
  //#endregion
194
199
  //#region src/factory/module/contributions.ts
@@ -227,11 +232,12 @@ async function collectModuleWorkflows(fastify, modules) {
227
232
  * modules. The caller passes the returned definitions to the canonical
228
233
  * schedules plugin; factories must therefore be resolved only once.
229
234
  */
230
- async function collectModuleScheduledJobs(fastify, modules) {
235
+ async function collectModuleScheduledJobs(fastify, modules, countsByModule) {
231
236
  const owner = /* @__PURE__ */ new Map();
232
237
  const out = [];
233
238
  for (const m of modules) {
234
239
  const jobs = await resolveModuleArm(m, "scheduledJobs", m.scheduledJobs, fastify);
240
+ countsByModule?.set(m.name, jobs.length);
235
241
  for (const job of jobs) {
236
242
  const prior = owner.get(job.name);
237
243
  if (prior !== void 0) throw new Error(`[arc] duplicate scheduled-job name "${job.name}" — declared by module "${prior}" and module "${m.name}". Scheduled-job names must be unique across the module graph.`);
@@ -242,6 +248,69 @@ async function collectModuleScheduledJobs(fastify, modules) {
242
248
  return out;
243
249
  }
244
250
  //#endregion
251
+ //#region src/factory/module/descriptor.ts
252
+ /**
253
+ * Build a descriptor. Called by `registerResources` once per module, after
254
+ * resources are resolved and `owns` is effective.
255
+ *
256
+ * Frozen at every level: a descriptor is a snapshot of a completed decision,
257
+ * and a consumer mutating it would silently disagree with the running app.
258
+ */
259
+ function describeResolvedModule(input) {
260
+ return Object.freeze({
261
+ name: input.name,
262
+ dependsOn: Object.freeze([...input.dependsOn ?? []]),
263
+ resources: Object.freeze(input.resources.map((r) => Object.freeze({
264
+ ...r.name !== void 0 ? { name: r.name } : {},
265
+ ...r.prefix !== void 0 ? { prefix: r.prefix } : {},
266
+ skipGlobalPrefix: r.skipGlobalPrefix === true
267
+ }))),
268
+ owns: Object.freeze([...input.owns]),
269
+ lifecycle: Object.freeze({ ...input.lifecycle })
270
+ });
271
+ }
272
+ //#endregion
273
+ //#region src/factory/module/disposers.ts
274
+ function createDisposerRegistry(fastify) {
275
+ const stacks = /* @__PURE__ */ new Map();
276
+ const drained = /* @__PURE__ */ new Set();
277
+ const contexts = /* @__PURE__ */ new Map();
278
+ return {
279
+ contextFor(moduleName) {
280
+ const existing = contexts.get(moduleName);
281
+ if (existing) return existing;
282
+ const ctx = { defer(disposer) {
283
+ if (typeof disposer !== "function") throw new TypeError(`[arc] module "${moduleName}" called defer() with a ${typeof disposer} — expected a function.`);
284
+ if (drained.has(moduleName)) {
285
+ (async () => {
286
+ try {
287
+ await disposer();
288
+ } catch (err) {
289
+ fastify.log?.error({
290
+ err,
291
+ module: moduleName
292
+ }, `[arc] module "${moduleName}" deferred a disposer after teardown had already run; it was invoked immediately and threw.`);
293
+ }
294
+ })();
295
+ return;
296
+ }
297
+ const stack = stacks.get(moduleName);
298
+ if (stack) stack.push(disposer);
299
+ else stacks.set(moduleName, [disposer]);
300
+ } };
301
+ contexts.set(moduleName, ctx);
302
+ return ctx;
303
+ },
304
+ take(moduleName) {
305
+ drained.add(moduleName);
306
+ const stack = stacks.get(moduleName);
307
+ if (!stack?.length) return [];
308
+ stacks.delete(moduleName);
309
+ return stack.reverse();
310
+ }
311
+ };
312
+ }
313
+ //#endregion
245
314
  //#region src/factory/module/lifecycle.ts
246
315
  /**
247
316
  * Subscribe every module's `eventHandlers` in dependency order (pass the
@@ -253,12 +322,13 @@ async function collectModuleScheduledJobs(fastify, modules) {
253
322
  * Resolves factory contributions (after bootstraps) so a handler can close over
254
323
  * booted engines rather than a global getter.
255
324
  */
256
- async function subscribeModuleEventHandlers(fastify, modules) {
325
+ async function subscribeModuleEventHandlers(fastify, modules, countsByModule) {
257
326
  const unsubscribes = [];
258
327
  const owner = /* @__PURE__ */ new Map();
259
328
  try {
260
329
  for (const m of modules) {
261
330
  const handlers = await resolveModuleArm(m, "eventHandlers", m.eventHandlers, fastify);
331
+ countsByModule?.set(m.name, handlers.length);
262
332
  if (handlers.length === 0) continue;
263
333
  const bus = fastify.events;
264
334
  if (!bus) throw new Error(`[arc] module "${m.name}" declares eventHandlers but the event subsystem is unavailable (fastify.events). Enable arcPlugins.events.`);
@@ -460,7 +530,7 @@ function getModuleState(fastify, name) {
460
530
  }
461
531
  //#endregion
462
532
  //#region src/factory/module/teardown.ts
463
- function createModuleTeardown(fastify, modules) {
533
+ function createModuleTeardown(fastify, modules, disposers) {
464
534
  const initializedNames = /* @__PURE__ */ new Set();
465
535
  const consumed = /* @__PURE__ */ new Set();
466
536
  async function closeInitialized() {
@@ -469,18 +539,30 @@ function createModuleTeardown(fastify, modules) {
469
539
  const m = modules[index];
470
540
  if (!m || !initializedNames.has(m.name) || consumed.has(m.name)) continue;
471
541
  consumed.add(m.name);
472
- if (!m.onClose) {
542
+ const stack = disposers?.take(m.name) ?? [];
543
+ if (!m.onClose && stack.length === 0) {
473
544
  setModuleState(fastify, m.name, "closed");
474
545
  continue;
475
546
  }
476
547
  setModuleState(fastify, m.name, "closing");
477
- try {
548
+ let teardownFailed = false;
549
+ if (m.onClose) try {
478
550
  await m.onClose(fastify);
479
- setModuleState(fastify, m.name, "closed");
480
551
  } catch (err) {
481
- setModuleState(fastify, m.name, "failed");
552
+ teardownFailed = true;
482
553
  errors.push(new Error(`[arc] module "${m.name}" onClose() threw during teardown: ${err instanceof Error ? err.message : String(err)}`, { cause: err }));
483
554
  }
555
+ for (let d = 0; d < stack.length; d++) {
556
+ const dispose = stack[d];
557
+ if (!dispose) continue;
558
+ try {
559
+ await dispose();
560
+ } catch (err) {
561
+ teardownFailed = true;
562
+ errors.push(new Error(`[arc] module "${m.name}" deferred disposer #${stack.length - d} threw during teardown: ${err instanceof Error ? err.message : String(err)}`, { cause: err }));
563
+ }
564
+ }
565
+ setModuleState(fastify, m.name, teardownFailed ? "failed" : "closed");
484
566
  }
485
567
  return errors;
486
568
  }
@@ -554,8 +636,8 @@ const productionPreset = {
554
636
  exposeStatusRoute: true,
555
637
  maxEventLoopDelay: 3e3,
556
638
  maxEventLoopUtilization: .98,
557
- maxHeapUsedBytes: 1024 * 1024 * 1024,
558
- maxRssBytes: 1024 * 1024 * 1024
639
+ maxHeapUsedBytes: 1073741824,
640
+ maxRssBytes: 1073741824
559
641
  }
560
642
  };
561
643
  /**
@@ -625,7 +707,7 @@ const testingPreset = {
625
707
  arcPlugins: { gracefulShutdown: false },
626
708
  sensible: true,
627
709
  multipart: { limits: {
628
- fileSize: 1024 * 1024,
710
+ fileSize: 1048576,
629
711
  files: 5
630
712
  } }
631
713
  };
@@ -719,9 +801,20 @@ async function registerArcCore(fastify, config, trackPlugin) {
719
801
  await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
720
802
  trackPlugin("arc-core");
721
803
  if (config.arcPlugins?.events !== false) {
722
- const { default: eventPlugin } = await import("./eventPlugin-CkffEihm.mjs").then((n) => n.n);
804
+ const { default: eventPlugin } = await import("./eventPlugin-Dar8qRaj.mjs").then((n) => n.n);
723
805
  const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
724
806
  await fastify.register(eventPlugin, {
807
+ /**
808
+ * An EXPLICIT `runtime: 'memory'` is the host's single-instance
809
+ * declaration (its JSDoc says exactly that), so it flows down as the
810
+ * event plugin's `singleProcess` flag — one topology axis, declared
811
+ * once, no second knob to keep in sync. The comparison is against the
812
+ * literal, not the resolved default: `runtime` UNSET also means memory
813
+ * stores, but an unstated topology must keep the forgotten-default
814
+ * warn. `eventOpts` spreads after, so a direct
815
+ * `arcPlugins.events.singleProcess` still wins either way.
816
+ */
817
+ ...config.runtime === "memory" ? { singleProcess: true } : {},
725
818
  ...eventOpts,
726
819
  transport: config.stores?.events
727
820
  });
@@ -767,43 +860,47 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules, moduleH
767
860
  trackPlugin("arc-graceful-shutdown");
768
861
  }
769
862
  if (config.arcPlugins?.caching) {
770
- const { default: cachingPlugin } = await import("./caching-BFHA6Scf.mjs").then((n) => n.r);
863
+ const { default: cachingPlugin } = await import("./caching-BcGmvnHh.mjs").then((n) => n.r);
771
864
  const opts = config.arcPlugins.caching === true ? {} : config.arcPlugins.caching;
772
865
  await fastify.register(cachingPlugin, opts);
773
866
  trackPlugin("arc-caching", opts);
774
867
  }
775
868
  if (config.arcPlugins?.queryCache) {
776
- const { queryCachePlugin } = await import("./queryCachePlugin-BZr8v46E.mjs").then((n) => n.n);
869
+ const { queryCachePlugin } = await import("./queryCachePlugin-DhsZ0ZIc.mjs").then((n) => n.n);
777
870
  const opts = config.arcPlugins.queryCache === true ? {} : config.arcPlugins.queryCache;
778
- const store = config.stores?.queryCache ?? new (await (import("./memory-DR6wpa9F.mjs").then((n) => n.n))).MemoryCacheStore();
871
+ const store = config.stores?.queryCache ?? new (await (import("./memory-qYSB7rMQ.mjs").then((n) => n.n))).MemoryCacheStore();
779
872
  await fastify.register(queryCachePlugin, {
780
873
  store,
781
874
  ...opts
782
875
  });
783
876
  trackPlugin("arc-query-cache", opts);
784
877
  }
785
- if (config.arcPlugins?.sse) if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
786
- else {
787
- const { default: ssePlugin } = await import("./sse-CUTo9y0X.mjs").then((n) => n.r);
788
- const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
789
- await fastify.register(ssePlugin, opts);
790
- trackPlugin("arc-sse", opts);
878
+ if (config.arcPlugins?.sse) {
879
+ if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
880
+ else {
881
+ const { default: ssePlugin } = await import("./sse-Cf0jUHsh.mjs").then((n) => n.r);
882
+ const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
883
+ await fastify.register(ssePlugin, opts);
884
+ trackPlugin("arc-sse", opts);
885
+ }
791
886
  }
792
- if (config.arcPlugins?.realtime) if (config.arcPlugins?.events === false) fastify.log.warn("Realtime plugin requires events plugin (arcPlugins.events). Realtime disabled.");
793
- else {
794
- const { default: realtimePlugin } = await import("./realtime-C5NoxWFY.mjs").then((n) => n.r);
795
- const opts = config.arcPlugins.realtime === true ? {} : config.arcPlugins.realtime;
796
- await fastify.register(realtimePlugin, opts);
797
- trackPlugin("arc-realtime", opts);
887
+ if (config.arcPlugins?.realtime) {
888
+ if (config.arcPlugins?.events === false) fastify.log.warn("Realtime plugin requires events plugin (arcPlugins.events). Realtime disabled.");
889
+ else {
890
+ const { default: realtimePlugin } = await import("./realtime-CTkv3O59.mjs").then((n) => n.r);
891
+ const opts = config.arcPlugins.realtime === true ? {} : config.arcPlugins.realtime;
892
+ await fastify.register(realtimePlugin, opts);
893
+ trackPlugin("arc-realtime", opts);
894
+ }
798
895
  }
799
896
  if (config.arcPlugins?.metrics) {
800
- const { default: metricsPlugin } = await import("./metrics-CUdw_wLB.mjs").then((n) => n.r);
897
+ const { default: metricsPlugin } = await import("./metrics-D7uchLjN.mjs").then((n) => n.r);
801
898
  const opts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
802
899
  await fastify.register(metricsPlugin, opts);
803
900
  trackPlugin("arc-metrics", opts);
804
901
  }
805
902
  if (config.arcPlugins?.versioning) {
806
- const { default: versioningPlugin } = await import("./versioning-CHDTor3P.mjs").then((n) => n.r);
903
+ const { default: versioningPlugin } = await import("./versioning-rZbyLVD-.mjs").then((n) => n.r);
807
904
  await fastify.register(versioningPlugin, config.arcPlugins.versioning);
808
905
  trackPlugin("arc-versioning", config.arcPlugins.versioning);
809
906
  }
@@ -811,6 +908,12 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules, moduleH
811
908
  //#endregion
812
909
  //#region src/factory/registerAuth.ts
813
910
  /**
911
+ * Authentication registration for createApp.
912
+ *
913
+ * Extracted from createApp step 9: scope decoration, auth strategy,
914
+ * elevation plugin, and error handler.
915
+ */
916
+ /**
814
917
  * Decorate request.scope with PUBLIC_SCOPE default.
815
918
  * Every request starts as public; auth hooks upgrade it.
816
919
  */
@@ -849,10 +952,12 @@ async function registerAuth(fastify, config, trackPlugin) {
849
952
  fastify.decorate("authenticate", async (request, reply) => {
850
953
  await authenticate(request, reply);
851
954
  });
852
- if (!fastify.hasDecorator("optionalAuthenticate")) if (optionalAuthenticate) fastify.decorate("optionalAuthenticate", async (request, reply) => {
853
- await optionalAuthenticate(request, reply);
854
- });
855
- else fastify.decorate("optionalAuthenticate", createOptionalAuthenticate(authenticate));
955
+ if (!fastify.hasDecorator("optionalAuthenticate")) {
956
+ if (optionalAuthenticate) fastify.decorate("optionalAuthenticate", async (request, reply) => {
957
+ await optionalAuthenticate(request, reply);
958
+ });
959
+ else fastify.decorate("optionalAuthenticate", createOptionalAuthenticate(authenticate));
960
+ }
856
961
  trackPlugin("auth-authenticator");
857
962
  fastify.log.debug("Custom authenticator enabled");
858
963
  break;
@@ -872,7 +977,7 @@ async function registerAuth(fastify, config, trackPlugin) {
872
977
  */
873
978
  async function registerElevation(fastify, config, trackPlugin) {
874
979
  if (!config.elevation) return;
875
- const { elevationPlugin } = await import("./elevation-D2l1mh4g.mjs").then((n) => n.r);
980
+ const { elevationPlugin } = await import("./elevation-B0tlY8y1.mjs").then((n) => n.r);
876
981
  await fastify.register(elevationPlugin, config.elevation);
877
982
  trackPlugin("arc-elevation", config.elevation);
878
983
  fastify.log.debug("Elevation plugin enabled");
@@ -887,8 +992,51 @@ async function registerElevation(fastify, config, trackPlugin) {
887
992
  */
888
993
  async function registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers) {
889
994
  if (config.errorHandler === false) return;
890
- const { errorHandlerPlugin } = await import("./errorHandler-C5NeD6bT.mjs").then((n) => n.r);
891
- let errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
995
+ const { errorHandlerPlugin } = await import("./errorHandler-C7xBqHsI.mjs").then((n) => n.r);
996
+ /**
997
+ * DISCLOSURE DEFAULTS MERGE — they used to be REPLACED, and that was two bugs.
998
+ *
999
+ * This was a ternary: an object from the host was used verbatim, otherwise
1000
+ * `{ includeStack: config.preset !== "production" }`. Both branches leaked.
1001
+ *
1002
+ * 1. A host that passed ANY key — even just `errorMappers` — lost the preset-derived
1003
+ * `includeStack` and fell through to `errorHandlerPlugin`'s own default, which is
1004
+ * `!(process.env.NODE_ENV === "production")`. That comparison knows only the long
1005
+ * spelling, so under `NODE_ENV=prod` it yields `true` and stack traces ship to clients in
1006
+ * production. Nobody writing `errorHandler: { errorMappers: [...] }` could reasonably
1007
+ * expect it to change stack-trace exposure.
1008
+ * 2. The derived branch never set `exposeInternalMessages` AT ALL, so that switch fell
1009
+ * through to the same raw read for EVERY arc app however it was configured. A truthy value
1010
+ * is what skips `wire.message = "Internal Server Error"` for a 500, putting the raw thrown
1011
+ * message — driver text, query fragments — on the wire.
1012
+ *
1013
+ * `preset` is the right source: it is the deployment's EXPLICIT declaration, and a specific
1014
+ * setting must never be overridden by an ambient default. Host keys still win over `derived`,
1015
+ * so an operator can widen deliberately; what they can no longer do is widen by accident.
1016
+ *
1017
+ * 3. And `preset` is OPTIONAL, so its ABSENCE must not read as "not production" either.
1018
+ * `config.preset !== "production"` is true for an unset preset, and plenty of hosts set
1019
+ * only `NODE_ENV` — which turns "the operator said nothing" into "the operator said
1020
+ * development", the widest possible reading of silence, on the two switches where being
1021
+ * wrong ships stack traces and raw driver text.
1022
+ *
1023
+ * So: an explicit `preset` wins; its absence falls back to the ENVIRONMENT rather than to a
1024
+ * guess; and that fallback goes through `isProductionEnv` from the shared classifier, never a raw
1025
+ * `process.env.NODE_ENV === "production"`. The raw form recognises only the long spelling, so a
1026
+ * preset-less host on `NODE_ENV=prod` read as non-production and re-opened both switches — the
1027
+ * same defect class this fallback exists to close, one layer in. Verified against a real 500 in
1028
+ * every state, including a Mongo connection string reaching the response body before the fix
1029
+ * (`tests/factory/error-disclosure-defaults.test.ts`).
1030
+ */
1031
+ const productionEnv = config.preset !== void 0 ? config.preset === "production" : isProductionEnv(process.env.NODE_ENV);
1032
+ const derived = {
1033
+ includeStack: !productionEnv,
1034
+ exposeInternalMessages: !productionEnv
1035
+ };
1036
+ let errorOpts = typeof config.errorHandler === "object" ? {
1037
+ ...derived,
1038
+ ...config.errorHandler
1039
+ } : derived;
892
1040
  if (moduleErrorMappers && moduleErrorMappers.length > 0) errorOpts = {
893
1041
  ...errorOpts,
894
1042
  errorMappers: [...errorOpts.errorMappers ?? [], ...moduleErrorMappers]
@@ -997,7 +1145,8 @@ async function registerResources(fastify, config, preResolvedModules) {
997
1145
  }
998
1146
  const modules = preResolvedModules ?? orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
999
1147
  initModuleStates(fastify, modules.map((m) => m.name));
1000
- const teardown = createModuleTeardown(fastify, modules);
1148
+ const disposers = createDisposerRegistry(fastify);
1149
+ const teardown = createModuleTeardown(fastify, modules, disposers);
1001
1150
  let eventUnsubscribes = [];
1002
1151
  try {
1003
1152
  if (config.plugins) {
@@ -1008,7 +1157,11 @@ async function registerResources(fastify, config, preResolvedModules) {
1008
1157
  if (!m.plugins) continue;
1009
1158
  teardown.markInitialized(m);
1010
1159
  try {
1011
- await m.plugins(fastify);
1160
+ const returned = await m.plugins(fastify, disposers.contextFor(m.name));
1161
+ if (typeof returned === "function") {
1162
+ assertNotAReturnedPlugin(returned, m.name, fastify);
1163
+ disposers.contextFor(m.name).defer(returned);
1164
+ }
1012
1165
  } catch (err) {
1013
1166
  setModuleState(fastify, m.name, "failed");
1014
1167
  const msg = err instanceof Error ? err.message : String(err);
@@ -1025,7 +1178,7 @@ async function registerResources(fastify, config, preResolvedModules) {
1025
1178
  teardown.markInitialized(m);
1026
1179
  setModuleState(fastify, m.name, "bootstrapping");
1027
1180
  try {
1028
- exported = await m.bootstrap(fastify);
1181
+ exported = await m.bootstrap(fastify, disposers.contextFor(m.name));
1029
1182
  } catch (err) {
1030
1183
  setModuleState(fastify, m.name, "failed");
1031
1184
  const msg = err instanceof Error ? err.message : String(err);
@@ -1048,6 +1201,7 @@ async function registerResources(fastify, config, preResolvedModules) {
1048
1201
  if (isResourcesFactory(config.resources)) {
1049
1202
  try {
1050
1203
  resolvedResources = await config.resources(fastify);
1204
+ if (!Array.isArray(resolvedResources)) throw new TypeError(`resources factory must return an array — received ${resolvedResources === null ? "null" : typeof resolvedResources}`);
1051
1205
  } catch (err) {
1052
1206
  const msg = err instanceof Error ? err.message : String(err);
1053
1207
  fastify.log.error(`Resources factory threw during boot: ${msg}`);
@@ -1062,7 +1216,7 @@ async function registerResources(fastify, config, preResolvedModules) {
1062
1216
  let discoveryPath;
1063
1217
  let discoveryYieldedZero = false;
1064
1218
  if (resolvedResources === void 0 && config.resourceDir) {
1065
- const { loadResources } = await import("./loadResources-yawi9tAE.mjs").then((n) => n.n);
1219
+ const { loadResources } = await import("./loadResources-Bj-aSsXi.mjs").then((n) => n.n);
1066
1220
  const { resolve, dirname } = await import("node:path");
1067
1221
  const { fileURLToPath } = await import("node:url");
1068
1222
  const rawDir = config.resourceDir;
@@ -1077,6 +1231,7 @@ async function registerResources(fastify, config, preResolvedModules) {
1077
1231
  resolvedResources = discovered;
1078
1232
  }
1079
1233
  const moduleResourceNames = /* @__PURE__ */ new Map();
1234
+ const perModuleResources = /* @__PURE__ */ new Map();
1080
1235
  const moduleResources = [];
1081
1236
  for (const m of modules) {
1082
1237
  if (!m.resources) continue;
@@ -1087,24 +1242,37 @@ async function registerResources(fastify, config, preResolvedModules) {
1087
1242
  const msg = err instanceof Error ? err.message : String(err);
1088
1243
  throw new Error(`[arc] module "${m.name}" resources factory threw: ${msg}. Check the module's bootstrap ran and its adapters received booted engines.`, { cause: err });
1089
1244
  }
1245
+ if (!Array.isArray(rs)) throw new TypeError(`[arc] module "${m.name}" resources factory must return an array — received ${rs === null ? "null" : typeof rs}.`);
1090
1246
  const names = /* @__PURE__ */ new Set();
1091
- for (const r of rs) if (r.name != null) names.add(r.name);
1247
+ for (const r of rs) {
1248
+ if (r.name == null) continue;
1249
+ if (names.has(r.name)) throw new Error(`[arc] module "${m.name}" provides two resources named "${r.name}". Resource names must be unique within a module — arc registers one route group per name, so the second would be dropped.`);
1250
+ names.add(r.name);
1251
+ }
1092
1252
  moduleResourceNames.set(m.name, names);
1253
+ perModuleResources.set(m.name, rs);
1093
1254
  moduleResources.push(...rs);
1094
1255
  }
1256
+ const effectiveOwns = /* @__PURE__ */ new Map();
1095
1257
  for (const m of modules) {
1096
- if (!m.owns?.length) continue;
1097
1258
  const supplied = moduleResourceNames.get(m.name);
1259
+ if (m.owns === "provided") {
1260
+ if (!supplied?.size) throw new Error(`[arc] module "${m.name}" declares \`owns: "provided"\` but supplies no named resources. Either give it resources to own, or drop the \`owns\` entry — an empty claim is almost always a resources factory that returned early.`);
1261
+ effectiveOwns.set(m.name, [...supplied]);
1262
+ continue;
1263
+ }
1264
+ if (!m.owns?.length) continue;
1098
1265
  const missing = m.owns.filter((name) => !supplied?.has(name));
1099
1266
  if (missing.length > 0) {
1100
1267
  const provided = supplied?.size ? [...supplied].join(", ") : "(none)";
1101
- throw new Error(`[arc] module "${m.name}" declares owns: [${missing.map((n) => `"${n}"`).join(", ")}] but its own \`resources\` do not supply ${missing.length === 1 ? "that name" : "those names"}. \`owns\` DROPS the app-level resource of that name, so an unmet claim removes the route entirely and nothing serves it. Provide it in this module's \`resources\`, or drop the \`owns\` entry.
1268
+ throw new Error(`[arc] module "${m.name}" declares owns: [${missing.map((n) => `"${n}"`).join(", ")}] but its own \`resources\` do not supply ${missing.length === 1 ? "that name" : "those names"}. \`owns\` DROPS the app-level resource of that name, so an unmet claim removes the route entirely and nothing serves it. Provide it in this module's \`resources\`, drop the \`owns\` entry, or use \`owns: "provided"\` to derive it.
1102
1269
  Resources supplied by "${m.name}": ${provided}.`);
1103
1270
  }
1271
+ effectiveOwns.set(m.name, m.owns);
1104
1272
  }
1105
1273
  if (modules.length && resolvedResources && resolvedResources.length > 0) {
1106
1274
  const owned = /* @__PURE__ */ new Set();
1107
- for (const m of modules) for (const name of m.owns ?? []) owned.add(name);
1275
+ for (const m of modules) for (const name of effectiveOwns.get(m.name) ?? []) owned.add(name);
1108
1276
  if (owned.size > 0) {
1109
1277
  const before = resolvedResources.length;
1110
1278
  resolvedResources = resolvedResources.filter((r) => !(r.name != null && owned.has(r.name)));
@@ -1128,10 +1296,12 @@ Resources supplied by "${m.name}": ${provided}.`);
1128
1296
  for (const resource of resolvedResources) if (resource.skipGlobalPrefix) root.push(resource);
1129
1297
  else prefixed.push(resource);
1130
1298
  for (const resource of root) await registerOne(fastify, resource, config.mountRoutes);
1131
- if (prefixed.length) if (config.resourcePrefix) await fastify.register(async (scoped) => {
1132
- for (const resource of prefixed) await registerOne(scoped, resource, config.mountRoutes);
1133
- }, { prefix: config.resourcePrefix });
1134
- else for (const resource of prefixed) await registerOne(fastify, resource, config.mountRoutes);
1299
+ if (prefixed.length) {
1300
+ if (config.resourcePrefix) await fastify.register(async (scoped) => {
1301
+ for (const resource of prefixed) await registerOne(scoped, resource, config.mountRoutes);
1302
+ }, { prefix: config.resourcePrefix });
1303
+ else for (const resource of prefixed) await registerOne(fastify, resource, config.mountRoutes);
1304
+ }
1135
1305
  const names = resolvedResources.map((r) => r.name ?? "?").join(", ");
1136
1306
  const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
1137
1307
  fastify.log.info(`${resolvedResources.length} resource(s) registered${prefix}: ${names}`);
@@ -1144,19 +1314,35 @@ Resources supplied by "${m.name}": ${provided}.`);
1144
1314
  for (const m of modules) {
1145
1315
  if (!m.afterResources) continue;
1146
1316
  try {
1147
- await m.afterResources(fastify);
1317
+ const returned = await m.afterResources(fastify, disposers.contextFor(m.name));
1318
+ if (typeof returned === "function") disposers.contextFor(m.name).defer(returned);
1148
1319
  } catch (err) {
1149
1320
  const msg = err instanceof Error ? err.message : String(err);
1150
1321
  throw new Error(`[arc] module "${m.name}" afterResources() threw: ${msg}`, { cause: err });
1151
1322
  }
1152
1323
  }
1153
- const scheduledJobs = await collectModuleScheduledJobs(fastify, modules);
1324
+ const scheduleCounts = /* @__PURE__ */ new Map();
1325
+ const collectedJobs = await collectModuleScheduledJobs(fastify, modules, scheduleCounts);
1326
+ /**
1327
+ * ROLE gate (CreateAppOptions.role). Arms a role does not run are SKIPPED
1328
+ * and logged by name, never fatal: a module legitimately declares arms
1329
+ * that a different deployment of the same codebase executes. 'relay' runs
1330
+ * only kind:'relay' arms (the outbox relays); 'api'/'worker' run none;
1331
+ * 'scheduler'/'all' run everything.
1332
+ */
1333
+ const role = config.role ?? "all";
1334
+ const scheduledJobs = role === "scheduler" || role === "all" ? collectedJobs : role === "relay" ? collectedJobs.filter((j) => j.kind === "relay") : [];
1335
+ const skipped = collectedJobs.filter((j) => !scheduledJobs.includes(j));
1336
+ if (skipped.length > 0) fastify.log.info({
1337
+ role,
1338
+ skipped: skipped.map((j) => j.name)
1339
+ }, `[arc] role '${role}': ${skipped.length} schedule arm(s) not mounted here — a deployment with the owning role runs them`);
1154
1340
  if (scheduledJobs.length > 0) {
1155
1341
  if (config.arcPlugins?.schedules === false) throw new Error("[arc] modules declare scheduledJobs but arcPlugins.schedules is false. Enable the scheduler or remove the declarations.");
1156
1342
  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.");
1157
1343
  const frozenJobs = Object.freeze(scheduledJobs.map((job) => Object.freeze({ ...job })));
1158
1344
  const scheduleOptions = typeof config.arcPlugins?.schedules === "object" ? config.arcPlugins.schedules : {};
1159
- const { default: schedulesPlugin } = await import("./schedules-DdtlICTf.mjs").then((n) => n.r);
1345
+ const { default: schedulesPlugin } = await import("./schedules-CAcZgia0.mjs").then((n) => n.r);
1160
1346
  await fastify.register(schedulesPlugin, {
1161
1347
  ...scheduleOptions,
1162
1348
  schedules: frozenJobs
@@ -1170,7 +1356,21 @@ Resources supplied by "${m.name}": ${provided}.`);
1170
1356
  });
1171
1357
  }
1172
1358
  }
1173
- eventUnsubscribes = await subscribeModuleEventHandlers(fastify, modules);
1359
+ const subscriptionCounts = /* @__PURE__ */ new Map();
1360
+ eventUnsubscribes = await subscribeModuleEventHandlers(fastify, modules, subscriptionCounts);
1361
+ if (fastify.arc) fastify.arc.moduleDescriptors = Object.freeze(modules.map((m) => describeResolvedModule({
1362
+ name: m.name,
1363
+ dependsOn: m.dependsOn,
1364
+ resources: perModuleResources.get(m.name) ?? [],
1365
+ owns: effectiveOwns.get(m.name) ?? [],
1366
+ lifecycle: {
1367
+ hasClose: typeof m.onClose === "function",
1368
+ subscriptions: subscriptionCounts.get(m.name) ?? 0,
1369
+ scheduledJobs: scheduleCounts.get(m.name) ?? 0,
1370
+ healthChecks: m.healthChecks?.length ?? 0,
1371
+ exports: typeof m.bootstrap === "function"
1372
+ }
1373
+ })));
1174
1374
  if (config.afterResources) {
1175
1375
  await config.afterResources(fastify);
1176
1376
  fastify.log.debug("afterResources hook executed");
@@ -1198,6 +1398,22 @@ Resources supplied by "${m.name}": ${provided}.`);
1198
1398
  if (closeErrors.length > 0) throw closeErrors[0];
1199
1399
  });
1200
1400
  }
1401
+ /**
1402
+ * `plugins()` is CALLED by arc, never handed to `fastify.register()` — so a
1403
+ * module author who returns a Fastify plugin expecting arc to register it
1404
+ * gets silence: nothing is registered, and the "disposer" shorthand then
1405
+ * invokes their plugin at shutdown with no arguments.
1406
+ *
1407
+ * The `fastify-plugin` case is unambiguous (its marker symbols are only ever
1408
+ * set by `fp()`), so it throws. A bare `(fastify, opts)` function is a strong
1409
+ * but not certain signal — arc's disposer contract calls it with ZERO
1410
+ * arguments, so a 2-parameter disposer is meaningless either way — and warns.
1411
+ */
1412
+ function assertNotAReturnedPlugin(returned, moduleName, fastify) {
1413
+ const fn = returned;
1414
+ 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) }\`.`);
1415
+ 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.`);
1416
+ }
1201
1417
  //#endregion
1202
1418
  //#region src/factory/security/cors.ts
1203
1419
  /**
@@ -1399,7 +1615,7 @@ async function registerUtilityPlugins(fastify, config) {
1399
1615
  throwFileSizeLimit: true,
1400
1616
  ...hostMultipart,
1401
1617
  limits: {
1402
- fileSize: 10 * 1024 * 1024,
1618
+ fileSize: 10485760,
1403
1619
  files: 10,
1404
1620
  fields: 100,
1405
1621
  parts: 120,
@@ -1589,6 +1805,14 @@ Missing:\n${lines}\n\nFix: pass durable adapters under \`stores\`, or use runtim
1589
1805
  * instance is closed before the error propagates — see the wrapper below.
1590
1806
  */
1591
1807
  async function createApp(options) {
1808
+ /**
1809
+ * ROLE contradiction check — before anything constructs. A relay/scheduler
1810
+ * process declaring HTTP resources is a wiring error, not a preference:
1811
+ * those roles exist to NOT serve traffic, and mounting routes anyway would
1812
+ * hide the misdeployment behind a working endpoint.
1813
+ */
1814
+ const declaredRole = options.role ?? "all";
1815
+ 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.`);
1592
1816
  let partial;
1593
1817
  try {
1594
1818
  return await buildApp(options, (instance) => {
@@ -1683,7 +1907,7 @@ async function buildApp(options, onInstance) {
1683
1907
  await registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers);
1684
1908
  await registerResources(fastify, config, resolvedModules);
1685
1909
  if (config.replyHelpers) {
1686
- const { replyHelpersPlugin } = await import("./replyHelpers-DIiZKGUt.mjs").then((n) => n.n);
1910
+ const { replyHelpersPlugin } = await import("./replyHelpers-BSBVCwL3.mjs").then((n) => n.n);
1687
1911
  await fastify.register(replyHelpersPlugin);
1688
1912
  }
1689
1913
  if (config.serializeBigInt) {
@@ -1700,6 +1924,7 @@ async function buildApp(options, onInstance) {
1700
1924
  });
1701
1925
  }
1702
1926
  const authMode = config.auth === false ? "none" : config.auth ? config.auth.type : "none";
1927
+ auditRuntimeCapabilities(fastify, config.runtime ?? "memory");
1703
1928
  fastify.log.info({
1704
1929
  preset: config.preset ?? "custom",
1705
1930
  runtime: config.runtime ?? "memory",
@@ -1734,4 +1959,4 @@ const ArcFactory = {
1734
1959
  }
1735
1960
  };
1736
1961
  //#endregion
1737
- export { resolveContribution as _, edgePreset as a, testingPreset as c, hasModule as d, orderModules as f, collectModuleWorkflows as g, collectModuleScheduledJobs as h, developmentPreset as i, workerPreset as l, collectModuleHealthChecks as m, createApp as n, getPreset as o, subscribeModuleEventHandlers as p, createApp_exports as r, productionPreset as s, ArcFactory as t, getModuleState as u, resolveModule as v };
1962
+ export { collectModuleScheduledJobs as _, developmentPreset as a, resolveModule as b, productionPreset as c, getModuleState as d, hasModule as f, collectModuleHealthChecks as g, describeResolvedModule as h, createApp_exports as i, testingPreset as l, subscribeModuleEventHandlers as m, DEFAULT_LOGGER_REDACT_PATHS as n, edgePreset as o, orderModules as p, createApp as r, getPreset as s, ArcFactory as t, workerPreset as u, collectModuleWorkflows as v, resolveContribution as y };
@@ -1,4 +1,4 @@
1
- /*! @classytic/arc v2.31.0 | MIT | https://classytic.com */
1
+ /*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
2
2
  //#region src/core/aggregation/defineAggregation.ts
3
3
  /**
4
4
  * Declare a single resource aggregation. Exported configs flow into