@classytic/arc 2.21.0 → 2.23.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.
- package/dist/{BaseController-BnLYgE9A.mjs → BaseController-BQ8KNvWC.mjs} +3 -2
- package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
- package/dist/{actionPermissions-ze70xXJi.mjs → actionPermissions-lszoJle1.mjs} +1 -1
- package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
- package/dist/audit/index.d.mts +2 -2
- package/dist/auth/index.d.mts +30 -2
- package/dist/auth/index.mjs +6 -3
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -3
- package/dist/{core-DZ6Xe1jR.mjs → core-DQ6rDTyH.mjs} +126 -68
- package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-BuOv-m4Y.mjs} +10 -6
- package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-CgJv_BU_.mjs} +3 -3
- package/dist/{createApp-Bi61XxqU.mjs → createApp-CjOeWJ9_.mjs} +252 -57
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/encryption/index.mjs +9 -5
- package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-Bq2OeEQL.mjs} +34 -10
- package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
- package/dist/events/index.mjs +5 -2
- package/dist/factory/index.d.mts +46 -9
- package/dist/factory/index.mjs +64 -13
- package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-g_f9omSc.d.mts} +2 -2
- package/dist/fields-D3lhgzhD.d.mts +121 -0
- package/dist/health-ScyjIVo5.mjs +202 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +7 -0
- package/dist/idempotency/index.mjs +7 -17
- package/dist/{index-Dtrj6XZk.d.mts → index-BBsvQt7U.d.mts} +1310 -1226
- package/dist/{types-DpxoNX5v.d.mts → index-Ba10fdKp.d.mts} +196 -66
- package/dist/{index-B5N-NaHs.d.mts → index-CC5ruDGh.d.mts} +3 -3
- package/dist/{index-B5PoNbfu.d.mts → index-wMoWBLkn.d.mts} +68 -2
- package/dist/index.d.mts +6 -5
- package/dist/index.mjs +6 -5
- package/dist/{init-0YhHkXGk.mjs → init-CjO4p_q1.mjs} +123 -4
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +67 -4
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +131 -7
- package/dist/integrations/webhooks.mjs +169 -10
- package/dist/interface-n5hpKg1d.mjs +16 -0
- package/dist/jsonBody-BlzAoGCe.mjs +43 -0
- package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
- package/dist/logger/index.d.mts +26 -1
- package/dist/logger/index.mjs +39 -1
- package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-BtzIL_kM.mjs} +312 -39
- package/dist/middleware/index.d.mts +1 -1
- package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
- package/dist/{openapi-Kyod_di6.mjs → openapi-jSDx-VDv.mjs} +3 -3
- package/dist/permissions/index.d.mts +4 -3
- package/dist/permissions/index.mjs +3 -2
- package/dist/{permissions-BJMuKMl7.mjs → permissions-tbxFHxjZ.mjs} +77 -264
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +24 -7
- package/dist/plugins/index.mjs +14 -282
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +7 -4
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-gw0ciIUD.mjs → presets-BOe4Ugkl.mjs} +1 -1
- package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
- package/dist/realtime-I8OrfnMR.mjs +375 -0
- package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-CWO6mdYe.mjs} +38 -18
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/requestId-5BhTVzBv.mjs +113 -0
- package/dist/schemaConverter-DJvodiHj.mjs +171 -0
- package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-Ca7ySNPx.mjs} +2 -2
- package/dist/scim/index.mjs +6 -7
- package/dist/scope/index.mjs +2 -76
- package/dist/scope-LONQ3mfZ.mjs +37 -0
- package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
- package/dist/{sse-CfUjwjDk.mjs → sse-0u4P4-OI.mjs} +7 -30
- package/dist/sseStream-BQ7Qaynp.mjs +57 -0
- package/dist/testing/index.d.mts +83 -3
- package/dist/testing/index.mjs +70 -3
- package/dist/types/index.d.mts +2 -2
- package/dist/{types-DRj-Ut4n.d.mts → types-5OuqeTDX.d.mts} +37 -2
- package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
- package/dist/usage/index.d.mts +142 -0
- package/dist/usage/index.mjs +0 -0
- package/dist/utils/index.d.mts +101 -19
- package/dist/utils/index.mjs +5 -3
- package/dist/{utils-CO8a3B_1.mjs → utils-CZyeRXTh.mjs} +80 -82
- package/dist/{versioning-DQ4mCvg0.d.mts → versioning-D6PvCwpq.d.mts} +43 -9
- package/llms.txt +39 -0
- package/package.json +513 -447
- package/skills/arc/SKILL.md +14 -8
- package/skills/arc/references/api-reference.md +34 -0
- package/skills/arc/references/auth.md +21 -4
- package/skills/arc/references/mcp.md +1 -1
- package/skills/arc/references/production.md +734 -654
- package/skills/arc-code-review/SKILL.md +1 -1
- package/skills/arc-code-review/references/anti-patterns.md +71 -18
- package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
- package/skills/arc-code-review/references/migration-recipes.md +7 -7
- package/skills/arc-code-review/references/mongokit-migration.md +4 -3
- package/skills/arc-module-publishing/SKILL.md +2 -1
- package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
- package/skills/arc-module-publishing/references/package-manifest.md +4 -2
- package/dist/preloadResources-CHw3_oA_.mjs +0 -45
- 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;
|
|
@@ -384,11 +456,18 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
|
|
|
384
456
|
}
|
|
385
457
|
if (config.arcPlugins?.sse) if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
|
|
386
458
|
else {
|
|
387
|
-
const { default: ssePlugin } = await import("./sse-
|
|
459
|
+
const { default: ssePlugin } = await import("./sse-0u4P4-OI.mjs").then((n) => n.r);
|
|
388
460
|
const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
|
|
389
461
|
await fastify.register(ssePlugin, opts);
|
|
390
462
|
trackPlugin("arc-sse", opts);
|
|
391
463
|
}
|
|
464
|
+
if (config.arcPlugins?.realtime) if (config.arcPlugins?.events === false) fastify.log.warn("Realtime plugin requires events plugin (arcPlugins.events). Realtime disabled.");
|
|
465
|
+
else {
|
|
466
|
+
const { default: realtimePlugin } = await import("./realtime-I8OrfnMR.mjs").then((n) => n.r);
|
|
467
|
+
const opts = config.arcPlugins.realtime === true ? {} : config.arcPlugins.realtime;
|
|
468
|
+
await fastify.register(realtimePlugin, opts);
|
|
469
|
+
trackPlugin("arc-realtime", opts);
|
|
470
|
+
}
|
|
392
471
|
if (config.arcPlugins?.metrics) {
|
|
393
472
|
const { default: metricsPlugin } = await import("./metrics-fdxI5mn_.mjs").then((n) => n.r);
|
|
394
473
|
const opts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
|
|
@@ -424,7 +503,7 @@ async function registerAuth(fastify, config, trackPlugin) {
|
|
|
424
503
|
}
|
|
425
504
|
switch (authConfig.type) {
|
|
426
505
|
case "betterAuth": {
|
|
427
|
-
const { plugin, openapi } = authConfig.betterAuth;
|
|
506
|
+
const { plugin, openapi } = typeof authConfig.betterAuth === "function" ? await authConfig.betterAuth() : authConfig.betterAuth;
|
|
428
507
|
await fastify.register(plugin);
|
|
429
508
|
trackPlugin("auth-better-auth");
|
|
430
509
|
const arc = fastify.arc;
|
|
@@ -480,7 +559,7 @@ async function registerElevation(fastify, config, trackPlugin) {
|
|
|
480
559
|
*/
|
|
481
560
|
async function registerErrorHandler(fastify, config, trackPlugin, moduleErrorMappers) {
|
|
482
561
|
if (config.errorHandler === false) return;
|
|
483
|
-
const { errorHandlerPlugin } = await import("./errorHandler-
|
|
562
|
+
const { errorHandlerPlugin } = await import("./errorHandler-Bq2OeEQL.mjs").then((n) => n.r);
|
|
484
563
|
let errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
|
|
485
564
|
if (moduleErrorMappers && moduleErrorMappers.length > 0) errorOpts = {
|
|
486
565
|
...errorOpts,
|
|
@@ -526,10 +605,10 @@ function isResourcesFactory(value) {
|
|
|
526
605
|
return typeof value === "function";
|
|
527
606
|
}
|
|
528
607
|
/** Register a single resource with descriptive error on failure. */
|
|
529
|
-
async function registerOne(parent, resource) {
|
|
608
|
+
async function registerOne(parent, resource, mountRoutes) {
|
|
530
609
|
const name = resource.name ?? "unknown";
|
|
531
610
|
try {
|
|
532
|
-
await parent.register(resource.toPlugin());
|
|
611
|
+
await parent.register(resource.toPlugin(), mountRoutes === false ? { arcMountRoutes: false } : {});
|
|
533
612
|
} catch (err) {
|
|
534
613
|
const rawMsg = err instanceof Error ? err.message : String(err);
|
|
535
614
|
const stripped = rawMsg.replace(new RegExp(`^Resource "${name}"\\s*`), "").replace(/\.+\s*$/, "");
|
|
@@ -640,6 +719,16 @@ async function registerResources(fastify, config, preResolvedModules) {
|
|
|
640
719
|
}
|
|
641
720
|
resolvedResources = discovered;
|
|
642
721
|
}
|
|
722
|
+
if (modules.length && resolvedResources && resolvedResources.length > 0) {
|
|
723
|
+
const owned = /* @__PURE__ */ new Set();
|
|
724
|
+
for (const m of modules) for (const name of m.owns ?? []) owned.add(name);
|
|
725
|
+
if (owned.size > 0) {
|
|
726
|
+
const before = resolvedResources.length;
|
|
727
|
+
resolvedResources = resolvedResources.filter((r) => !(r.name != null && owned.has(r.name)));
|
|
728
|
+
const dropped = before - resolvedResources.length;
|
|
729
|
+
if (dropped > 0) fastify.log.debug(`[arc] ${dropped} app resource(s) superseded by module owns() — the module version registers instead`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
643
732
|
if (modules.length) {
|
|
644
733
|
const moduleResources = [];
|
|
645
734
|
for (const m of modules) {
|
|
@@ -668,11 +757,11 @@ async function registerResources(fastify, config, preResolvedModules) {
|
|
|
668
757
|
const root = [];
|
|
669
758
|
for (const resource of resolvedResources) if (resource.skipGlobalPrefix) root.push(resource);
|
|
670
759
|
else prefixed.push(resource);
|
|
671
|
-
for (const resource of root) await registerOne(fastify, resource);
|
|
760
|
+
for (const resource of root) await registerOne(fastify, resource, config.mountRoutes);
|
|
672
761
|
if (prefixed.length) if (config.resourcePrefix) await fastify.register(async (scoped) => {
|
|
673
|
-
for (const resource of prefixed) await registerOne(scoped, resource);
|
|
762
|
+
for (const resource of prefixed) await registerOne(scoped, resource, config.mountRoutes);
|
|
674
763
|
}, { prefix: config.resourcePrefix });
|
|
675
|
-
else for (const resource of prefixed) await registerOne(fastify, resource);
|
|
764
|
+
else for (const resource of prefixed) await registerOne(fastify, resource, config.mountRoutes);
|
|
676
765
|
const names = resolvedResources.map((r) => r.name ?? "?").join(", ");
|
|
677
766
|
const prefix = config.resourcePrefix ? ` (prefix: ${config.resourcePrefix})` : "";
|
|
678
767
|
fastify.log.info(`${resolvedResources.length} resource(s) registered${prefix}: ${names}`);
|
|
@@ -709,42 +798,69 @@ async function registerResources(fastify, config, preResolvedModules) {
|
|
|
709
798
|
});
|
|
710
799
|
}
|
|
711
800
|
//#endregion
|
|
712
|
-
//#region src/factory/
|
|
801
|
+
//#region src/factory/security/cors.ts
|
|
713
802
|
/**
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
803
|
+
* Headers ARC ITSELF reads from cross-origin requests. Auto-merged into a
|
|
804
|
+
* host-declared CORS `allowedHeaders` allow-list (2.22) so hosts never
|
|
805
|
+
* enumerate framework internals. Lowercase — header names are
|
|
806
|
+
* case-insensitive and @fastify/cors joins them verbatim.
|
|
717
807
|
*/
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
808
|
+
const ARC_PROTOCOL_HEADERS = [
|
|
809
|
+
"x-organization-id",
|
|
810
|
+
"x-arc-scope",
|
|
811
|
+
"x-request-id"
|
|
812
|
+
];
|
|
813
|
+
/**
|
|
814
|
+
* Response headers ARC'S OWN auth protocol emits that cross-origin JS MUST
|
|
815
|
+
* be able to read. Better Auth's bearer plugin returns the session token in
|
|
816
|
+
* `set-auth-token` — without exposing it, cross-origin clients (Capacitor /
|
|
817
|
+
* Ionic WebViews at `https://localhost`, cross-domain SPAs) complete the
|
|
818
|
+
* login POST but can never READ the token, so bearer auth silently dies
|
|
819
|
+
* (login appears to succeed, then every request is unauthenticated).
|
|
820
|
+
* Unlike `allowedHeaders`, an UNSET `exposedHeaders` is NOT safe — CORS
|
|
821
|
+
* exposes nothing beyond the safelist by default — so this is always
|
|
822
|
+
* merged, creating the list when the host declared none.
|
|
823
|
+
*/
|
|
824
|
+
const ARC_EXPOSED_HEADERS = ["set-auth-token"];
|
|
825
|
+
/**
|
|
826
|
+
* Default `Access-Control-Max-Age` (seconds) when the host didn't set one.
|
|
827
|
+
* Arc's CORS method/header lists are static per deploy, so letting browsers
|
|
828
|
+
* cache the preflight kills the extra OPTIONS round-trip on every request —
|
|
829
|
+
* on high-latency links (mobile webviews) that's a visible chunk of every
|
|
830
|
+
* API call. Browsers clamp internally (Chromium 2h, Firefox 24h).
|
|
831
|
+
*/
|
|
832
|
+
const DEFAULT_CORS_MAX_AGE = 86400;
|
|
833
|
+
/**
|
|
834
|
+
* Resolve the host's CORS config into `@fastify/cors` options + warnings.
|
|
835
|
+
* Throws on the one configuration arc refuses to ship (see module header).
|
|
836
|
+
* Callers decide only HOW to surface warnings — the policy is all here.
|
|
837
|
+
*/
|
|
838
|
+
function resolveCorsOptions(config) {
|
|
839
|
+
const corsOptions = { ...config.cors ?? {} };
|
|
840
|
+
const warnings = [];
|
|
841
|
+
const originDeclared = "origin" in corsOptions && corsOptions.origin !== void 0;
|
|
842
|
+
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.");
|
|
843
|
+
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");
|
|
844
|
+
if (Array.isArray(corsOptions.allowedHeaders)) {
|
|
845
|
+
const allowed = [...corsOptions.allowedHeaders];
|
|
846
|
+
const declared = new Set(allowed.map((h) => h.toLowerCase()));
|
|
847
|
+
for (const arcHeader of ARC_PROTOCOL_HEADERS) if (!declared.has(arcHeader)) allowed.push(arcHeader);
|
|
848
|
+
corsOptions.allowedHeaders = allowed;
|
|
849
|
+
}
|
|
850
|
+
{
|
|
851
|
+
const exposed = Array.isArray(corsOptions.exposedHeaders) ? [...corsOptions.exposedHeaders] : typeof corsOptions.exposedHeaders === "string" ? corsOptions.exposedHeaders.split(",").map((h) => h.trim()) : [];
|
|
852
|
+
const declared = new Set(exposed.map((h) => h.toLowerCase()));
|
|
853
|
+
for (const arcHeader of ARC_EXPOSED_HEADERS) if (!declared.has(arcHeader)) exposed.push(arcHeader);
|
|
854
|
+
corsOptions.exposedHeaders = exposed;
|
|
855
|
+
}
|
|
856
|
+
if (corsOptions.maxAge === void 0) corsOptions.maxAge = DEFAULT_CORS_MAX_AGE;
|
|
732
857
|
return {
|
|
733
|
-
|
|
734
|
-
|
|
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;
|
|
858
|
+
options: corsOptions,
|
|
859
|
+
warnings
|
|
746
860
|
};
|
|
747
861
|
}
|
|
862
|
+
//#endregion
|
|
863
|
+
//#region src/factory/security/pluginLoader.ts
|
|
748
864
|
const PLUGIN_REGISTRY = {
|
|
749
865
|
cors: {
|
|
750
866
|
package: "@fastify/cors",
|
|
@@ -793,6 +909,77 @@ async function loadPlugin(name, logger) {
|
|
|
793
909
|
throw new Error(`Failed to load plugin '${name}': ${err.message}`);
|
|
794
910
|
}
|
|
795
911
|
}
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/factory/security/rateLimit.ts
|
|
914
|
+
/** "Unlimited" as a number — @fastify/rate-limit's `max` fn must return one. */
|
|
915
|
+
const UNLIMITED_MAX = Number.MAX_SAFE_INTEGER;
|
|
916
|
+
/**
|
|
917
|
+
* Expand `plan` sugar (2.22) into `@fastify/rate-limit` primitives: a
|
|
918
|
+
* per-request async `max` that resolves the caller's plan once (cached
|
|
919
|
+
* per request), and — unless the host supplied one — the tenant
|
|
920
|
+
* keyGenerator so buckets follow actors, not IPs. Unknown plans and
|
|
921
|
+
* resolver errors fall back to `plan.default`, then the global `max`
|
|
922
|
+
* (fail-safe: never fail-open to unlimited).
|
|
923
|
+
*/
|
|
924
|
+
function buildPlanOpts(rest, plan) {
|
|
925
|
+
const globalMax = typeof rest.max === "number" ? rest.max : 100;
|
|
926
|
+
const fallbackLimit = plan.default !== void 0 ? plan.limits[plan.default] ?? { max: globalMax } : { max: globalMax };
|
|
927
|
+
const planCache = /* @__PURE__ */ new WeakMap();
|
|
928
|
+
const limitOf = (req) => {
|
|
929
|
+
let cached = planCache.get(req);
|
|
930
|
+
if (!cached) {
|
|
931
|
+
cached = Promise.resolve().then(() => plan.resolve(req)).then((name) => name !== void 0 ? plan.limits[name] ?? fallbackLimit : fallbackLimit).catch(() => fallbackLimit);
|
|
932
|
+
planCache.set(req, cached);
|
|
933
|
+
}
|
|
934
|
+
return cached;
|
|
935
|
+
};
|
|
936
|
+
return {
|
|
937
|
+
...rest,
|
|
938
|
+
max: async (req, _key) => {
|
|
939
|
+
const limit = await limitOf(req);
|
|
940
|
+
return limit === false ? UNLIMITED_MAX : limit.max;
|
|
941
|
+
},
|
|
942
|
+
keyGenerator: rest.keyGenerator ?? createTenantKeyGenerator()
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Translate `skipPaths` sugar into a `@fastify/rate-limit` `allowList`
|
|
947
|
+
* function. A user-supplied `allowList` (array of IPs or function) is
|
|
948
|
+
* preserved and OR-ed with the path match.
|
|
949
|
+
*/
|
|
950
|
+
function buildRateLimitOpts(input) {
|
|
951
|
+
const { skipPaths, allowList, plan, ...bare } = input;
|
|
952
|
+
const rest = plan ? buildPlanOpts(bare, plan) : bare;
|
|
953
|
+
if (!skipPaths || skipPaths.length === 0) return allowList === void 0 ? rest : {
|
|
954
|
+
...rest,
|
|
955
|
+
allowList
|
|
956
|
+
};
|
|
957
|
+
const matchesPath = compilePathMatcher(skipPaths);
|
|
958
|
+
const combined = async (req, key) => {
|
|
959
|
+
const path = (req.url ?? "").split("?", 1)[0] ?? "";
|
|
960
|
+
if (matchesPath(path)) return true;
|
|
961
|
+
if (typeof allowList === "function") return await allowList(req, key);
|
|
962
|
+
if (Array.isArray(allowList)) return allowList.includes(key);
|
|
963
|
+
return false;
|
|
964
|
+
};
|
|
965
|
+
return {
|
|
966
|
+
...rest,
|
|
967
|
+
allowList: combined
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function compilePathMatcher(patterns) {
|
|
971
|
+
const prefixes = [];
|
|
972
|
+
const exact = /* @__PURE__ */ new Set();
|
|
973
|
+
for (const p of patterns) if (p.endsWith("*")) prefixes.push(p.slice(0, -1));
|
|
974
|
+
else exact.add(p);
|
|
975
|
+
return (path) => {
|
|
976
|
+
if (exact.has(path)) return true;
|
|
977
|
+
for (const pre of prefixes) if (path.startsWith(pre)) return true;
|
|
978
|
+
return false;
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
//#endregion
|
|
982
|
+
//#region src/factory/registerSecurity.ts
|
|
796
983
|
/**
|
|
797
984
|
* Register security plugins (Helmet, CORS, Rate Limiting).
|
|
798
985
|
* All enabled by default — set to `false` to opt out.
|
|
@@ -805,10 +992,8 @@ async function registerSecurityPlugins(fastify, config) {
|
|
|
805
992
|
} else fastify.log.warn("Helmet disabled - security headers not applied");
|
|
806
993
|
if (config.cors !== false) {
|
|
807
994
|
const cors = await loadPlugin("cors");
|
|
808
|
-
const
|
|
809
|
-
const
|
|
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;
|
|
995
|
+
const { options: corsOptions, warnings } = resolveCorsOptions(config);
|
|
996
|
+
for (const warning of warnings) fastify.log.warn(warning);
|
|
812
997
|
await fastify.register(cors, corsOptions);
|
|
813
998
|
fastify.log.debug("CORS enabled");
|
|
814
999
|
} else fastify.log.warn("CORS disabled");
|
|
@@ -844,13 +1029,17 @@ async function registerUtilityPlugins(fastify, config) {
|
|
|
844
1029
|
if (config.multipart !== false) {
|
|
845
1030
|
const multipart = await loadPlugin("multipart", fastify.log);
|
|
846
1031
|
if (multipart) {
|
|
1032
|
+
const hostMultipart = typeof config.multipart === "object" && config.multipart !== null ? config.multipart : {};
|
|
847
1033
|
await fastify.register(multipart, {
|
|
1034
|
+
throwFileSizeLimit: true,
|
|
1035
|
+
...hostMultipart,
|
|
848
1036
|
limits: {
|
|
849
1037
|
fileSize: 10 * 1024 * 1024,
|
|
850
|
-
files: 10
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1038
|
+
files: 10,
|
|
1039
|
+
fields: 100,
|
|
1040
|
+
parts: 120,
|
|
1041
|
+
...hostMultipart.limits
|
|
1042
|
+
}
|
|
854
1043
|
});
|
|
855
1044
|
fastify.log.debug("Multipart (file uploads) enabled");
|
|
856
1045
|
}
|
|
@@ -1030,10 +1219,7 @@ Missing:\n${lines}\n\nFix: pass durable adapters under \`stores\`, or use runtim
|
|
|
1030
1219
|
* ```
|
|
1031
1220
|
*/
|
|
1032
1221
|
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
|
-
}
|
|
1222
|
+
if (options.debug !== void 0 && options.debug !== false) configureArcLogger({ debug: options.debug });
|
|
1037
1223
|
validateAuthOptions(options);
|
|
1038
1224
|
const deferredWarnings = validateDistributedRuntime(options);
|
|
1039
1225
|
if (options.beforeBoot) await options.beforeBoot();
|
|
@@ -1041,13 +1227,19 @@ async function createApp(options) {
|
|
|
1041
1227
|
...options.preset ? getPreset(options.preset) : {},
|
|
1042
1228
|
...options
|
|
1043
1229
|
};
|
|
1230
|
+
const inheritedPermissiveTrustProxy = options.preset === "production" && options.trustProxy === void 0;
|
|
1044
1231
|
const resolvedModules = orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
|
|
1045
1232
|
const moduleErrorMappers = resolvedModules.flatMap((m) => m.errorMappers ?? []);
|
|
1233
|
+
const resolvedLogger = resolveLoggerConfig(config.logger);
|
|
1046
1234
|
const fastify = Fastify({
|
|
1047
|
-
logger:
|
|
1235
|
+
logger: resolvedLogger,
|
|
1236
|
+
genReqId: createRequestIdGenerator(),
|
|
1048
1237
|
trustProxy: config.trustProxy ?? false,
|
|
1049
1238
|
pluginTimeout: config.pluginTimeout ?? 1e4,
|
|
1050
1239
|
...config.bodyLimit !== void 0 ? { bodyLimit: config.bodyLimit } : {},
|
|
1240
|
+
...config.requestTimeout !== void 0 ? { requestTimeout: config.requestTimeout } : {},
|
|
1241
|
+
...config.connectionTimeout !== void 0 ? { connectionTimeout: config.connectionTimeout } : {},
|
|
1242
|
+
...config.keepAliveTimeout !== void 0 ? { keepAliveTimeout: config.keepAliveTimeout } : {},
|
|
1051
1243
|
allowErrorHandlerOverride: true,
|
|
1052
1244
|
routerOptions: {
|
|
1053
1245
|
querystringParser: (str) => qs.parse(str, {
|
|
@@ -1066,19 +1258,22 @@ async function createApp(options) {
|
|
|
1066
1258
|
keywords: ["example", ...config.ajv?.keywords ?? []]
|
|
1067
1259
|
} }
|
|
1068
1260
|
});
|
|
1261
|
+
if (resolvedLogger !== false) configureArcLogger({
|
|
1262
|
+
...options.debug !== void 0 && options.debug !== false ? { debug: options.debug } : {},
|
|
1263
|
+
writer: createPinoWriter(fastify.log)
|
|
1264
|
+
});
|
|
1069
1265
|
for (const warning of deferredWarnings) fastify.log.warn(warning);
|
|
1266
|
+
if (inheritedPermissiveTrustProxy) fastify.log.warn("[arc] preset: 'production' defaults to trustProxy: true (trust EVERY proxy). If this app is reachable without your load balancer, clients can spoof X-Forwarded-For/Proto (rate-limit bypass, wrong audit IPs, forged protocol). Silence this by setting trustProxy explicitly: a hop count (trustProxy: 1), a CIDR allow-list ('10.0.0.0/8'), a resolver function, or true if you accept the trade-off. v3 flips this preset default to false — set it now to be upgrade-proof.");
|
|
1070
1267
|
if (config.typeProvider === "typebox") try {
|
|
1071
1268
|
const { TypeBoxValidatorCompiler } = await import("@fastify/type-provider-typebox");
|
|
1072
1269
|
fastify.setValidatorCompiler(TypeBoxValidatorCompiler);
|
|
1073
1270
|
} catch {
|
|
1074
1271
|
fastify.log.warn("typeProvider: \"typebox\" requested but @fastify/type-provider-typebox is not installed.");
|
|
1075
1272
|
}
|
|
1076
|
-
const sjp = await import("secure-json-parse");
|
|
1077
1273
|
fastify.removeContentTypeParser("application/json");
|
|
1078
1274
|
fastify.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
|
|
1079
|
-
if (!body || body.length === 0) return done(null, void 0);
|
|
1080
1275
|
try {
|
|
1081
|
-
done(null,
|
|
1276
|
+
done(null, parseJsonBody(body));
|
|
1082
1277
|
} catch (err) {
|
|
1083
1278
|
done(err);
|
|
1084
1279
|
}
|
|
@@ -1152,4 +1347,4 @@ const ArcFactory = {
|
|
|
1152
1347
|
}
|
|
1153
1348
|
};
|
|
1154
1349
|
//#endregion
|
|
1155
|
-
export { edgePreset as a, testingPreset as c,
|
|
1350
|
+
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 };
|
package/dist/docs/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { p as RegistryEntry } from "../index-
|
|
1
|
+
import { p as RegistryEntry } from "../index-BBsvQt7U.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
|
package/dist/docs/index.mjs
CHANGED
|
@@ -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-
|
|
2
|
+
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-jSDx-VDv.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
|
-
|
|
186
|
-
request.requestDecrypted = true;
|
|
187
|
-
if (plaintext.length === 0) return void 0;
|
|
187
|
+
let plaintext;
|
|
188
188
|
try {
|
|
189
|
-
|
|
189
|
+
plaintext = await jweDecrypt(body, (kid) => provider.decryptionKey(kid, { request }), allowedAlgs, allowedEncs);
|
|
190
190
|
} catch (cause) {
|
|
191
|
-
throw
|
|
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);
|
|
@@ -14,13 +14,16 @@ var errorHandler_exports = /* @__PURE__ */ __exportAll({
|
|
|
14
14
|
* {@link ErrorHandlerOptions.isDuplicateKeyError}.
|
|
15
15
|
*/
|
|
16
16
|
function defaultIsDuplicateKeyError(err) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
let current = err;
|
|
18
|
+
for (let depth = 0; depth < 5 && current && typeof current === "object"; depth++) {
|
|
19
|
+
const e = current;
|
|
20
|
+
if (e.code === 11e3 || e.codeName === "DuplicateKey") return true;
|
|
21
|
+
if (e.code === "P2002") return true;
|
|
22
|
+
if (e.code === "23505") return true;
|
|
23
|
+
if (e.code === "ER_DUP_ENTRY" || e.errno === 1062) return true;
|
|
24
|
+
if (e.code === "SQLITE_CONSTRAINT_UNIQUE" || e.code === "SQLITE_CONSTRAINT_PRIMARYKEY") return true;
|
|
25
|
+
current = e.cause;
|
|
26
|
+
}
|
|
24
27
|
return false;
|
|
25
28
|
}
|
|
26
29
|
function extractDuplicateFields(err) {
|
|
@@ -91,7 +94,7 @@ function mongooseValidationDetails(errors) {
|
|
|
91
94
|
}
|
|
92
95
|
async function errorHandlerPluginFn(fastify, options = {}) {
|
|
93
96
|
const isProduction = process.env.NODE_ENV === "production";
|
|
94
|
-
const { includeStack = !isProduction, onError, errorMap = {}, errorMappers = [], isDuplicateKeyError = defaultIsDuplicateKeyError } = options;
|
|
97
|
+
const { includeStack = !isProduction, exposeInternalMessages = !isProduction, onError, errorMap = {}, errorMappers = [], isDuplicateKeyError = defaultIsDuplicateKeyError } = options;
|
|
95
98
|
fastify.setErrorHandler(async (error, request, reply) => {
|
|
96
99
|
if (onError) try {
|
|
97
100
|
await onError(error, request);
|
|
@@ -112,6 +115,7 @@ async function errorHandlerPluginFn(fastify, options = {}) {
|
|
|
112
115
|
...Object.keys(meta).length > 0 ? { meta } : {}
|
|
113
116
|
};
|
|
114
117
|
const status = wire.status ?? 500;
|
|
118
|
+
if (status >= 500 && wire.code === "arc.internal_error" && !exposeInternalMessages) wire.message = "Internal Server Error";
|
|
115
119
|
if (status >= 500) request.log.error({
|
|
116
120
|
err: error,
|
|
117
121
|
status
|
|
@@ -120,11 +124,31 @@ async function errorHandlerPluginFn(fastify, options = {}) {
|
|
|
120
124
|
err: error,
|
|
121
125
|
status
|
|
122
126
|
}, "Client error");
|
|
123
|
-
if (isArcError(error) && error.cause)
|
|
127
|
+
if (isArcError(error) && error.cause) {
|
|
128
|
+
const causeLevel = status >= 500 ? "error" : "warn";
|
|
129
|
+
request.log[causeLevel]({ cause: error.cause }, "Error cause chain");
|
|
130
|
+
}
|
|
124
131
|
return reply.status(status).send(wire);
|
|
125
132
|
});
|
|
126
133
|
}
|
|
127
134
|
/**
|
|
135
|
+
* Is `code` a caller-defined DOMAIN error code that is safe to surface on the
|
|
136
|
+
* wire, as opposed to a framework/runtime code that must never leak?
|
|
137
|
+
*
|
|
138
|
+
* Rejects Fastify internals (`FST_*`) and Node system errnos (`ECONNREFUSED`,
|
|
139
|
+
* `ENOENT`, …). Domain codes in the wild use separators (`PURCHASE_NOT_FOUND`,
|
|
140
|
+
* `access.forbidden`) so they pass; a bare all-caps single word that collides
|
|
141
|
+
* with the Node-errno shape (e.g. `EXPIRED`) is conservatively treated as a
|
|
142
|
+
* runtime code — use a separatored code (`TOKEN_EXPIRED`) to surface it, or
|
|
143
|
+
* throw via `createDomainError` which bypasses this heuristic entirely.
|
|
144
|
+
*/
|
|
145
|
+
function isDomainErrorCode(code) {
|
|
146
|
+
if (typeof code !== "string" || code.length === 0) return false;
|
|
147
|
+
if (code.startsWith("FST_")) return false;
|
|
148
|
+
if (/^E[A-Z]{2,}$/.test(code)) return false;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
128
152
|
* Single-pass error → `ErrorContract` classifier. Dispatch order is fixed:
|
|
129
153
|
*
|
|
130
154
|
* 1. Class-based mappers (`instanceof`) — user-registered domain errors
|
|
@@ -167,7 +191,7 @@ function classify(error, ctx) {
|
|
|
167
191
|
};
|
|
168
192
|
}
|
|
169
193
|
if (typeof fastifyErr.statusCode === "number") return {
|
|
170
|
-
code: statusToArcCode(fastifyErr.statusCode),
|
|
194
|
+
code: (isDomainErrorCode(fastifyErr.code) ? fastifyErr.code : void 0) ?? statusToArcCode(fastifyErr.statusCode),
|
|
171
195
|
message: error.message || "Error",
|
|
172
196
|
status: fastifyErr.statusCode
|
|
173
197
|
};
|
|
@@ -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`. */
|
package/dist/events/index.mjs
CHANGED
|
@@ -176,8 +176,11 @@ function validatePayload(payload, schema) {
|
|
|
176
176
|
for (const [key, propSchema] of Object.entries(schema.properties)) if (key in record && record[key] !== void 0 && record[key] !== null) {
|
|
177
177
|
const expectedType = propSchema.type;
|
|
178
178
|
if (expectedType) {
|
|
179
|
-
const
|
|
180
|
-
|
|
179
|
+
const value = record[key];
|
|
180
|
+
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
181
|
+
if (expectedType === "integer") {
|
|
182
|
+
if (actualType !== "number" || !Number.isInteger(value)) errors.push(`Field '${key}': expected integer, got ${actualType}`);
|
|
183
|
+
} else if (expectedType !== actualType) errors.push(`Field '${key}': expected ${expectedType}, got ${actualType}`);
|
|
181
184
|
}
|
|
182
185
|
}
|
|
183
186
|
}
|