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.
Files changed (131) hide show
  1. package/dist/{actions-virtual-module-SQDY3V5X.js → actions-virtual-module-3CDQTWOC.js} +6 -6
  2. package/dist/{actions-virtual-module-PNPRCEOS.js → actions-virtual-module-EIPXX4ZB.js} +3 -3
  3. package/dist/adapters/web-shim.d.ts +67 -0
  4. package/dist/adapters/ws-shim.d.ts +55 -0
  5. package/dist/agent-events-DosDXkSV.d.ts +94 -0
  6. package/dist/agents-typed-client-SAWAAH7K.js +142 -0
  7. package/dist/agents-typed-client-SAWAAH7K.js.map +1 -0
  8. package/dist/agents-typed-client-UTEQUA63.js +143 -0
  9. package/dist/agents-typed-client-UTEQUA63.js.map +1 -0
  10. package/dist/{app-typed-client-5GYEOYP3.js → app-typed-client-7PBFWZUE.js} +3 -3
  11. package/dist/{app-typed-client-QG7BVZYW.js → app-typed-client-CSOK7NPC.js} +6 -6
  12. package/dist/audit-log-BQWM5YLG.d.ts +60 -0
  13. package/dist/body-parser-web-FV5HWCY3.js +71 -0
  14. package/dist/body-parser-web-FV5HWCY3.js.map +1 -0
  15. package/dist/boot/index.d.ts +39 -0
  16. package/dist/{build-QFRLSEZ4.js → build-HXND27XG.js} +11 -11
  17. package/dist/{chunk-223EFY5X.js → chunk-2J7XU3PW.js} +68 -27
  18. package/dist/chunk-2J7XU3PW.js.map +1 -0
  19. package/dist/{chunk-RESN62GB.js → chunk-2KZQPDYR.js} +5 -48
  20. package/dist/chunk-2KZQPDYR.js.map +1 -0
  21. package/dist/chunk-3S3BNW5K.js +445 -0
  22. package/dist/chunk-3S3BNW5K.js.map +1 -0
  23. package/dist/{chunk-6FYD34NX.js → chunk-BQDGES7C.js} +28 -28
  24. package/dist/{chunk-6FYD34NX.js.map → chunk-BQDGES7C.js.map} +1 -1
  25. package/dist/chunk-EXP56GFQ.js +52 -0
  26. package/dist/chunk-EXP56GFQ.js.map +1 -0
  27. package/dist/chunk-F4YUPDJ2.js +115 -0
  28. package/dist/chunk-F4YUPDJ2.js.map +1 -0
  29. package/dist/{chunk-NAZ4E2GT.js → chunk-KXA37ONC.js} +2 -2
  30. package/dist/chunk-NHJMZCAS.js +32 -0
  31. package/dist/chunk-NHJMZCAS.js.map +1 -0
  32. package/dist/{chunk-43D6XNDR.js → chunk-O62MW4MT.js} +91 -18
  33. package/dist/chunk-O62MW4MT.js.map +1 -0
  34. package/dist/chunk-RSVN727G.js +1 -0
  35. package/dist/{chunk-7CBRKNQA.js → chunk-RYTZYFSD.js} +198 -6
  36. package/dist/chunk-RYTZYFSD.js.map +1 -0
  37. package/dist/chunk-UNLA45FY.js +235 -0
  38. package/dist/chunk-UNLA45FY.js.map +1 -0
  39. package/dist/{chunk-GFMQJHXX.js → chunk-WR4F4EEZ.js} +1082 -1074
  40. package/dist/chunk-WR4F4EEZ.js.map +1 -0
  41. package/dist/{chunk-AD74EAK3.js → chunk-ZSTZXR2D.js} +1 -30
  42. package/dist/chunk-ZSTZXR2D.js.map +1 -0
  43. package/dist/cli/index.js +5 -5
  44. package/dist/client/index.d.ts +418 -0
  45. package/dist/client/index.js +84 -3
  46. package/dist/client/index.js.map +1 -1
  47. package/dist/csrf-BBrEZSBW.d.ts +107 -0
  48. package/dist/csrf-readiness-store-CjIoub3U.d.ts +43 -0
  49. package/dist/define-websocket-CdK94O-D.d.ts +64 -0
  50. package/dist/{dev-GBXOTXUP.js → dev-OWW4XVIH.js} +10 -10
  51. package/dist/{dev-emit-FEFEDLZF.js → dev-emit-5MDSBP5D.js} +3 -3
  52. package/dist/{dev-emit-O4EGOSNV.js → dev-emit-QH2YGZXN.js} +2 -2
  53. package/dist/devtools/entry.d.ts +5 -0
  54. package/dist/error-envelope-BsNzzAV5.d.ts +62 -0
  55. package/dist/health-route-C0hk64_U.d.ts +57 -0
  56. package/dist/index-B40qUSrQ.d.ts +575 -0
  57. package/dist/index.d.ts +361 -0
  58. package/dist/index.js +6 -4
  59. package/dist/index.js.map +1 -1
  60. package/dist/internal-api-4YTJDITC.js +83 -0
  61. package/dist/internal-api-EFKZWIYZ.js +66 -0
  62. package/dist/internal-api-EFKZWIYZ.js.map +1 -0
  63. package/dist/job-backend-CgC8Xf33.d.ts +68 -0
  64. package/dist/match-CfbEFRG4.d.ts +26 -0
  65. package/dist/{openapi-VR6AFBLJ.js → openapi-FHY6HC6I.js} +7 -7
  66. package/dist/plugin-runner-BGBkzgi0.d.ts +95 -0
  67. package/dist/plugin-types-DNJGxr4Z.d.ts +79 -0
  68. package/dist/rate-limit-BdNDZ3vt.d.ts +58 -0
  69. package/dist/rate-limit-store-BEJnhWdw.d.ts +72 -0
  70. package/dist/react-query/index.d.ts +33 -0
  71. package/dist/{registry-Q2TZQLUH.js → registry-34LL7NF4.js} +1 -1
  72. package/dist/{routes-LRYOIIAI.js → routes-EW7TP7NJ.js} +2 -2
  73. package/dist/schema-BpH6ivDY.d.ts +74 -0
  74. package/dist/server/agent/index.d.ts +229 -0
  75. package/dist/server/agent/index.js +2 -1
  76. package/dist/server/auth/index.d.ts +419 -0
  77. package/dist/server/cost/index.d.ts +177 -0
  78. package/dist/server/cron/index.d.ts +208 -0
  79. package/dist/server/define/index.d.ts +313 -0
  80. package/dist/server/define/index.js +4 -2
  81. package/dist/server/http/index.d.ts +11 -0
  82. package/dist/server/index.d.ts +848 -0
  83. package/dist/server/index.js +9 -294
  84. package/dist/server/index.js.map +1 -1
  85. package/dist/server/jobs/index.d.ts +348 -0
  86. package/dist/server/observability/index.d.ts +324 -0
  87. package/dist/server/plugins/index.d.ts +17 -0
  88. package/dist/server/rate-limit/index.d.ts +105 -0
  89. package/dist/server/realtime/index.d.ts +15 -0
  90. package/dist/server/scan/index.d.ts +126 -0
  91. package/dist/server/scan/index.js +1 -1
  92. package/dist/server/security/index.d.ts +193 -0
  93. package/dist/server/storage/index.d.ts +22 -0
  94. package/dist/server/webhook/index.d.ts +148 -0
  95. package/dist/{start-3ZHAXSJE.js → start-KIQ5TTLR.js} +76 -13
  96. package/dist/start-KIQ5TTLR.js.map +1 -0
  97. package/dist/storage-manager-C4jsO0Tp.d.ts +89 -0
  98. package/dist/storage-types-DsDTCPbp.d.ts +96 -0
  99. package/dist/vite-plugin/index.d.ts +115 -0
  100. package/dist/vite-plugin/index.js +6 -4
  101. package/dist/{vite-plugin-WO72VLYR.js → vite-plugin-RK66K26Z.js} +7 -7
  102. package/dist/vite-plugin-RK66K26Z.js.map +1 -0
  103. package/package.json +4 -4
  104. package/dist/chunk-223EFY5X.js.map +0 -1
  105. package/dist/chunk-3LVRAGAZ.js +0 -73
  106. package/dist/chunk-3LVRAGAZ.js.map +0 -1
  107. package/dist/chunk-43D6XNDR.js.map +0 -1
  108. package/dist/chunk-7CBRKNQA.js.map +0 -1
  109. package/dist/chunk-AD74EAK3.js.map +0 -1
  110. package/dist/chunk-GFMQJHXX.js.map +0 -1
  111. package/dist/chunk-PBEH6NXR.js +0 -44
  112. package/dist/chunk-PBEH6NXR.js.map +0 -1
  113. package/dist/chunk-PIVX3DYW.js +0 -142
  114. package/dist/chunk-PIVX3DYW.js.map +0 -1
  115. package/dist/chunk-PPPR5DGR.js +0 -1
  116. package/dist/chunk-RESN62GB.js.map +0 -1
  117. package/dist/start-3ZHAXSJE.js.map +0 -1
  118. /package/dist/{actions-virtual-module-SQDY3V5X.js.map → actions-virtual-module-3CDQTWOC.js.map} +0 -0
  119. /package/dist/{actions-virtual-module-PNPRCEOS.js.map → actions-virtual-module-EIPXX4ZB.js.map} +0 -0
  120. /package/dist/{app-typed-client-5GYEOYP3.js.map → app-typed-client-7PBFWZUE.js.map} +0 -0
  121. /package/dist/{app-typed-client-QG7BVZYW.js.map → app-typed-client-CSOK7NPC.js.map} +0 -0
  122. /package/dist/{build-QFRLSEZ4.js.map → build-HXND27XG.js.map} +0 -0
  123. /package/dist/{chunk-NAZ4E2GT.js.map → chunk-KXA37ONC.js.map} +0 -0
  124. /package/dist/{chunk-PPPR5DGR.js.map → chunk-RSVN727G.js.map} +0 -0
  125. /package/dist/{dev-GBXOTXUP.js.map → dev-OWW4XVIH.js.map} +0 -0
  126. /package/dist/{dev-emit-FEFEDLZF.js.map → dev-emit-5MDSBP5D.js.map} +0 -0
  127. /package/dist/{dev-emit-O4EGOSNV.js.map → dev-emit-QH2YGZXN.js.map} +0 -0
  128. /package/dist/{vite-plugin-WO72VLYR.js.map → internal-api-4YTJDITC.js.map} +0 -0
  129. /package/dist/{openapi-VR6AFBLJ.js.map → openapi-FHY6HC6I.js.map} +0 -0
  130. /package/dist/{registry-Q2TZQLUH.js.map → registry-34LL7NF4.js.map} +0 -0
  131. /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
- res.end();
1248
- if (pluginRunner) await pluginRunner.runOnResponse(buildPluginCtx(ctx2));
1249
- return;
1114
+ out[key] = unwrapMissingDefault(rawValidator);
1115
+ continue;
1250
1116
  }
1251
- let responseBody = handlerResult;
1252
- if (isZodLike(rc.response)) {
1253
- const parsed = rc.response.safeParse(handlerResult);
1254
- if (!parsed.success) {
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
- sendJson(res, responseBody, rc.status ?? 200, transformer);
1264
- if (pluginRunner) await pluginRunner.runOnResponse(buildPluginCtx(ctx2));
1265
- } catch (err) {
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 (isAuthRequiredError(err)) {
1276
- const authErr = err;
1277
- sendError(res, authErr.code, authErr.message, authErr.status, void 0, requestId);
1278
- if (pluginRunner) {
1279
- const errCtxObj = {};
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
- sendError(
1286
- res,
1287
- "INTERNAL_ERROR",
1288
- err instanceof Error && err.message ? err.message : "Internal server error",
1289
- 500,
1290
- void 0,
1291
- requestId
1292
- );
1293
- if (pluginRunner) {
1294
- const errCtxObj = {};
1295
- pluginRunner.applyDecorations(errCtxObj);
1296
- await pluginRunner.runOnResponse(buildPluginCtx(errCtxObj), { inErrorPath: true });
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/action-protocol.ts
1302
- var CODE_TO_STATUS = {
1303
- VALIDATION_ERROR: 422,
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
- CONTENT_TOO_LARGE: 413,
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
- INTERNAL_SERVER_ERROR: 500
1315
- };
1316
- var STATUS_TO_CODE = {
1317
- 400: "BAD_REQUEST",
1318
- 401: "UNAUTHORIZED",
1319
- 403: "FORBIDDEN",
1320
- 404: "NOT_FOUND",
1321
- 405: "METHOD_NOT_ALLOWED",
1322
- 409: "CONFLICT",
1323
- 413: "PAYLOAD_TOO_LARGE",
1324
- 415: "UNSUPPORTED_MEDIA_TYPE",
1325
- 422: "VALIDATION_ERROR",
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
- var ActionError = class _ActionError extends Error {
1330
- // Discriminator widened to the full union so subclasses can narrow to their
1331
- // specific literal (TypeScript would otherwise reject the override). Concrete
1332
- // values are still always exact literals at runtime.
1333
- type = "TheoActionError";
1334
- code;
1335
- status;
1336
- constructor(params) {
1337
- super(params.message ?? params.code);
1338
- this.code = params.code;
1339
- this.status = _ActionError.codeToStatus(params.code);
1340
- if (params.stack) {
1341
- this.stack = params.stack;
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
- * G5 T2.4 — canonical envelope view of the action error. Maps the G3
1346
- * ActionErrorCode to a canonical TheoErrorCode (VALIDATION_ERROR ↔
1347
- * UNPROCESSABLE_ENTITY, CONTENT_TOO_LARGE ↔ PAYLOAD_TOO_LARGE) so consumer
1348
- * UI / SDK code can switch on the unified envelope.
1349
- *
1350
- * Subclasses override to populate `ext` (see `ActionInputError.envelope`).
1351
- */
1352
- get envelope() {
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
- code: _ActionError.toTheoErrorCode(this.code),
1355
- message: this.message
1303
+ type: "error",
1304
+ status: result.error.status,
1305
+ contentType: "application/json",
1306
+ body: body2
1356
1307
  };
1357
1308
  }
1358
- /**
1359
- * Translate G3 ActionErrorCode canonical TheoErrorCode (blueprint
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
- static statusToCode(status) {
1371
- return STATUS_TO_CODE[status] ?? "INTERNAL_SERVER_ERROR";
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
- * Parse a serialized error JSON back into the typed class hierarchy.
1375
- * Distinguishes `TheoActionInputError` (with `issues` array) from
1376
- * `TheoActionError` via the `type` discriminator. Falls back to
1377
- * `INTERNAL_SERVER_ERROR` for malformed bodies (non-object, missing
1378
- * `type`, unknown `code`).
1379
- */
1380
- static fromJson(body) {
1381
- if (typeof body !== "object" || body === null) {
1382
- return new _ActionError({ code: "INTERNAL_SERVER_ERROR" });
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
- var ActionInputError = class extends ActionError {
1398
- type = "TheoActionInputError";
1399
- issues;
1400
- fields;
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
- * G5 T2.4 — envelope view with ValidationFieldsExt populated from .fields.
1408
- * UI consumers can `switch (env.code)` on `UNPROCESSABLE_ENTITY` and read
1409
- * `(env.ext as ValidationFieldsExt).fields` for field-level rendering.
1410
- */
1411
- get envelope() {
1412
- return {
1413
- code: "UNPROCESSABLE_ENTITY",
1414
- message: this.message,
1415
- ext: { fields: this.fields }
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 buildFieldsMap(issues) {
1420
- const fields = {};
1421
- const seen = /* @__PURE__ */ new Set();
1422
- for (const issue of issues) {
1423
- const key = issue.path.length === 0 ? "" : issue.path.join(".");
1424
- const dedupeKey = `${key}\0${issue.message}`;
1425
- if (seen.has(dedupeKey)) continue;
1426
- seen.add(dedupeKey);
1427
- const bucket = fields[key] ?? [];
1428
- bucket.push(issue.message);
1429
- fields[key] = bucket;
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 fields;
1379
+ return exportedValue;
1432
1380
  }
1433
- function extractUniversalIssues(raw) {
1434
- if (!Array.isArray(raw)) return [];
1435
- const out = [];
1436
- for (const entry of raw) {
1437
- if (typeof entry !== "object" || entry === null) continue;
1438
- const obj = entry;
1439
- if (!Array.isArray(obj.path)) continue;
1440
- if (typeof obj.message !== "string") continue;
1441
- const path = [];
1442
- let pathValid = true;
1443
- for (const seg of obj.path) {
1444
- if (typeof seg === "string" || typeof seg === "number") {
1445
- path.push(seg);
1446
- } else {
1447
- pathValid = false;
1448
- break;
1449
- }
1450
- }
1451
- if (!pathValid) continue;
1452
- out.push({
1453
- path,
1454
- message: obj.message,
1455
- code: typeof obj.code === "string" ? obj.code : void 0
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 out;
1415
+ return true;
1459
1416
  }
1460
-
1461
- // src/server/http/form-data-to-object.ts
1462
- import { z } from "zod";
1463
- function formDataToObject(formData, schema, prefix = "") {
1464
- const shape = schema.shape;
1465
- const out = {};
1466
- for (const [key, rawValidator] of Object.entries(shape)) {
1467
- const fullKey = prefix + key;
1468
- const validator = unwrapWrappers(rawValidator);
1469
- if (validator instanceof z.ZodObject) {
1470
- const nestedPrefix = `${fullKey}.`;
1471
- const hasNestedKeys = [...formData.keys()].some((k) => k.startsWith(nestedPrefix));
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
- out[key] = unwrapMissingDefault(rawValidator);
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 unwrapWrappers(validator) {
1498
- let inner = validator;
1499
- while (inner instanceof z.ZodOptional || inner instanceof z.ZodNullable || inner instanceof z.ZodDefault) {
1500
- inner = getInnerType(inner);
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
- return inner;
1503
- }
1504
- function getInnerType(wrapper) {
1505
- const def = wrapper.def;
1506
- if (def.innerType) return def.innerType;
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 void 0;
1449
+ return fd;
1522
1450
  }
1523
- function coerceScalar(raw, validator) {
1524
- if (raw === null) return void 0;
1525
- if (validator instanceof z.ZodNumber) {
1526
- return typeof raw === "string" ? Number(raw) : raw;
1451
+ function writeSerialized(res, serialized) {
1452
+ if (serialized.type === "empty") {
1453
+ res.statusCode = serialized.status;
1454
+ res.end();
1455
+ return;
1527
1456
  }
1528
- return raw;
1457
+ res.statusCode = serialized.status;
1458
+ res.setHeader("Content-Type", serialized.contentType);
1459
+ res.end(serialized.body);
1529
1460
  }
1530
- function coerceArrayElements(values, arrayValidator) {
1531
- const def = arrayValidator.def;
1532
- const elementSchema = def.element ?? (def.type instanceof z.ZodType ? def.type : void 0);
1533
- const elementType = elementSchema ? unwrapWrappers(elementSchema) : void 0;
1534
- if (elementType instanceof z.ZodNumber) {
1535
- return values.map((v) => typeof v === "string" ? Number(v) : v);
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
- if (elementType instanceof z.ZodBoolean) {
1538
- return values.map((v) => {
1539
- if (v === "true") return true;
1540
- if (v === "false") return false;
1541
- return Boolean(v);
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 coerceBoolean(formData, key) {
1547
- if (!formData.has(key)) return void 0;
1548
- const val = formData.get(key);
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
- // src/core/contracts/envelope-code-to-status.ts
1555
- var CODE_TO_STATUS2 = {
1556
- // 4xx client errors
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
- // src/server/http/handle-request-error.ts
1586
- async function handleRequestError(err, c) {
1587
- if (c.pluginRunner) {
1588
- const errCtxObj = {};
1589
- c.pluginRunner.applyDecorations(errCtxObj);
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
- if (c.res.writableEnded) {
1595
- try {
1596
- await c.pluginRunner.runOnResponse(c.buildPluginCtx(errCtxObj), {
1597
- inErrorPath: true
1598
- });
1599
- } catch {
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
- if (isAuthRequiredError(err)) {
1605
- const authErr = err;
1606
- sendError(c.res, authErr.code, authErr.message, authErr.status, void 0, c.requestId);
1607
- } else {
1608
- const envelope = serverErrorToEnvelope(err);
1609
- if (envelope.code === "INTERNAL_SERVER_ERROR") {
1610
- sendError(
1611
- c.res,
1612
- "INTERNAL_ERROR",
1613
- err instanceof Error ? err.message : "Internal server error",
1614
- 500,
1615
- void 0,
1616
- c.requestId
1617
- );
1618
- } else {
1619
- sendError(
1620
- c.res,
1621
- envelope.code,
1622
- envelope.message,
1623
- envelopeCodeToStatus(envelope.code),
1624
- void 0,
1625
- c.requestId
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
- if (c.pluginRunner) {
1630
- const errCtxObj = {};
1631
- c.pluginRunner.applyDecorations(errCtxObj);
1632
- try {
1633
- await c.pluginRunner.runOnResponse(c.buildPluginCtx(errCtxObj), {
1634
- inErrorPath: true
1635
- });
1636
- } catch {
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/http/serialize-action-result.ts
1642
- import { stringify as devalueStringify } from "devalue";
1643
- var DEFAULT_RESPONSE_BODY_SIZE_LIMIT = 5 * 1024 * 1024;
1644
- function serializeActionResult(result, options = {}) {
1645
- const limit = options.responseBodySizeLimit ?? DEFAULT_RESPONSE_BODY_SIZE_LIMIT;
1646
- if (result.error !== void 0) {
1647
- const body2 = result.error instanceof ActionInputError ? JSON.stringify({
1648
- type: result.error.type,
1649
- code: result.error.code,
1650
- message: result.error.message,
1651
- issues: result.error.issues,
1652
- fields: result.error.fields
1653
- }) : JSON.stringify({
1654
- type: result.error.type,
1655
- code: result.error.code,
1656
- message: result.error.message
1657
- });
1658
- if (Buffer.byteLength(body2, "utf8") > limit) {
1659
- throw new ActionError({
1660
- code: "PAYLOAD_TOO_LARGE",
1661
- message: `Serialized error body exceeds limit (${limit} bytes)`
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
- return {
1665
- type: "error",
1666
- status: result.error.status,
1667
- contentType: "application/json",
1668
- body: body2
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
- if (result.data === void 0) {
1672
- return { type: "empty", status: 204 };
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
- if (result.data instanceof Response) {
1675
- throw new ActionError({
1676
- code: "INTERNAL_SERVER_ERROR",
1677
- message: "Action handler cannot serialize Response objects \u2014 return plain data instead"
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
- let body;
1681
- try {
1682
- body = devalueStringify(result.data, {
1683
- URL: (value) => value instanceof URL && value.href
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
- if (Buffer.byteLength(body, "utf8") > limit) {
1692
- throw new ActionError({
1693
- code: "PAYLOAD_TOO_LARGE",
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
- return {
1698
- type: "data",
1699
- status: 200,
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
- function isActionConfig(value) {
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
- return exportedValue;
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
- if (p.pluginRunner) {
1774
- const preResult = await p.pluginRunner.runPreHandler(p.buildPluginCtx(p.ctx));
1775
- if (preResult.shortCircuited) return false;
1801
+ async runOnResponse(ctx, options = {}) {
1802
+ return this.runHookList(this.onResponseHooks, ctx, options);
1776
1803
  }
1777
- return true;
1778
- }
1779
- async function readActionBody(req, res, requestId, actionConfig) {
1780
- try {
1781
- const parsed = await parseRequestBody(req);
1782
- const accept = actionConfig.accept ?? "json";
1783
- let body;
1784
- if (accept === "form") {
1785
- body = formDataToObject(
1786
- synthesizeFormData(parsed),
1787
- actionConfig.input
1788
- );
1789
- } else if (parsed.json !== void 0) {
1790
- body = parsed.json;
1791
- } else {
1792
- body = parsed.fields;
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 { ok: true, body };
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
- for (const file of parsed.files) {
1806
- const blob = new Blob([file.buffer], {
1807
- type: file.mimeType
1808
- });
1809
- fd.append(file.fieldname, blob, file.filename);
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
- return fd;
1812
- }
1813
- function writeSerialized(res, serialized) {
1814
- if (serialized.type === "empty") {
1815
- res.statusCode = serialized.status;
1816
- res.end();
1817
- return;
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
- res.statusCode = serialized.status;
1820
- res.setHeader("Content-Type", serialized.contentType);
1821
- res.end(serialized.body);
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
- async function executeActionWithOptions(opts) {
1846
- const {
1847
- filePath,
1848
- exportName,
1849
- req,
1850
- res,
1851
- loadModule,
1852
- serverDir,
1853
- requestId,
1854
- pluginRunner,
1855
- csrfMode = "strict",
1856
- disallowed
1857
- } = opts;
1858
- const buildPluginCtx = (ctxObj) => ({
1859
- request: req,
1860
- response: res,
1861
- ctx: ctxObj,
1862
- requestId: requestId ?? "no-id"
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
- if (pluginRunner) {
1871
- pluginRunner.applyDecorations(ctx);
1872
- const onReqResult = await pluginRunner.runOnRequest(buildPluginCtx(ctx));
1873
- if (onReqResult.shortCircuited) return;
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
- const actionConfig = await loadActionConfig(loadModule, filePath, exportName, res, requestId);
1876
- if (!actionConfig) return;
1877
- if (!enforceCsrfForAction(req, res, { actionConfig, csrfMode, disallowed, requestId })) {
1878
- return;
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 pipeline = {
1881
- ctx,
1882
- buildPluginCtx,
1883
- pluginRunner,
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
- await runActionHandler({
1906
- actionConfig,
1907
- actionName,
1908
- startedAt,
1909
- input: result.data,
1910
- ctx,
1911
- res,
1912
- pluginRunner,
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
- function deriveActionNameFromPath(filePath) {
1920
- const base = filePath.split(/[\\/]/).pop() ?? "unknown";
1921
- return base.replace(/\.[jt]sx?$/, "");
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
- await emitActionCallTelemetry(args.actionName, args.startedAt, args.input, {
1936
- status: "success",
1937
- output: handlerResult
1938
- });
1939
- } catch (err) {
1940
- if (isActionErrorLike(err)) {
1941
- writeSerialized(args.res, serializeActionResult({ data: void 0, error: err }));
1942
- await emitActionCallTelemetry(args.actionName, args.startedAt, args.input, {
1943
- status: "error",
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
- async function handleActionError(err, c) {
1960
- return handleRequestError(err, {
1961
- req: c.req,
1962
- res: c.res,
1963
- requestId: c.requestId,
1964
- pluginRunner: c.pluginRunner,
1965
- buildPluginCtx: c.buildPluginCtx
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/observability/suggest.ts
1970
- function levenshtein(a, b) {
1971
- const m = a.length;
1972
- const n = b.length;
1973
- const dp = Array.from(
1974
- { length: m + 1 },
1975
- () => Array.from({ length: n + 1 }).fill(0)
1976
- );
1977
- for (let i = 0; i <= m; i++) dp[i][0] = i;
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
- return dp[m][n];
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 findSuggestion(input, candidates, maxDistance = 3) {
1988
- let best = null;
1989
- let bestDist = maxDistance + 1;
1990
- for (const c of candidates) {
1991
- const d = levenshtein(input, c);
1992
- if (d < bestDist) {
1993
- bestDist = d;
1994
- best = c;
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 best;
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-GFMQJHXX.js.map
2106
+ //# sourceMappingURL=chunk-WR4F4EEZ.js.map