@classytic/arc 2.20.0 → 2.21.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 (111) hide show
  1. package/dist/{BaseController-D-MHZ27_.mjs → BaseController-BnLYgE9A.mjs} +2 -2
  2. package/dist/{EventTransport-IEn7Tn-Q.d.mts → EventTransport-QmY7A258.d.mts} +0 -1
  3. package/dist/{QueryCache-TRfXau3W.d.mts → QueryCache-Z1B38koh.d.mts} +0 -1
  4. package/dist/{actionPermissions-C9RMiIpc.mjs → actionPermissions-ze70xXJi.mjs} +1 -1
  5. package/dist/audit/index.d.mts +9 -5
  6. package/dist/auth/audit.d.mts +7 -4
  7. package/dist/auth/audit.mjs +5 -0
  8. package/dist/auth/index.d.mts +7 -14
  9. package/dist/auth/index.mjs +1 -1
  10. package/dist/auth/redis-session.d.mts +1 -2
  11. package/dist/cache/index.d.mts +2 -3
  12. package/dist/cli/commands/describe.d.mts +1 -2
  13. package/dist/cli/commands/docs.mjs +1 -1
  14. package/dist/cli/commands/init.mjs +1 -1
  15. package/dist/cli/index.mjs +1 -1
  16. package/dist/context/index.d.mts +2 -2
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -4
  19. package/dist/{core-CWhyGkiM.mjs → core-DZ6Xe1jR.mjs} +65 -10
  20. package/dist/{createActionRouter-HlQso0jO.mjs → createActionRouter-BWMDW-dL.mjs} +2 -2
  21. package/dist/{createAggregationRouter-5LzPzgw1.mjs → createAggregationRouter-l5mNdYrZ.mjs} +1 -1
  22. package/dist/{createApp-D6uwzCKu.mjs → createApp-Bi61XxqU.mjs} +149 -137
  23. package/dist/discovery/index.d.mts +0 -1
  24. package/dist/docs/index.d.mts +1 -2
  25. package/dist/docs/index.mjs +1 -1
  26. package/dist/{elevation-xGUBq6p_.d.mts → elevation-C9s5hFP_.d.mts} +0 -1
  27. package/dist/encryption/index.d.mts +2 -144
  28. package/dist/{errorHandler-BtUUDPEa.d.mts → errorHandler-B0e-bq-O.d.mts} +0 -1
  29. package/dist/{errors-DOHkjQlD.d.mts → errors-DHcHVAgk.d.mts} +0 -1
  30. package/dist/{eventPlugin-DbBFd3x_.d.mts → eventPlugin-A6EbthXR.d.mts} +10 -6
  31. package/dist/events/index.d.mts +9 -6
  32. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  33. package/dist/events/transports/redis.d.mts +1 -2
  34. package/dist/factory/index.d.mts +4 -4
  35. package/dist/factory/index.mjs +2 -2
  36. package/dist/{fastify-augmentation-CCQJEDgh.d.mts → fastify-augmentation-DafigGQp.d.mts} +2 -3
  37. package/dist/{fields-C7ahdGYf.d.mts → fields-BGqrAa_u.d.mts} +1 -2
  38. package/dist/hooks/index.d.mts +1 -1
  39. package/dist/idempotency/index.d.mts +4 -3
  40. package/dist/idempotency/redis.d.mts +1 -1
  41. package/dist/{index-DUa8WHz3.d.mts → index-B5N-NaHs.d.mts} +88 -7
  42. package/dist/{index-CdFkIMEd.d.mts → index-B5PoNbfu.d.mts} +36 -7
  43. package/dist/{index-_--GyzcO.d.mts → index-Dtrj6XZk.d.mts} +62 -28
  44. package/dist/index.d.mts +7 -7
  45. package/dist/index.mjs +6 -6
  46. package/dist/{init-YH_nslQR.mjs → init-0YhHkXGk.mjs} +154 -137
  47. package/dist/integrations/event-gateway.d.mts +2 -3
  48. package/dist/integrations/index.d.mts +2 -2
  49. package/dist/integrations/jobs.d.mts +0 -1
  50. package/dist/integrations/mcp/index.d.mts +6 -7
  51. package/dist/integrations/mcp/index.mjs +1 -1
  52. package/dist/integrations/mcp/testing.d.mts +1 -2
  53. package/dist/integrations/mcp/testing.mjs +1 -1
  54. package/dist/integrations/streamline.d.mts +2 -2
  55. package/dist/integrations/webhooks.d.mts +0 -1
  56. package/dist/integrations/websocket-pushref-redis.d.mts +1 -2
  57. package/dist/integrations/websocket-redis.d.mts +1 -2
  58. package/dist/integrations/websocket.d.mts +2 -2
  59. package/dist/{loadResources-B8UmpITt.mjs → loadResources-DpU8pfjV.mjs} +4 -3
  60. package/dist/{mcpPlugin-CuW6L_uD.mjs → mcpPlugin-B9XjjNVG.mjs} +3 -3
  61. package/dist/middleware/index.d.mts +1 -2
  62. package/dist/{module-JYH9V0I8.d.mts → module-WeuU5jfd.d.mts} +46 -1
  63. package/dist/{openapi-Dq6RbEGK.mjs → openapi-Kyod_di6.mjs} +2 -2
  64. package/dist/permissions/index.d.mts +3 -3
  65. package/dist/permissions/index.mjs +2 -2
  66. package/dist/{permissions-CgbOLJZG.mjs → permissions-BJMuKMl7.mjs} +33 -1
  67. package/dist/pipeline/index.d.mts +1 -2
  68. package/dist/plugins/index.d.mts +79 -7
  69. package/dist/plugins/index.mjs +88 -1
  70. package/dist/plugins/response-cache.d.mts +5 -3
  71. package/dist/plugins/tracing-entry.d.mts +1 -1
  72. package/dist/plugins/tracing-entry.mjs +1 -1
  73. package/dist/presets/filesUpload.d.mts +4 -5
  74. package/dist/presets/filesUpload.mjs +1 -1
  75. package/dist/presets/index.d.mts +1 -2
  76. package/dist/presets/index.mjs +1 -1
  77. package/dist/presets/multiTenant.d.mts +1 -2
  78. package/dist/presets/search.d.mts +2 -3
  79. package/dist/presets/search.mjs +1 -1
  80. package/dist/{presets-DWsYQdbV.mjs → presets-gw0ciIUD.mjs} +1 -1
  81. package/dist/{pushref-registry-DnCqL3aa.d.mts → pushref-registry-BbcnpNTm.d.mts} +4 -2
  82. package/dist/{queryCachePlugin-xzFyKAjT.d.mts → queryCachePlugin-CfYWq-Ak.d.mts} +1 -2
  83. package/dist/{redis-C3W0qbz8.d.mts → redis-UDtLaNj6.d.mts} +0 -1
  84. package/dist/{redis-stream-DsP1vqZB.d.mts → redis-stream-iVR2l7aS.d.mts} +1 -2
  85. package/dist/{registerRoute-DGSREwPw.mjs → registerRoute-oGAl0oA8.mjs} +1 -1
  86. package/dist/registry/index.d.mts +2 -2
  87. package/dist/schemas/index.d.mts +0 -1
  88. package/dist/scim/index.d.mts +0 -1
  89. package/dist/scope/index.d.mts +2 -3
  90. package/dist/{sessionManager-BXG-HGwG.d.mts → sessionManager-BHOM3N_0.d.mts} +10 -6
  91. package/dist/{storage-Co_PQpeQ.d.mts → storage-DLUZ04kK.d.mts} +8 -4
  92. package/dist/testing/index.d.mts +2 -3
  93. package/dist/testing/index.mjs +5 -2
  94. package/dist/testing/storageContract.d.mts +1 -2
  95. package/dist/{tracing-D8jlGXP7.d.mts → tracing-CsGKQJGg.d.mts} +0 -1
  96. package/dist/types/index.d.mts +4 -4
  97. package/dist/types/storage.d.mts +1 -1
  98. package/dist/types-Cc91nXuA.d.mts +132 -0
  99. package/dist/{types-LNebCoJx.d.mts → types-DH0GupI7.d.mts} +8 -4
  100. package/dist/{types-C9b2HR5W.d.mts → types-DRj-Ut4n.d.mts} +1 -2
  101. package/dist/{types-BHMKlMBU.d.mts → types-DpxoNX5v.d.mts} +61 -21
  102. package/dist/utils/index.d.mts +182 -128
  103. package/dist/utils/index.mjs +2 -2
  104. package/dist/{utils-ChjYQpjn.mjs → utils-CO8a3B_1.mjs} +70 -26
  105. package/dist/{versioning-D_eBCfNr.d.mts → versioning-DQ4mCvg0.d.mts} +1 -2
  106. package/dist/{websocket-MlYlrYq0.d.mts → websocket-uuWqM6UY.d.mts} +5 -4
  107. package/package.json +7 -7
  108. package/skills/arc-module-publishing/SKILL.md +242 -0
  109. package/skills/arc-module-publishing/references/authoring-modules.md +272 -0
  110. package/skills/arc-module-publishing/references/package-manifest.md +155 -0
  111. package/skills/arc-module-publishing/references/publishing-workflow.md +182 -0
@@ -3,6 +3,134 @@ import { arcLog } from "./logger/index.mjs";
3
3
  import { n as PUBLIC_SCOPE } from "./types-bHblZ9YN.mjs";
4
4
  import Fastify from "fastify";
5
5
  import qs from "qs";
6
+ //#region src/factory/module.ts
7
+ /**
8
+ * Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
9
+ * (this is where a dynamic `import()` fires); a promise (or plain module) is
10
+ * awaited. Called once per module at the start of the bootstrap phase.
11
+ */
12
+ async function resolveModule(input) {
13
+ const resolved = typeof input === "function" ? await input() : await input;
14
+ 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.");
15
+ return resolved;
16
+ }
17
+ /**
18
+ * Order modules for composition by their `dependsOn` edges — a STABLE
19
+ * topological sort. Called once at the start of the bootstrap phase; every
20
+ * subsequent phase (bootstrap, resources, afterResources, and reverse-order
21
+ * onClose) iterates the returned list, so a module's declared dependencies are
22
+ * always composed before it.
23
+ *
24
+ * "Stable" = modules with no edge between them keep their original list order,
25
+ * so declaring `dependsOn` on one module never silently reorders an unrelated
26
+ * one. Backward compatible: a `modules` array with NO `dependsOn` anywhere is
27
+ * returned unchanged.
28
+ *
29
+ * Fail-fast — throws (never reorders past a broken contract) on:
30
+ * - duplicate module names (the name is the graph key)
31
+ * - a `dependsOn` name not present in the composed set
32
+ * - a self-reference (`dependsOn` includes the module's own name)
33
+ * - a dependency cycle (reports the concrete `a → b → … → a` path)
34
+ */
35
+ function orderModules(modules) {
36
+ const byName = /* @__PURE__ */ new Map();
37
+ for (const m of modules) {
38
+ if (byName.has(m.name)) throw new Error(`[arc] Duplicate module name "${m.name}" — composed twice; check your modules array.`);
39
+ byName.set(m.name, m);
40
+ }
41
+ for (const m of modules) for (const dep of m.dependsOn ?? []) {
42
+ if (dep === m.name) throw new Error(`[arc] module "${m.name}" dependsOn itself — remove the self-reference.`);
43
+ 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)"}.`);
44
+ }
45
+ if (modules.every((m) => !m.dependsOn || m.dependsOn.length === 0)) return [...modules];
46
+ const originalIndex = /* @__PURE__ */ new Map();
47
+ let nextIndex = 0;
48
+ for (const m of modules) originalIndex.set(m.name, nextIndex++);
49
+ const pendingDeps = new Map(modules.map((m) => [m.name, (m.dependsOn ?? []).length]));
50
+ const dependents = /* @__PURE__ */ new Map();
51
+ for (const m of modules) for (const dep of m.dependsOn ?? []) {
52
+ const list = dependents.get(dep);
53
+ if (list) list.push(m.name);
54
+ else dependents.set(dep, [m.name]);
55
+ }
56
+ const ordered = [];
57
+ const remaining = new Set(modules.map((m) => m.name));
58
+ while (remaining.size > 0) {
59
+ let pick;
60
+ let pickIndex = Number.POSITIVE_INFINITY;
61
+ for (const name of remaining) {
62
+ if (pendingDeps.get(name) !== 0) continue;
63
+ const index = originalIndex.get(name) ?? Number.POSITIVE_INFINITY;
64
+ if (index < pickIndex) {
65
+ pick = name;
66
+ pickIndex = index;
67
+ }
68
+ }
69
+ if (pick === void 0) throw new Error(describeModuleCycle(remaining, byName));
70
+ const picked = byName.get(pick);
71
+ if (!picked) throw new Error(`[arc] internal: ordered module "${pick}" is missing from the index`);
72
+ ordered.push(picked);
73
+ remaining.delete(pick);
74
+ for (const dependent of dependents.get(pick) ?? []) pendingDeps.set(dependent, (pendingDeps.get(dependent) ?? 1) - 1);
75
+ }
76
+ return ordered;
77
+ }
78
+ /** Walk the still-unordered subgraph for one concrete cycle path. */
79
+ function describeModuleCycle(remaining, byName) {
80
+ const stack = [];
81
+ const onStack = /* @__PURE__ */ new Set();
82
+ const done = /* @__PURE__ */ new Set();
83
+ let cycle = null;
84
+ const walk = (name) => {
85
+ if (cycle) return;
86
+ stack.push(name);
87
+ onStack.add(name);
88
+ for (const dep of byName.get(name)?.dependsOn ?? []) {
89
+ if (!remaining.has(dep)) continue;
90
+ if (onStack.has(dep)) {
91
+ cycle = [...stack.slice(stack.indexOf(dep)), dep];
92
+ return;
93
+ }
94
+ if (!done.has(dep)) walk(dep);
95
+ if (cycle) return;
96
+ }
97
+ onStack.delete(name);
98
+ stack.pop();
99
+ done.add(name);
100
+ };
101
+ for (const name of remaining) {
102
+ if (!done.has(name)) walk(name);
103
+ if (cycle) break;
104
+ }
105
+ 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.`;
106
+ }
107
+ /**
108
+ * Identity helper for authoring a typed module — mirrors `defineResource`.
109
+ * Gives inference + a single obvious construction site; does no work.
110
+ *
111
+ * `TExports` is inferred from the `bootstrap` return value, so a module
112
+ * author gets a typed public export for free:
113
+ *
114
+ * ```ts
115
+ * export const accountingModule = (deps: Deps) =>
116
+ * defineModule({
117
+ * name: "accounting",
118
+ * bootstrap: async () => createAccountingEngine(deps), // TExports inferred
119
+ * });
120
+ * ```
121
+ */
122
+ function defineModule(module) {
123
+ return module;
124
+ }
125
+ function getModuleExports(fastify, name) {
126
+ const modules = fastify.arc?.modules;
127
+ if (!modules || !Object.hasOwn(modules, name)) {
128
+ const available = modules ? Object.keys(modules) : [];
129
+ throw new Error(`[arc] no public export recorded for module "${name}".\nA module's export is its bootstrap() return value; it is readable only AFTER that module's bootstrap ran (dependsOn / list order = init order).\nModules with recorded exports: ${available.length ? available.join(", ") : "(none)"}.`);
130
+ }
131
+ return modules[name];
132
+ }
133
+ //#endregion
6
134
  //#region src/factory/presets.ts
7
135
  /**
8
136
  * Production preset - strict security, performance optimized
@@ -344,11 +472,20 @@ async function registerElevation(fastify, config, trackPlugin) {
344
472
  }
345
473
  /**
346
474
  * Register error handler plugin (opt-out).
475
+ *
476
+ * `moduleErrorMappers` — mappers shipped by composed arc modules
477
+ * (`defineModule({ errorMappers })`), appended AFTER host-declared mappers
478
+ * so host config keeps priority. Collected by `createApp` from the
479
+ * pre-resolved module graph.
347
480
  */
348
- async function registerErrorHandler(fastify, config, trackPlugin) {
481
+ async function registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers) {
349
482
  if (config.errorHandler === false) return;
350
483
  const { errorHandlerPlugin } = await import("./errorHandler-Dxf6P_kV.mjs").then((n) => n.r);
351
- const errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
484
+ let errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
485
+ if (moduleErrorMappers && moduleErrorMappers.length > 0) errorOpts = {
486
+ ...errorOpts,
487
+ errorMappers: [...errorOpts.errorMappers ?? [], ...moduleErrorMappers]
488
+ };
352
489
  await fastify.register(errorHandlerPlugin, errorOpts);
353
490
  trackPlugin("arc-error-handler", errorOpts);
354
491
  fastify.log.debug("Arc error handler enabled");
@@ -384,134 +521,6 @@ function createOptionalAuthenticate(authenticate) {
384
521
  };
385
522
  }
386
523
  //#endregion
387
- //#region src/factory/module.ts
388
- /**
389
- * Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
390
- * (this is where a dynamic `import()` fires); a promise (or plain module) is
391
- * awaited. Called once per module at the start of the bootstrap phase.
392
- */
393
- async function resolveModule(input) {
394
- const resolved = typeof input === "function" ? await input() : await input;
395
- 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.");
396
- return resolved;
397
- }
398
- /**
399
- * Order modules for composition by their `dependsOn` edges — a STABLE
400
- * topological sort. Called once at the start of the bootstrap phase; every
401
- * subsequent phase (bootstrap, resources, afterResources, and reverse-order
402
- * onClose) iterates the returned list, so a module's declared dependencies are
403
- * always composed before it.
404
- *
405
- * "Stable" = modules with no edge between them keep their original list order,
406
- * so declaring `dependsOn` on one module never silently reorders an unrelated
407
- * one. Backward compatible: a `modules` array with NO `dependsOn` anywhere is
408
- * returned unchanged.
409
- *
410
- * Fail-fast — throws (never reorders past a broken contract) on:
411
- * - duplicate module names (the name is the graph key)
412
- * - a `dependsOn` name not present in the composed set
413
- * - a self-reference (`dependsOn` includes the module's own name)
414
- * - a dependency cycle (reports the concrete `a → b → … → a` path)
415
- */
416
- function orderModules(modules) {
417
- const byName = /* @__PURE__ */ new Map();
418
- for (const m of modules) {
419
- if (byName.has(m.name)) throw new Error(`[arc] Duplicate module name "${m.name}" — composed twice; check your modules array.`);
420
- byName.set(m.name, m);
421
- }
422
- for (const m of modules) for (const dep of m.dependsOn ?? []) {
423
- if (dep === m.name) throw new Error(`[arc] module "${m.name}" dependsOn itself — remove the self-reference.`);
424
- 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)"}.`);
425
- }
426
- if (modules.every((m) => !m.dependsOn || m.dependsOn.length === 0)) return [...modules];
427
- const originalIndex = /* @__PURE__ */ new Map();
428
- let nextIndex = 0;
429
- for (const m of modules) originalIndex.set(m.name, nextIndex++);
430
- const pendingDeps = new Map(modules.map((m) => [m.name, (m.dependsOn ?? []).length]));
431
- const dependents = /* @__PURE__ */ new Map();
432
- for (const m of modules) for (const dep of m.dependsOn ?? []) {
433
- const list = dependents.get(dep);
434
- if (list) list.push(m.name);
435
- else dependents.set(dep, [m.name]);
436
- }
437
- const ordered = [];
438
- const remaining = new Set(modules.map((m) => m.name));
439
- while (remaining.size > 0) {
440
- let pick;
441
- let pickIndex = Number.POSITIVE_INFINITY;
442
- for (const name of remaining) {
443
- if (pendingDeps.get(name) !== 0) continue;
444
- const index = originalIndex.get(name) ?? Number.POSITIVE_INFINITY;
445
- if (index < pickIndex) {
446
- pick = name;
447
- pickIndex = index;
448
- }
449
- }
450
- if (pick === void 0) throw new Error(describeModuleCycle(remaining, byName));
451
- const picked = byName.get(pick);
452
- if (!picked) throw new Error(`[arc] internal: ordered module "${pick}" is missing from the index`);
453
- ordered.push(picked);
454
- remaining.delete(pick);
455
- for (const dependent of dependents.get(pick) ?? []) pendingDeps.set(dependent, (pendingDeps.get(dependent) ?? 1) - 1);
456
- }
457
- return ordered;
458
- }
459
- /** Walk the still-unordered subgraph for one concrete cycle path. */
460
- function describeModuleCycle(remaining, byName) {
461
- const stack = [];
462
- const onStack = /* @__PURE__ */ new Set();
463
- const done = /* @__PURE__ */ new Set();
464
- let cycle = null;
465
- const walk = (name) => {
466
- if (cycle) return;
467
- stack.push(name);
468
- onStack.add(name);
469
- for (const dep of byName.get(name)?.dependsOn ?? []) {
470
- if (!remaining.has(dep)) continue;
471
- if (onStack.has(dep)) {
472
- cycle = [...stack.slice(stack.indexOf(dep)), dep];
473
- return;
474
- }
475
- if (!done.has(dep)) walk(dep);
476
- if (cycle) return;
477
- }
478
- onStack.delete(name);
479
- stack.pop();
480
- done.add(name);
481
- };
482
- for (const name of remaining) {
483
- if (!done.has(name)) walk(name);
484
- if (cycle) break;
485
- }
486
- 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.`;
487
- }
488
- /**
489
- * Identity helper for authoring a typed module — mirrors `defineResource`.
490
- * Gives inference + a single obvious construction site; does no work.
491
- *
492
- * `TExports` is inferred from the `bootstrap` return value, so a module
493
- * author gets a typed public export for free:
494
- *
495
- * ```ts
496
- * export const accountingModule = (deps: Deps) =>
497
- * defineModule({
498
- * name: "accounting",
499
- * bootstrap: async () => createAccountingEngine(deps), // TExports inferred
500
- * });
501
- * ```
502
- */
503
- function defineModule(module) {
504
- return module;
505
- }
506
- function getModuleExports(fastify, name) {
507
- const modules = fastify.arc?.modules;
508
- if (!modules || !Object.hasOwn(modules, name)) {
509
- const available = modules ? Object.keys(modules) : [];
510
- throw new Error(`[arc] no public export recorded for module "${name}".\nA module's export is its bootstrap() return value; it is readable only AFTER that module's bootstrap ran (dependsOn / list order = init order).\nModules with recorded exports: ${available.length ? available.join(", ") : "(none)"}.`);
511
- }
512
- return modules[name];
513
- }
514
- //#endregion
515
524
  //#region src/factory/registerResources.ts
516
525
  function isResourcesFactory(value) {
517
526
  return typeof value === "function";
@@ -553,7 +562,7 @@ async function registerOne(parent, resource) {
553
562
  * THEN app onClose, in ONE hook so module
554
563
  * teardown runs before shared infra closes.
555
564
  */
556
- async function registerResources(fastify, config) {
565
+ async function registerResources(fastify, config, preResolvedModules) {
557
566
  if (config.preset === "production") {
558
567
  if (config.strictResources === void 0) config = {
559
568
  ...config,
@@ -564,7 +573,7 @@ async function registerResources(fastify, config) {
564
573
  strictResourceDir: true
565
574
  };
566
575
  }
567
- const modules = orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
576
+ const modules = preResolvedModules ?? orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
568
577
  if (config.plugins) {
569
578
  await config.plugins(fastify);
570
579
  fastify.log.debug("Custom plugins registered");
@@ -617,7 +626,7 @@ async function registerResources(fastify, config) {
617
626
  let discoveryPath;
618
627
  let discoveryYieldedZero = false;
619
628
  if (resolvedResources === void 0 && config.resourceDir) {
620
- const { loadResources } = await import("./loadResources-B8UmpITt.mjs").then((n) => n.n);
629
+ const { loadResources } = await import("./loadResources-DpU8pfjV.mjs").then((n) => n.n);
621
630
  const { resolve, dirname } = await import("node:path");
622
631
  const { fileURLToPath } = await import("node:url");
623
632
  const rawDir = config.resourceDir;
@@ -1006,7 +1015,7 @@ Missing:\n${lines}\n\nFix: pass durable adapters under \`stores\`, or use runtim
1006
1015
  *
1007
1016
  * Boot order:
1008
1017
  * ```
1009
- * 0. Logger, validation, preset merge
1018
+ * 0. Logger, validation → beforeBoot() (DB connect etc.), preset merge
1010
1019
  * 1. Create Fastify instance
1011
1020
  * 2. Security plugins (Helmet, CORS, Rate Limit) — opt-out
1012
1021
  * 3. Utility plugins (Under Pressure, Sensible, Multipart, Raw Body)
@@ -1027,10 +1036,13 @@ async function createApp(options) {
1027
1036
  }
1028
1037
  validateAuthOptions(options);
1029
1038
  const deferredWarnings = validateDistributedRuntime(options);
1039
+ if (options.beforeBoot) await options.beforeBoot();
1030
1040
  const config = {
1031
1041
  ...options.preset ? getPreset(options.preset) : {},
1032
1042
  ...options
1033
1043
  };
1044
+ const resolvedModules = orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
1045
+ const moduleErrorMappers = resolvedModules.flatMap((m) => m.errorMappers ?? []);
1034
1046
  const fastify = Fastify({
1035
1047
  logger: resolveLoggerConfig(config.logger),
1036
1048
  trustProxy: config.trustProxy ?? false,
@@ -1086,8 +1098,8 @@ async function createApp(options) {
1086
1098
  decorateRequestScope(fastify);
1087
1099
  await registerAuth(fastify, config, trackPlugin);
1088
1100
  await registerElevation(fastify, config, trackPlugin);
1089
- await registerErrorHandler(fastify, config, trackPlugin);
1090
- await registerResources(fastify, config);
1101
+ await registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers);
1102
+ await registerResources(fastify, config, resolvedModules);
1091
1103
  if (config.replyHelpers) {
1092
1104
  const { replyHelpersPlugin } = await import("./replyHelpers-67tRawQZ.mjs").then((n) => n.n);
1093
1105
  await fastify.register(replyHelpersPlugin);
@@ -1140,4 +1152,4 @@ const ArcFactory = {
1140
1152
  }
1141
1153
  };
1142
1154
  //#endregion
1143
- export { getModuleExports as a, developmentPreset as c, productionPreset as d, testingPreset as f, defineModule as i, edgePreset as l, createApp as n, orderModules as o, createApp_exports as r, resolveModule as s, ArcFactory as t, getPreset as u };
1155
+ export { edgePreset as a, testingPreset as c, orderModules as d, resolveModule as f, developmentPreset as i, defineModule as l, createApp as n, getPreset as o, createApp_exports as r, productionPreset as s, ArcFactory as t, getModuleExports as u };
@@ -1,5 +1,4 @@
1
1
  import { FastifyPluginAsync } from "fastify";
2
-
3
2
  //#region src/discovery/index.d.ts
4
3
  /** A discovered resource — must have toPlugin() method */
5
4
  interface DiscoverableResource {
@@ -1,7 +1,6 @@
1
- import { p as RegistryEntry } from "../index-_--GyzcO.mjs";
1
+ import { p as RegistryEntry } from "../index-Dtrj6XZk.mjs";
2
2
  import { t as ExternalOpenApiPaths } from "../externalPaths-BE6wFpYv.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
-
5
4
  //#region src/docs/openapi/types.d.ts
6
5
  /**
7
6
  * OpenAPI 3.0 type primitives used by arc's spec emitter.
@@ -1,5 +1,5 @@
1
1
  import { t as getUserRoles } from "../types-XTApa8b6.mjs";
2
- import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-Dq6RbEGK.mjs";
2
+ import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-Kyod_di6.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/docs/scalar.ts
5
5
  const scalarPlugin = async (fastify, opts = {}) => {
@@ -1,5 +1,4 @@
1
1
  import { FastifyPluginAsync, FastifyRequest } from "fastify";
2
-
3
2
  //#region src/scope/elevation.d.ts
4
3
  interface ElevationOptions {
5
4
  /** Roles that can use elevation (default: ['superadmin']) */
@@ -1,135 +1,5 @@
1
- import { FastifyPluginAsync, FastifyRequest } from "fastify";
2
-
3
- //#region src/encryption/types.d.ts
4
- /** JWE key-management algorithm — the protected-header `alg`. */
5
- type JweAlg = "RSA-OAEP-256" | "RSA-OAEP-384" | "RSA-OAEP-512" | "ECDH-ES" | "ECDH-ES+A256KW" | "A256GCMKW" | "dir";
6
- /** JWE content-encryption algorithm — the protected-header `enc`. AEAD only. */
7
- type JweEnc = "A128GCM" | "A192GCM" | "A256GCM";
8
- /** Encryption strategy for a route's response. */
9
- type EncryptionMode = "jwe" | "fields";
10
- /**
11
- * Opaque key material. For `jwe` mode this is a `jose` `KeyLike` /
12
- * `CryptoKey` / `Uint8Array`; for `fields` mode a 32-byte secret
13
- * (`Buffer` / `Uint8Array`). Kept `unknown` at the contract layer so the
14
- * `KeyProvider` interface carries no hard `jose` type dependency — hosts
15
- * without `jose` installed can still implement field-mode providers.
16
- */
17
- type KeyMaterial = unknown;
18
- /** A key paired with its identifier — what encrypt operations consume. */
19
- interface ActiveKey {
20
- /** Key id, stamped into the JWE `kid` header / field envelope for rotation. */
21
- readonly kid: string;
22
- /** The key material (public key for JWE, 32-byte secret for fields). */
23
- readonly key: KeyMaterial;
24
- /** Optional per-key `alg` override (else the plugin default applies). */
25
- readonly alg?: JweAlg;
26
- }
27
- /** Context handed to the key provider so resolution can be request-scoped. */
28
- interface KeyContext {
29
- readonly request: FastifyRequest;
30
- }
31
- type MaybePromise<T> = T | Promise<T>;
32
- /**
33
- * Pluggable key resolution. Arc owns the request-time plumbing — `kid` →
34
- * key, algorithm allowlist enforcement; the host owns the trust store —
35
- * which keys are valid and where they live (file / env / AWS KMS / GCP KMS
36
- * / Vault). Encryption and decryption are deliberately separate operations
37
- * because they often use DIFFERENT keys (JWE: a public key encrypts the
38
- * outbound response, a private key by `kid` decrypts the inbound request —
39
- * exactly Visa's two-key-pair model).
40
- *
41
- * Rotation mirrors `@fastify/secure-session`: the active key encrypts; any
42
- * key resolvable by `kid` can decrypt, so you can roll keys without dropping
43
- * in-flight traffic.
44
- */
45
- interface KeyProvider {
46
- /** Active key for encrypting OUTBOUND responses. */
47
- encryptionKey(ctx: KeyContext): MaybePromise<ActiveKey>;
48
- /** Resolve a DECRYPTION key by the inbound message's `kid`. */
49
- decryptionKey(kid: string, ctx: KeyContext): MaybePromise<KeyMaterial | undefined>;
50
- }
51
- /**
52
- * Per-resource encryption directive — the typed slice plugins register on
53
- * `ResourceExtensions`. Declared on a resource via
54
- * `defineResource({ extensions: { encryption: { … } } })`; arc threads it
55
- * to request time on `request.routeOptions.config.arcExtensions.encryption`.
56
- */
57
- interface EncryptionDirective {
58
- /** Strategy. Defaults to the plugin-level `mode` (default `'jwe'`). */
59
- readonly mode?: EncryptionMode;
60
- /**
61
- * For `mode: 'fields'` — dot-paths of fields to encrypt, resolved against
62
- * the serialized response body (e.g. `'cardNumber'`, `'account.iban'`).
63
- * Ignored for `mode: 'jwe'` (the whole body is encrypted).
64
- */
65
- readonly fields?: readonly string[];
66
- /**
67
- * Explicit opt-out — set `false` to disable encryption for this resource
68
- * even when a plugin-level default (`routes`) would otherwise apply.
69
- */
70
- readonly enabled?: boolean;
71
- }
72
- /**
73
- * Plugin-level options. `keyProvider` is the only required field; everything
74
- * else has a secure default.
75
- */
76
- interface EncryptionOptions {
77
- /**
78
- * Master on/off. Default `true`. Set `false` to register the plugin as a
79
- * typed no-op (config-gated environments) — same pattern as
80
- * `idempotencyPlugin`.
81
- */
82
- readonly enabled?: boolean;
83
- /** Key provider — resolves `kid` → key for encrypt/decrypt. Required. */
84
- readonly keyProvider: KeyProvider;
85
- /** Default mode when a route opts in without naming one. Default `'jwe'`. */
86
- readonly mode?: EncryptionMode;
87
- /** JWE key-management algorithm. Default `'RSA-OAEP-256'`. */
88
- readonly alg?: JweAlg;
89
- /** JWE content-encryption algorithm. Default `'A256GCM'`. */
90
- readonly enc?: JweEnc;
91
- /**
92
- * Inbound-decryption algorithm allowlists — block algorithm-substitution
93
- * downgrade attacks. Default `[alg]` / `[enc]`. Set explicitly only when a
94
- * client legitimately uses more than one algorithm.
95
- */
96
- readonly allowedAlgs?: readonly JweAlg[];
97
- readonly allowedEncs?: readonly JweEnc[];
98
- /**
99
- * Fallback opt-in for routes that don't declare
100
- * `extensions.encryption`. Return a directive (or `true` for the default
101
- * mode) to encrypt, `false`/`undefined` to skip. The per-resource
102
- * `extensions.encryption` ALWAYS wins when present. Use this to scope ALE
103
- * to sensitive prefixes without touching every resource.
104
- *
105
- * @example confine ALE to `/payments` and `/accounts`
106
- * ```ts
107
- * routes: (req) => /^\/(payments|accounts)\b/.test(req.url)
108
- * ```
109
- */
110
- readonly routes?: (request: FastifyRequest) => EncryptionDirective | boolean | undefined;
111
- /** Decrypt inbound `application/jose` request bodies. Default `true`. */
112
- readonly decryptRequests?: boolean;
113
- /** Media type that signals an encrypted REQUEST body. Default `'application/jose'`. */
114
- readonly contentType?: string;
115
- /** Content-Type set on full-body JWE RESPONSES. Default `'application/jose'`. */
116
- readonly responseContentType?: string;
117
- /**
118
- * Response header advertising that the body is encrypted, so generic
119
- * client SDKs know to decrypt. Default `'x-encrypted'`; set `false` to omit.
120
- */
121
- readonly flagHeader?: string | false;
122
- }
123
- /** Request decorations added by the plugin. */
124
- declare module "fastify" {
125
- interface FastifyRequest {
126
- /** `true` when the inbound body was decrypted from a JWE by this plugin. */
127
- requestDecrypted?: boolean;
128
- /** `true` when the outbound body was encrypted by this plugin. */
129
- responseEncrypted?: boolean;
130
- }
131
- }
132
- //#endregion
1
+ import { a as JweAlg, c as KeyMaterial, i as EncryptionOptions, l as KeyProvider, n as EncryptionDirective, o as JweEnc, r as EncryptionMode, s as KeyContext, t as ActiveKey } from "../types-Cc91nXuA.mjs";
2
+ import { FastifyPluginAsync } from "fastify";
133
3
  //#region src/encryption/encryptionPlugin.d.ts
134
4
  declare const encryptionPlugin: FastifyPluginAsync<EncryptionOptions>;
135
5
  declare const _default: FastifyPluginAsync<EncryptionOptions>;
@@ -207,16 +77,4 @@ interface StaticKeyProviderOptions {
207
77
  */
208
78
  declare function createStaticKeyProvider(options: StaticKeyProviderOptions): KeyProvider;
209
79
  //#endregion
210
- //#region src/encryption/index.d.ts
211
- declare module "../types/resource.js" {
212
- interface ResourceExtensions {
213
- /**
214
- * Encrypt this resource's responses — full-body JWE or specific fields.
215
- * Threaded to request time on
216
- * `request.routeOptions.config.arcExtensions.encryption`.
217
- */
218
- encryption?: EncryptionDirective;
219
- }
220
- }
221
- //#endregion
222
80
  export { type ActiveKey, type EncryptionDirective, type EncryptionMode, type EncryptionOptions, type JweAlg, type JweEnc, type KeyContext, type KeyMaterial, type KeyProvider, type ParsedFieldEnvelope, type StaticKeyProviderOptions, createStaticKeyProvider, decryptField, _default as default, encryptField, encryptionPlugin, parseFieldEnvelope };
@@ -1,6 +1,5 @@
1
1
  import { ErrorDetail } from "@classytic/repo-core/errors";
2
2
  import { FastifyInstance, FastifyRequest } from "fastify";
3
-
4
3
  //#region src/plugins/errorHandler.d.ts
5
4
  /**
6
5
  * Class-based error mapper — `instanceof` check converts a thrown class
@@ -1,5 +1,4 @@
1
1
  import { HttpError } from "@classytic/repo-core/errors";
2
-
3
2
  //#region src/utils/errors.d.ts
4
3
  interface ErrorOptions {
5
4
  code?: string;
@@ -1,6 +1,5 @@
1
- import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "./EventTransport-IEn7Tn-Q.mjs";
1
+ import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "./EventTransport-QmY7A258.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
-
4
3
  //#region src/events/defineEvent.d.ts
5
4
  interface EventSchema {
6
5
  type: "object";
@@ -249,7 +248,8 @@ interface EventPluginOptions {
249
248
  * failed events are published to the `$deadLetter` event type by default.
250
249
  */
251
250
  deadLetterQueue?: {
252
- /** Custom store function. If omitted, publishes to '$deadLetter' event type. */store?: (event: DomainEvent, errors: Error[]) => void | Promise<void>;
251
+ /** Custom store function. If omitted, publishes to '$deadLetter' event type. */
252
+ store?: (event: DomainEvent, errors: Error[]) => void | Promise<void>;
253
253
  };
254
254
  /** Callback after successful publish (for metrics/tracking) */
255
255
  onPublish?: (event: DomainEvent) => void;
@@ -302,9 +302,13 @@ interface EventPluginOptions {
302
302
  declare module "fastify" {
303
303
  interface FastifyInstance {
304
304
  events: {
305
- /** Publish an event */publish: <T>(type: string, payload: T, meta?: Partial<DomainEvent["meta"]>) => Promise<void>; /** Subscribe to events */
306
- subscribe: (pattern: string, handler: EventHandler) => Promise<() => void>; /** Get transport name */
307
- transportName: string; /** Event registry for introspection (undefined when no registry configured) */
305
+ /** Publish an event */
306
+ publish: <T>(type: string, payload: T, meta?: Partial<DomainEvent["meta"]>) => Promise<void>;
307
+ /** Subscribe to events */
308
+ subscribe: (pattern: string, handler: EventHandler) => Promise<() => void>;
309
+ /** Get transport name */
310
+ transportName: string;
311
+ /** Event registry for introspection (undefined when no registry configured) */
308
312
  registry?: EventRegistry;
309
313
  };
310
314
  }
@@ -1,9 +1,8 @@
1
- import { a as EventTransport, i as EventLogger, n as DomainEvent, o as MemoryEventTransport, r as EventHandler, s as MemoryEventTransportOptions, t as DeadLetteredEvent } from "../EventTransport-IEn7Tn-Q.mjs";
2
- import { a as withRetry, c as EventDefinitionOutput, d as EventSchema, f as ValidationResult, i as createDeadLetterPublisher, l as EventRegistry, m as defineEvent, n as eventPlugin, o as CustomValidator, p as createEventRegistry, r as RetryOptions, s as EventDefinitionInput, t as EventPluginOptions, u as EventRegistryOptions } from "../eventPlugin-DbBFd3x_.mjs";
1
+ import { a as EventTransport, i as EventLogger, n as DomainEvent, o as MemoryEventTransport, r as EventHandler, s as MemoryEventTransportOptions, t as DeadLetteredEvent } from "../EventTransport-QmY7A258.mjs";
2
+ import { a as withRetry, c as EventDefinitionOutput, d as EventSchema, f as ValidationResult, i as createDeadLetterPublisher, l as EventRegistry, m as defineEvent, n as eventPlugin, o as CustomValidator, p as createEventRegistry, r as RetryOptions, s as EventDefinitionInput, t as EventPluginOptions, u as EventRegistryOptions } from "../eventPlugin-A6EbthXR.mjs";
3
3
  import { RedisEventTransportOptions, RedisLike } from "./transports/redis.mjs";
4
- import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-DsP1vqZB.mjs";
4
+ import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-iVR2l7aS.mjs";
5
5
  import { RepositoryLike } from "@classytic/repo-core/adapter";
6
-
7
6
  //#region src/events/eventTypes.d.ts
8
7
  /**
9
8
  * Event Type Constants and Helpers
@@ -38,14 +37,18 @@ type CrudEventSuffix = (typeof CRUD_EVENT_SUFFIXES)[number];
38
37
  declare function crudEventType(resource: string, suffix: CrudEventSuffix): string;
39
38
  /** Arc framework lifecycle events — emitted automatically by the framework */
40
39
  declare const ARC_LIFECYCLE_EVENTS: Readonly<{
41
- /** Emitted when a resource plugin is registered */readonly RESOURCE_REGISTERED: "arc.resource.registered"; /** Emitted when Arc is fully ready (all resources registered, onReady fired) */
40
+ /** Emitted when a resource plugin is registered */
41
+ readonly RESOURCE_REGISTERED: "arc.resource.registered";
42
+ /** Emitted when Arc is fully ready (all resources registered, onReady fired) */
42
43
  readonly READY: "arc.ready";
43
44
  }>;
44
45
  /** Type for Arc lifecycle event names */
45
46
  type ArcLifecycleEvent = (typeof ARC_LIFECYCLE_EVENTS)[keyof typeof ARC_LIFECYCLE_EVENTS];
46
47
  /** Cache-specific event types for observability and external triggers */
47
48
  declare const CACHE_EVENTS: Readonly<{
48
- /** Emitted when a resource's cache version is bumped */readonly VERSION_BUMPED: "arc.cache.version.bumped"; /** Emitted when a tag version is bumped */
49
+ /** Emitted when a resource's cache version is bumped */
50
+ readonly VERSION_BUMPED: "arc.cache.version.bumped";
51
+ /** Emitted when a tag version is bumped */
49
52
  readonly TAG_VERSION_BUMPED: "arc.cache.tag.bumped";
50
53
  }>;
51
54
  /** Type for cache event names */
@@ -1,2 +1,2 @@
1
- import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-DsP1vqZB.mjs";
1
+ import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-iVR2l7aS.mjs";
2
2
  export { type RedisStreamLike, RedisStreamTransport, type RedisStreamTransportOptions };
@@ -1,5 +1,4 @@
1
- import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "../../EventTransport-IEn7Tn-Q.mjs";
2
-
1
+ import { a as EventTransport, i as EventLogger, n as DomainEvent, r as EventHandler } from "../../EventTransport-QmY7A258.mjs";
3
2
  //#region src/events/transports/redis.d.ts
4
3
  interface RedisLike {
5
4
  publish(channel: string, message: string): Promise<number>;
@@ -1,14 +1,14 @@
1
- import { a as getModuleExports, c as LoadResourcesOptions, d as loadResources, i as defineModule, l as ResourceLike, n as ArcModuleInput, o as orderModules, r as ArcModuleRegistry, s as resolveModule, t as ArcModule, u as ResourceModule } from "../module-JYH9V0I8.mjs";
2
- import { a as CustomPluginAuthOption, c as RawBodyOptions, d as preloadResourcesAsync, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as preloadResources } from "../types-BHMKlMBU.mjs";
1
+ import { a as getModuleExports, c as LoadResourcesOptions, d as loadResources, i as defineModule, l as ResourceLike, n as ArcModuleInput, o as orderModules, r as ArcModuleRegistry, s as resolveModule, t as ArcModule, u as ResourceModule } from "../module-WeuU5jfd.mjs";
2
+ import "../fastify-augmentation-DafigGQp.mjs";
3
+ import { a as CustomPluginAuthOption, c as RawBodyOptions, d as preloadResourcesAsync, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as preloadResources } from "../types-DpxoNX5v.mjs";
3
4
  import { FastifyInstance, FastifyServerOptions } from "fastify";
4
-
5
5
  //#region src/factory/createApp.d.ts
6
6
  /**
7
7
  * Create a production-ready Fastify application with Arc framework.
8
8
  *
9
9
  * Boot order:
10
10
  * ```
11
- * 0. Logger, validation, preset merge
11
+ * 0. Logger, validation → beforeBoot() (DB connect etc.), preset merge
12
12
  * 1. Create Fastify instance
13
13
  * 2. Security plugins (Helmet, CORS, Rate Limit) — opt-out
14
14
  * 3. Utility plugins (Under Pressure, Sensible, Multipart, Raw Body)