@classytic/arc 2.21.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 (94) hide show
  1. package/dist/{BaseController-BnLYgE9A.mjs → BaseController-C6FlRWxF.mjs} +1 -1
  2. package/dist/audit/index.d.mts +2 -2
  3. package/dist/auth/index.d.mts +30 -2
  4. package/dist/auth/index.mjs +5 -2
  5. package/dist/cli/commands/describe.d.mts +1 -1
  6. package/dist/cli/commands/docs.mjs +1 -1
  7. package/dist/cli/commands/init.mjs +1 -1
  8. package/dist/cli/index.mjs +1 -1
  9. package/dist/core/index.d.mts +2 -2
  10. package/dist/core/index.mjs +2 -2
  11. package/dist/{core-DZ6Xe1jR.mjs → core-b6fgFgnd.mjs} +123 -66
  12. package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-h1cEEaEX.mjs} +9 -5
  13. package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-B1C0kvTL.mjs} +3 -3
  14. package/dist/{createApp-Bi61XxqU.mjs → createApp-Ci7yG5gv.mjs} +242 -56
  15. package/dist/docs/index.d.mts +1 -1
  16. package/dist/docs/index.mjs +1 -1
  17. package/dist/encryption/index.mjs +9 -5
  18. package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-BTzXSwIg.mjs} +24 -3
  19. package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
  20. package/dist/factory/index.d.mts +29 -8
  21. package/dist/factory/index.mjs +34 -3
  22. package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-9_fuetB4.d.mts} +2 -2
  23. package/dist/fields-D3lhgzhD.d.mts +121 -0
  24. package/dist/health-ScyjIVo5.mjs +202 -0
  25. package/dist/hooks/index.d.mts +1 -1
  26. package/dist/idempotency/index.mjs +2 -0
  27. package/dist/{types-DpxoNX5v.d.mts → index-BEu4wjbL.d.mts} +187 -66
  28. package/dist/{index-B5N-NaHs.d.mts → index-D8MKYGDa.d.mts} +3 -3
  29. package/dist/{index-Dtrj6XZk.d.mts → index-DbtgCOCj.d.mts} +1287 -1225
  30. package/dist/{index-B5PoNbfu.d.mts → index-ogIHvOjo.d.mts} +1 -1
  31. package/dist/index.d.mts +6 -5
  32. package/dist/index.mjs +3 -3
  33. package/dist/{init-0YhHkXGk.mjs → init-DleK3p9f.mjs} +118 -2
  34. package/dist/integrations/index.d.mts +1 -1
  35. package/dist/integrations/mcp/index.d.mts +3 -3
  36. package/dist/integrations/mcp/index.mjs +1 -1
  37. package/dist/integrations/mcp/testing.d.mts +1 -1
  38. package/dist/integrations/mcp/testing.mjs +1 -1
  39. package/dist/integrations/webhooks.d.mts +8 -4
  40. package/dist/integrations/webhooks.mjs +9 -5
  41. package/dist/jsonBody-BlzAoGCe.mjs +43 -0
  42. package/dist/logger/index.d.mts +26 -1
  43. package/dist/logger/index.mjs +39 -1
  44. package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-ByA2tf6D.mjs} +6 -2
  45. package/dist/middleware/index.d.mts +1 -1
  46. package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
  47. package/dist/{openapi-Kyod_di6.mjs → openapi-BRpMZD7C.mjs} +2 -2
  48. package/dist/permissions/index.d.mts +3 -2
  49. package/dist/pipeline/index.d.mts +1 -1
  50. package/dist/plugins/index.d.mts +22 -5
  51. package/dist/plugins/index.mjs +11 -280
  52. package/dist/plugins/response-cache.mjs +1 -0
  53. package/dist/plugins/tracing-entry.mjs +7 -4
  54. package/dist/presets/filesUpload.d.mts +2 -2
  55. package/dist/presets/index.d.mts +1 -1
  56. package/dist/presets/multiTenant.d.mts +1 -1
  57. package/dist/presets/search.d.mts +2 -2
  58. package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
  59. package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-xHc42fwB.mjs} +37 -17
  60. package/dist/registry/index.d.mts +2 -2
  61. package/dist/requestId-5BhTVzBv.mjs +113 -0
  62. package/dist/schemaConverter-Ds5reSGs.mjs +168 -0
  63. package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-B-6SPe7g.mjs} +2 -2
  64. package/dist/scim/index.mjs +6 -7
  65. package/dist/scope/index.mjs +2 -76
  66. package/dist/scope-k15RXfcq.mjs +64 -0
  67. package/dist/testing/index.d.mts +83 -3
  68. package/dist/testing/index.mjs +70 -3
  69. package/dist/types/index.d.mts +2 -2
  70. package/dist/{types-DRj-Ut4n.d.mts → types-Bc42CUyk.d.mts} +1 -1
  71. package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
  72. package/dist/usage/index.d.mts +142 -0
  73. package/dist/usage/index.mjs +0 -0
  74. package/dist/utils/index.d.mts +40 -18
  75. package/dist/utils/index.mjs +2 -2
  76. package/dist/{utils-CO8a3B_1.mjs → utils-FhJ88TJE.mjs} +59 -13
  77. package/dist/{versioning-DQ4mCvg0.d.mts → versioning-DLwklPlG.d.mts} +0 -5
  78. package/llms.txt +39 -0
  79. package/package.json +453 -447
  80. package/skills/arc/SKILL.md +14 -8
  81. package/skills/arc/references/api-reference.md +34 -0
  82. package/skills/arc/references/auth.md +21 -4
  83. package/skills/arc/references/mcp.md +1 -1
  84. package/skills/arc/references/production.md +44 -2
  85. package/skills/arc-code-review/SKILL.md +1 -1
  86. package/skills/arc-code-review/references/anti-patterns.md +71 -18
  87. package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
  88. package/skills/arc-code-review/references/migration-recipes.md +7 -7
  89. package/skills/arc-code-review/references/mongokit-migration.md +4 -3
  90. package/skills/arc-module-publishing/SKILL.md +2 -1
  91. package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
  92. package/skills/arc-module-publishing/references/package-manifest.md +4 -2
  93. package/dist/preloadResources-CHw3_oA_.mjs +0 -45
  94. package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
@@ -1,8 +1,55 @@
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
6
53
  //#region src/factory/module.ts
7
54
  /**
8
55
  * Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
@@ -310,8 +357,33 @@ const edgePreset = {
310
357
  /**
311
358
  * Get preset by name
312
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
+ };
313
384
  function getPreset(name) {
314
385
  switch (name) {
386
+ case "worker": return workerPreset;
315
387
  case "production": return productionPreset;
316
388
  case "development": return developmentPreset;
317
389
  case "testing": return testingPreset;
@@ -424,7 +496,7 @@ async function registerAuth(fastify, config, trackPlugin) {
424
496
  }
425
497
  switch (authConfig.type) {
426
498
  case "betterAuth": {
427
- const { plugin, openapi } = authConfig.betterAuth;
499
+ const { plugin, openapi } = typeof authConfig.betterAuth === "function" ? await authConfig.betterAuth() : authConfig.betterAuth;
428
500
  await fastify.register(plugin);
429
501
  trackPlugin("auth-better-auth");
430
502
  const arc = fastify.arc;
@@ -480,7 +552,7 @@ async function registerElevation(fastify, config, trackPlugin) {
480
552
  */
481
553
  async function registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers) {
482
554
  if (config.errorHandler === false) return;
483
- const { errorHandlerPlugin } = await import("./errorHandler-Dxf6P_kV.mjs").then((n) => n.r);
555
+ const { errorHandlerPlugin } = await import("./errorHandler-BTzXSwIg.mjs").then((n) => n.r);
484
556
  let errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
485
557
  if (moduleErrorMappers && moduleErrorMappers.length > 0) errorOpts = {
486
558
  ...errorOpts,
@@ -526,10 +598,10 @@ function isResourcesFactory(value) {
526
598
  return typeof value === "function";
527
599
  }
528
600
  /** Register a single resource with descriptive error on failure. */
529
- async function registerOne(parent, resource) {
601
+ async function registerOne(parent, resource, mountRoutes) {
530
602
  const name = resource.name ?? "unknown";
531
603
  try {
532
- await parent.register(resource.toPlugin());
604
+ await parent.register(resource.toPlugin(), mountRoutes === false ? { arcMountRoutes: false } : {});
533
605
  } catch (err) {
534
606
  const rawMsg = err instanceof Error ? err.message : String(err);
535
607
  const stripped = rawMsg.replace(new RegExp(`^Resource "${name}"\\s*`), "").replace(/\.+\s*$/, "");
@@ -640,6 +712,16 @@ async function registerResources(fastify, config, preResolvedModules) {
640
712
  }
641
713
  resolvedResources = discovered;
642
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
+ }
643
725
  if (modules.length) {
644
726
  const moduleResources = [];
645
727
  for (const m of modules) {
@@ -668,11 +750,11 @@ async function registerResources(fastify, config, preResolvedModules) {
668
750
  const root = [];
669
751
  for (const resource of resolvedResources) if (resource.skipGlobalPrefix) root.push(resource);
670
752
  else prefixed.push(resource);
671
- for (const resource of root) await registerOne(fastify, resource);
753
+ for (const resource of root) await registerOne(fastify, resource, config.mountRoutes);
672
754
  if (prefixed.length) if (config.resourcePrefix) await fastify.register(async (scoped) => {
673
- for (const resource of prefixed) await registerOne(scoped, resource);
755
+ for (const resource of prefixed) await registerOne(scoped, resource, config.mountRoutes);
674
756
  }, { prefix: config.resourcePrefix });
675
- else for (const resource of prefixed) await registerOne(fastify, resource);
757
+ else for (const resource of prefixed) await registerOne(fastify, resource, config.mountRoutes);
676
758
  const names = resolvedResources.map((r) => r.name ?? "?").join(", ");
677
759
  const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
678
760
  fastify.log.info(`${resolvedResources.length} resource(s) registered${prefix}: ${names}`);
@@ -709,42 +791,69 @@ async function registerResources(fastify, config, preResolvedModules) {
709
791
  });
710
792
  }
711
793
  //#endregion
712
- //#region src/factory/registerSecurity.ts
794
+ //#region src/factory/security/cors.ts
713
795
  /**
714
- * Translate `skipPaths` sugar into a `@fastify/rate-limit` `allowList`
715
- * function. A user-supplied `allowList` (array of IPs or function) is
716
- * 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.
717
800
  */
718
- function buildRateLimitOpts(input) {
719
- const { skipPaths, allowList, ...rest } = input;
720
- if (!skipPaths || skipPaths.length === 0) return allowList === void 0 ? rest : {
721
- ...rest,
722
- allowList
723
- };
724
- const matchesPath = compilePathMatcher(skipPaths);
725
- const combined = async (req, key) => {
726
- const path = (req.url ?? "").split("?", 1)[0] ?? "";
727
- if (matchesPath(path)) return true;
728
- if (typeof allowList === "function") return await allowList(req, key);
729
- if (Array.isArray(allowList)) return allowList.includes(key);
730
- return false;
731
- };
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;
732
850
  return {
733
- ...rest,
734
- allowList: combined
735
- };
736
- }
737
- function compilePathMatcher(patterns) {
738
- const prefixes = [];
739
- const exact = /* @__PURE__ */ new Set();
740
- for (const p of patterns) if (p.endsWith("*")) prefixes.push(p.slice(0, -1));
741
- else exact.add(p);
742
- return (path) => {
743
- if (exact.has(path)) return true;
744
- for (const pre of prefixes) if (path.startsWith(pre)) return true;
745
- return false;
851
+ options: corsOptions,
852
+ warnings
746
853
  };
747
854
  }
855
+ //#endregion
856
+ //#region src/factory/security/pluginLoader.ts
748
857
  const PLUGIN_REGISTRY = {
749
858
  cors: {
750
859
  package: "@fastify/cors",
@@ -793,6 +902,77 @@ async function loadPlugin(name, logger) {
793
902
  throw new Error(`Failed to load plugin '${name}': ${err.message}`);
794
903
  }
795
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
796
976
  /**
797
977
  * Register security plugins (Helmet, CORS, Rate Limiting).
798
978
  * All enabled by default — set to `false` to opt out.
@@ -805,10 +985,8 @@ async function registerSecurityPlugins(fastify, config) {
805
985
  } else fastify.log.warn("Helmet disabled - security headers not applied");
806
986
  if (config.cors !== false) {
807
987
  const cors = await loadPlugin("cors");
808
- const corsOptions = { ...config.cors ?? {} };
809
- const originDeclared = "origin" in corsOptions && corsOptions.origin !== void 0;
810
- 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.");
811
- 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);
812
990
  await fastify.register(cors, corsOptions);
813
991
  fastify.log.debug("CORS enabled");
814
992
  } else fastify.log.warn("CORS disabled");
@@ -844,13 +1022,17 @@ async function registerUtilityPlugins(fastify, config) {
844
1022
  if (config.multipart !== false) {
845
1023
  const multipart = await loadPlugin("multipart", fastify.log);
846
1024
  if (multipart) {
1025
+ const hostMultipart = typeof config.multipart === "object" && config.multipart !== null ? config.multipart : {};
847
1026
  await fastify.register(multipart, {
1027
+ throwFileSizeLimit: true,
1028
+ ...hostMultipart,
848
1029
  limits: {
849
1030
  fileSize: 10 * 1024 * 1024,
850
- files: 10
851
- },
852
- throwFileSizeLimit: true,
853
- ...config.multipart
1031
+ files: 10,
1032
+ fields: 100,
1033
+ parts: 120,
1034
+ ...hostMultipart.limits
1035
+ }
854
1036
  });
855
1037
  fastify.log.debug("Multipart (file uploads) enabled");
856
1038
  }
@@ -1030,10 +1212,7 @@ Missing:\n${lines}\n\nFix: pass durable adapters under \`stores\`, or use runtim
1030
1212
  * ```
1031
1213
  */
1032
1214
  async function createApp(options) {
1033
- if (options.debug !== void 0 && options.debug !== false) {
1034
- const { configureArcLogger } = await import("./logger/index.mjs");
1035
- configureArcLogger({ debug: options.debug });
1036
- }
1215
+ if (options.debug !== void 0 && options.debug !== false) configureArcLogger({ debug: options.debug });
1037
1216
  validateAuthOptions(options);
1038
1217
  const deferredWarnings = validateDistributedRuntime(options);
1039
1218
  if (options.beforeBoot) await options.beforeBoot();
@@ -1043,11 +1222,16 @@ async function createApp(options) {
1043
1222
  };
1044
1223
  const resolvedModules = orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
1045
1224
  const moduleErrorMappers = resolvedModules.flatMap((m) => m.errorMappers ?? []);
1225
+ const resolvedLogger = resolveLoggerConfig(config.logger);
1046
1226
  const fastify = Fastify({
1047
- logger: resolveLoggerConfig(config.logger),
1227
+ logger: resolvedLogger,
1228
+ genReqId: createRequestIdGenerator(),
1048
1229
  trustProxy: config.trustProxy ?? false,
1049
1230
  pluginTimeout: config.pluginTimeout ?? 1e4,
1050
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 } : {},
1051
1235
  allowErrorHandlerOverride: true,
1052
1236
  routerOptions: {
1053
1237
  querystringParser: (str) => qs.parse(str, {
@@ -1066,6 +1250,10 @@ async function createApp(options) {
1066
1250
  keywords: ["example", ...config.ajv?.keywords ?? []]
1067
1251
  } }
1068
1252
  });
1253
+ if (resolvedLogger !== false) configureArcLogger({
1254
+ ...options.debug !== void 0 && options.debug !== false ? { debug: options.debug } : {},
1255
+ writer: createPinoWriter(fastify.log)
1256
+ });
1069
1257
  for (const warning of deferredWarnings) fastify.log.warn(warning);
1070
1258
  if (config.typeProvider === "typebox") try {
1071
1259
  const { TypeBoxValidatorCompiler } = await import("@fastify/type-provider-typebox");
@@ -1073,12 +1261,10 @@ async function createApp(options) {
1073
1261
  } catch {
1074
1262
  fastify.log.warn("typeProvider: \"typebox\" requested but @fastify/type-provider-typebox is not installed.");
1075
1263
  }
1076
- const sjp = await import("secure-json-parse");
1077
1264
  fastify.removeContentTypeParser("application/json");
1078
1265
  fastify.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
1079
- if (!body || body.length === 0) return done(null, void 0);
1080
1266
  try {
1081
- done(null, sjp.parse(body));
1267
+ done(null, parseJsonBody(body));
1082
1268
  } catch (err) {
1083
1269
  done(err);
1084
1270
  }
@@ -1152,4 +1338,4 @@ const ArcFactory = {
1152
1338
  }
1153
1339
  };
1154
1340
  //#endregion
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 };
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 };
@@ -1,4 +1,4 @@
1
- import { p as RegistryEntry } from "../index-Dtrj6XZk.mjs";
1
+ import { p as RegistryEntry } from "../index-DbtgCOCj.mjs";
2
2
  import { t as ExternalOpenApiPaths } from "../externalPaths-BE6wFpYv.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
  //#region src/docs/openapi/types.d.ts
@@ -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-Kyod_di6.mjs";
2
+ import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-BRpMZD7C.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/docs/scalar.ts
5
5
  const scalarPlugin = async (fastify, opts = {}) => {
@@ -1,3 +1,5 @@
1
+ import { t as ArcError } from "../errors-DkO8Nk6-.mjs";
2
+ import { t as parseJsonBody } from "../jsonBody-BlzAoGCe.mjs";
1
3
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
4
  import fp from "fastify-plugin";
3
5
  //#region src/encryption/fieldCipher.ts
@@ -182,17 +184,19 @@ const encryptionPlugin = async (fastify, opts) => {
182
184
  }
183
185
  if (decryptRequests) fastify.addContentTypeParser(contentType, { parseAs: "string" }, async (request, body) => {
184
186
  const provider = keyProvider;
185
- const plaintext = await jweDecrypt(body, (kid) => provider.decryptionKey(kid, { request }), allowedAlgs, allowedEncs);
186
- request.requestDecrypted = true;
187
- if (plaintext.length === 0) return void 0;
187
+ let plaintext;
188
188
  try {
189
- return JSON.parse(plaintext);
189
+ plaintext = await jweDecrypt(body, (kid) => provider.decryptionKey(kid, { request }), allowedAlgs, allowedEncs);
190
190
  } catch (cause) {
191
- throw Object.assign(/* @__PURE__ */ new Error("[arc/encryption] decrypted request body is not valid JSON"), {
191
+ throw new ArcError("Invalid encrypted request body", {
192
+ code: "arc.bad_request",
192
193
  statusCode: 400,
193
194
  cause
194
195
  });
195
196
  }
197
+ request.requestDecrypted = true;
198
+ if (plaintext.length === 0) return void 0;
199
+ return parseJsonBody(plaintext, "Decrypted request body is not valid JSON");
196
200
  });
197
201
  fastify.addHook("preSerialization", async (request, reply, payload) => {
198
202
  const directive = resolveDirective(request, defaultMode, fallback);
@@ -91,7 +91,7 @@ function mongooseValidationDetails(errors) {
91
91
  }
92
92
  async function errorHandlerPluginFn(fastify, options = {}) {
93
93
  const isProduction = process.env.NODE_ENV === "production";
94
- const { includeStack = !isProduction, onError, errorMap = {}, errorMappers = [], isDuplicateKeyError = defaultIsDuplicateKeyError } = options;
94
+ const { includeStack = !isProduction, exposeInternalMessages = !isProduction, onError, errorMap = {}, errorMappers = [], isDuplicateKeyError = defaultIsDuplicateKeyError } = options;
95
95
  fastify.setErrorHandler(async (error, request, reply) => {
96
96
  if (onError) try {
97
97
  await onError(error, request);
@@ -112,6 +112,7 @@ async function errorHandlerPluginFn(fastify, options = {}) {
112
112
  ...Object.keys(meta).length > 0 ? { meta } : {}
113
113
  };
114
114
  const status = wire.status ?? 500;
115
+ if (status >= 500 && wire.code === "arc.internal_error" && !exposeInternalMessages) wire.message = "Internal Server Error";
115
116
  if (status >= 500) request.log.error({
116
117
  err: error,
117
118
  status
@@ -120,11 +121,31 @@ async function errorHandlerPluginFn(fastify, options = {}) {
120
121
  err: error,
121
122
  status
122
123
  }, "Client error");
123
- if (isArcError(error) && error.cause) request.log.error({ cause: error.cause }, "Error cause chain");
124
+ if (isArcError(error) && error.cause) {
125
+ const causeLevel = status >= 500 ? "error" : "warn";
126
+ request.log[causeLevel]({ cause: error.cause }, "Error cause chain");
127
+ }
124
128
  return reply.status(status).send(wire);
125
129
  });
126
130
  }
127
131
  /**
132
+ * Is `code` a caller-defined DOMAIN error code that is safe to surface on the
133
+ * wire, as opposed to a framework/runtime code that must never leak?
134
+ *
135
+ * Rejects Fastify internals (`FST_*`) and Node system errnos (`ECONNREFUSED`,
136
+ * `ENOENT`, …). Domain codes in the wild use separators (`PURCHASE_NOT_FOUND`,
137
+ * `access.forbidden`) so they pass; a bare all-caps single word that collides
138
+ * with the Node-errno shape (e.g. `EXPIRED`) is conservatively treated as a
139
+ * runtime code — use a separatored code (`TOKEN_EXPIRED`) to surface it, or
140
+ * throw via `createDomainError` which bypasses this heuristic entirely.
141
+ */
142
+ function isDomainErrorCode(code) {
143
+ if (typeof code !== "string" || code.length === 0) return false;
144
+ if (code.startsWith("FST_")) return false;
145
+ if (/^E[A-Z]{2,}$/.test(code)) return false;
146
+ return true;
147
+ }
148
+ /**
128
149
  * Single-pass error → `ErrorContract` classifier. Dispatch order is fixed:
129
150
  *
130
151
  * 1. Class-based mappers (`instanceof`) — user-registered domain errors
@@ -167,7 +188,7 @@ function classify(error, ctx) {
167
188
  };
168
189
  }
169
190
  if (typeof fastifyErr.statusCode === "number") return {
170
- code: statusToArcCode(fastifyErr.statusCode),
191
+ code: (isDomainErrorCode(fastifyErr.code) ? fastifyErr.code : void 0) ?? statusToArcCode(fastifyErr.statusCode),
171
192
  message: error.message || "Error",
172
193
  status: fastifyErr.statusCode
173
194
  };
@@ -18,6 +18,19 @@ interface ErrorMapper<T extends Error = Error> {
18
18
  interface ErrorHandlerOptions {
19
19
  /** Include `meta.stack` on the wire (defaults to `NODE_ENV !== 'production'`). */
20
20
  includeStack?: boolean;
21
+ /**
22
+ * Ship the raw `error.message` of UNCLASSIFIED 500s on the wire
23
+ * (defaults to `NODE_ENV !== 'production'`).
24
+ *
25
+ * Only the `arc.internal_error` fallback is affected — an unclassified
26
+ * throw carries whatever some driver/library put in the message (connection
27
+ * strings, file paths, SQL fragments), which must not reach clients in
28
+ * production. Deliberate 5xx contracts are untouched: `ArcError` /
29
+ * domain-coded throws (`payment.gateway.timeout`, …) keep their message
30
+ * because the thrower chose it for the client. The raw message is always
31
+ * logged via `request.log.error({ err })` regardless of this flag.
32
+ */
33
+ exposeInternalMessages?: boolean;
21
34
  /** Custom callback fired for every error — log to Sentry / Datadog / etc. */
22
35
  onError?: (error: Error, request: FastifyRequest) => void | Promise<void>;
23
36
  /** Map by `error.name` string. Lower priority than `errorMappers`. */
@@ -1,6 +1,7 @@
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";
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-B4u10iOJ.mjs";
2
+ import "../fastify-augmentation-9_fuetB4.mjs";
3
+ import { a as AuthOption, c as CustomPluginAuthOption, d as preloadResourcesAsync, i as UnderPressureOptions, l as JwtAuthOption, n as MultipartOptions, o as BetterAuthOption, r as RawBodyOptions, s as CustomAuthenticatorOption, t as CreateAppOptions, u as preloadResources } from "../index-BEu4wjbL.mjs";
4
+ import { g as HealthCheck } from "../versioning-DLwklPlG.mjs";
4
5
  import { FastifyInstance, FastifyServerOptions } from "fastify";
5
6
  //#region src/factory/createApp.d.ts
6
7
  /**
@@ -32,6 +33,29 @@ declare const ArcFactory: {
32
33
  testing(options: Omit<CreateAppOptions, "preset">): Promise<FastifyInstance>;
33
34
  };
34
35
  //#endregion
36
+ //#region src/factory/createWorker.d.ts
37
+ interface WorkerHealthOptions {
38
+ /** Port for the worker's ONLY listener (liveness/readiness probes). */
39
+ port: number;
40
+ /** Bind host. Default `0.0.0.0`. */
41
+ host?: string;
42
+ /** Readiness checks — same contract as the API role's healthPlugin. */
43
+ checks?: HealthCheck[];
44
+ }
45
+ interface CreateWorkerOptions {
46
+ /** Opt-in probe listener. Omit = the worker binds NOTHING. */
47
+ health?: WorkerHealthOptions;
48
+ }
49
+ interface ArcWorker {
50
+ /** The underlying Fastify instance (decorations, inject, logs). */
51
+ app: FastifyInstance;
52
+ /** Typed module-export accessor — same contract as `getModuleExports`. */
53
+ exports<TExports = unknown>(name: string): TExports;
54
+ /** Close app → module onClose (reverse) → app onClose. Idempotent. */
55
+ close(): Promise<void>;
56
+ }
57
+ declare function createWorker(options: CreateAppOptions, workerOptions?: CreateWorkerOptions): Promise<ArcWorker>;
58
+ //#endregion
35
59
  //#region src/factory/edge.d.ts
36
60
  interface FetchHandlerOptions {
37
61
  /**
@@ -91,9 +115,6 @@ declare const testingPreset: Partial<CreateAppOptions>;
91
115
  * when using `toFetchHandler()` (routes through Fastify's `.inject()`).
92
116
  */
93
117
  declare const edgePreset: Partial<CreateAppOptions>;
94
- /**
95
- * Get preset by name
96
- */
97
- declare function getPreset(name: "production" | "development" | "testing" | "edge"): Partial<CreateAppOptions>;
118
+ declare function getPreset(name: "production" | "development" | "testing" | "edge" | "worker"): Partial<CreateAppOptions>;
98
119
  //#endregion
99
- export { ArcFactory, type ArcModule, type ArcModuleInput, type ArcModuleRegistry, type AuthOption, type BetterAuthOption, type CreateAppOptions, type CustomAuthenticatorOption, type CustomPluginAuthOption, type FetchHandlerOptions, type JwtAuthOption, type LoadResourcesOptions, type MultipartOptions, type RawBodyOptions, type ResourceLike, type ResourceModule, type UnderPressureOptions, createApp, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };
120
+ export { ArcFactory, type ArcModule, type ArcModuleInput, type ArcModuleRegistry, type ArcWorker, type AuthOption, type BetterAuthOption, type CreateAppOptions, type CreateWorkerOptions, type CustomAuthenticatorOption, type CustomPluginAuthOption, type FetchHandlerOptions, type JwtAuthOption, type LoadResourcesOptions, type MultipartOptions, type RawBodyOptions, type ResourceLike, type ResourceModule, type UnderPressureOptions, type WorkerHealthOptions, createApp, createWorker, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };
@@ -1,6 +1,37 @@
1
- import { n as preloadResourcesAsync, t as preloadResources } from "../preloadResources-CHw3_oA_.mjs";
2
- import { a as edgePreset, c as testingPreset, d as orderModules, f as resolveModule, i as developmentPreset, l as defineModule, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory, u as getModuleExports } from "../createApp-Bi61XxqU.mjs";
1
+ import { a as edgePreset, c as testingPreset, d as getModuleExports, f as orderModules, h as preloadResourcesAsync, i as developmentPreset, l as workerPreset, m as preloadResources, n as createApp, o as getPreset, p as resolveModule, s as productionPreset, t as ArcFactory, u as defineModule } from "../createApp-Ci7yG5gv.mjs";
2
+ import { t as healthPlugin } from "../health-ScyjIVo5.mjs";
3
3
  import { t as loadResources } from "../loadResources-DpU8pfjV.mjs";
4
+ //#region src/factory/createWorker.ts
5
+ async function createWorker(options, workerOptions = {}) {
6
+ const { auth: _auth, helmet: _helmet, cors: _cors, rateLimit: _rateLimit, multipart: _multipart, rawBody: _rawBody, sensible: _sensible, underPressure: _underPressure, replyHelpers: _replyHelpers, ...runtimeOptions } = options;
7
+ const app = await createApp({
8
+ ...runtimeOptions,
9
+ preset: "worker",
10
+ ...runtimeOptions.arcPlugins ? { arcPlugins: {
11
+ ...workerPreset.arcPlugins,
12
+ ...runtimeOptions.arcPlugins
13
+ } } : {}
14
+ });
15
+ if (workerOptions.health) {
16
+ const { port, host = "0.0.0.0", checks = [] } = workerOptions.health;
17
+ await app.register(healthPlugin, { checks });
18
+ await app.listen({
19
+ port,
20
+ host
21
+ });
22
+ } else await app.ready();
23
+ let closed = false;
24
+ return {
25
+ app,
26
+ exports: (name) => getModuleExports(app, name),
27
+ close: async () => {
28
+ if (closed) return;
29
+ closed = true;
30
+ await app.close();
31
+ }
32
+ };
33
+ }
34
+ //#endregion
4
35
  //#region src/factory/edge.ts
5
36
  /**
6
37
  * Convert a Fastify app into a Web Standards fetch handler.
@@ -47,4 +78,4 @@ function toFetchHandler(app, options = {}) {
47
78
  };
48
79
  }
49
80
  //#endregion
50
- export { ArcFactory, createApp, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };
81
+ export { ArcFactory, createApp, createWorker, defineModule, developmentPreset, edgePreset, getModuleExports, getPreset, loadResources, orderModules, preloadResources, preloadResourcesAsync, productionPreset, resolveModule, testingPreset, toFetchHandler };