@classytic/arc 2.20.0 → 2.22.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 (143) hide show
  1. package/dist/{BaseController-D-MHZ27_.mjs → BaseController-C6FlRWxF.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 +35 -14
  9. package/dist/auth/index.mjs +6 -3
  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-b6fgFgnd.mjs} +129 -17
  20. package/dist/{createActionRouter-HlQso0jO.mjs → createActionRouter-h1cEEaEX.mjs} +10 -6
  21. package/dist/{createAggregationRouter-5LzPzgw1.mjs → createAggregationRouter-B1C0kvTL.mjs} +3 -3
  22. package/dist/{createApp-D6uwzCKu.mjs → createApp-Ci7yG5gv.mjs} +390 -192
  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/encryption/index.mjs +9 -5
  29. package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-BTzXSwIg.mjs} +24 -3
  30. package/dist/{errorHandler-BtUUDPEa.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -1
  31. package/dist/{errors-DOHkjQlD.d.mts → errors-DHcHVAgk.d.mts} +0 -1
  32. package/dist/{eventPlugin-DbBFd3x_.d.mts → eventPlugin-A6EbthXR.d.mts} +10 -6
  33. package/dist/events/index.d.mts +9 -6
  34. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  35. package/dist/events/transports/redis.d.mts +1 -2
  36. package/dist/factory/index.d.mts +30 -9
  37. package/dist/factory/index.mjs +35 -4
  38. package/dist/{fastify-augmentation-CCQJEDgh.d.mts → fastify-augmentation-9_fuetB4.d.mts} +2 -3
  39. package/dist/fields-D3lhgzhD.d.mts +121 -0
  40. package/dist/health-ScyjIVo5.mjs +202 -0
  41. package/dist/hooks/index.d.mts +1 -1
  42. package/dist/idempotency/index.d.mts +4 -3
  43. package/dist/idempotency/index.mjs +2 -0
  44. package/dist/idempotency/redis.d.mts +1 -1
  45. package/dist/{types-BHMKlMBU.d.mts → index-BEu4wjbL.d.mts} +241 -80
  46. package/dist/{index-DUa8WHz3.d.mts → index-D8MKYGDa.d.mts} +88 -7
  47. package/dist/{index-_--GyzcO.d.mts → index-DbtgCOCj.d.mts} +1324 -1228
  48. package/dist/{index-CdFkIMEd.d.mts → index-ogIHvOjo.d.mts} +36 -7
  49. package/dist/index.d.mts +8 -7
  50. package/dist/index.mjs +6 -6
  51. package/dist/{init-YH_nslQR.mjs → init-DleK3p9f.mjs} +272 -139
  52. package/dist/integrations/event-gateway.d.mts +2 -3
  53. package/dist/integrations/index.d.mts +2 -2
  54. package/dist/integrations/jobs.d.mts +0 -1
  55. package/dist/integrations/mcp/index.d.mts +6 -7
  56. package/dist/integrations/mcp/index.mjs +1 -1
  57. package/dist/integrations/mcp/testing.d.mts +1 -2
  58. package/dist/integrations/mcp/testing.mjs +1 -1
  59. package/dist/integrations/streamline.d.mts +2 -2
  60. package/dist/integrations/webhooks.d.mts +8 -5
  61. package/dist/integrations/webhooks.mjs +9 -5
  62. package/dist/integrations/websocket-pushref-redis.d.mts +1 -2
  63. package/dist/integrations/websocket-redis.d.mts +1 -2
  64. package/dist/integrations/websocket.d.mts +2 -2
  65. package/dist/jsonBody-BlzAoGCe.mjs +43 -0
  66. package/dist/{loadResources-B8UmpITt.mjs → loadResources-DpU8pfjV.mjs} +4 -3
  67. package/dist/logger/index.d.mts +26 -1
  68. package/dist/logger/index.mjs +39 -1
  69. package/dist/{mcpPlugin-CuW6L_uD.mjs → mcpPlugin-ByA2tf6D.mjs} +8 -4
  70. package/dist/middleware/index.d.mts +1 -2
  71. package/dist/{module-JYH9V0I8.d.mts → module-B4u10iOJ.d.mts} +71 -1
  72. package/dist/{openapi-Dq6RbEGK.mjs → openapi-BRpMZD7C.mjs} +3 -3
  73. package/dist/permissions/index.d.mts +4 -3
  74. package/dist/permissions/index.mjs +2 -2
  75. package/dist/{permissions-CgbOLJZG.mjs → permissions-BJMuKMl7.mjs} +33 -1
  76. package/dist/pipeline/index.d.mts +1 -2
  77. package/dist/plugins/index.d.mts +96 -7
  78. package/dist/plugins/index.mjs +86 -268
  79. package/dist/plugins/response-cache.d.mts +5 -3
  80. package/dist/plugins/response-cache.mjs +1 -0
  81. package/dist/plugins/tracing-entry.d.mts +1 -1
  82. package/dist/plugins/tracing-entry.mjs +7 -4
  83. package/dist/presets/filesUpload.d.mts +4 -5
  84. package/dist/presets/filesUpload.mjs +1 -1
  85. package/dist/presets/index.d.mts +1 -2
  86. package/dist/presets/index.mjs +1 -1
  87. package/dist/presets/multiTenant.d.mts +1 -2
  88. package/dist/presets/search.d.mts +2 -3
  89. package/dist/presets/search.mjs +1 -1
  90. package/dist/{presets-DWsYQdbV.mjs → presets-gw0ciIUD.mjs} +1 -1
  91. package/dist/{pushref-registry-DnCqL3aa.d.mts → pushref-registry-BbcnpNTm.d.mts} +4 -2
  92. package/dist/{queryCachePlugin-xzFyKAjT.d.mts → queryCachePlugin-CfYWq-Ak.d.mts} +1 -2
  93. package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
  94. package/dist/{redis-C3W0qbz8.d.mts → redis-UDtLaNj6.d.mts} +0 -1
  95. package/dist/{redis-stream-DsP1vqZB.d.mts → redis-stream-iVR2l7aS.d.mts} +1 -2
  96. package/dist/{registerRoute-DGSREwPw.mjs → registerRoute-xHc42fwB.mjs} +38 -18
  97. package/dist/registry/index.d.mts +2 -2
  98. package/dist/requestId-5BhTVzBv.mjs +113 -0
  99. package/dist/schemaConverter-Ds5reSGs.mjs +168 -0
  100. package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-B-6SPe7g.mjs} +2 -2
  101. package/dist/schemas/index.d.mts +0 -1
  102. package/dist/scim/index.d.mts +0 -1
  103. package/dist/scim/index.mjs +6 -7
  104. package/dist/scope/index.d.mts +2 -3
  105. package/dist/scope/index.mjs +2 -76
  106. package/dist/scope-k15RXfcq.mjs +64 -0
  107. package/dist/{sessionManager-BXG-HGwG.d.mts → sessionManager-BHOM3N_0.d.mts} +10 -6
  108. package/dist/{storage-Co_PQpeQ.d.mts → storage-DLUZ04kK.d.mts} +8 -4
  109. package/dist/testing/index.d.mts +83 -4
  110. package/dist/testing/index.mjs +74 -4
  111. package/dist/testing/storageContract.d.mts +1 -2
  112. package/dist/{tracing-D8jlGXP7.d.mts → tracing-CsGKQJGg.d.mts} +0 -1
  113. package/dist/types/index.d.mts +4 -4
  114. package/dist/types/storage.d.mts +1 -1
  115. package/dist/{types-C9b2HR5W.d.mts → types-Bc42CUyk.d.mts} +1 -2
  116. package/dist/types-Cc91nXuA.d.mts +132 -0
  117. package/dist/{fields-C7ahdGYf.d.mts → types-Csxga7d3.d.mts} +2 -123
  118. package/dist/{types-LNebCoJx.d.mts → types-DH0GupI7.d.mts} +8 -4
  119. package/dist/usage/index.d.mts +142 -0
  120. package/dist/usage/index.mjs +0 -0
  121. package/dist/utils/index.d.mts +220 -144
  122. package/dist/utils/index.mjs +3 -3
  123. package/dist/{utils-ChjYQpjn.mjs → utils-FhJ88TJE.mjs} +129 -39
  124. package/dist/{versioning-D_eBCfNr.d.mts → versioning-DLwklPlG.d.mts} +1 -7
  125. package/dist/{websocket-MlYlrYq0.d.mts → websocket-uuWqM6UY.d.mts} +5 -4
  126. package/llms.txt +39 -0
  127. package/package.json +453 -447
  128. package/skills/arc/SKILL.md +14 -8
  129. package/skills/arc/references/api-reference.md +34 -0
  130. package/skills/arc/references/auth.md +21 -4
  131. package/skills/arc/references/mcp.md +1 -1
  132. package/skills/arc/references/production.md +44 -2
  133. package/skills/arc-code-review/SKILL.md +1 -1
  134. package/skills/arc-code-review/references/anti-patterns.md +71 -18
  135. package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
  136. package/skills/arc-code-review/references/migration-recipes.md +7 -7
  137. package/skills/arc-code-review/references/mongokit-migration.md +4 -3
  138. package/skills/arc-module-publishing/SKILL.md +243 -0
  139. package/skills/arc-module-publishing/references/authoring-modules.md +327 -0
  140. package/skills/arc-module-publishing/references/package-manifest.md +157 -0
  141. package/skills/arc-module-publishing/references/publishing-workflow.md +182 -0
  142. package/dist/preloadResources-CHw3_oA_.mjs +0 -45
  143. package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
@@ -1,8 +1,183 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
- import { arcLog } from "./logger/index.mjs";
2
+ import { arcLog, configureArcLogger, createPinoWriter } from "./logger/index.mjs";
3
3
  import { n as PUBLIC_SCOPE } from "./types-bHblZ9YN.mjs";
4
+ import { t as createTenantKeyGenerator } from "./rateLimitKey-BoWRH3e7.mjs";
5
+ import { t as createRequestIdGenerator } from "./requestId-5BhTVzBv.mjs";
6
+ import { t as parseJsonBody } from "./jsonBody-BlzAoGCe.mjs";
4
7
  import Fastify from "fastify";
5
8
  import qs from "qs";
9
+ //#region src/testing/preloadResources.ts
10
+ /**
11
+ * Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
12
+ *
13
+ * Accepts either:
14
+ * - `{ import: 'default' }` form: values are the resource directly
15
+ * - default form: values are the full module — picks first export with `toPlugin()`
16
+ *
17
+ * Throws if any module doesn't yield a valid `ResourceLike`.
18
+ */
19
+ function preloadResources(globResult) {
20
+ const resources = [];
21
+ for (const [path, value] of Object.entries(globResult)) {
22
+ const resource = pickResource(value);
23
+ if (!resource) throw new Error(`preloadResources: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
24
+ resources.push(resource);
25
+ }
26
+ return resources.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
27
+ }
28
+ /**
29
+ * Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
30
+ *
31
+ * Use this when resources depend on prior bootstrap (e.g., engine init) and
32
+ * cannot be evaluated at import time of the preload file.
33
+ */
34
+ async function preloadResourcesAsync(globResult) {
35
+ return (await Promise.all(Object.entries(globResult).map(async ([path, loader]) => {
36
+ const resource = pickResource(await loader());
37
+ if (!resource) throw new Error(`preloadResourcesAsync: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
38
+ return resource;
39
+ }))).sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
40
+ }
41
+ function pickResource(value) {
42
+ if (!value || typeof value !== "object") return void 0;
43
+ if (typeof value.toPlugin === "function") return value;
44
+ const mod = value;
45
+ const candidates = [
46
+ mod.default,
47
+ mod.resource,
48
+ ...Object.values(mod)
49
+ ];
50
+ for (const c of candidates) if (c && typeof c === "object" && typeof c.toPlugin === "function") return c;
51
+ }
52
+ //#endregion
53
+ //#region src/factory/module.ts
54
+ /**
55
+ * Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
56
+ * (this is where a dynamic `import()` fires); a promise (or plain module) is
57
+ * awaited. Called once per module at the start of the bootstrap phase.
58
+ */
59
+ async function resolveModule(input) {
60
+ const resolved = typeof input === "function" ? await input() : await input;
61
+ 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.");
62
+ return resolved;
63
+ }
64
+ /**
65
+ * Order modules for composition by their `dependsOn` edges — a STABLE
66
+ * topological sort. Called once at the start of the bootstrap phase; every
67
+ * subsequent phase (bootstrap, resources, afterResources, and reverse-order
68
+ * onClose) iterates the returned list, so a module's declared dependencies are
69
+ * always composed before it.
70
+ *
71
+ * "Stable" = modules with no edge between them keep their original list order,
72
+ * so declaring `dependsOn` on one module never silently reorders an unrelated
73
+ * one. Backward compatible: a `modules` array with NO `dependsOn` anywhere is
74
+ * returned unchanged.
75
+ *
76
+ * Fail-fast — throws (never reorders past a broken contract) on:
77
+ * - duplicate module names (the name is the graph key)
78
+ * - a `dependsOn` name not present in the composed set
79
+ * - a self-reference (`dependsOn` includes the module's own name)
80
+ * - a dependency cycle (reports the concrete `a → b → … → a` path)
81
+ */
82
+ function orderModules(modules) {
83
+ const byName = /* @__PURE__ */ new Map();
84
+ for (const m of modules) {
85
+ if (byName.has(m.name)) throw new Error(`[arc] Duplicate module name "${m.name}" — composed twice; check your modules array.`);
86
+ byName.set(m.name, m);
87
+ }
88
+ for (const m of modules) for (const dep of m.dependsOn ?? []) {
89
+ if (dep === m.name) throw new Error(`[arc] module "${m.name}" dependsOn itself — remove the self-reference.`);
90
+ 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)"}.`);
91
+ }
92
+ if (modules.every((m) => !m.dependsOn || m.dependsOn.length === 0)) return [...modules];
93
+ const originalIndex = /* @__PURE__ */ new Map();
94
+ let nextIndex = 0;
95
+ for (const m of modules) originalIndex.set(m.name, nextIndex++);
96
+ const pendingDeps = new Map(modules.map((m) => [m.name, (m.dependsOn ?? []).length]));
97
+ const dependents = /* @__PURE__ */ new Map();
98
+ for (const m of modules) for (const dep of m.dependsOn ?? []) {
99
+ const list = dependents.get(dep);
100
+ if (list) list.push(m.name);
101
+ else dependents.set(dep, [m.name]);
102
+ }
103
+ const ordered = [];
104
+ const remaining = new Set(modules.map((m) => m.name));
105
+ while (remaining.size > 0) {
106
+ let pick;
107
+ let pickIndex = Number.POSITIVE_INFINITY;
108
+ for (const name of remaining) {
109
+ if (pendingDeps.get(name) !== 0) continue;
110
+ const index = originalIndex.get(name) ?? Number.POSITIVE_INFINITY;
111
+ if (index < pickIndex) {
112
+ pick = name;
113
+ pickIndex = index;
114
+ }
115
+ }
116
+ if (pick === void 0) throw new Error(describeModuleCycle(remaining, byName));
117
+ const picked = byName.get(pick);
118
+ if (!picked) throw new Error(`[arc] internal: ordered module "${pick}" is missing from the index`);
119
+ ordered.push(picked);
120
+ remaining.delete(pick);
121
+ for (const dependent of dependents.get(pick) ?? []) pendingDeps.set(dependent, (pendingDeps.get(dependent) ?? 1) - 1);
122
+ }
123
+ return ordered;
124
+ }
125
+ /** Walk the still-unordered subgraph for one concrete cycle path. */
126
+ function describeModuleCycle(remaining, byName) {
127
+ const stack = [];
128
+ const onStack = /* @__PURE__ */ new Set();
129
+ const done = /* @__PURE__ */ new Set();
130
+ let cycle = null;
131
+ const walk = (name) => {
132
+ if (cycle) return;
133
+ stack.push(name);
134
+ onStack.add(name);
135
+ for (const dep of byName.get(name)?.dependsOn ?? []) {
136
+ if (!remaining.has(dep)) continue;
137
+ if (onStack.has(dep)) {
138
+ cycle = [...stack.slice(stack.indexOf(dep)), dep];
139
+ return;
140
+ }
141
+ if (!done.has(dep)) walk(dep);
142
+ if (cycle) return;
143
+ }
144
+ onStack.delete(name);
145
+ stack.pop();
146
+ done.add(name);
147
+ };
148
+ for (const name of remaining) {
149
+ if (!done.has(name)) walk(name);
150
+ if (cycle) break;
151
+ }
152
+ 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.`;
153
+ }
154
+ /**
155
+ * Identity helper for authoring a typed module — mirrors `defineResource`.
156
+ * Gives inference + a single obvious construction site; does no work.
157
+ *
158
+ * `TExports` is inferred from the `bootstrap` return value, so a module
159
+ * author gets a typed public export for free:
160
+ *
161
+ * ```ts
162
+ * export const accountingModule = (deps: Deps) =>
163
+ * defineModule({
164
+ * name: "accounting",
165
+ * bootstrap: async () => createAccountingEngine(deps), // TExports inferred
166
+ * });
167
+ * ```
168
+ */
169
+ function defineModule(module) {
170
+ return module;
171
+ }
172
+ function getModuleExports(fastify, name) {
173
+ const modules = fastify.arc?.modules;
174
+ if (!modules || !Object.hasOwn(modules, name)) {
175
+ const available = modules ? Object.keys(modules) : [];
176
+ 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)"}.`);
177
+ }
178
+ return modules[name];
179
+ }
180
+ //#endregion
6
181
  //#region src/factory/presets.ts
7
182
  /**
8
183
  * Production preset - strict security, performance optimized
@@ -182,8 +357,33 @@ const edgePreset = {
182
357
  /**
183
358
  * Get preset by name
184
359
  */
360
+ /**
361
+ * Worker preset (2.23) — the headless process role. Flips the HTTP surface
362
+ * off and enables the `mountRoutes: false` primitive; runtime tiers
363
+ * (events, jobs, schedules, caching, audit, usage) stay AS CONFIGURED by
364
+ * the host's options. Per the preset contract, explicit narrow settings
365
+ * win — `arcPlugins: { metrics: true }` re-enables Prometheus on workers,
366
+ * `auth: {...}` keeps auth. See wiki/factory.md § createWorker.
367
+ */
368
+ const workerPreset = {
369
+ mountRoutes: false,
370
+ auth: false,
371
+ helmet: false,
372
+ cors: false,
373
+ rateLimit: false,
374
+ underPressure: false,
375
+ sensible: false,
376
+ arcPlugins: {
377
+ requestId: false,
378
+ health: false,
379
+ sse: false,
380
+ metrics: false,
381
+ gracefulShutdown: true
382
+ }
383
+ };
185
384
  function getPreset(name) {
186
385
  switch (name) {
386
+ case "worker": return workerPreset;
187
387
  case "production": return productionPreset;
188
388
  case "development": return developmentPreset;
189
389
  case "testing": return testingPreset;
@@ -296,7 +496,7 @@ async function registerAuth(fastify, config, trackPlugin) {
296
496
  }
297
497
  switch (authConfig.type) {
298
498
  case "betterAuth": {
299
- const { plugin, openapi } = authConfig.betterAuth;
499
+ const { plugin, openapi } = typeof authConfig.betterAuth === "function" ? await authConfig.betterAuth() : authConfig.betterAuth;
300
500
  await fastify.register(plugin);
301
501
  trackPlugin("auth-better-auth");
302
502
  const arc = fastify.arc;
@@ -344,11 +544,20 @@ async function registerElevation(fastify, config, trackPlugin) {
344
544
  }
345
545
  /**
346
546
  * Register error handler plugin (opt-out).
547
+ *
548
+ * `moduleErrorMappers` — mappers shipped by composed arc modules
549
+ * (`defineModule({ errorMappers })`), appended AFTER host-declared mappers
550
+ * so host config keeps priority. Collected by `createApp` from the
551
+ * pre-resolved module graph.
347
552
  */
348
- async function registerErrorHandler(fastify, config, trackPlugin) {
553
+ async function registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers) {
349
554
  if (config.errorHandler === false) return;
350
- 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" };
555
+ const { errorHandlerPlugin } = await import("./errorHandler-BTzXSwIg.mjs").then((n) => n.r);
556
+ let errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
557
+ if (moduleErrorMappers && moduleErrorMappers.length > 0) errorOpts = {
558
+ ...errorOpts,
559
+ errorMappers: [...errorOpts.errorMappers ?? [], ...moduleErrorMappers]
560
+ };
352
561
  await fastify.register(errorHandlerPlugin, errorOpts);
353
562
  trackPlugin("arc-error-handler", errorOpts);
354
563
  fastify.log.debug("Arc error handler enabled");
@@ -384,143 +593,15 @@ function createOptionalAuthenticate(authenticate) {
384
593
  };
385
594
  }
386
595
  //#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
596
  //#region src/factory/registerResources.ts
516
597
  function isResourcesFactory(value) {
517
598
  return typeof value === "function";
518
599
  }
519
600
  /** Register a single resource with descriptive error on failure. */
520
- async function registerOne(parent, resource) {
601
+ async function registerOne(parent, resource, mountRoutes) {
521
602
  const name = resource.name ?? "unknown";
522
603
  try {
523
- await parent.register(resource.toPlugin());
604
+ await parent.register(resource.toPlugin(), mountRoutes === false ? { arcMountRoutes: false } : {});
524
605
  } catch (err) {
525
606
  const rawMsg = err instanceof Error ? err.message : String(err);
526
607
  const stripped = rawMsg.replace(new RegExp(`^Resource "${name}"\\s*`), "").replace(/\.+\s*$/, "");
@@ -553,7 +634,7 @@ async function registerOne(parent, resource) {
553
634
  * THEN app onClose, in ONE hook so module
554
635
  * teardown runs before shared infra closes.
555
636
  */
556
- async function registerResources(fastify, config) {
637
+ async function registerResources(fastify, config, preResolvedModules) {
557
638
  if (config.preset === "production") {
558
639
  if (config.strictResources === void 0) config = {
559
640
  ...config,
@@ -564,7 +645,7 @@ async function registerResources(fastify, config) {
564
645
  strictResourceDir: true
565
646
  };
566
647
  }
567
- const modules = orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
648
+ const modules = preResolvedModules ?? orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
568
649
  if (config.plugins) {
569
650
  await config.plugins(fastify);
570
651
  fastify.log.debug("Custom plugins registered");
@@ -617,7 +698,7 @@ async function registerResources(fastify, config) {
617
698
  let discoveryPath;
618
699
  let discoveryYieldedZero = false;
619
700
  if (resolvedResources === void 0 && config.resourceDir) {
620
- const { loadResources } = await import("./loadResources-B8UmpITt.mjs").then((n) => n.n);
701
+ const { loadResources } = await import("./loadResources-DpU8pfjV.mjs").then((n) => n.n);
621
702
  const { resolve, dirname } = await import("node:path");
622
703
  const { fileURLToPath } = await import("node:url");
623
704
  const rawDir = config.resourceDir;
@@ -631,6 +712,16 @@ async function registerResources(fastify, config) {
631
712
  }
632
713
  resolvedResources = discovered;
633
714
  }
715
+ if (modules.length && resolvedResources && resolvedResources.length > 0) {
716
+ const owned = /* @__PURE__ */ new Set();
717
+ for (const m of modules) for (const name of m.owns ?? []) owned.add(name);
718
+ if (owned.size > 0) {
719
+ const before = resolvedResources.length;
720
+ resolvedResources = resolvedResources.filter((r) => !(r.name != null && owned.has(r.name)));
721
+ const dropped = before - resolvedResources.length;
722
+ if (dropped > 0) fastify.log.debug(`[arc] ${dropped} app resource(s) superseded by module owns() — the module version registers instead`);
723
+ }
724
+ }
634
725
  if (modules.length) {
635
726
  const moduleResources = [];
636
727
  for (const m of modules) {
@@ -659,11 +750,11 @@ async function registerResources(fastify, config) {
659
750
  const root = [];
660
751
  for (const resource of resolvedResources) if (resource.skipGlobalPrefix) root.push(resource);
661
752
  else prefixed.push(resource);
662
- for (const resource of root) await registerOne(fastify, resource);
753
+ for (const resource of root) await registerOne(fastify, resource, config.mountRoutes);
663
754
  if (prefixed.length) if (config.resourcePrefix) await fastify.register(async (scoped) => {
664
- for (const resource of prefixed) await registerOne(scoped, resource);
755
+ for (const resource of prefixed) await registerOne(scoped, resource, config.mountRoutes);
665
756
  }, { prefix: config.resourcePrefix });
666
- else for (const resource of prefixed) await registerOne(fastify, resource);
757
+ else for (const resource of prefixed) await registerOne(fastify, resource, config.mountRoutes);
667
758
  const names = resolvedResources.map((r) => r.name ?? "?").join(", ");
668
759
  const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
669
760
  fastify.log.info(`${resolvedResources.length} resource(s) registered${prefix}: ${names}`);
@@ -700,42 +791,69 @@ async function registerResources(fastify, config) {
700
791
  });
701
792
  }
702
793
  //#endregion
703
- //#region src/factory/registerSecurity.ts
794
+ //#region src/factory/security/cors.ts
704
795
  /**
705
- * Translate `skipPaths` sugar into a `@fastify/rate-limit` `allowList`
706
- * function. A user-supplied `allowList` (array of IPs or function) is
707
- * preserved and OR-ed with the path match.
796
+ * Headers ARC ITSELF reads from cross-origin requests. Auto-merged into a
797
+ * host-declared CORS `allowedHeaders` allow-list (2.22) so hosts never
798
+ * enumerate framework internals. Lowercase — header names are
799
+ * case-insensitive and @fastify/cors joins them verbatim.
708
800
  */
709
- function buildRateLimitOpts(input) {
710
- const { skipPaths, allowList, ...rest } = input;
711
- if (!skipPaths || skipPaths.length === 0) return allowList === void 0 ? rest : {
712
- ...rest,
713
- allowList
714
- };
715
- const matchesPath = compilePathMatcher(skipPaths);
716
- const combined = async (req, key) => {
717
- const path = (req.url ?? "").split("?", 1)[0] ?? "";
718
- if (matchesPath(path)) return true;
719
- if (typeof allowList === "function") return await allowList(req, key);
720
- if (Array.isArray(allowList)) return allowList.includes(key);
721
- return false;
722
- };
801
+ const ARC_PROTOCOL_HEADERS = [
802
+ "x-organization-id",
803
+ "x-arc-scope",
804
+ "x-request-id"
805
+ ];
806
+ /**
807
+ * Response headers ARC'S OWN auth protocol emits that cross-origin JS MUST
808
+ * be able to read. Better Auth's bearer plugin returns the session token in
809
+ * `set-auth-token` — without exposing it, cross-origin clients (Capacitor /
810
+ * Ionic WebViews at `https://localhost`, cross-domain SPAs) complete the
811
+ * login POST but can never READ the token, so bearer auth silently dies
812
+ * (login appears to succeed, then every request is unauthenticated).
813
+ * Unlike `allowedHeaders`, an UNSET `exposedHeaders` is NOT safe — CORS
814
+ * exposes nothing beyond the safelist by default — so this is always
815
+ * merged, creating the list when the host declared none.
816
+ */
817
+ const ARC_EXPOSED_HEADERS = ["set-auth-token"];
818
+ /**
819
+ * Default `Access-Control-Max-Age` (seconds) when the host didn't set one.
820
+ * Arc's CORS method/header lists are static per deploy, so letting browsers
821
+ * cache the preflight kills the extra OPTIONS round-trip on every request —
822
+ * on high-latency links (mobile webviews) that's a visible chunk of every
823
+ * API call. Browsers clamp internally (Chromium 2h, Firefox 24h).
824
+ */
825
+ const DEFAULT_CORS_MAX_AGE = 86400;
826
+ /**
827
+ * Resolve the host's CORS config into `@fastify/cors` options + warnings.
828
+ * Throws on the one configuration arc refuses to ship (see module header).
829
+ * Callers decide only HOW to surface warnings — the policy is all here.
830
+ */
831
+ function resolveCorsOptions(config) {
832
+ const corsOptions = { ...config.cors ?? {} };
833
+ const warnings = [];
834
+ const originDeclared = "origin" in corsOptions && corsOptions.origin !== void 0;
835
+ if (config.preset === "production" && !originDeclared) warnings.push("CORS origin is not explicitly configured in production. Browser apps: set cors.origin to allowed domains (e.g. ['https://app.example.com']) with credentials: true. Server-to-server / API-key services: cors: { origin: '*', credentials: false } OR cors: false to disable. Tip: when wiring cors.origin from an env var, fail fast on missing (`if (!process.env.ALLOWED_ORIGINS) throw ...`) instead of letting `undefined` slip through.");
836
+ if (corsOptions.credentials === true && corsOptions.origin === "*") throw new Error("[Arc] cors: `origin: '*'` cannot be combined with `credentials: true`.\nReflecting arbitrary origins with credentials lets any website read authenticated responses (CSRF-adjacent data theft).\nFix one of:\n • cors: { origin: ['https://app.example.com'], credentials: true } — explicit allow-list\n • cors: { origin: /\\.example\\.com$/, credentials: true } — pattern allow-list\n • cors: { origin: '*', credentials: false } — public, uncredentialed API");
837
+ if (Array.isArray(corsOptions.allowedHeaders)) {
838
+ const allowed = [...corsOptions.allowedHeaders];
839
+ const declared = new Set(allowed.map((h) => h.toLowerCase()));
840
+ for (const arcHeader of ARC_PROTOCOL_HEADERS) if (!declared.has(arcHeader)) allowed.push(arcHeader);
841
+ corsOptions.allowedHeaders = allowed;
842
+ }
843
+ {
844
+ const exposed = Array.isArray(corsOptions.exposedHeaders) ? [...corsOptions.exposedHeaders] : typeof corsOptions.exposedHeaders === "string" ? corsOptions.exposedHeaders.split(",").map((h) => h.trim()) : [];
845
+ const declared = new Set(exposed.map((h) => h.toLowerCase()));
846
+ for (const arcHeader of ARC_EXPOSED_HEADERS) if (!declared.has(arcHeader)) exposed.push(arcHeader);
847
+ corsOptions.exposedHeaders = exposed;
848
+ }
849
+ if (corsOptions.maxAge === void 0) corsOptions.maxAge = DEFAULT_CORS_MAX_AGE;
723
850
  return {
724
- ...rest,
725
- allowList: combined
726
- };
727
- }
728
- function compilePathMatcher(patterns) {
729
- const prefixes = [];
730
- const exact = /* @__PURE__ */ new Set();
731
- for (const p of patterns) if (p.endsWith("*")) prefixes.push(p.slice(0, -1));
732
- else exact.add(p);
733
- return (path) => {
734
- if (exact.has(path)) return true;
735
- for (const pre of prefixes) if (path.startsWith(pre)) return true;
736
- return false;
851
+ options: corsOptions,
852
+ warnings
737
853
  };
738
854
  }
855
+ //#endregion
856
+ //#region src/factory/security/pluginLoader.ts
739
857
  const PLUGIN_REGISTRY = {
740
858
  cors: {
741
859
  package: "@fastify/cors",
@@ -784,6 +902,77 @@ async function loadPlugin(name, logger) {
784
902
  throw new Error(`Failed to load plugin '${name}': ${err.message}`);
785
903
  }
786
904
  }
905
+ //#endregion
906
+ //#region src/factory/security/rateLimit.ts
907
+ /** "Unlimited" as a number — @fastify/rate-limit's `max` fn must return one. */
908
+ const UNLIMITED_MAX = Number.MAX_SAFE_INTEGER;
909
+ /**
910
+ * Expand `plan` sugar (2.22) into `@fastify/rate-limit` primitives: a
911
+ * per-request async `max` that resolves the caller's plan once (cached
912
+ * per request), and — unless the host supplied one — the tenant
913
+ * keyGenerator so buckets follow actors, not IPs. Unknown plans and
914
+ * resolver errors fall back to `plan.default`, then the global `max`
915
+ * (fail-safe: never fail-open to unlimited).
916
+ */
917
+ function buildPlanOpts(rest, plan) {
918
+ const globalMax = typeof rest.max === "number" ? rest.max : 100;
919
+ const fallbackLimit = plan.default !== void 0 ? plan.limits[plan.default] ?? { max: globalMax } : { max: globalMax };
920
+ const planCache = /* @__PURE__ */ new WeakMap();
921
+ const limitOf = (req) => {
922
+ let cached = planCache.get(req);
923
+ if (!cached) {
924
+ cached = Promise.resolve().then(() => plan.resolve(req)).then((name) => name !== void 0 ? plan.limits[name] ?? fallbackLimit : fallbackLimit).catch(() => fallbackLimit);
925
+ planCache.set(req, cached);
926
+ }
927
+ return cached;
928
+ };
929
+ return {
930
+ ...rest,
931
+ max: async (req, _key) => {
932
+ const limit = await limitOf(req);
933
+ return limit === false ? UNLIMITED_MAX : limit.max;
934
+ },
935
+ keyGenerator: rest.keyGenerator ?? createTenantKeyGenerator()
936
+ };
937
+ }
938
+ /**
939
+ * Translate `skipPaths` sugar into a `@fastify/rate-limit` `allowList`
940
+ * function. A user-supplied `allowList` (array of IPs or function) is
941
+ * preserved and OR-ed with the path match.
942
+ */
943
+ function buildRateLimitOpts(input) {
944
+ const { skipPaths, allowList, plan, ...bare } = input;
945
+ const rest = plan ? buildPlanOpts(bare, plan) : bare;
946
+ if (!skipPaths || skipPaths.length === 0) return allowList === void 0 ? rest : {
947
+ ...rest,
948
+ allowList
949
+ };
950
+ const matchesPath = compilePathMatcher(skipPaths);
951
+ const combined = async (req, key) => {
952
+ const path = (req.url ?? "").split("?", 1)[0] ?? "";
953
+ if (matchesPath(path)) return true;
954
+ if (typeof allowList === "function") return await allowList(req, key);
955
+ if (Array.isArray(allowList)) return allowList.includes(key);
956
+ return false;
957
+ };
958
+ return {
959
+ ...rest,
960
+ allowList: combined
961
+ };
962
+ }
963
+ function compilePathMatcher(patterns) {
964
+ const prefixes = [];
965
+ const exact = /* @__PURE__ */ new Set();
966
+ for (const p of patterns) if (p.endsWith("*")) prefixes.push(p.slice(0, -1));
967
+ else exact.add(p);
968
+ return (path) => {
969
+ if (exact.has(path)) return true;
970
+ for (const pre of prefixes) if (path.startsWith(pre)) return true;
971
+ return false;
972
+ };
973
+ }
974
+ //#endregion
975
+ //#region src/factory/registerSecurity.ts
787
976
  /**
788
977
  * Register security plugins (Helmet, CORS, Rate Limiting).
789
978
  * All enabled by default — set to `false` to opt out.
@@ -796,10 +985,8 @@ async function registerSecurityPlugins(fastify, config) {
796
985
  } else fastify.log.warn("Helmet disabled - security headers not applied");
797
986
  if (config.cors !== false) {
798
987
  const cors = await loadPlugin("cors");
799
- const corsOptions = { ...config.cors ?? {} };
800
- const originDeclared = "origin" in corsOptions && corsOptions.origin !== void 0;
801
- if (config.preset === "production" && !originDeclared) fastify.log.warn("CORS origin is not explicitly configured in production. Browser apps: set cors.origin to allowed domains (e.g. ['https://app.example.com']) with credentials: true. Server-to-server / API-key services: cors: { origin: '*', credentials: false } OR cors: false to disable. Tip: when wiring cors.origin from an env var, fail fast on missing (`if (!process.env.ALLOWED_ORIGINS) throw ...`) instead of letting `undefined` slip through.");
802
- if (corsOptions.credentials && corsOptions.origin === "*") corsOptions.origin = true;
988
+ const { options: corsOptions, warnings } = resolveCorsOptions(config);
989
+ for (const warning of warnings) fastify.log.warn(warning);
803
990
  await fastify.register(cors, corsOptions);
804
991
  fastify.log.debug("CORS enabled");
805
992
  } else fastify.log.warn("CORS disabled");
@@ -835,13 +1022,17 @@ async function registerUtilityPlugins(fastify, config) {
835
1022
  if (config.multipart !== false) {
836
1023
  const multipart = await loadPlugin("multipart", fastify.log);
837
1024
  if (multipart) {
1025
+ const hostMultipart = typeof config.multipart === "object" && config.multipart !== null ? config.multipart : {};
838
1026
  await fastify.register(multipart, {
1027
+ throwFileSizeLimit: true,
1028
+ ...hostMultipart,
839
1029
  limits: {
840
1030
  fileSize: 10 * 1024 * 1024,
841
- files: 10
842
- },
843
- throwFileSizeLimit: true,
844
- ...config.multipart
1031
+ files: 10,
1032
+ fields: 100,
1033
+ parts: 120,
1034
+ ...hostMultipart.limits
1035
+ }
845
1036
  });
846
1037
  fastify.log.debug("Multipart (file uploads) enabled");
847
1038
  }
@@ -1006,7 +1197,7 @@ Missing:\n${lines}\n\nFix: pass durable adapters under \`stores\`, or use runtim
1006
1197
  *
1007
1198
  * Boot order:
1008
1199
  * ```
1009
- * 0. Logger, validation, preset merge
1200
+ * 0. Logger, validation → beforeBoot() (DB connect etc.), preset merge
1010
1201
  * 1. Create Fastify instance
1011
1202
  * 2. Security plugins (Helmet, CORS, Rate Limit) — opt-out
1012
1203
  * 3. Utility plugins (Under Pressure, Sensible, Multipart, Raw Body)
@@ -1021,21 +1212,26 @@ Missing:\n${lines}\n\nFix: pass durable adapters under \`stores\`, or use runtim
1021
1212
  * ```
1022
1213
  */
1023
1214
  async function createApp(options) {
1024
- if (options.debug !== void 0 && options.debug !== false) {
1025
- const { configureArcLogger } = await import("./logger/index.mjs");
1026
- configureArcLogger({ debug: options.debug });
1027
- }
1215
+ if (options.debug !== void 0 && options.debug !== false) configureArcLogger({ debug: options.debug });
1028
1216
  validateAuthOptions(options);
1029
1217
  const deferredWarnings = validateDistributedRuntime(options);
1218
+ if (options.beforeBoot) await options.beforeBoot();
1030
1219
  const config = {
1031
1220
  ...options.preset ? getPreset(options.preset) : {},
1032
1221
  ...options
1033
1222
  };
1223
+ const resolvedModules = orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
1224
+ const moduleErrorMappers = resolvedModules.flatMap((m) => m.errorMappers ?? []);
1225
+ const resolvedLogger = resolveLoggerConfig(config.logger);
1034
1226
  const fastify = Fastify({
1035
- logger: resolveLoggerConfig(config.logger),
1227
+ logger: resolvedLogger,
1228
+ genReqId: createRequestIdGenerator(),
1036
1229
  trustProxy: config.trustProxy ?? false,
1037
1230
  pluginTimeout: config.pluginTimeout ?? 1e4,
1038
1231
  ...config.bodyLimit !== void 0 ? { bodyLimit: config.bodyLimit } : {},
1232
+ ...config.requestTimeout !== void 0 ? { requestTimeout: config.requestTimeout } : {},
1233
+ ...config.connectionTimeout !== void 0 ? { connectionTimeout: config.connectionTimeout } : {},
1234
+ ...config.keepAliveTimeout !== void 0 ? { keepAliveTimeout: config.keepAliveTimeout } : {},
1039
1235
  allowErrorHandlerOverride: true,
1040
1236
  routerOptions: {
1041
1237
  querystringParser: (str) => qs.parse(str, {
@@ -1054,6 +1250,10 @@ async function createApp(options) {
1054
1250
  keywords: ["example", ...config.ajv?.keywords ?? []]
1055
1251
  } }
1056
1252
  });
1253
+ if (resolvedLogger !== false) configureArcLogger({
1254
+ ...options.debug !== void 0 && options.debug !== false ? { debug: options.debug } : {},
1255
+ writer: createPinoWriter(fastify.log)
1256
+ });
1057
1257
  for (const warning of deferredWarnings) fastify.log.warn(warning);
1058
1258
  if (config.typeProvider === "typebox") try {
1059
1259
  const { TypeBoxValidatorCompiler } = await import("@fastify/type-provider-typebox");
@@ -1061,12 +1261,10 @@ async function createApp(options) {
1061
1261
  } catch {
1062
1262
  fastify.log.warn("typeProvider: \"typebox\" requested but @fastify/type-provider-typebox is not installed.");
1063
1263
  }
1064
- const sjp = await import("secure-json-parse");
1065
1264
  fastify.removeContentTypeParser("application/json");
1066
1265
  fastify.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
1067
- if (!body || body.length === 0) return done(null, void 0);
1068
1266
  try {
1069
- done(null, sjp.parse(body));
1267
+ done(null, parseJsonBody(body));
1070
1268
  } catch (err) {
1071
1269
  done(err);
1072
1270
  }
@@ -1086,8 +1284,8 @@ async function createApp(options) {
1086
1284
  decorateRequestScope(fastify);
1087
1285
  await registerAuth(fastify, config, trackPlugin);
1088
1286
  await registerElevation(fastify, config, trackPlugin);
1089
- await registerErrorHandler(fastify, config, trackPlugin);
1090
- await registerResources(fastify, config);
1287
+ await registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers);
1288
+ await registerResources(fastify, config, resolvedModules);
1091
1289
  if (config.replyHelpers) {
1092
1290
  const { replyHelpersPlugin } = await import("./replyHelpers-67tRawQZ.mjs").then((n) => n.n);
1093
1291
  await fastify.register(replyHelpersPlugin);
@@ -1140,4 +1338,4 @@ const ArcFactory = {
1140
1338
  }
1141
1339
  };
1142
1340
  //#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 };
1341
+ export { edgePreset as a, testingPreset as c, getModuleExports as d, orderModules as f, preloadResourcesAsync as h, developmentPreset as i, workerPreset as l, preloadResources as m, createApp as n, getPreset as o, resolveModule as p, createApp_exports as r, productionPreset as s, ArcFactory as t, defineModule as u };