@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.
- package/dist/{BaseController-D-MHZ27_.mjs → BaseController-C6FlRWxF.mjs} +2 -2
- package/dist/{EventTransport-IEn7Tn-Q.d.mts → EventTransport-QmY7A258.d.mts} +0 -1
- package/dist/{QueryCache-TRfXau3W.d.mts → QueryCache-Z1B38koh.d.mts} +0 -1
- package/dist/{actionPermissions-C9RMiIpc.mjs → actionPermissions-ze70xXJi.mjs} +1 -1
- package/dist/audit/index.d.mts +9 -5
- package/dist/auth/audit.d.mts +7 -4
- package/dist/auth/audit.mjs +5 -0
- package/dist/auth/index.d.mts +35 -14
- package/dist/auth/index.mjs +6 -3
- package/dist/auth/redis-session.d.mts +1 -2
- package/dist/cache/index.d.mts +2 -3
- package/dist/cli/commands/describe.d.mts +1 -2
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/context/index.d.mts +2 -2
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -4
- package/dist/{core-CWhyGkiM.mjs → core-b6fgFgnd.mjs} +129 -17
- package/dist/{createActionRouter-HlQso0jO.mjs → createActionRouter-h1cEEaEX.mjs} +10 -6
- package/dist/{createAggregationRouter-5LzPzgw1.mjs → createAggregationRouter-B1C0kvTL.mjs} +3 -3
- package/dist/{createApp-D6uwzCKu.mjs → createApp-Ci7yG5gv.mjs} +390 -192
- package/dist/discovery/index.d.mts +0 -1
- package/dist/docs/index.d.mts +1 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{elevation-xGUBq6p_.d.mts → elevation-C9s5hFP_.d.mts} +0 -1
- package/dist/encryption/index.d.mts +2 -144
- package/dist/encryption/index.mjs +9 -5
- package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-BTzXSwIg.mjs} +24 -3
- package/dist/{errorHandler-BtUUDPEa.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -1
- package/dist/{errors-DOHkjQlD.d.mts → errors-DHcHVAgk.d.mts} +0 -1
- package/dist/{eventPlugin-DbBFd3x_.d.mts → eventPlugin-A6EbthXR.d.mts} +10 -6
- package/dist/events/index.d.mts +9 -6
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -2
- package/dist/factory/index.d.mts +30 -9
- package/dist/factory/index.mjs +35 -4
- package/dist/{fastify-augmentation-CCQJEDgh.d.mts → fastify-augmentation-9_fuetB4.d.mts} +2 -3
- 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 +4 -3
- package/dist/idempotency/index.mjs +2 -0
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{types-BHMKlMBU.d.mts → index-BEu4wjbL.d.mts} +241 -80
- package/dist/{index-DUa8WHz3.d.mts → index-D8MKYGDa.d.mts} +88 -7
- package/dist/{index-_--GyzcO.d.mts → index-DbtgCOCj.d.mts} +1324 -1228
- package/dist/{index-CdFkIMEd.d.mts → index-ogIHvOjo.d.mts} +36 -7
- package/dist/index.d.mts +8 -7
- package/dist/index.mjs +6 -6
- package/dist/{init-YH_nslQR.mjs → init-DleK3p9f.mjs} +272 -139
- package/dist/integrations/event-gateway.d.mts +2 -3
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/jobs.d.mts +0 -1
- package/dist/integrations/mcp/index.d.mts +6 -7
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -2
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +2 -2
- package/dist/integrations/webhooks.d.mts +8 -5
- package/dist/integrations/webhooks.mjs +9 -5
- package/dist/integrations/websocket-pushref-redis.d.mts +1 -2
- package/dist/integrations/websocket-redis.d.mts +1 -2
- package/dist/integrations/websocket.d.mts +2 -2
- package/dist/jsonBody-BlzAoGCe.mjs +43 -0
- package/dist/{loadResources-B8UmpITt.mjs → loadResources-DpU8pfjV.mjs} +4 -3
- package/dist/logger/index.d.mts +26 -1
- package/dist/logger/index.mjs +39 -1
- package/dist/{mcpPlugin-CuW6L_uD.mjs → mcpPlugin-ByA2tf6D.mjs} +8 -4
- package/dist/middleware/index.d.mts +1 -2
- package/dist/{module-JYH9V0I8.d.mts → module-B4u10iOJ.d.mts} +71 -1
- package/dist/{openapi-Dq6RbEGK.mjs → openapi-BRpMZD7C.mjs} +3 -3
- package/dist/permissions/index.d.mts +4 -3
- package/dist/permissions/index.mjs +2 -2
- package/dist/{permissions-CgbOLJZG.mjs → permissions-BJMuKMl7.mjs} +33 -1
- package/dist/pipeline/index.d.mts +1 -2
- package/dist/plugins/index.d.mts +96 -7
- package/dist/plugins/index.mjs +86 -268
- package/dist/plugins/response-cache.d.mts +5 -3
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +7 -4
- package/dist/presets/filesUpload.d.mts +4 -5
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +1 -2
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -2
- package/dist/presets/search.d.mts +2 -3
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-DWsYQdbV.mjs → presets-gw0ciIUD.mjs} +1 -1
- package/dist/{pushref-registry-DnCqL3aa.d.mts → pushref-registry-BbcnpNTm.d.mts} +4 -2
- package/dist/{queryCachePlugin-xzFyKAjT.d.mts → queryCachePlugin-CfYWq-Ak.d.mts} +1 -2
- package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
- package/dist/{redis-C3W0qbz8.d.mts → redis-UDtLaNj6.d.mts} +0 -1
- package/dist/{redis-stream-DsP1vqZB.d.mts → redis-stream-iVR2l7aS.d.mts} +1 -2
- package/dist/{registerRoute-DGSREwPw.mjs → registerRoute-xHc42fwB.mjs} +38 -18
- package/dist/registry/index.d.mts +2 -2
- package/dist/requestId-5BhTVzBv.mjs +113 -0
- package/dist/schemaConverter-Ds5reSGs.mjs +168 -0
- package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-B-6SPe7g.mjs} +2 -2
- package/dist/schemas/index.d.mts +0 -1
- package/dist/scim/index.d.mts +0 -1
- package/dist/scim/index.mjs +6 -7
- package/dist/scope/index.d.mts +2 -3
- package/dist/scope/index.mjs +2 -76
- package/dist/scope-k15RXfcq.mjs +64 -0
- package/dist/{sessionManager-BXG-HGwG.d.mts → sessionManager-BHOM3N_0.d.mts} +10 -6
- package/dist/{storage-Co_PQpeQ.d.mts → storage-DLUZ04kK.d.mts} +8 -4
- package/dist/testing/index.d.mts +83 -4
- package/dist/testing/index.mjs +74 -4
- package/dist/testing/storageContract.d.mts +1 -2
- package/dist/{tracing-D8jlGXP7.d.mts → tracing-CsGKQJGg.d.mts} +0 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-C9b2HR5W.d.mts → types-Bc42CUyk.d.mts} +1 -2
- package/dist/types-Cc91nXuA.d.mts +132 -0
- package/dist/{fields-C7ahdGYf.d.mts → types-Csxga7d3.d.mts} +2 -123
- package/dist/{types-LNebCoJx.d.mts → types-DH0GupI7.d.mts} +8 -4
- package/dist/usage/index.d.mts +142 -0
- package/dist/usage/index.mjs +0 -0
- package/dist/utils/index.d.mts +220 -144
- package/dist/utils/index.mjs +3 -3
- package/dist/{utils-ChjYQpjn.mjs → utils-FhJ88TJE.mjs} +129 -39
- package/dist/{versioning-D_eBCfNr.d.mts → versioning-DLwklPlG.d.mts} +1 -7
- package/dist/{websocket-MlYlrYq0.d.mts → websocket-uuWqM6UY.d.mts} +5 -4
- package/llms.txt +39 -0
- package/package.json +453 -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 +44 -2
- 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 +243 -0
- package/skills/arc-module-publishing/references/authoring-modules.md +327 -0
- package/skills/arc-module-publishing/references/package-manifest.md +157 -0
- package/skills/arc-module-publishing/references/publishing-workflow.md +182 -0
- package/dist/preloadResources-CHw3_oA_.mjs +0 -45
- 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-
|
|
351
|
-
|
|
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-
|
|
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/
|
|
794
|
+
//#region src/factory/security/cors.ts
|
|
704
795
|
/**
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
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
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
725
|
-
|
|
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
|
|
800
|
-
const
|
|
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
|
-
|
|
844
|
-
|
|
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:
|
|
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,
|
|
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 {
|
|
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 };
|