theokit 0.12.0 → 0.13.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/{actions-virtual-module-SQDY3V5X.js → actions-virtual-module-3CDQTWOC.js} +6 -6
- package/dist/{actions-virtual-module-PNPRCEOS.js → actions-virtual-module-EIPXX4ZB.js} +3 -3
- package/dist/adapters/web-shim.d.ts +67 -0
- package/dist/adapters/ws-shim.d.ts +55 -0
- package/dist/agent-events-DosDXkSV.d.ts +94 -0
- package/dist/agents-typed-client-SAWAAH7K.js +142 -0
- package/dist/agents-typed-client-SAWAAH7K.js.map +1 -0
- package/dist/agents-typed-client-UTEQUA63.js +143 -0
- package/dist/agents-typed-client-UTEQUA63.js.map +1 -0
- package/dist/{app-typed-client-5GYEOYP3.js → app-typed-client-7PBFWZUE.js} +3 -3
- package/dist/{app-typed-client-QG7BVZYW.js → app-typed-client-CSOK7NPC.js} +6 -6
- package/dist/audit-log-BQWM5YLG.d.ts +60 -0
- package/dist/body-parser-web-FV5HWCY3.js +71 -0
- package/dist/body-parser-web-FV5HWCY3.js.map +1 -0
- package/dist/boot/index.d.ts +39 -0
- package/dist/{build-QFRLSEZ4.js → build-HXND27XG.js} +11 -11
- package/dist/{chunk-223EFY5X.js → chunk-2J7XU3PW.js} +68 -27
- package/dist/chunk-2J7XU3PW.js.map +1 -0
- package/dist/{chunk-RESN62GB.js → chunk-2KZQPDYR.js} +5 -48
- package/dist/chunk-2KZQPDYR.js.map +1 -0
- package/dist/chunk-3S3BNW5K.js +445 -0
- package/dist/chunk-3S3BNW5K.js.map +1 -0
- package/dist/{chunk-6FYD34NX.js → chunk-BQDGES7C.js} +28 -28
- package/dist/{chunk-6FYD34NX.js.map → chunk-BQDGES7C.js.map} +1 -1
- package/dist/chunk-EXP56GFQ.js +52 -0
- package/dist/chunk-EXP56GFQ.js.map +1 -0
- package/dist/chunk-F4YUPDJ2.js +115 -0
- package/dist/chunk-F4YUPDJ2.js.map +1 -0
- package/dist/{chunk-NAZ4E2GT.js → chunk-KXA37ONC.js} +2 -2
- package/dist/chunk-NHJMZCAS.js +32 -0
- package/dist/chunk-NHJMZCAS.js.map +1 -0
- package/dist/{chunk-43D6XNDR.js → chunk-O62MW4MT.js} +91 -18
- package/dist/chunk-O62MW4MT.js.map +1 -0
- package/dist/chunk-RSVN727G.js +1 -0
- package/dist/{chunk-7CBRKNQA.js → chunk-RYTZYFSD.js} +198 -6
- package/dist/chunk-RYTZYFSD.js.map +1 -0
- package/dist/chunk-UNLA45FY.js +235 -0
- package/dist/chunk-UNLA45FY.js.map +1 -0
- package/dist/{chunk-GFMQJHXX.js → chunk-WR4F4EEZ.js} +1082 -1074
- package/dist/chunk-WR4F4EEZ.js.map +1 -0
- package/dist/{chunk-AD74EAK3.js → chunk-ZSTZXR2D.js} +1 -30
- package/dist/chunk-ZSTZXR2D.js.map +1 -0
- package/dist/cli/index.js +5 -5
- package/dist/client/index.d.ts +418 -0
- package/dist/client/index.js +84 -3
- package/dist/client/index.js.map +1 -1
- package/dist/csrf-BBrEZSBW.d.ts +107 -0
- package/dist/csrf-readiness-store-CjIoub3U.d.ts +43 -0
- package/dist/define-websocket-CdK94O-D.d.ts +64 -0
- package/dist/{dev-GBXOTXUP.js → dev-OWW4XVIH.js} +10 -10
- package/dist/{dev-emit-FEFEDLZF.js → dev-emit-5MDSBP5D.js} +3 -3
- package/dist/{dev-emit-O4EGOSNV.js → dev-emit-QH2YGZXN.js} +2 -2
- package/dist/devtools/entry.d.ts +5 -0
- package/dist/error-envelope-BsNzzAV5.d.ts +62 -0
- package/dist/health-route-C0hk64_U.d.ts +57 -0
- package/dist/index-B40qUSrQ.d.ts +575 -0
- package/dist/index.d.ts +361 -0
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/internal-api-4YTJDITC.js +83 -0
- package/dist/internal-api-EFKZWIYZ.js +66 -0
- package/dist/internal-api-EFKZWIYZ.js.map +1 -0
- package/dist/job-backend-CgC8Xf33.d.ts +68 -0
- package/dist/match-CfbEFRG4.d.ts +26 -0
- package/dist/{openapi-VR6AFBLJ.js → openapi-FHY6HC6I.js} +7 -7
- package/dist/plugin-runner-BGBkzgi0.d.ts +95 -0
- package/dist/plugin-types-DNJGxr4Z.d.ts +79 -0
- package/dist/rate-limit-BdNDZ3vt.d.ts +58 -0
- package/dist/rate-limit-store-BEJnhWdw.d.ts +72 -0
- package/dist/react-query/index.d.ts +33 -0
- package/dist/{registry-Q2TZQLUH.js → registry-34LL7NF4.js} +1 -1
- package/dist/{routes-LRYOIIAI.js → routes-EW7TP7NJ.js} +2 -2
- package/dist/schema-BpH6ivDY.d.ts +74 -0
- package/dist/server/agent/index.d.ts +229 -0
- package/dist/server/agent/index.js +2 -1
- package/dist/server/auth/index.d.ts +419 -0
- package/dist/server/cost/index.d.ts +177 -0
- package/dist/server/cron/index.d.ts +208 -0
- package/dist/server/define/index.d.ts +313 -0
- package/dist/server/define/index.js +4 -2
- package/dist/server/http/index.d.ts +11 -0
- package/dist/server/index.d.ts +848 -0
- package/dist/server/index.js +9 -294
- package/dist/server/index.js.map +1 -1
- package/dist/server/jobs/index.d.ts +348 -0
- package/dist/server/observability/index.d.ts +324 -0
- package/dist/server/plugins/index.d.ts +17 -0
- package/dist/server/rate-limit/index.d.ts +105 -0
- package/dist/server/realtime/index.d.ts +15 -0
- package/dist/server/scan/index.d.ts +126 -0
- package/dist/server/scan/index.js +1 -1
- package/dist/server/security/index.d.ts +193 -0
- package/dist/server/storage/index.d.ts +22 -0
- package/dist/server/webhook/index.d.ts +148 -0
- package/dist/{start-3ZHAXSJE.js → start-KIQ5TTLR.js} +76 -13
- package/dist/start-KIQ5TTLR.js.map +1 -0
- package/dist/storage-manager-C4jsO0Tp.d.ts +89 -0
- package/dist/storage-types-DsDTCPbp.d.ts +96 -0
- package/dist/vite-plugin/index.d.ts +115 -0
- package/dist/vite-plugin/index.js +6 -4
- package/dist/{vite-plugin-WO72VLYR.js → vite-plugin-RK66K26Z.js} +7 -7
- package/dist/vite-plugin-RK66K26Z.js.map +1 -0
- package/package.json +4 -4
- package/dist/chunk-223EFY5X.js.map +0 -1
- package/dist/chunk-3LVRAGAZ.js +0 -73
- package/dist/chunk-3LVRAGAZ.js.map +0 -1
- package/dist/chunk-43D6XNDR.js.map +0 -1
- package/dist/chunk-7CBRKNQA.js.map +0 -1
- package/dist/chunk-AD74EAK3.js.map +0 -1
- package/dist/chunk-GFMQJHXX.js.map +0 -1
- package/dist/chunk-PBEH6NXR.js +0 -44
- package/dist/chunk-PBEH6NXR.js.map +0 -1
- package/dist/chunk-PIVX3DYW.js +0 -142
- package/dist/chunk-PIVX3DYW.js.map +0 -1
- package/dist/chunk-PPPR5DGR.js +0 -1
- package/dist/chunk-RESN62GB.js.map +0 -1
- package/dist/start-3ZHAXSJE.js.map +0 -1
- /package/dist/{actions-virtual-module-SQDY3V5X.js.map → actions-virtual-module-3CDQTWOC.js.map} +0 -0
- /package/dist/{actions-virtual-module-PNPRCEOS.js.map → actions-virtual-module-EIPXX4ZB.js.map} +0 -0
- /package/dist/{app-typed-client-5GYEOYP3.js.map → app-typed-client-7PBFWZUE.js.map} +0 -0
- /package/dist/{app-typed-client-QG7BVZYW.js.map → app-typed-client-CSOK7NPC.js.map} +0 -0
- /package/dist/{build-QFRLSEZ4.js.map → build-HXND27XG.js.map} +0 -0
- /package/dist/{chunk-NAZ4E2GT.js.map → chunk-KXA37ONC.js.map} +0 -0
- /package/dist/{chunk-PPPR5DGR.js.map → chunk-RSVN727G.js.map} +0 -0
- /package/dist/{dev-GBXOTXUP.js.map → dev-OWW4XVIH.js.map} +0 -0
- /package/dist/{dev-emit-FEFEDLZF.js.map → dev-emit-5MDSBP5D.js.map} +0 -0
- /package/dist/{dev-emit-O4EGOSNV.js.map → dev-emit-QH2YGZXN.js.map} +0 -0
- /package/dist/{vite-plugin-WO72VLYR.js.map → internal-api-4YTJDITC.js.map} +0 -0
- /package/dist/{openapi-VR6AFBLJ.js.map → openapi-FHY6HC6I.js.map} +0 -0
- /package/dist/{registry-Q2TZQLUH.js.map → registry-34LL7NF4.js.map} +0 -0
- /package/dist/{routes-LRYOIIAI.js.map → routes-EW7TP7NJ.js.map} +0 -0
|
@@ -5,375 +5,6 @@ import {
|
|
|
5
5
|
serverErrorToEnvelope
|
|
6
6
|
} from "./chunk-HGZL5EOI.js";
|
|
7
7
|
|
|
8
|
-
// src/server/plugins/plugin-runner.ts
|
|
9
|
-
var DuplicatePluginError = class extends Error {
|
|
10
|
-
constructor(name) {
|
|
11
|
-
super(`Plugin "${name}" is already registered.`);
|
|
12
|
-
this.name = "DuplicatePluginError";
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
var PluginRunner = class {
|
|
16
|
-
plugins = /* @__PURE__ */ new Set();
|
|
17
|
-
pluginScopes = /* @__PURE__ */ new Map();
|
|
18
|
-
onRequestHooks = [];
|
|
19
|
-
preHandlerHooks = [];
|
|
20
|
-
onResponseHooks = [];
|
|
21
|
-
onErrorHooks = [];
|
|
22
|
-
/**
|
|
23
|
-
* Parent app — proto-chain root for all child scopes. Has its OWN empty
|
|
24
|
-
* decorations map (parent never receives `decorateRequest` calls under
|
|
25
|
-
* the T3.1 contract; only child scopes do).
|
|
26
|
-
*/
|
|
27
|
-
parentDecorations = /* @__PURE__ */ new Map();
|
|
28
|
-
parentApp = this.buildParentAppFacade();
|
|
29
|
-
has(name) {
|
|
30
|
-
return this.plugins.has(name);
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Per T3.1 (C1 plugin scope encapsulation):
|
|
34
|
-
* 1. Reserve the plugin name (still rejects duplicates).
|
|
35
|
-
* 2. Build a CHILD TheoApp via `Object.create(parentApp)` — Fastify
|
|
36
|
-
* `plugin-override.js:38` pattern. The child's own `decorateRequest`
|
|
37
|
-
* populates per-scope `decorations`; the child's `addHook` still
|
|
38
|
-
* forwards into the shared hook lists (hooks ARE process-global —
|
|
39
|
-
* only decorations are scoped, mirroring Fastify's decoration vs
|
|
40
|
-
* hook semantics).
|
|
41
|
-
* 3. Invoke `plugin.register(childApp)`.
|
|
42
|
-
*
|
|
43
|
-
* Cross-plugin decoration-key collisions are PERMITTED (per blueprint
|
|
44
|
-
* D1). The legacy `DuplicateDecorationError` is no longer thrown.
|
|
45
|
-
*/
|
|
46
|
-
async register(plugin) {
|
|
47
|
-
if (this.plugins.has(plugin.name)) {
|
|
48
|
-
throw new DuplicatePluginError(plugin.name);
|
|
49
|
-
}
|
|
50
|
-
this.plugins.add(plugin.name);
|
|
51
|
-
const scope = this.buildPluginScope(plugin.name);
|
|
52
|
-
this.pluginScopes.set(plugin.name, scope);
|
|
53
|
-
try {
|
|
54
|
-
await plugin.register(scope.app);
|
|
55
|
-
} catch (err) {
|
|
56
|
-
this.plugins.delete(plugin.name);
|
|
57
|
-
this.pluginScopes.delete(plugin.name);
|
|
58
|
-
throw err;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Build the parent app facade. Hooks forward to shared lists.
|
|
63
|
-
* `decorateRequest` writes into the parent decorations map — used only
|
|
64
|
-
* if a future consumer registers a non-plugin "app-level" decoration
|
|
65
|
-
* directly. T3.1 contract: plugins decorate ONLY through child scopes.
|
|
66
|
-
*/
|
|
67
|
-
buildParentAppFacade() {
|
|
68
|
-
const facade = {
|
|
69
|
-
addHook: (name, fn) => {
|
|
70
|
-
this.routeHookByName(name, fn);
|
|
71
|
-
},
|
|
72
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T documents the per-decoration type for consumers
|
|
73
|
-
decorateRequest: (key, value) => {
|
|
74
|
-
this.parentDecorations.set(key, value);
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
return facade;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Build a child scope via `Object.create(parentApp)` so the child
|
|
81
|
-
* inherits the parent's facade methods through the prototype chain.
|
|
82
|
-
* Then override `decorateRequest` on the child instance so writes
|
|
83
|
-
* land in the per-scope `decorations` map (parent is NOT mutated).
|
|
84
|
-
*/
|
|
85
|
-
buildPluginScope(_pluginName) {
|
|
86
|
-
const decorations = /* @__PURE__ */ new Map();
|
|
87
|
-
const childApp = Object.create(this.parentApp);
|
|
88
|
-
Object.defineProperty(childApp, "decorateRequest", {
|
|
89
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T documents the per-decoration type for consumers
|
|
90
|
-
value: (key, value) => {
|
|
91
|
-
if (typeof key !== "string") {
|
|
92
|
-
throw new TypeError(
|
|
93
|
-
`decorateRequest: invalid key (expected string, got ${typeof key}). Plugin authors MUST pass string keys.`
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
decorations.set(key, value);
|
|
97
|
-
},
|
|
98
|
-
enumerable: false,
|
|
99
|
-
writable: false,
|
|
100
|
-
configurable: false
|
|
101
|
-
});
|
|
102
|
-
Object.defineProperty(childApp, "decorations", {
|
|
103
|
-
get: () => Object.fromEntries(decorations),
|
|
104
|
-
enumerable: false,
|
|
105
|
-
configurable: false
|
|
106
|
-
});
|
|
107
|
-
return { app: childApp, decorations };
|
|
108
|
-
}
|
|
109
|
-
routeHookByName(name, fn) {
|
|
110
|
-
switch (name) {
|
|
111
|
-
case "onRequest":
|
|
112
|
-
this.onRequestHooks.push(fn);
|
|
113
|
-
return;
|
|
114
|
-
case "preHandler":
|
|
115
|
-
this.preHandlerHooks.push(fn);
|
|
116
|
-
return;
|
|
117
|
-
case "onResponse":
|
|
118
|
-
this.onResponseHooks.push(fn);
|
|
119
|
-
return;
|
|
120
|
-
case "onError":
|
|
121
|
-
this.onErrorHooks.push(fn);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Apply ALL plugin scopes' decorations into the request ctx. Per T3.1,
|
|
127
|
-
* sibling plugins MAY decorate the same key — last-writer-wins at apply
|
|
128
|
-
* time (ordering = registration order). This keeps the legacy
|
|
129
|
-
* `ctx.<key>` flat surface working for consumers that already aggregate
|
|
130
|
-
* decorations into a single bag.
|
|
131
|
-
*/
|
|
132
|
-
applyDecorations(ctx) {
|
|
133
|
-
for (const scope of this.pluginScopes.values()) {
|
|
134
|
-
for (const [key, value] of scope.decorations.entries()) {
|
|
135
|
-
ctx[key] = value;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Apply ONLY one plugin's scoped decorations into `target`. Used by
|
|
141
|
-
* the T1.1 RED-3 test (sibling isolation proof) and by future scope-aware
|
|
142
|
-
* dispatch paths. Throws if the plugin name isn't registered.
|
|
143
|
-
*/
|
|
144
|
-
applyScopedDecorations(pluginName, target) {
|
|
145
|
-
const scope = this.pluginScopes.get(pluginName);
|
|
146
|
-
if (!scope) throw new Error(`PluginRunner: unknown plugin "${pluginName}"`);
|
|
147
|
-
for (const [key, value] of scope.decorations.entries()) {
|
|
148
|
-
target[key] = value;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
/** T1.1 RED-1/RED-4 introspection: returns the child TheoApp built for `pluginName`. */
|
|
152
|
-
getPluginScope(pluginName) {
|
|
153
|
-
const scope = this.pluginScopes.get(pluginName);
|
|
154
|
-
if (!scope) throw new Error(`PluginRunner: unknown plugin "${pluginName}"`);
|
|
155
|
-
return scope.app;
|
|
156
|
-
}
|
|
157
|
-
/** T1.1 RED-4: returns the parent TheoApp (root of every child's proto chain). */
|
|
158
|
-
getParentApp() {
|
|
159
|
-
return this.parentApp;
|
|
160
|
-
}
|
|
161
|
-
/** T1.1 RED-2: returns the parent's decorations map (NEVER touched by plugin decorate calls under T3.1 contract). */
|
|
162
|
-
getParentDecorations() {
|
|
163
|
-
return this.parentDecorations;
|
|
164
|
-
}
|
|
165
|
-
async runOnRequest(ctx) {
|
|
166
|
-
return this.runHookList(this.onRequestHooks, ctx);
|
|
167
|
-
}
|
|
168
|
-
async runPreHandler(ctx) {
|
|
169
|
-
return this.runHookList(this.preHandlerHooks, ctx);
|
|
170
|
-
}
|
|
171
|
-
async runOnResponse(ctx, options = {}) {
|
|
172
|
-
return this.runHookList(this.onResponseHooks, ctx, options);
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Run all onError hooks. Swallows errors thrown inside hooks themselves to
|
|
176
|
-
* prevent recursion (an error in an error handler must not trigger onError
|
|
177
|
-
* again).
|
|
178
|
-
*/
|
|
179
|
-
async runOnError(ctx, error) {
|
|
180
|
-
const errorCtx = { ...ctx, error };
|
|
181
|
-
for (const hook of this.onErrorHooks) {
|
|
182
|
-
try {
|
|
183
|
-
await hook(errorCtx);
|
|
184
|
-
} catch (innerErr) {
|
|
185
|
-
console.error(
|
|
186
|
-
`[plugin-runner] onError hook threw; suppressed to avoid recursion:`,
|
|
187
|
-
innerErr
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return { shortCircuited: false };
|
|
192
|
-
}
|
|
193
|
-
async runHookList(hooks, ctx, options = {}) {
|
|
194
|
-
for (const hook of hooks) {
|
|
195
|
-
try {
|
|
196
|
-
await hook(ctx);
|
|
197
|
-
} catch (err) {
|
|
198
|
-
if (options.inErrorPath) {
|
|
199
|
-
console.error(`[plugin-runner] hook threw during error path; suppressed (EC-9):`, err);
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
throw err;
|
|
203
|
-
}
|
|
204
|
-
if (ctx.response.writableEnded || ctx.response.headersSent) {
|
|
205
|
-
return { shortCircuited: true };
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return { shortCircuited: false };
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// src/server/plugins/load-plugins.ts
|
|
213
|
-
var InvalidPluginShapeError = class extends Error {
|
|
214
|
-
constructor(index, reason) {
|
|
215
|
-
super(`plugins[${index}] is not a valid TheoPlugin: ${reason}`);
|
|
216
|
-
this.name = "InvalidPluginShapeError";
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
function isPlugin(value, index) {
|
|
220
|
-
if (value == null || typeof value !== "object") {
|
|
221
|
-
throw new InvalidPluginShapeError(index, "expected an object");
|
|
222
|
-
}
|
|
223
|
-
const v = value;
|
|
224
|
-
if (typeof v.name !== "string" || v.name.length === 0) {
|
|
225
|
-
throw new InvalidPluginShapeError(index, 'missing "name" string');
|
|
226
|
-
}
|
|
227
|
-
if (typeof v.register !== "function") {
|
|
228
|
-
throw new InvalidPluginShapeError(index, 'missing "register" function');
|
|
229
|
-
}
|
|
230
|
-
return true;
|
|
231
|
-
}
|
|
232
|
-
async function createPluginRunnerFromConfig(plugins) {
|
|
233
|
-
if (plugins == null) return void 0;
|
|
234
|
-
if (!Array.isArray(plugins)) return void 0;
|
|
235
|
-
if (plugins.length === 0) return void 0;
|
|
236
|
-
const runner = new PluginRunner();
|
|
237
|
-
const pluginsArray = plugins;
|
|
238
|
-
for (let i = 0; i < pluginsArray.length; i++) {
|
|
239
|
-
const candidate = pluginsArray[i];
|
|
240
|
-
if (!isPlugin(candidate, i)) {
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
await runner.register(candidate);
|
|
244
|
-
}
|
|
245
|
-
return runner;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// src/server/rate-limit/rate-limit-store.ts
|
|
249
|
-
var InMemoryStore = class _InMemoryStore {
|
|
250
|
-
store = /* @__PURE__ */ new Map();
|
|
251
|
-
/** Bound the map to prevent unbounded growth from pathological inputs. */
|
|
252
|
-
static MAX_ENTRIES = 1e5;
|
|
253
|
-
/** GC sweep interval, milliseconds. */
|
|
254
|
-
static GC_INTERVAL_MS = 3e4;
|
|
255
|
-
gcTimer = null;
|
|
256
|
-
constructor() {
|
|
257
|
-
if (typeof setInterval !== "undefined") {
|
|
258
|
-
const timer = setInterval(() => {
|
|
259
|
-
this.sweepExpired();
|
|
260
|
-
}, _InMemoryStore.GC_INTERVAL_MS);
|
|
261
|
-
this.gcTimer = timer;
|
|
262
|
-
const maybeUnref = timer.unref;
|
|
263
|
-
if (typeof maybeUnref === "function") maybeUnref.call(timer);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Synchronous fast-path used by the legacy sync `createRateLimiter`
|
|
268
|
-
* surface (`api-middleware.ts` is sync). The async `incr` delegates to
|
|
269
|
-
* this for in-memory; external adapters override `incr` directly.
|
|
270
|
-
*/
|
|
271
|
-
incrSync(key, windowMs) {
|
|
272
|
-
if (!Number.isFinite(windowMs) || windowMs <= 0) {
|
|
273
|
-
throw new Error(
|
|
274
|
-
`InMemoryStore.incr: windowMs must be a positive finite number (got ${windowMs})`
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
const now = Date.now();
|
|
278
|
-
if (this.store.size >= _InMemoryStore.MAX_ENTRIES) {
|
|
279
|
-
const first = this.store.keys().next().value;
|
|
280
|
-
if (first !== void 0) this.store.delete(first);
|
|
281
|
-
}
|
|
282
|
-
const entry = this.store.get(key);
|
|
283
|
-
if (!entry || now >= entry.resetAt) {
|
|
284
|
-
const fresh = { count: 1, resetAt: now + windowMs };
|
|
285
|
-
this.store.set(key, fresh);
|
|
286
|
-
return { ...fresh };
|
|
287
|
-
}
|
|
288
|
-
entry.count++;
|
|
289
|
-
return { count: entry.count, resetAt: entry.resetAt };
|
|
290
|
-
}
|
|
291
|
-
/** Sweep expired entries. Called by the GC timer; safe to call manually. */
|
|
292
|
-
sweepExpired() {
|
|
293
|
-
const now = Date.now();
|
|
294
|
-
for (const [k, v] of this.store) {
|
|
295
|
-
if (v.resetAt <= now) this.store.delete(k);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Stop the GC timer. Call from tests or when discarding the store. After
|
|
300
|
-
* `dispose`, the store still works but no longer auto-sweeps.
|
|
301
|
-
*/
|
|
302
|
-
dispose() {
|
|
303
|
-
if (this.gcTimer) {
|
|
304
|
-
clearInterval(this.gcTimer);
|
|
305
|
-
this.gcTimer = null;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
// The async surface is required by the `RateLimitStore` interface
|
|
309
|
-
// (Redis/etc. adapters are inherently async). For the in-memory case
|
|
310
|
-
// we just adapt the sync implementations to Promises.
|
|
311
|
-
// eslint-disable-next-line @typescript-eslint/require-await -- interface contract: async return
|
|
312
|
-
async incr(key, windowMs) {
|
|
313
|
-
return this.incrSync(key, windowMs);
|
|
314
|
-
}
|
|
315
|
-
// eslint-disable-next-line @typescript-eslint/require-await -- interface contract: async return
|
|
316
|
-
async get(key) {
|
|
317
|
-
const now = Date.now();
|
|
318
|
-
const entry = this.store.get(key);
|
|
319
|
-
if (!entry) return null;
|
|
320
|
-
if (now >= entry.resetAt) return null;
|
|
321
|
-
return { count: entry.count, resetAt: entry.resetAt };
|
|
322
|
-
}
|
|
323
|
-
// eslint-disable-next-line @typescript-eslint/require-await -- interface contract: async return
|
|
324
|
-
async reset(key) {
|
|
325
|
-
this.store.delete(key);
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
// src/server/rate-limit/rate-limit.ts
|
|
330
|
-
function createRateLimiter(config, opts = {}) {
|
|
331
|
-
const store = opts.store ?? new InMemoryStore();
|
|
332
|
-
const isInMemory = store instanceof InMemoryStore;
|
|
333
|
-
return function checkRateLimit(req) {
|
|
334
|
-
const key = req.socket?.remoteAddress ?? "unknown";
|
|
335
|
-
if (isInMemory) {
|
|
336
|
-
const state = store.incrSync(key, config.windowMs);
|
|
337
|
-
return resultFromState(state, config);
|
|
338
|
-
}
|
|
339
|
-
throw new Error(
|
|
340
|
-
"createRateLimiter: async RateLimitStore implementations are not supported by this sync fa\xE7ade. Use the InMemoryStore default or build a custom middleware around the async store directly."
|
|
341
|
-
);
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
function resultFromState(state, config) {
|
|
345
|
-
if (state.count > config.max) {
|
|
346
|
-
const retryAfter = Math.ceil((state.resetAt - Date.now()) / 1e3);
|
|
347
|
-
return {
|
|
348
|
-
limited: true,
|
|
349
|
-
headers: {
|
|
350
|
-
"X-RateLimit-Limit": String(config.max),
|
|
351
|
-
"X-RateLimit-Remaining": "0",
|
|
352
|
-
"Retry-After": String(retryAfter)
|
|
353
|
-
}
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
return {
|
|
357
|
-
limited: false,
|
|
358
|
-
headers: {
|
|
359
|
-
"X-RateLimit-Limit": String(config.max),
|
|
360
|
-
"X-RateLimit-Remaining": String(Math.max(0, config.max - state.count))
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// src/server/scan/module-loader.ts
|
|
366
|
-
import { pathToFileURL } from "url";
|
|
367
|
-
function createViteLoader(vite) {
|
|
368
|
-
return (path) => vite.ssrLoadModule(path);
|
|
369
|
-
}
|
|
370
|
-
function createProductionLoader() {
|
|
371
|
-
return async (path) => {
|
|
372
|
-
const url = pathToFileURL(path).href;
|
|
373
|
-
return import(url);
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
|
|
377
8
|
// src/server/body-parser.ts
|
|
378
9
|
import { basename } from "path";
|
|
379
10
|
import Busboy from "busboy";
|
|
@@ -872,6 +503,13 @@ function validateCsrf(req) {
|
|
|
872
503
|
host: host !== void 0 ? pickHeader(host) || null : null
|
|
873
504
|
});
|
|
874
505
|
}
|
|
506
|
+
function validateCsrfRequest(request) {
|
|
507
|
+
return isCsrfValidFromHeaders({
|
|
508
|
+
csrfActionHeader: request.headers.get("x-theo-action"),
|
|
509
|
+
origin: request.headers.get("origin"),
|
|
510
|
+
host: request.headers.get("host")
|
|
511
|
+
});
|
|
512
|
+
}
|
|
875
513
|
function dispatchCsrfWarn2(req, reason, logger, auditLogger, pathFallback = "") {
|
|
876
514
|
const payload = {
|
|
877
515
|
event: "csrf.warn",
|
|
@@ -1205,796 +843,1165 @@ async function executeRoute(ctx) {
|
|
|
1205
843
|
return;
|
|
1206
844
|
}
|
|
1207
845
|
}
|
|
1208
|
-
const rc = routeConfig;
|
|
1209
|
-
const parseResult = await parseQueryAndBody(req, res, requestId);
|
|
1210
|
-
if (!parseResult.ok) return;
|
|
1211
|
-
const { query } = parseResult.data;
|
|
1212
|
-
let { body } = parseResult.data;
|
|
1213
|
-
const validationResult = runZodValidation(rc, res, requestId, { query, body, params });
|
|
1214
|
-
if (!validationResult.ok) return;
|
|
1215
|
-
body = validationResult.data.body;
|
|
1216
|
-
if (pluginRunner) {
|
|
1217
|
-
const preResult = await pluginRunner.runPreHandler(buildPluginCtx(ctx2));
|
|
1218
|
-
if (preResult.shortCircuited) return;
|
|
1219
|
-
}
|
|
1220
|
-
const callableHandler = handler;
|
|
1221
|
-
const handlerResult = await callableHandler({ query, body, params, request: req, ctx: ctx2 });
|
|
1222
|
-
if (handlerResult === void 0 || handlerResult === null) {
|
|
1223
|
-
sendJson(res, null, rc.status ?? 204, transformer);
|
|
1224
|
-
if (pluginRunner) await pluginRunner.runOnResponse(buildPluginCtx(ctx2));
|
|
1225
|
-
return;
|
|
1226
|
-
}
|
|
1227
|
-
if (handlerResult instanceof Response) {
|
|
1228
|
-
const headersBag = {};
|
|
1229
|
-
for (const [k, v] of handlerResult.headers) {
|
|
1230
|
-
if (k.toLowerCase() !== "set-cookie") headersBag[k] = v;
|
|
1231
|
-
}
|
|
1232
|
-
const setCookies = handlerResult.headers.getSetCookie();
|
|
1233
|
-
if (setCookies.length > 0) {
|
|
1234
|
-
res.setHeader("Set-Cookie", setCookies);
|
|
1235
|
-
}
|
|
1236
|
-
res.writeHead(handlerResult.status, headersBag);
|
|
1237
|
-
if (handlerResult.body) {
|
|
1238
|
-
await pipeWebStreamToResponse(handlerResult.body, res, {
|
|
1239
|
-
buildPluginCtx,
|
|
1240
|
-
ctx: ctx2,
|
|
1241
|
-
method,
|
|
1242
|
-
pluginRunner,
|
|
1243
|
-
requestId,
|
|
1244
|
-
routePath: route.routePath
|
|
1245
|
-
});
|
|
846
|
+
const rc = routeConfig;
|
|
847
|
+
const parseResult = await parseQueryAndBody(req, res, requestId);
|
|
848
|
+
if (!parseResult.ok) return;
|
|
849
|
+
const { query } = parseResult.data;
|
|
850
|
+
let { body } = parseResult.data;
|
|
851
|
+
const validationResult = runZodValidation(rc, res, requestId, { query, body, params });
|
|
852
|
+
if (!validationResult.ok) return;
|
|
853
|
+
body = validationResult.data.body;
|
|
854
|
+
if (pluginRunner) {
|
|
855
|
+
const preResult = await pluginRunner.runPreHandler(buildPluginCtx(ctx2));
|
|
856
|
+
if (preResult.shortCircuited) return;
|
|
857
|
+
}
|
|
858
|
+
const callableHandler = handler;
|
|
859
|
+
const handlerResult = await callableHandler({ query, body, params, request: req, ctx: ctx2 });
|
|
860
|
+
if (handlerResult === void 0 || handlerResult === null) {
|
|
861
|
+
sendJson(res, null, rc.status ?? 204, transformer);
|
|
862
|
+
if (pluginRunner) await pluginRunner.runOnResponse(buildPluginCtx(ctx2));
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (handlerResult instanceof Response) {
|
|
866
|
+
const headersBag = {};
|
|
867
|
+
for (const [k, v] of handlerResult.headers) {
|
|
868
|
+
if (k.toLowerCase() !== "set-cookie") headersBag[k] = v;
|
|
869
|
+
}
|
|
870
|
+
const setCookies = handlerResult.headers.getSetCookie();
|
|
871
|
+
if (setCookies.length > 0) {
|
|
872
|
+
res.setHeader("Set-Cookie", setCookies);
|
|
873
|
+
}
|
|
874
|
+
res.writeHead(handlerResult.status, headersBag);
|
|
875
|
+
if (handlerResult.body) {
|
|
876
|
+
await pipeWebStreamToResponse(handlerResult.body, res, {
|
|
877
|
+
buildPluginCtx,
|
|
878
|
+
ctx: ctx2,
|
|
879
|
+
method,
|
|
880
|
+
pluginRunner,
|
|
881
|
+
requestId,
|
|
882
|
+
routePath: route.routePath
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
res.end();
|
|
886
|
+
if (pluginRunner) await pluginRunner.runOnResponse(buildPluginCtx(ctx2));
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
let responseBody = handlerResult;
|
|
890
|
+
if (isZodLike(rc.response)) {
|
|
891
|
+
const parsed = rc.response.safeParse(handlerResult);
|
|
892
|
+
if (!parsed.success) {
|
|
893
|
+
throw new TheoError({
|
|
894
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
895
|
+
message: "response validation failed",
|
|
896
|
+
ext: { issues: parsed.error?.issues }
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
responseBody = parsed.data;
|
|
900
|
+
}
|
|
901
|
+
sendJson(res, responseBody, rc.status ?? 200, transformer);
|
|
902
|
+
if (pluginRunner) await pluginRunner.runOnResponse(buildPluginCtx(ctx2));
|
|
903
|
+
} catch (err) {
|
|
904
|
+
if (pluginRunner) {
|
|
905
|
+
const errCtxObj = {};
|
|
906
|
+
pluginRunner.applyDecorations(errCtxObj);
|
|
907
|
+
await pluginRunner.runOnError(buildPluginCtx(errCtxObj), err);
|
|
908
|
+
if (res.writableEnded) {
|
|
909
|
+
await pluginRunner.runOnResponse(buildPluginCtx(errCtxObj), { inErrorPath: true });
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
if (isAuthRequiredError(err)) {
|
|
914
|
+
const authErr = err;
|
|
915
|
+
sendError(res, authErr.code, authErr.message, authErr.status, void 0, requestId);
|
|
916
|
+
if (pluginRunner) {
|
|
917
|
+
const errCtxObj = {};
|
|
918
|
+
pluginRunner.applyDecorations(errCtxObj);
|
|
919
|
+
await pluginRunner.runOnResponse(buildPluginCtx(errCtxObj), { inErrorPath: true });
|
|
920
|
+
}
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
sendError(
|
|
924
|
+
res,
|
|
925
|
+
"INTERNAL_ERROR",
|
|
926
|
+
err instanceof Error && err.message ? err.message : "Internal server error",
|
|
927
|
+
500,
|
|
928
|
+
void 0,
|
|
929
|
+
requestId
|
|
930
|
+
);
|
|
931
|
+
if (pluginRunner) {
|
|
932
|
+
const errCtxObj = {};
|
|
933
|
+
pluginRunner.applyDecorations(errCtxObj);
|
|
934
|
+
await pluginRunner.runOnResponse(buildPluginCtx(errCtxObj), { inErrorPath: true });
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// src/core/contracts/action-protocol.ts
|
|
940
|
+
var CODE_TO_STATUS = {
|
|
941
|
+
VALIDATION_ERROR: 422,
|
|
942
|
+
BAD_REQUEST: 400,
|
|
943
|
+
UNAUTHORIZED: 401,
|
|
944
|
+
FORBIDDEN: 403,
|
|
945
|
+
NOT_FOUND: 404,
|
|
946
|
+
METHOD_NOT_ALLOWED: 405,
|
|
947
|
+
CONFLICT: 409,
|
|
948
|
+
CONTENT_TOO_LARGE: 413,
|
|
949
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
950
|
+
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
951
|
+
TOO_MANY_REQUESTS: 429,
|
|
952
|
+
INTERNAL_SERVER_ERROR: 500
|
|
953
|
+
};
|
|
954
|
+
var STATUS_TO_CODE = {
|
|
955
|
+
400: "BAD_REQUEST",
|
|
956
|
+
401: "UNAUTHORIZED",
|
|
957
|
+
403: "FORBIDDEN",
|
|
958
|
+
404: "NOT_FOUND",
|
|
959
|
+
405: "METHOD_NOT_ALLOWED",
|
|
960
|
+
409: "CONFLICT",
|
|
961
|
+
413: "PAYLOAD_TOO_LARGE",
|
|
962
|
+
415: "UNSUPPORTED_MEDIA_TYPE",
|
|
963
|
+
422: "VALIDATION_ERROR",
|
|
964
|
+
429: "TOO_MANY_REQUESTS",
|
|
965
|
+
500: "INTERNAL_SERVER_ERROR"
|
|
966
|
+
};
|
|
967
|
+
var ActionError = class _ActionError extends Error {
|
|
968
|
+
// Discriminator widened to the full union so subclasses can narrow to their
|
|
969
|
+
// specific literal (TypeScript would otherwise reject the override). Concrete
|
|
970
|
+
// values are still always exact literals at runtime.
|
|
971
|
+
type = "TheoActionError";
|
|
972
|
+
code;
|
|
973
|
+
status;
|
|
974
|
+
constructor(params) {
|
|
975
|
+
super(params.message ?? params.code);
|
|
976
|
+
this.code = params.code;
|
|
977
|
+
this.status = _ActionError.codeToStatus(params.code);
|
|
978
|
+
if (params.stack) {
|
|
979
|
+
this.stack = params.stack;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* G5 T2.4 — canonical envelope view of the action error. Maps the G3
|
|
984
|
+
* ActionErrorCode to a canonical TheoErrorCode (VALIDATION_ERROR ↔
|
|
985
|
+
* UNPROCESSABLE_ENTITY, CONTENT_TOO_LARGE ↔ PAYLOAD_TOO_LARGE) so consumer
|
|
986
|
+
* UI / SDK code can switch on the unified envelope.
|
|
987
|
+
*
|
|
988
|
+
* Subclasses override to populate `ext` (see `ActionInputError.envelope`).
|
|
989
|
+
*/
|
|
990
|
+
get envelope() {
|
|
991
|
+
return {
|
|
992
|
+
code: _ActionError.toTheoErrorCode(this.code),
|
|
993
|
+
message: this.message
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Translate G3 ActionErrorCode → canonical TheoErrorCode (blueprint
|
|
998
|
+
* Recommendations § "G3 ActionError becomes inaugural envelope user").
|
|
999
|
+
*/
|
|
1000
|
+
static toTheoErrorCode(code) {
|
|
1001
|
+
if (code === "VALIDATION_ERROR") return "UNPROCESSABLE_ENTITY";
|
|
1002
|
+
if (code === "CONTENT_TOO_LARGE") return "PAYLOAD_TOO_LARGE";
|
|
1003
|
+
return code;
|
|
1004
|
+
}
|
|
1005
|
+
static codeToStatus(code) {
|
|
1006
|
+
return CODE_TO_STATUS[code];
|
|
1007
|
+
}
|
|
1008
|
+
static statusToCode(status) {
|
|
1009
|
+
return STATUS_TO_CODE[status] ?? "INTERNAL_SERVER_ERROR";
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Parse a serialized error JSON back into the typed class hierarchy.
|
|
1013
|
+
* Distinguishes `TheoActionInputError` (with `issues` array) from
|
|
1014
|
+
* `TheoActionError` via the `type` discriminator. Falls back to
|
|
1015
|
+
* `INTERNAL_SERVER_ERROR` for malformed bodies (non-object, missing
|
|
1016
|
+
* `type`, unknown `code`).
|
|
1017
|
+
*/
|
|
1018
|
+
static fromJson(body) {
|
|
1019
|
+
if (typeof body !== "object" || body === null) {
|
|
1020
|
+
return new _ActionError({ code: "INTERNAL_SERVER_ERROR" });
|
|
1021
|
+
}
|
|
1022
|
+
const obj = body;
|
|
1023
|
+
if (obj.type === "TheoActionInputError" && Array.isArray(obj.issues)) {
|
|
1024
|
+
return new ActionInputError(obj.issues);
|
|
1025
|
+
}
|
|
1026
|
+
if (obj.type === "TheoActionError" && typeof obj.code === "string" && obj.code in CODE_TO_STATUS) {
|
|
1027
|
+
return new _ActionError({
|
|
1028
|
+
code: obj.code,
|
|
1029
|
+
message: typeof obj.message === "string" ? obj.message : void 0
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
return new _ActionError({ code: "INTERNAL_SERVER_ERROR" });
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
var ActionInputError = class extends ActionError {
|
|
1036
|
+
type = "TheoActionInputError";
|
|
1037
|
+
issues;
|
|
1038
|
+
fields;
|
|
1039
|
+
constructor(rawIssues) {
|
|
1040
|
+
super({ code: "VALIDATION_ERROR", message: "Validation failed" });
|
|
1041
|
+
this.issues = extractUniversalIssues(rawIssues);
|
|
1042
|
+
this.fields = buildFieldsMap(this.issues);
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* G5 T2.4 — envelope view with ValidationFieldsExt populated from .fields.
|
|
1046
|
+
* UI consumers can `switch (env.code)` on `UNPROCESSABLE_ENTITY` and read
|
|
1047
|
+
* `(env.ext as ValidationFieldsExt).fields` for field-level rendering.
|
|
1048
|
+
*/
|
|
1049
|
+
get envelope() {
|
|
1050
|
+
return {
|
|
1051
|
+
code: "UNPROCESSABLE_ENTITY",
|
|
1052
|
+
message: this.message,
|
|
1053
|
+
ext: { fields: this.fields }
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
function buildFieldsMap(issues) {
|
|
1058
|
+
const fields = {};
|
|
1059
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1060
|
+
for (const issue of issues) {
|
|
1061
|
+
const key = issue.path.length === 0 ? "" : issue.path.join(".");
|
|
1062
|
+
const dedupeKey = `${key}\0${issue.message}`;
|
|
1063
|
+
if (seen.has(dedupeKey)) continue;
|
|
1064
|
+
seen.add(dedupeKey);
|
|
1065
|
+
const bucket = fields[key] ?? [];
|
|
1066
|
+
bucket.push(issue.message);
|
|
1067
|
+
fields[key] = bucket;
|
|
1068
|
+
}
|
|
1069
|
+
return fields;
|
|
1070
|
+
}
|
|
1071
|
+
function extractUniversalIssues(raw) {
|
|
1072
|
+
if (!Array.isArray(raw)) return [];
|
|
1073
|
+
const out = [];
|
|
1074
|
+
for (const entry of raw) {
|
|
1075
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
1076
|
+
const obj = entry;
|
|
1077
|
+
if (!Array.isArray(obj.path)) continue;
|
|
1078
|
+
if (typeof obj.message !== "string") continue;
|
|
1079
|
+
const path = [];
|
|
1080
|
+
let pathValid = true;
|
|
1081
|
+
for (const seg of obj.path) {
|
|
1082
|
+
if (typeof seg === "string" || typeof seg === "number") {
|
|
1083
|
+
path.push(seg);
|
|
1084
|
+
} else {
|
|
1085
|
+
pathValid = false;
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (!pathValid) continue;
|
|
1090
|
+
out.push({
|
|
1091
|
+
path,
|
|
1092
|
+
message: obj.message,
|
|
1093
|
+
code: typeof obj.code === "string" ? obj.code : void 0
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
return out;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// src/server/http/form-data-to-object.ts
|
|
1100
|
+
import { z } from "zod";
|
|
1101
|
+
function formDataToObject(formData, schema, prefix = "") {
|
|
1102
|
+
const shape = schema.shape;
|
|
1103
|
+
const out = {};
|
|
1104
|
+
for (const [key, rawValidator] of Object.entries(shape)) {
|
|
1105
|
+
const fullKey = prefix + key;
|
|
1106
|
+
const validator = unwrapWrappers(rawValidator);
|
|
1107
|
+
if (validator instanceof z.ZodObject) {
|
|
1108
|
+
const nestedPrefix = `${fullKey}.`;
|
|
1109
|
+
const hasNestedKeys = [...formData.keys()].some((k) => k.startsWith(nestedPrefix));
|
|
1110
|
+
if (hasNestedKeys) {
|
|
1111
|
+
out[key] = formDataToObject(formData, validator, nestedPrefix);
|
|
1112
|
+
continue;
|
|
1246
1113
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
return;
|
|
1114
|
+
out[key] = unwrapMissingDefault(rawValidator);
|
|
1115
|
+
continue;
|
|
1250
1116
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
throw new TheoError({
|
|
1256
|
-
code: "INTERNAL_SERVER_ERROR",
|
|
1257
|
-
message: "response validation failed",
|
|
1258
|
-
ext: { issues: parsed.error?.issues }
|
|
1259
|
-
});
|
|
1260
|
-
}
|
|
1261
|
-
responseBody = parsed.data;
|
|
1117
|
+
if (validator instanceof z.ZodArray) {
|
|
1118
|
+
const values = formData.getAll(fullKey);
|
|
1119
|
+
out[key] = coerceArrayElements(values, validator);
|
|
1120
|
+
continue;
|
|
1262
1121
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
if (pluginRunner) {
|
|
1267
|
-
const errCtxObj = {};
|
|
1268
|
-
pluginRunner.applyDecorations(errCtxObj);
|
|
1269
|
-
await pluginRunner.runOnError(buildPluginCtx(errCtxObj), err);
|
|
1270
|
-
if (res.writableEnded) {
|
|
1271
|
-
await pluginRunner.runOnResponse(buildPluginCtx(errCtxObj), { inErrorPath: true });
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1122
|
+
if (validator instanceof z.ZodBoolean) {
|
|
1123
|
+
out[key] = coerceBoolean(formData, fullKey);
|
|
1124
|
+
continue;
|
|
1274
1125
|
}
|
|
1275
|
-
if (
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
pluginRunner.applyDecorations(errCtxObj);
|
|
1281
|
-
await pluginRunner.runOnResponse(buildPluginCtx(errCtxObj), { inErrorPath: true });
|
|
1282
|
-
}
|
|
1283
|
-
return;
|
|
1126
|
+
if (formData.has(fullKey)) {
|
|
1127
|
+
const raw = formData.get(fullKey);
|
|
1128
|
+
out[key] = coerceScalar(raw, validator);
|
|
1129
|
+
} else {
|
|
1130
|
+
out[key] = unwrapMissingDefault(rawValidator);
|
|
1284
1131
|
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1132
|
+
}
|
|
1133
|
+
return out;
|
|
1134
|
+
}
|
|
1135
|
+
function unwrapWrappers(validator) {
|
|
1136
|
+
let inner = validator;
|
|
1137
|
+
while (inner instanceof z.ZodOptional || inner instanceof z.ZodNullable || inner instanceof z.ZodDefault) {
|
|
1138
|
+
inner = getInnerType(inner);
|
|
1139
|
+
}
|
|
1140
|
+
return inner;
|
|
1141
|
+
}
|
|
1142
|
+
function getInnerType(wrapper) {
|
|
1143
|
+
const def = wrapper.def;
|
|
1144
|
+
if (def.innerType) return def.innerType;
|
|
1145
|
+
const legacyDef = wrapper._def;
|
|
1146
|
+
if (legacyDef?.innerType) return legacyDef.innerType;
|
|
1147
|
+
return wrapper;
|
|
1148
|
+
}
|
|
1149
|
+
function unwrapMissingDefault(validator) {
|
|
1150
|
+
let cursor = validator;
|
|
1151
|
+
while (cursor instanceof z.ZodOptional || cursor instanceof z.ZodNullable || cursor instanceof z.ZodDefault) {
|
|
1152
|
+
if (cursor instanceof z.ZodDefault) {
|
|
1153
|
+
const def = cursor.def;
|
|
1154
|
+
return typeof def.defaultValue === "function" ? def.defaultValue() : def.defaultValue;
|
|
1297
1155
|
}
|
|
1156
|
+
if (cursor instanceof z.ZodNullable) return null;
|
|
1157
|
+
cursor = getInnerType(cursor);
|
|
1158
|
+
}
|
|
1159
|
+
return void 0;
|
|
1160
|
+
}
|
|
1161
|
+
function coerceScalar(raw, validator) {
|
|
1162
|
+
if (raw === null) return void 0;
|
|
1163
|
+
if (validator instanceof z.ZodNumber) {
|
|
1164
|
+
return typeof raw === "string" ? Number(raw) : raw;
|
|
1165
|
+
}
|
|
1166
|
+
return raw;
|
|
1167
|
+
}
|
|
1168
|
+
function coerceArrayElements(values, arrayValidator) {
|
|
1169
|
+
const def = arrayValidator.def;
|
|
1170
|
+
const elementSchema = def.element ?? (def.type instanceof z.ZodType ? def.type : void 0);
|
|
1171
|
+
const elementType = elementSchema ? unwrapWrappers(elementSchema) : void 0;
|
|
1172
|
+
if (elementType instanceof z.ZodNumber) {
|
|
1173
|
+
return values.map((v) => typeof v === "string" ? Number(v) : v);
|
|
1174
|
+
}
|
|
1175
|
+
if (elementType instanceof z.ZodBoolean) {
|
|
1176
|
+
return values.map((v) => {
|
|
1177
|
+
if (v === "true") return true;
|
|
1178
|
+
if (v === "false") return false;
|
|
1179
|
+
return Boolean(v);
|
|
1180
|
+
});
|
|
1298
1181
|
}
|
|
1182
|
+
return values;
|
|
1183
|
+
}
|
|
1184
|
+
function coerceBoolean(formData, key) {
|
|
1185
|
+
if (!formData.has(key)) return void 0;
|
|
1186
|
+
const val = formData.get(key);
|
|
1187
|
+
if (val === "true") return true;
|
|
1188
|
+
if (val === "false") return false;
|
|
1189
|
+
return Boolean(val);
|
|
1299
1190
|
}
|
|
1300
1191
|
|
|
1301
|
-
// src/core/contracts/
|
|
1302
|
-
var
|
|
1303
|
-
|
|
1192
|
+
// src/core/contracts/envelope-code-to-status.ts
|
|
1193
|
+
var CODE_TO_STATUS2 = {
|
|
1194
|
+
// 4xx — client errors
|
|
1304
1195
|
BAD_REQUEST: 400,
|
|
1305
1196
|
UNAUTHORIZED: 401,
|
|
1306
1197
|
FORBIDDEN: 403,
|
|
1307
1198
|
NOT_FOUND: 404,
|
|
1308
1199
|
METHOD_NOT_ALLOWED: 405,
|
|
1309
1200
|
CONFLICT: 409,
|
|
1310
|
-
|
|
1201
|
+
PRECONDITION_FAILED: 412,
|
|
1311
1202
|
PAYLOAD_TOO_LARGE: 413,
|
|
1312
1203
|
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
1204
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
1313
1205
|
TOO_MANY_REQUESTS: 429,
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
429: "TOO_MANY_REQUESTS",
|
|
1327
|
-
500: "INTERNAL_SERVER_ERROR"
|
|
1206
|
+
RATE_LIMITED: 429,
|
|
1207
|
+
// 5xx — server errors
|
|
1208
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
1209
|
+
NOT_IMPLEMENTED: 501,
|
|
1210
|
+
BAD_GATEWAY: 502,
|
|
1211
|
+
SERVICE_UNAVAILABLE: 503,
|
|
1212
|
+
GATEWAY_TIMEOUT: 504,
|
|
1213
|
+
// SDK-domain codes — intentionally 500
|
|
1214
|
+
AGENT_RUN_ERROR: 500,
|
|
1215
|
+
PROVIDER_KEY_MISSING: 500,
|
|
1216
|
+
BUDGET_EXCEEDED: 500,
|
|
1217
|
+
CREDENTIAL_POOL_EXHAUSTED: 500
|
|
1328
1218
|
};
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1219
|
+
function envelopeCodeToStatus(code) {
|
|
1220
|
+
return CODE_TO_STATUS2[code] ?? 500;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/server/http/handle-request-error.ts
|
|
1224
|
+
async function handleRequestError(err, c) {
|
|
1225
|
+
if (c.pluginRunner) {
|
|
1226
|
+
const errCtxObj = {};
|
|
1227
|
+
c.pluginRunner.applyDecorations(errCtxObj);
|
|
1228
|
+
try {
|
|
1229
|
+
await c.pluginRunner.runOnError(c.buildPluginCtx(errCtxObj), err);
|
|
1230
|
+
} catch {
|
|
1231
|
+
}
|
|
1232
|
+
if (c.res.writableEnded) {
|
|
1233
|
+
try {
|
|
1234
|
+
await c.pluginRunner.runOnResponse(c.buildPluginCtx(errCtxObj), {
|
|
1235
|
+
inErrorPath: true
|
|
1236
|
+
});
|
|
1237
|
+
} catch {
|
|
1238
|
+
}
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (isAuthRequiredError(err)) {
|
|
1243
|
+
const authErr = err;
|
|
1244
|
+
sendError(c.res, authErr.code, authErr.message, authErr.status, void 0, c.requestId);
|
|
1245
|
+
} else {
|
|
1246
|
+
const envelope = serverErrorToEnvelope(err);
|
|
1247
|
+
if (envelope.code === "INTERNAL_SERVER_ERROR") {
|
|
1248
|
+
sendError(
|
|
1249
|
+
c.res,
|
|
1250
|
+
"INTERNAL_ERROR",
|
|
1251
|
+
err instanceof Error ? err.message : "Internal server error",
|
|
1252
|
+
500,
|
|
1253
|
+
void 0,
|
|
1254
|
+
c.requestId
|
|
1255
|
+
);
|
|
1256
|
+
} else {
|
|
1257
|
+
sendError(
|
|
1258
|
+
c.res,
|
|
1259
|
+
envelope.code,
|
|
1260
|
+
envelope.message,
|
|
1261
|
+
envelopeCodeToStatus(envelope.code),
|
|
1262
|
+
void 0,
|
|
1263
|
+
c.requestId
|
|
1264
|
+
);
|
|
1342
1265
|
}
|
|
1343
1266
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1267
|
+
if (c.pluginRunner) {
|
|
1268
|
+
const errCtxObj = {};
|
|
1269
|
+
c.pluginRunner.applyDecorations(errCtxObj);
|
|
1270
|
+
try {
|
|
1271
|
+
await c.pluginRunner.runOnResponse(c.buildPluginCtx(errCtxObj), {
|
|
1272
|
+
inErrorPath: true
|
|
1273
|
+
});
|
|
1274
|
+
} catch {
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/server/http/serialize-action-result.ts
|
|
1280
|
+
import { stringify as devalueStringify } from "devalue";
|
|
1281
|
+
var DEFAULT_RESPONSE_BODY_SIZE_LIMIT = 5 * 1024 * 1024;
|
|
1282
|
+
function serializeActionResult(result, options = {}) {
|
|
1283
|
+
const limit = options.responseBodySizeLimit ?? DEFAULT_RESPONSE_BODY_SIZE_LIMIT;
|
|
1284
|
+
if (result.error !== void 0) {
|
|
1285
|
+
const body2 = result.error instanceof ActionInputError ? JSON.stringify({
|
|
1286
|
+
type: result.error.type,
|
|
1287
|
+
code: result.error.code,
|
|
1288
|
+
message: result.error.message,
|
|
1289
|
+
issues: result.error.issues,
|
|
1290
|
+
fields: result.error.fields
|
|
1291
|
+
}) : JSON.stringify({
|
|
1292
|
+
type: result.error.type,
|
|
1293
|
+
code: result.error.code,
|
|
1294
|
+
message: result.error.message
|
|
1295
|
+
});
|
|
1296
|
+
if (Buffer.byteLength(body2, "utf8") > limit) {
|
|
1297
|
+
throw new ActionError({
|
|
1298
|
+
code: "PAYLOAD_TOO_LARGE",
|
|
1299
|
+
message: `Serialized error body exceeds limit (${limit} bytes)`
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1353
1302
|
return {
|
|
1354
|
-
|
|
1355
|
-
|
|
1303
|
+
type: "error",
|
|
1304
|
+
status: result.error.status,
|
|
1305
|
+
contentType: "application/json",
|
|
1306
|
+
body: body2
|
|
1356
1307
|
};
|
|
1357
1308
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
* Recommendations § "G3 ActionError becomes inaugural envelope user").
|
|
1361
|
-
*/
|
|
1362
|
-
static toTheoErrorCode(code) {
|
|
1363
|
-
if (code === "VALIDATION_ERROR") return "UNPROCESSABLE_ENTITY";
|
|
1364
|
-
if (code === "CONTENT_TOO_LARGE") return "PAYLOAD_TOO_LARGE";
|
|
1365
|
-
return code;
|
|
1366
|
-
}
|
|
1367
|
-
static codeToStatus(code) {
|
|
1368
|
-
return CODE_TO_STATUS[code];
|
|
1309
|
+
if (result.data === void 0) {
|
|
1310
|
+
return { type: "empty", status: 204 };
|
|
1369
1311
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1312
|
+
if (result.data instanceof Response) {
|
|
1313
|
+
throw new ActionError({
|
|
1314
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
1315
|
+
message: "Action handler cannot serialize Response objects \u2014 return plain data instead"
|
|
1316
|
+
});
|
|
1372
1317
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
}
|
|
1384
|
-
const obj = body;
|
|
1385
|
-
if (obj.type === "TheoActionInputError" && Array.isArray(obj.issues)) {
|
|
1386
|
-
return new ActionInputError(obj.issues);
|
|
1387
|
-
}
|
|
1388
|
-
if (obj.type === "TheoActionError" && typeof obj.code === "string" && obj.code in CODE_TO_STATUS) {
|
|
1389
|
-
return new _ActionError({
|
|
1390
|
-
code: obj.code,
|
|
1391
|
-
message: typeof obj.message === "string" ? obj.message : void 0
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
return new _ActionError({ code: "INTERNAL_SERVER_ERROR" });
|
|
1318
|
+
let body;
|
|
1319
|
+
try {
|
|
1320
|
+
body = devalueStringify(result.data, {
|
|
1321
|
+
URL: (value) => value instanceof URL && value.href
|
|
1322
|
+
});
|
|
1323
|
+
} catch (e) {
|
|
1324
|
+
throw new ActionError({
|
|
1325
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
1326
|
+
message: `Action data not serializable: ${e instanceof Error ? e.message : String(e)}`
|
|
1327
|
+
});
|
|
1395
1328
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
constructor(rawIssues) {
|
|
1402
|
-
super({ code: "VALIDATION_ERROR", message: "Validation failed" });
|
|
1403
|
-
this.issues = extractUniversalIssues(rawIssues);
|
|
1404
|
-
this.fields = buildFieldsMap(this.issues);
|
|
1329
|
+
if (Buffer.byteLength(body, "utf8") > limit) {
|
|
1330
|
+
throw new ActionError({
|
|
1331
|
+
code: "PAYLOAD_TOO_LARGE",
|
|
1332
|
+
message: `Serialized response body exceeds limit (${limit} bytes)`
|
|
1333
|
+
});
|
|
1405
1334
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1335
|
+
return {
|
|
1336
|
+
type: "data",
|
|
1337
|
+
status: 200,
|
|
1338
|
+
contentType: "application/json+devalue",
|
|
1339
|
+
body
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// src/server/http/action-execute.ts
|
|
1344
|
+
var __IS_DEV = (() => {
|
|
1345
|
+
try {
|
|
1346
|
+
return import.meta.env?.DEV === true;
|
|
1347
|
+
} catch {
|
|
1348
|
+
return process.env.NODE_ENV !== "production";
|
|
1417
1349
|
}
|
|
1418
|
-
};
|
|
1419
|
-
function
|
|
1420
|
-
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1350
|
+
})();
|
|
1351
|
+
function isActionConfig(value) {
|
|
1352
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1353
|
+
const candidate = value;
|
|
1354
|
+
if (typeof candidate.handler !== "function") return false;
|
|
1355
|
+
const input = candidate.input;
|
|
1356
|
+
return typeof input?.safeParse === "function";
|
|
1357
|
+
}
|
|
1358
|
+
async function executeAction(filePath, exportName, req, res, loadModule, serverDir, requestId, pluginRunner, csrfMode = "strict", disallowed) {
|
|
1359
|
+
return executeActionWithOptions({
|
|
1360
|
+
filePath,
|
|
1361
|
+
exportName,
|
|
1362
|
+
req,
|
|
1363
|
+
res,
|
|
1364
|
+
loadModule,
|
|
1365
|
+
serverDir,
|
|
1366
|
+
requestId,
|
|
1367
|
+
pluginRunner,
|
|
1368
|
+
csrfMode,
|
|
1369
|
+
disallowed
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
async function loadActionConfig(loadModule, filePath, exportName, res, requestId) {
|
|
1373
|
+
const mod = await loadModule(filePath);
|
|
1374
|
+
const exportedValue = mod[exportName];
|
|
1375
|
+
if (!isActionConfig(exportedValue)) {
|
|
1376
|
+
sendError(res, "NOT_FOUND", `Action "${exportName}" not found`, 404, void 0, requestId);
|
|
1377
|
+
return null;
|
|
1430
1378
|
}
|
|
1431
|
-
return
|
|
1379
|
+
return exportedValue;
|
|
1432
1380
|
}
|
|
1433
|
-
function
|
|
1434
|
-
if (
|
|
1435
|
-
const
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1381
|
+
function enforceCsrfForAction(req, res, ctx) {
|
|
1382
|
+
if (ctx.actionConfig.csrf === false) return true;
|
|
1383
|
+
const decision = enforceCsrf(
|
|
1384
|
+
req,
|
|
1385
|
+
ctx.csrfMode,
|
|
1386
|
+
{
|
|
1387
|
+
// T3.3 DRY — canonical dispatcher
|
|
1388
|
+
warn: dispatchCsrfWarn,
|
|
1389
|
+
path: req.url
|
|
1390
|
+
},
|
|
1391
|
+
ctx.disallowed
|
|
1392
|
+
);
|
|
1393
|
+
if (decision.allow) return true;
|
|
1394
|
+
sendError(
|
|
1395
|
+
res,
|
|
1396
|
+
"CSRF_INVALID",
|
|
1397
|
+
decision.reason ?? "CSRF check failed",
|
|
1398
|
+
403,
|
|
1399
|
+
void 0,
|
|
1400
|
+
ctx.requestId
|
|
1401
|
+
);
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
async function runPreHandlerPipeline(p) {
|
|
1405
|
+
if (p.serverDir) {
|
|
1406
|
+
const result = await runMiddlewareAndContext(p.req, p.res, p.loadModule, p.serverDir);
|
|
1407
|
+
if (result.aborted) return false;
|
|
1408
|
+
Object.assign(p.ctx, result.ctx ?? {});
|
|
1409
|
+
p.pluginRunner?.applyDecorations(p.ctx);
|
|
1410
|
+
}
|
|
1411
|
+
if (p.pluginRunner) {
|
|
1412
|
+
const preResult = await p.pluginRunner.runPreHandler(p.buildPluginCtx(p.ctx));
|
|
1413
|
+
if (preResult.shortCircuited) return false;
|
|
1457
1414
|
}
|
|
1458
|
-
return
|
|
1415
|
+
return true;
|
|
1459
1416
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
if (hasNestedKeys) {
|
|
1473
|
-
out[key] = formDataToObject(formData, validator, nestedPrefix);
|
|
1474
|
-
continue;
|
|
1475
|
-
}
|
|
1476
|
-
out[key] = unwrapMissingDefault(rawValidator);
|
|
1477
|
-
continue;
|
|
1478
|
-
}
|
|
1479
|
-
if (validator instanceof z.ZodArray) {
|
|
1480
|
-
const values = formData.getAll(fullKey);
|
|
1481
|
-
out[key] = coerceArrayElements(values, validator);
|
|
1482
|
-
continue;
|
|
1483
|
-
}
|
|
1484
|
-
if (validator instanceof z.ZodBoolean) {
|
|
1485
|
-
out[key] = coerceBoolean(formData, fullKey);
|
|
1486
|
-
continue;
|
|
1487
|
-
}
|
|
1488
|
-
if (formData.has(fullKey)) {
|
|
1489
|
-
const raw = formData.get(fullKey);
|
|
1490
|
-
out[key] = coerceScalar(raw, validator);
|
|
1417
|
+
async function readActionBody(req, res, requestId, actionConfig) {
|
|
1418
|
+
try {
|
|
1419
|
+
const parsed = await parseRequestBody(req);
|
|
1420
|
+
const accept = actionConfig.accept ?? "json";
|
|
1421
|
+
let body;
|
|
1422
|
+
if (accept === "form") {
|
|
1423
|
+
body = formDataToObject(
|
|
1424
|
+
synthesizeFormData(parsed),
|
|
1425
|
+
actionConfig.input
|
|
1426
|
+
);
|
|
1427
|
+
} else if (parsed.json !== void 0) {
|
|
1428
|
+
body = parsed.json;
|
|
1491
1429
|
} else {
|
|
1492
|
-
|
|
1430
|
+
body = parsed.fields;
|
|
1493
1431
|
}
|
|
1432
|
+
return { ok: true, body };
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
sendError(res, "VALIDATION_ERROR", err.message, 400, void 0, requestId);
|
|
1435
|
+
return { ok: false };
|
|
1494
1436
|
}
|
|
1495
|
-
return out;
|
|
1496
1437
|
}
|
|
1497
|
-
function
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1438
|
+
function synthesizeFormData(parsed) {
|
|
1439
|
+
const fd = new FormData();
|
|
1440
|
+
for (const [name, value] of Object.entries(parsed.fields)) {
|
|
1441
|
+
fd.append(name, value);
|
|
1501
1442
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
const legacyDef = wrapper._def;
|
|
1508
|
-
if (legacyDef?.innerType) return legacyDef.innerType;
|
|
1509
|
-
return wrapper;
|
|
1510
|
-
}
|
|
1511
|
-
function unwrapMissingDefault(validator) {
|
|
1512
|
-
let cursor = validator;
|
|
1513
|
-
while (cursor instanceof z.ZodOptional || cursor instanceof z.ZodNullable || cursor instanceof z.ZodDefault) {
|
|
1514
|
-
if (cursor instanceof z.ZodDefault) {
|
|
1515
|
-
const def = cursor.def;
|
|
1516
|
-
return typeof def.defaultValue === "function" ? def.defaultValue() : def.defaultValue;
|
|
1517
|
-
}
|
|
1518
|
-
if (cursor instanceof z.ZodNullable) return null;
|
|
1519
|
-
cursor = getInnerType(cursor);
|
|
1443
|
+
for (const file of parsed.files) {
|
|
1444
|
+
const blob = new Blob([file.buffer], {
|
|
1445
|
+
type: file.mimeType
|
|
1446
|
+
});
|
|
1447
|
+
fd.append(file.fieldname, blob, file.filename);
|
|
1520
1448
|
}
|
|
1521
|
-
return
|
|
1449
|
+
return fd;
|
|
1522
1450
|
}
|
|
1523
|
-
function
|
|
1524
|
-
if (
|
|
1525
|
-
|
|
1526
|
-
|
|
1451
|
+
function writeSerialized(res, serialized) {
|
|
1452
|
+
if (serialized.type === "empty") {
|
|
1453
|
+
res.statusCode = serialized.status;
|
|
1454
|
+
res.end();
|
|
1455
|
+
return;
|
|
1527
1456
|
}
|
|
1528
|
-
|
|
1457
|
+
res.statusCode = serialized.status;
|
|
1458
|
+
res.setHeader("Content-Type", serialized.contentType);
|
|
1459
|
+
res.end(serialized.body);
|
|
1529
1460
|
}
|
|
1530
|
-
function
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1461
|
+
async function emitActionCallTelemetry(name, startedAt, input, outcome) {
|
|
1462
|
+
if (!__IS_DEV) return;
|
|
1463
|
+
try {
|
|
1464
|
+
const mod = await import("./dispatcher-EJHL6JMJ.js");
|
|
1465
|
+
mod.dispatcher.onActionCall({
|
|
1466
|
+
// eslint-disable-next-line sonarjs/pseudo-random -- non-secret correlation id
|
|
1467
|
+
id: `act-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1468
|
+
timestamp: startedAt,
|
|
1469
|
+
name,
|
|
1470
|
+
input,
|
|
1471
|
+
output: outcome.status === "success" ? outcome.output : void 0,
|
|
1472
|
+
error: outcome.status === "error" ? {
|
|
1473
|
+
code: outcome.error.code,
|
|
1474
|
+
message: outcome.error.message,
|
|
1475
|
+
fields: outcome.error instanceof ActionInputError ? outcome.error.fields : void 0
|
|
1476
|
+
} : void 0,
|
|
1477
|
+
durationMs: Date.now() - startedAt,
|
|
1478
|
+
status: outcome.status
|
|
1479
|
+
});
|
|
1480
|
+
} catch {
|
|
1536
1481
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1482
|
+
}
|
|
1483
|
+
async function executeActionWithOptions(opts) {
|
|
1484
|
+
const {
|
|
1485
|
+
filePath,
|
|
1486
|
+
exportName,
|
|
1487
|
+
req,
|
|
1488
|
+
res,
|
|
1489
|
+
loadModule,
|
|
1490
|
+
serverDir,
|
|
1491
|
+
requestId,
|
|
1492
|
+
pluginRunner,
|
|
1493
|
+
csrfMode = "strict",
|
|
1494
|
+
disallowed
|
|
1495
|
+
} = opts;
|
|
1496
|
+
const buildPluginCtx = (ctxObj) => ({
|
|
1497
|
+
request: req,
|
|
1498
|
+
response: res,
|
|
1499
|
+
ctx: ctxObj,
|
|
1500
|
+
requestId: requestId ?? "no-id"
|
|
1501
|
+
});
|
|
1502
|
+
let ctx = {};
|
|
1503
|
+
try {
|
|
1504
|
+
if ((req.method ?? "GET").toUpperCase() !== "POST") {
|
|
1505
|
+
sendError(res, "METHOD_NOT_ALLOWED", "Actions only accept POST", 405, void 0, requestId);
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
if (pluginRunner) {
|
|
1509
|
+
pluginRunner.applyDecorations(ctx);
|
|
1510
|
+
const onReqResult = await pluginRunner.runOnRequest(buildPluginCtx(ctx));
|
|
1511
|
+
if (onReqResult.shortCircuited) return;
|
|
1512
|
+
}
|
|
1513
|
+
const actionConfig = await loadActionConfig(loadModule, filePath, exportName, res, requestId);
|
|
1514
|
+
if (!actionConfig) return;
|
|
1515
|
+
if (!enforceCsrfForAction(req, res, { actionConfig, csrfMode, disallowed, requestId })) {
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
const pipeline = {
|
|
1519
|
+
ctx,
|
|
1520
|
+
buildPluginCtx,
|
|
1521
|
+
pluginRunner,
|
|
1522
|
+
serverDir,
|
|
1523
|
+
loadModule,
|
|
1524
|
+
req,
|
|
1525
|
+
res
|
|
1526
|
+
};
|
|
1527
|
+
if (!await runPreHandlerPipeline(pipeline)) return;
|
|
1528
|
+
ctx = pipeline.ctx;
|
|
1529
|
+
const bodyOutcome = await readActionBody(req, res, requestId, actionConfig);
|
|
1530
|
+
if (!bodyOutcome.ok) return;
|
|
1531
|
+
const actionName = exportName === "default" ? deriveActionNameFromPath(filePath) : exportName;
|
|
1532
|
+
const startedAt = Date.now();
|
|
1533
|
+
const result = actionConfig.input.safeParse(bodyOutcome.body);
|
|
1534
|
+
if (!result.success) {
|
|
1535
|
+
const inputErr = new ActionInputError(result.error?.issues ?? []);
|
|
1536
|
+
writeSerialized(res, serializeActionResult({ data: void 0, error: inputErr }));
|
|
1537
|
+
await emitActionCallTelemetry(actionName, startedAt, bodyOutcome.body, {
|
|
1538
|
+
status: "error",
|
|
1539
|
+
error: inputErr
|
|
1540
|
+
});
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
await runActionHandler({
|
|
1544
|
+
actionConfig,
|
|
1545
|
+
actionName,
|
|
1546
|
+
startedAt,
|
|
1547
|
+
input: result.data,
|
|
1548
|
+
ctx,
|
|
1549
|
+
res,
|
|
1550
|
+
pluginRunner,
|
|
1551
|
+
buildPluginCtx
|
|
1542
1552
|
});
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
await handleActionError(err, { req, res, ctx, requestId, pluginRunner, buildPluginCtx });
|
|
1543
1555
|
}
|
|
1544
|
-
return values;
|
|
1545
1556
|
}
|
|
1546
|
-
function
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
if (val === "true") return true;
|
|
1550
|
-
if (val === "false") return false;
|
|
1551
|
-
return Boolean(val);
|
|
1557
|
+
function deriveActionNameFromPath(filePath) {
|
|
1558
|
+
const base = filePath.split(/[\\/]/).pop() ?? "unknown";
|
|
1559
|
+
return base.replace(/\.[jt]sx?$/, "");
|
|
1552
1560
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
BAD_REQUEST: 400,
|
|
1558
|
-
UNAUTHORIZED: 401,
|
|
1559
|
-
FORBIDDEN: 403,
|
|
1560
|
-
NOT_FOUND: 404,
|
|
1561
|
-
METHOD_NOT_ALLOWED: 405,
|
|
1562
|
-
CONFLICT: 409,
|
|
1563
|
-
PRECONDITION_FAILED: 412,
|
|
1564
|
-
PAYLOAD_TOO_LARGE: 413,
|
|
1565
|
-
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
1566
|
-
UNPROCESSABLE_ENTITY: 422,
|
|
1567
|
-
TOO_MANY_REQUESTS: 429,
|
|
1568
|
-
RATE_LIMITED: 429,
|
|
1569
|
-
// 5xx — server errors
|
|
1570
|
-
INTERNAL_SERVER_ERROR: 500,
|
|
1571
|
-
NOT_IMPLEMENTED: 501,
|
|
1572
|
-
BAD_GATEWAY: 502,
|
|
1573
|
-
SERVICE_UNAVAILABLE: 503,
|
|
1574
|
-
GATEWAY_TIMEOUT: 504,
|
|
1575
|
-
// SDK-domain codes — intentionally 500
|
|
1576
|
-
AGENT_RUN_ERROR: 500,
|
|
1577
|
-
PROVIDER_KEY_MISSING: 500,
|
|
1578
|
-
BUDGET_EXCEEDED: 500,
|
|
1579
|
-
CREDENTIAL_POOL_EXHAUSTED: 500
|
|
1580
|
-
};
|
|
1581
|
-
function envelopeCodeToStatus(code) {
|
|
1582
|
-
return CODE_TO_STATUS2[code] ?? 500;
|
|
1561
|
+
function isActionErrorLike(err) {
|
|
1562
|
+
if (err === null || typeof err !== "object") return false;
|
|
1563
|
+
const obj = err;
|
|
1564
|
+
return (obj.type === "TheoActionError" || obj.type === "TheoActionInputError") && typeof obj.code === "string" && typeof obj.status === "number";
|
|
1583
1565
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
try {
|
|
1591
|
-
await c.pluginRunner.runOnError(c.buildPluginCtx(errCtxObj), err);
|
|
1592
|
-
} catch {
|
|
1566
|
+
async function runActionHandler(args) {
|
|
1567
|
+
try {
|
|
1568
|
+
const handlerResult = await args.actionConfig.handler({ input: args.input, ctx: args.ctx });
|
|
1569
|
+
writeSerialized(args.res, serializeActionResult({ data: handlerResult, error: void 0 }));
|
|
1570
|
+
if (args.pluginRunner) {
|
|
1571
|
+
await args.pluginRunner.runOnResponse(args.buildPluginCtx(args.ctx));
|
|
1593
1572
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
}
|
|
1573
|
+
await emitActionCallTelemetry(args.actionName, args.startedAt, args.input, {
|
|
1574
|
+
status: "success",
|
|
1575
|
+
output: handlerResult
|
|
1576
|
+
});
|
|
1577
|
+
} catch (err) {
|
|
1578
|
+
if (isActionErrorLike(err)) {
|
|
1579
|
+
writeSerialized(args.res, serializeActionResult({ data: void 0, error: err }));
|
|
1580
|
+
await emitActionCallTelemetry(args.actionName, args.startedAt, args.input, {
|
|
1581
|
+
status: "error",
|
|
1582
|
+
error: err
|
|
1583
|
+
});
|
|
1601
1584
|
return;
|
|
1602
1585
|
}
|
|
1586
|
+
const wrapped = new ActionError({
|
|
1587
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
1588
|
+
message: err instanceof Error ? err.message : "Action handler threw"
|
|
1589
|
+
});
|
|
1590
|
+
await emitActionCallTelemetry(args.actionName, args.startedAt, args.input, {
|
|
1591
|
+
status: "error",
|
|
1592
|
+
error: wrapped
|
|
1593
|
+
});
|
|
1594
|
+
throw err;
|
|
1603
1595
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1596
|
+
}
|
|
1597
|
+
async function handleActionError(err, c) {
|
|
1598
|
+
return handleRequestError(err, {
|
|
1599
|
+
req: c.req,
|
|
1600
|
+
res: c.res,
|
|
1601
|
+
requestId: c.requestId,
|
|
1602
|
+
pluginRunner: c.pluginRunner,
|
|
1603
|
+
buildPluginCtx: c.buildPluginCtx
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// src/server/observability/suggest.ts
|
|
1608
|
+
function levenshtein(a, b) {
|
|
1609
|
+
const m = a.length;
|
|
1610
|
+
const n = b.length;
|
|
1611
|
+
const dp = Array.from(
|
|
1612
|
+
{ length: m + 1 },
|
|
1613
|
+
() => Array.from({ length: n + 1 }).fill(0)
|
|
1614
|
+
);
|
|
1615
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
1616
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
1617
|
+
for (let i = 1; i <= m; i++) {
|
|
1618
|
+
for (let j = 1; j <= n; j++) {
|
|
1619
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
1620
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
1627
1621
|
}
|
|
1628
1622
|
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1623
|
+
return dp[m][n];
|
|
1624
|
+
}
|
|
1625
|
+
function findSuggestion(input, candidates, maxDistance = 3) {
|
|
1626
|
+
let best = null;
|
|
1627
|
+
let bestDist = maxDistance + 1;
|
|
1628
|
+
for (const c of candidates) {
|
|
1629
|
+
const d = levenshtein(input, c);
|
|
1630
|
+
if (d < bestDist) {
|
|
1631
|
+
bestDist = d;
|
|
1632
|
+
best = c;
|
|
1637
1633
|
}
|
|
1638
1634
|
}
|
|
1635
|
+
return best;
|
|
1639
1636
|
}
|
|
1640
1637
|
|
|
1641
|
-
// src/server/
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1638
|
+
// src/server/plugins/plugin-runner.ts
|
|
1639
|
+
var DuplicatePluginError = class extends Error {
|
|
1640
|
+
constructor(name) {
|
|
1641
|
+
super(`Plugin "${name}" is already registered.`);
|
|
1642
|
+
this.name = "DuplicatePluginError";
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
var PluginRunner = class {
|
|
1646
|
+
plugins = /* @__PURE__ */ new Set();
|
|
1647
|
+
pluginScopes = /* @__PURE__ */ new Map();
|
|
1648
|
+
onRequestHooks = [];
|
|
1649
|
+
preHandlerHooks = [];
|
|
1650
|
+
onResponseHooks = [];
|
|
1651
|
+
onErrorHooks = [];
|
|
1652
|
+
/**
|
|
1653
|
+
* Parent app — proto-chain root for all child scopes. Has its OWN empty
|
|
1654
|
+
* decorations map (parent never receives `decorateRequest` calls under
|
|
1655
|
+
* the T3.1 contract; only child scopes do).
|
|
1656
|
+
*/
|
|
1657
|
+
parentDecorations = /* @__PURE__ */ new Map();
|
|
1658
|
+
parentApp = this.buildParentAppFacade();
|
|
1659
|
+
has(name) {
|
|
1660
|
+
return this.plugins.has(name);
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Per T3.1 (C1 plugin scope encapsulation):
|
|
1664
|
+
* 1. Reserve the plugin name (still rejects duplicates).
|
|
1665
|
+
* 2. Build a CHILD TheoApp via `Object.create(parentApp)` — Fastify
|
|
1666
|
+
* `plugin-override.js:38` pattern. The child's own `decorateRequest`
|
|
1667
|
+
* populates per-scope `decorations`; the child's `addHook` still
|
|
1668
|
+
* forwards into the shared hook lists (hooks ARE process-global —
|
|
1669
|
+
* only decorations are scoped, mirroring Fastify's decoration vs
|
|
1670
|
+
* hook semantics).
|
|
1671
|
+
* 3. Invoke `plugin.register(childApp)`.
|
|
1672
|
+
*
|
|
1673
|
+
* Cross-plugin decoration-key collisions are PERMITTED (per blueprint
|
|
1674
|
+
* D1). The legacy `DuplicateDecorationError` is no longer thrown.
|
|
1675
|
+
*/
|
|
1676
|
+
async register(plugin) {
|
|
1677
|
+
if (this.plugins.has(plugin.name)) {
|
|
1678
|
+
throw new DuplicatePluginError(plugin.name);
|
|
1663
1679
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1680
|
+
this.plugins.add(plugin.name);
|
|
1681
|
+
const scope = this.buildPluginScope(plugin.name);
|
|
1682
|
+
this.pluginScopes.set(plugin.name, scope);
|
|
1683
|
+
try {
|
|
1684
|
+
await plugin.register(scope.app);
|
|
1685
|
+
} catch (err) {
|
|
1686
|
+
this.plugins.delete(plugin.name);
|
|
1687
|
+
this.pluginScopes.delete(plugin.name);
|
|
1688
|
+
throw err;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Build the parent app facade. Hooks forward to shared lists.
|
|
1693
|
+
* `decorateRequest` writes into the parent decorations map — used only
|
|
1694
|
+
* if a future consumer registers a non-plugin "app-level" decoration
|
|
1695
|
+
* directly. T3.1 contract: plugins decorate ONLY through child scopes.
|
|
1696
|
+
*/
|
|
1697
|
+
buildParentAppFacade() {
|
|
1698
|
+
const facade = {
|
|
1699
|
+
addHook: (name, fn) => {
|
|
1700
|
+
this.routeHookByName(name, fn);
|
|
1701
|
+
},
|
|
1702
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T documents the per-decoration type for consumers
|
|
1703
|
+
decorateRequest: (key, value) => {
|
|
1704
|
+
this.parentDecorations.set(key, value);
|
|
1705
|
+
}
|
|
1669
1706
|
};
|
|
1707
|
+
return facade;
|
|
1670
1708
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1709
|
+
/**
|
|
1710
|
+
* Build a child scope via `Object.create(parentApp)` so the child
|
|
1711
|
+
* inherits the parent's facade methods through the prototype chain.
|
|
1712
|
+
* Then override `decorateRequest` on the child instance so writes
|
|
1713
|
+
* land in the per-scope `decorations` map (parent is NOT mutated).
|
|
1714
|
+
*/
|
|
1715
|
+
buildPluginScope(_pluginName) {
|
|
1716
|
+
const decorations = /* @__PURE__ */ new Map();
|
|
1717
|
+
const childApp = Object.create(this.parentApp);
|
|
1718
|
+
Object.defineProperty(childApp, "decorateRequest", {
|
|
1719
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T documents the per-decoration type for consumers
|
|
1720
|
+
value: (key, value) => {
|
|
1721
|
+
if (typeof key !== "string") {
|
|
1722
|
+
throw new TypeError(
|
|
1723
|
+
`decorateRequest: invalid key (expected string, got ${typeof key}). Plugin authors MUST pass string keys.`
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
decorations.set(key, value);
|
|
1727
|
+
},
|
|
1728
|
+
enumerable: false,
|
|
1729
|
+
writable: false,
|
|
1730
|
+
configurable: false
|
|
1731
|
+
});
|
|
1732
|
+
Object.defineProperty(childApp, "decorations", {
|
|
1733
|
+
get: () => Object.fromEntries(decorations),
|
|
1734
|
+
enumerable: false,
|
|
1735
|
+
configurable: false
|
|
1736
|
+
});
|
|
1737
|
+
return { app: childApp, decorations };
|
|
1738
|
+
}
|
|
1739
|
+
routeHookByName(name, fn) {
|
|
1740
|
+
switch (name) {
|
|
1741
|
+
case "onRequest":
|
|
1742
|
+
this.onRequestHooks.push(fn);
|
|
1743
|
+
return;
|
|
1744
|
+
case "preHandler":
|
|
1745
|
+
this.preHandlerHooks.push(fn);
|
|
1746
|
+
return;
|
|
1747
|
+
case "onResponse":
|
|
1748
|
+
this.onResponseHooks.push(fn);
|
|
1749
|
+
return;
|
|
1750
|
+
case "onError":
|
|
1751
|
+
this.onErrorHooks.push(fn);
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Apply ALL plugin scopes' decorations into the request ctx. Per T3.1,
|
|
1757
|
+
* sibling plugins MAY decorate the same key — last-writer-wins at apply
|
|
1758
|
+
* time (ordering = registration order). This keeps the legacy
|
|
1759
|
+
* `ctx.<key>` flat surface working for consumers that already aggregate
|
|
1760
|
+
* decorations into a single bag.
|
|
1761
|
+
*/
|
|
1762
|
+
applyDecorations(ctx) {
|
|
1763
|
+
for (const scope of this.pluginScopes.values()) {
|
|
1764
|
+
for (const [key, value] of scope.decorations.entries()) {
|
|
1765
|
+
ctx[key] = value;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1673
1768
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1769
|
+
/**
|
|
1770
|
+
* Apply ONLY one plugin's scoped decorations into `target`. Used by
|
|
1771
|
+
* the T1.1 RED-3 test (sibling isolation proof) and by future scope-aware
|
|
1772
|
+
* dispatch paths. Throws if the plugin name isn't registered.
|
|
1773
|
+
*/
|
|
1774
|
+
applyScopedDecorations(pluginName, target) {
|
|
1775
|
+
const scope = this.pluginScopes.get(pluginName);
|
|
1776
|
+
if (!scope) throw new Error(`PluginRunner: unknown plugin "${pluginName}"`);
|
|
1777
|
+
for (const [key, value] of scope.decorations.entries()) {
|
|
1778
|
+
target[key] = value;
|
|
1779
|
+
}
|
|
1679
1780
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
} catch (e) {
|
|
1686
|
-
throw new ActionError({
|
|
1687
|
-
code: "INTERNAL_SERVER_ERROR",
|
|
1688
|
-
message: `Action data not serializable: ${e instanceof Error ? e.message : String(e)}`
|
|
1689
|
-
});
|
|
1781
|
+
/** T1.1 RED-1/RED-4 introspection: returns the child TheoApp built for `pluginName`. */
|
|
1782
|
+
getPluginScope(pluginName) {
|
|
1783
|
+
const scope = this.pluginScopes.get(pluginName);
|
|
1784
|
+
if (!scope) throw new Error(`PluginRunner: unknown plugin "${pluginName}"`);
|
|
1785
|
+
return scope.app;
|
|
1690
1786
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
message: `Serialized response body exceeds limit (${limit} bytes)`
|
|
1695
|
-
});
|
|
1787
|
+
/** T1.1 RED-4: returns the parent TheoApp (root of every child's proto chain). */
|
|
1788
|
+
getParentApp() {
|
|
1789
|
+
return this.parentApp;
|
|
1696
1790
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
contentType: "application/json+devalue",
|
|
1701
|
-
body
|
|
1702
|
-
};
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
// src/server/http/action-execute.ts
|
|
1706
|
-
var __IS_DEV = (() => {
|
|
1707
|
-
try {
|
|
1708
|
-
return import.meta.env?.DEV === true;
|
|
1709
|
-
} catch {
|
|
1710
|
-
return process.env.NODE_ENV !== "production";
|
|
1791
|
+
/** T1.1 RED-2: returns the parent's decorations map (NEVER touched by plugin decorate calls under T3.1 contract). */
|
|
1792
|
+
getParentDecorations() {
|
|
1793
|
+
return this.parentDecorations;
|
|
1711
1794
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1715
|
-
const candidate = value;
|
|
1716
|
-
if (typeof candidate.handler !== "function") return false;
|
|
1717
|
-
const input = candidate.input;
|
|
1718
|
-
return typeof input?.safeParse === "function";
|
|
1719
|
-
}
|
|
1720
|
-
async function executeAction(filePath, exportName, req, res, loadModule, serverDir, requestId, pluginRunner, csrfMode = "strict", disallowed) {
|
|
1721
|
-
return executeActionWithOptions({
|
|
1722
|
-
filePath,
|
|
1723
|
-
exportName,
|
|
1724
|
-
req,
|
|
1725
|
-
res,
|
|
1726
|
-
loadModule,
|
|
1727
|
-
serverDir,
|
|
1728
|
-
requestId,
|
|
1729
|
-
pluginRunner,
|
|
1730
|
-
csrfMode,
|
|
1731
|
-
disallowed
|
|
1732
|
-
});
|
|
1733
|
-
}
|
|
1734
|
-
async function loadActionConfig(loadModule, filePath, exportName, res, requestId) {
|
|
1735
|
-
const mod = await loadModule(filePath);
|
|
1736
|
-
const exportedValue = mod[exportName];
|
|
1737
|
-
if (!isActionConfig(exportedValue)) {
|
|
1738
|
-
sendError(res, "NOT_FOUND", `Action "${exportName}" not found`, 404, void 0, requestId);
|
|
1739
|
-
return null;
|
|
1795
|
+
async runOnRequest(ctx) {
|
|
1796
|
+
return this.runHookList(this.onRequestHooks, ctx);
|
|
1740
1797
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
function enforceCsrfForAction(req, res, ctx) {
|
|
1744
|
-
if (ctx.actionConfig.csrf === false) return true;
|
|
1745
|
-
const decision = enforceCsrf(
|
|
1746
|
-
req,
|
|
1747
|
-
ctx.csrfMode,
|
|
1748
|
-
{
|
|
1749
|
-
// T3.3 DRY — canonical dispatcher
|
|
1750
|
-
warn: dispatchCsrfWarn,
|
|
1751
|
-
path: req.url
|
|
1752
|
-
},
|
|
1753
|
-
ctx.disallowed
|
|
1754
|
-
);
|
|
1755
|
-
if (decision.allow) return true;
|
|
1756
|
-
sendError(
|
|
1757
|
-
res,
|
|
1758
|
-
"CSRF_INVALID",
|
|
1759
|
-
decision.reason ?? "CSRF check failed",
|
|
1760
|
-
403,
|
|
1761
|
-
void 0,
|
|
1762
|
-
ctx.requestId
|
|
1763
|
-
);
|
|
1764
|
-
return false;
|
|
1765
|
-
}
|
|
1766
|
-
async function runPreHandlerPipeline(p) {
|
|
1767
|
-
if (p.serverDir) {
|
|
1768
|
-
const result = await runMiddlewareAndContext(p.req, p.res, p.loadModule, p.serverDir);
|
|
1769
|
-
if (result.aborted) return false;
|
|
1770
|
-
Object.assign(p.ctx, result.ctx ?? {});
|
|
1771
|
-
p.pluginRunner?.applyDecorations(p.ctx);
|
|
1798
|
+
async runPreHandler(ctx) {
|
|
1799
|
+
return this.runHookList(this.preHandlerHooks, ctx);
|
|
1772
1800
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
if (preResult.shortCircuited) return false;
|
|
1801
|
+
async runOnResponse(ctx, options = {}) {
|
|
1802
|
+
return this.runHookList(this.onResponseHooks, ctx, options);
|
|
1776
1803
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1804
|
+
/**
|
|
1805
|
+
* Run all onError hooks. Swallows errors thrown inside hooks themselves to
|
|
1806
|
+
* prevent recursion (an error in an error handler must not trigger onError
|
|
1807
|
+
* again).
|
|
1808
|
+
*/
|
|
1809
|
+
async runOnError(ctx, error) {
|
|
1810
|
+
const errorCtx = { ...ctx, error };
|
|
1811
|
+
for (const hook of this.onErrorHooks) {
|
|
1812
|
+
try {
|
|
1813
|
+
await hook(errorCtx);
|
|
1814
|
+
} catch (innerErr) {
|
|
1815
|
+
console.error(
|
|
1816
|
+
`[plugin-runner] onError hook threw; suppressed to avoid recursion:`,
|
|
1817
|
+
innerErr
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1793
1820
|
}
|
|
1794
|
-
return {
|
|
1795
|
-
} catch (err) {
|
|
1796
|
-
sendError(res, "VALIDATION_ERROR", err.message, 400, void 0, requestId);
|
|
1797
|
-
return { ok: false };
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
function synthesizeFormData(parsed) {
|
|
1801
|
-
const fd = new FormData();
|
|
1802
|
-
for (const [name, value] of Object.entries(parsed.fields)) {
|
|
1803
|
-
fd.append(name, value);
|
|
1821
|
+
return { shortCircuited: false };
|
|
1804
1822
|
}
|
|
1805
|
-
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1823
|
+
async runHookList(hooks, ctx, options = {}) {
|
|
1824
|
+
for (const hook of hooks) {
|
|
1825
|
+
try {
|
|
1826
|
+
await hook(ctx);
|
|
1827
|
+
} catch (err) {
|
|
1828
|
+
if (options.inErrorPath) {
|
|
1829
|
+
console.error(`[plugin-runner] hook threw during error path; suppressed (EC-9):`, err);
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
throw err;
|
|
1833
|
+
}
|
|
1834
|
+
if (ctx.response.writableEnded || ctx.response.headersSent) {
|
|
1835
|
+
return { shortCircuited: true };
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return { shortCircuited: false };
|
|
1810
1839
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1840
|
+
};
|
|
1841
|
+
|
|
1842
|
+
// src/server/plugins/load-plugins.ts
|
|
1843
|
+
var InvalidPluginShapeError = class extends Error {
|
|
1844
|
+
constructor(index, reason) {
|
|
1845
|
+
super(`plugins[${index}] is not a valid TheoPlugin: ${reason}`);
|
|
1846
|
+
this.name = "InvalidPluginShapeError";
|
|
1818
1847
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
async function emitActionCallTelemetry(name, startedAt, input, outcome) {
|
|
1824
|
-
if (!__IS_DEV) return;
|
|
1825
|
-
try {
|
|
1826
|
-
const mod = await import("./dispatcher-EJHL6JMJ.js");
|
|
1827
|
-
mod.dispatcher.onActionCall({
|
|
1828
|
-
// eslint-disable-next-line sonarjs/pseudo-random -- non-secret correlation id
|
|
1829
|
-
id: `act-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1830
|
-
timestamp: startedAt,
|
|
1831
|
-
name,
|
|
1832
|
-
input,
|
|
1833
|
-
output: outcome.status === "success" ? outcome.output : void 0,
|
|
1834
|
-
error: outcome.status === "error" ? {
|
|
1835
|
-
code: outcome.error.code,
|
|
1836
|
-
message: outcome.error.message,
|
|
1837
|
-
fields: outcome.error instanceof ActionInputError ? outcome.error.fields : void 0
|
|
1838
|
-
} : void 0,
|
|
1839
|
-
durationMs: Date.now() - startedAt,
|
|
1840
|
-
status: outcome.status
|
|
1841
|
-
});
|
|
1842
|
-
} catch {
|
|
1848
|
+
};
|
|
1849
|
+
function isPlugin(value, index) {
|
|
1850
|
+
if (value == null || typeof value !== "object") {
|
|
1851
|
+
throw new InvalidPluginShapeError(index, "expected an object");
|
|
1843
1852
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
});
|
|
1864
|
-
let ctx = {};
|
|
1865
|
-
try {
|
|
1866
|
-
if ((req.method ?? "GET").toUpperCase() !== "POST") {
|
|
1867
|
-
sendError(res, "METHOD_NOT_ALLOWED", "Actions only accept POST", 405, void 0, requestId);
|
|
1868
|
-
return;
|
|
1853
|
+
const v = value;
|
|
1854
|
+
if (typeof v.name !== "string" || v.name.length === 0) {
|
|
1855
|
+
throw new InvalidPluginShapeError(index, 'missing "name" string');
|
|
1856
|
+
}
|
|
1857
|
+
if (typeof v.register !== "function") {
|
|
1858
|
+
throw new InvalidPluginShapeError(index, 'missing "register" function');
|
|
1859
|
+
}
|
|
1860
|
+
return true;
|
|
1861
|
+
}
|
|
1862
|
+
async function createPluginRunnerFromConfig(plugins) {
|
|
1863
|
+
if (plugins == null) return void 0;
|
|
1864
|
+
if (!Array.isArray(plugins)) return void 0;
|
|
1865
|
+
if (plugins.length === 0) return void 0;
|
|
1866
|
+
const runner = new PluginRunner();
|
|
1867
|
+
const pluginsArray = plugins;
|
|
1868
|
+
for (let i = 0; i < pluginsArray.length; i++) {
|
|
1869
|
+
const candidate = pluginsArray[i];
|
|
1870
|
+
if (!isPlugin(candidate, i)) {
|
|
1871
|
+
continue;
|
|
1869
1872
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1873
|
+
await runner.register(candidate);
|
|
1874
|
+
}
|
|
1875
|
+
return runner;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// src/server/rate-limit/rate-limit-store.ts
|
|
1879
|
+
var InMemoryStore = class _InMemoryStore {
|
|
1880
|
+
store = /* @__PURE__ */ new Map();
|
|
1881
|
+
/** Bound the map to prevent unbounded growth from pathological inputs. */
|
|
1882
|
+
static MAX_ENTRIES = 1e5;
|
|
1883
|
+
/** GC sweep interval, milliseconds. */
|
|
1884
|
+
static GC_INTERVAL_MS = 3e4;
|
|
1885
|
+
gcTimer = null;
|
|
1886
|
+
constructor() {
|
|
1887
|
+
if (typeof setInterval !== "undefined") {
|
|
1888
|
+
const timer = setInterval(() => {
|
|
1889
|
+
this.sweepExpired();
|
|
1890
|
+
}, _InMemoryStore.GC_INTERVAL_MS);
|
|
1891
|
+
this.gcTimer = timer;
|
|
1892
|
+
const maybeUnref = timer.unref;
|
|
1893
|
+
if (typeof maybeUnref === "function") maybeUnref.call(timer);
|
|
1874
1894
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Synchronous fast-path used by the legacy sync `createRateLimiter`
|
|
1898
|
+
* surface (`api-middleware.ts` is sync). The async `incr` delegates to
|
|
1899
|
+
* this for in-memory; external adapters override `incr` directly.
|
|
1900
|
+
*/
|
|
1901
|
+
incrSync(key, windowMs) {
|
|
1902
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) {
|
|
1903
|
+
throw new Error(
|
|
1904
|
+
`InMemoryStore.incr: windowMs must be a positive finite number (got ${windowMs})`
|
|
1905
|
+
);
|
|
1879
1906
|
}
|
|
1880
|
-
const
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
serverDir,
|
|
1885
|
-
loadModule,
|
|
1886
|
-
req,
|
|
1887
|
-
res
|
|
1888
|
-
};
|
|
1889
|
-
if (!await runPreHandlerPipeline(pipeline)) return;
|
|
1890
|
-
ctx = pipeline.ctx;
|
|
1891
|
-
const bodyOutcome = await readActionBody(req, res, requestId, actionConfig);
|
|
1892
|
-
if (!bodyOutcome.ok) return;
|
|
1893
|
-
const actionName = exportName === "default" ? deriveActionNameFromPath(filePath) : exportName;
|
|
1894
|
-
const startedAt = Date.now();
|
|
1895
|
-
const result = actionConfig.input.safeParse(bodyOutcome.body);
|
|
1896
|
-
if (!result.success) {
|
|
1897
|
-
const inputErr = new ActionInputError(result.error?.issues ?? []);
|
|
1898
|
-
writeSerialized(res, serializeActionResult({ data: void 0, error: inputErr }));
|
|
1899
|
-
await emitActionCallTelemetry(actionName, startedAt, bodyOutcome.body, {
|
|
1900
|
-
status: "error",
|
|
1901
|
-
error: inputErr
|
|
1902
|
-
});
|
|
1903
|
-
return;
|
|
1907
|
+
const now = Date.now();
|
|
1908
|
+
if (this.store.size >= _InMemoryStore.MAX_ENTRIES) {
|
|
1909
|
+
const first = this.store.keys().next().value;
|
|
1910
|
+
if (first !== void 0) this.store.delete(first);
|
|
1904
1911
|
}
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
buildPluginCtx
|
|
1914
|
-
});
|
|
1915
|
-
} catch (err) {
|
|
1916
|
-
await handleActionError(err, { req, res, ctx, requestId, pluginRunner, buildPluginCtx });
|
|
1912
|
+
const entry = this.store.get(key);
|
|
1913
|
+
if (!entry || now >= entry.resetAt) {
|
|
1914
|
+
const fresh = { count: 1, resetAt: now + windowMs };
|
|
1915
|
+
this.store.set(key, fresh);
|
|
1916
|
+
return { ...fresh };
|
|
1917
|
+
}
|
|
1918
|
+
entry.count++;
|
|
1919
|
+
return { count: entry.count, resetAt: entry.resetAt };
|
|
1917
1920
|
}
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
function isActionErrorLike(err) {
|
|
1924
|
-
if (err === null || typeof err !== "object") return false;
|
|
1925
|
-
const obj = err;
|
|
1926
|
-
return (obj.type === "TheoActionError" || obj.type === "TheoActionInputError") && typeof obj.code === "string" && typeof obj.status === "number";
|
|
1927
|
-
}
|
|
1928
|
-
async function runActionHandler(args) {
|
|
1929
|
-
try {
|
|
1930
|
-
const handlerResult = await args.actionConfig.handler({ input: args.input, ctx: args.ctx });
|
|
1931
|
-
writeSerialized(args.res, serializeActionResult({ data: handlerResult, error: void 0 }));
|
|
1932
|
-
if (args.pluginRunner) {
|
|
1933
|
-
await args.pluginRunner.runOnResponse(args.buildPluginCtx(args.ctx));
|
|
1921
|
+
/** Sweep expired entries. Called by the GC timer; safe to call manually. */
|
|
1922
|
+
sweepExpired() {
|
|
1923
|
+
const now = Date.now();
|
|
1924
|
+
for (const [k, v] of this.store) {
|
|
1925
|
+
if (v.resetAt <= now) this.store.delete(k);
|
|
1934
1926
|
}
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
error: err
|
|
1945
|
-
});
|
|
1946
|
-
return;
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Stop the GC timer. Call from tests or when discarding the store. After
|
|
1930
|
+
* `dispose`, the store still works but no longer auto-sweeps.
|
|
1931
|
+
*/
|
|
1932
|
+
dispose() {
|
|
1933
|
+
if (this.gcTimer) {
|
|
1934
|
+
clearInterval(this.gcTimer);
|
|
1935
|
+
this.gcTimer = null;
|
|
1947
1936
|
}
|
|
1948
|
-
const wrapped = new ActionError({
|
|
1949
|
-
code: "INTERNAL_SERVER_ERROR",
|
|
1950
|
-
message: err instanceof Error ? err.message : "Action handler threw"
|
|
1951
|
-
});
|
|
1952
|
-
await emitActionCallTelemetry(args.actionName, args.startedAt, args.input, {
|
|
1953
|
-
status: "error",
|
|
1954
|
-
error: wrapped
|
|
1955
|
-
});
|
|
1956
|
-
throw err;
|
|
1957
1937
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1938
|
+
// The async surface is required by the `RateLimitStore` interface
|
|
1939
|
+
// (Redis/etc. adapters are inherently async). For the in-memory case
|
|
1940
|
+
// we just adapt the sync implementations to Promises.
|
|
1941
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- interface contract: async return
|
|
1942
|
+
async incr(key, windowMs) {
|
|
1943
|
+
return this.incrSync(key, windowMs);
|
|
1944
|
+
}
|
|
1945
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- interface contract: async return
|
|
1946
|
+
async get(key) {
|
|
1947
|
+
const now = Date.now();
|
|
1948
|
+
const entry = this.store.get(key);
|
|
1949
|
+
if (!entry) return null;
|
|
1950
|
+
if (now >= entry.resetAt) return null;
|
|
1951
|
+
return { count: entry.count, resetAt: entry.resetAt };
|
|
1952
|
+
}
|
|
1953
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- interface contract: async return
|
|
1954
|
+
async reset(key) {
|
|
1955
|
+
this.store.delete(key);
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1968
1958
|
|
|
1969
|
-
// src/server/
|
|
1970
|
-
function
|
|
1971
|
-
const
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
()
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
1979
|
-
for (let i = 1; i <= m; i++) {
|
|
1980
|
-
for (let j = 1; j <= n; j++) {
|
|
1981
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
1982
|
-
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
1959
|
+
// src/server/rate-limit/rate-limit.ts
|
|
1960
|
+
function createRateLimiter(config, opts = {}) {
|
|
1961
|
+
const store = opts.store ?? new InMemoryStore();
|
|
1962
|
+
const isInMemory = store instanceof InMemoryStore;
|
|
1963
|
+
return function checkRateLimit(req) {
|
|
1964
|
+
const key = req.socket?.remoteAddress ?? "unknown";
|
|
1965
|
+
if (isInMemory) {
|
|
1966
|
+
const state = store.incrSync(key, config.windowMs);
|
|
1967
|
+
return resultFromState(state, config);
|
|
1983
1968
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
1969
|
+
throw new Error(
|
|
1970
|
+
"createRateLimiter: async RateLimitStore implementations are not supported by this sync fa\xE7ade. Use the InMemoryStore default or build a custom middleware around the async store directly."
|
|
1971
|
+
);
|
|
1972
|
+
};
|
|
1986
1973
|
}
|
|
1987
|
-
function
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1974
|
+
function resultFromState(state, config) {
|
|
1975
|
+
if (state.count > config.max) {
|
|
1976
|
+
const retryAfter = Math.ceil((state.resetAt - Date.now()) / 1e3);
|
|
1977
|
+
return {
|
|
1978
|
+
limited: true,
|
|
1979
|
+
headers: {
|
|
1980
|
+
"X-RateLimit-Limit": String(config.max),
|
|
1981
|
+
"X-RateLimit-Remaining": "0",
|
|
1982
|
+
"Retry-After": String(retryAfter)
|
|
1983
|
+
}
|
|
1984
|
+
};
|
|
1996
1985
|
}
|
|
1997
|
-
return
|
|
1986
|
+
return {
|
|
1987
|
+
limited: false,
|
|
1988
|
+
headers: {
|
|
1989
|
+
"X-RateLimit-Limit": String(config.max),
|
|
1990
|
+
"X-RateLimit-Remaining": String(Math.max(0, config.max - state.count))
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
// src/server/scan/module-loader.ts
|
|
1996
|
+
import { pathToFileURL } from "url";
|
|
1997
|
+
function createViteLoader(vite) {
|
|
1998
|
+
return (path) => vite.ssrLoadModule(path);
|
|
1999
|
+
}
|
|
2000
|
+
function createProductionLoader() {
|
|
2001
|
+
return async (path) => {
|
|
2002
|
+
const url = pathToFileURL(path).href;
|
|
2003
|
+
return import(url);
|
|
2004
|
+
};
|
|
1998
2005
|
}
|
|
1999
2006
|
|
|
2000
2007
|
// src/server/security/security-headers.ts
|
|
@@ -2083,6 +2090,7 @@ export {
|
|
|
2083
2090
|
logRequest,
|
|
2084
2091
|
warnOnce,
|
|
2085
2092
|
safeAudit,
|
|
2093
|
+
validateCsrfRequest,
|
|
2086
2094
|
sendError,
|
|
2087
2095
|
executeRoute,
|
|
2088
2096
|
executeAction,
|
|
@@ -2095,4 +2103,4 @@ export {
|
|
|
2095
2103
|
applySecurityHeaders,
|
|
2096
2104
|
generateNonce
|
|
2097
2105
|
};
|
|
2098
|
-
//# sourceMappingURL=chunk-
|
|
2106
|
+
//# sourceMappingURL=chunk-WR4F4EEZ.js.map
|