theokit 0.57.0 → 0.58.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 (52) hide show
  1. package/dist/{actions-virtual-module-XAWZOWSY.js → actions-virtual-module-GMVT4ZDV.js} +3 -3
  2. package/dist/{actions-virtual-module-GMRXCAVN.js → actions-virtual-module-X3CSM3AF.js} +2 -2
  3. package/dist/{agent-LS7I3ZQB.js → agent-NTEVC7XA.js} +2 -2
  4. package/dist/{app-typed-client-W74QO5LH.js → app-typed-client-KA4JFYF4.js} +2 -2
  5. package/dist/{app-typed-client-NMKJ4DR7.js → app-typed-client-UQA2K65X.js} +3 -3
  6. package/dist/{build-FHE72YXR.js → build-BBPWXXC6.js} +4 -4
  7. package/dist/{chunk-G5JYB2LO.js → chunk-IF7CZXPW.js} +2 -2
  8. package/dist/{chunk-CLBJTDI6.js → chunk-KZ3PXNPT.js} +2 -2
  9. package/dist/{chunk-CTRVTBGY.js → chunk-L4TB6FXQ.js} +26 -4
  10. package/dist/chunk-L4TB6FXQ.js.map +1 -0
  11. package/dist/{chunk-KEDRCI2L.js → chunk-N2OXYOV6.js} +8 -8
  12. package/dist/{chunk-Q6HS3NFG.js → chunk-NLBJ27DJ.js} +6 -6
  13. package/dist/{chunk-NC2OPE22.js → chunk-S46ZHINM.js} +2 -2
  14. package/dist/{chunk-OKDCBENR.js → chunk-VJRPOUKM.js} +3 -3
  15. package/dist/{chunk-CMCEGW3B.js → chunk-WOANPZBT.js} +26 -4
  16. package/dist/chunk-WOANPZBT.js.map +1 -0
  17. package/dist/cli/index.js +6 -6
  18. package/dist/{dev-WDKROZBS.js → dev-2SNAEDZH.js} +5 -5
  19. package/dist/index.js +2 -2
  20. package/dist/{internal-api-732K3H75.js → internal-api-LDPEUWNZ.js} +3 -3
  21. package/dist/{internal-api-EVJSGGO3.js → internal-api-VY5LXKVZ.js} +2 -2
  22. package/dist/{mcp-PBUCHHAS.js → mcp-HNR2EH4K.js} +2 -2
  23. package/dist/{preview-VFIFKVP4.js → preview-YBYAPNQW.js} +3 -3
  24. package/dist/{server-boundary-QRVK3OIJ.js → server-boundary-6EM7WWNH.js} +2 -2
  25. package/dist/{server-boundary-B6U4DRRH.js → server-boundary-LXV2W3LA.js} +3 -3
  26. package/dist/{start-UAMJ5TSC.js → start-ZFT6OVMF.js} +5 -5
  27. package/dist/vite-plugin/index.js +2 -2
  28. package/dist/{vite-plugin-ABJIDKE6.js → vite-plugin-MQXUYKCN.js} +5 -5
  29. package/package.json +2 -2
  30. package/dist/chunk-CMCEGW3B.js.map +0 -1
  31. package/dist/chunk-CTRVTBGY.js.map +0 -1
  32. /package/dist/{actions-virtual-module-XAWZOWSY.js.map → actions-virtual-module-GMVT4ZDV.js.map} +0 -0
  33. /package/dist/{actions-virtual-module-GMRXCAVN.js.map → actions-virtual-module-X3CSM3AF.js.map} +0 -0
  34. /package/dist/{agent-LS7I3ZQB.js.map → agent-NTEVC7XA.js.map} +0 -0
  35. /package/dist/{app-typed-client-W74QO5LH.js.map → app-typed-client-KA4JFYF4.js.map} +0 -0
  36. /package/dist/{app-typed-client-NMKJ4DR7.js.map → app-typed-client-UQA2K65X.js.map} +0 -0
  37. /package/dist/{build-FHE72YXR.js.map → build-BBPWXXC6.js.map} +0 -0
  38. /package/dist/{chunk-G5JYB2LO.js.map → chunk-IF7CZXPW.js.map} +0 -0
  39. /package/dist/{chunk-CLBJTDI6.js.map → chunk-KZ3PXNPT.js.map} +0 -0
  40. /package/dist/{chunk-KEDRCI2L.js.map → chunk-N2OXYOV6.js.map} +0 -0
  41. /package/dist/{chunk-Q6HS3NFG.js.map → chunk-NLBJ27DJ.js.map} +0 -0
  42. /package/dist/{chunk-NC2OPE22.js.map → chunk-S46ZHINM.js.map} +0 -0
  43. /package/dist/{chunk-OKDCBENR.js.map → chunk-VJRPOUKM.js.map} +0 -0
  44. /package/dist/{dev-WDKROZBS.js.map → dev-2SNAEDZH.js.map} +0 -0
  45. /package/dist/{internal-api-732K3H75.js.map → internal-api-LDPEUWNZ.js.map} +0 -0
  46. /package/dist/{internal-api-EVJSGGO3.js.map → internal-api-VY5LXKVZ.js.map} +0 -0
  47. /package/dist/{mcp-PBUCHHAS.js.map → mcp-HNR2EH4K.js.map} +0 -0
  48. /package/dist/{preview-VFIFKVP4.js.map → preview-YBYAPNQW.js.map} +0 -0
  49. /package/dist/{server-boundary-QRVK3OIJ.js.map → server-boundary-6EM7WWNH.js.map} +0 -0
  50. /package/dist/{server-boundary-B6U4DRRH.js.map → server-boundary-LXV2W3LA.js.map} +0 -0
  51. /package/dist/{start-UAMJ5TSC.js.map → start-ZFT6OVMF.js.map} +0 -0
  52. /package/dist/{vite-plugin-ABJIDKE6.js.map → vite-plugin-MQXUYKCN.js.map} +0 -0
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import "tsx/esm";
3
- import "./chunk-G5JYB2LO.js";
3
+ import "./chunk-IF7CZXPW.js";
4
4
  import "./chunk-35QIWOHT.js";
5
- import "./chunk-CTRVTBGY.js";
5
+ import "./chunk-L4TB6FXQ.js";
6
6
  import "./chunk-RPET332G.js";
7
7
  import "./chunk-5VLP3GQ2.js";
8
8
  import "./chunk-C27VRUVN.js";
@@ -218,4 +218,4 @@ export const actions = manifest
218
218
  export {
219
219
  actionsVirtualModule
220
220
  };
221
- //# sourceMappingURL=actions-virtual-module-XAWZOWSY.js.map
221
+ //# sourceMappingURL=actions-virtual-module-GMVT4ZDV.js.map
@@ -1,4 +1,4 @@
1
- import "./chunk-CMCEGW3B.js";
1
+ import "./chunk-WOANPZBT.js";
2
2
  import "./chunk-M357ILK5.js";
3
3
  import "./chunk-BPYMC7SU.js";
4
4
  import "./chunk-IRQGAC4L.js";
@@ -234,4 +234,4 @@ export const actions = manifest
234
234
  export {
235
235
  actionsVirtualModule
236
236
  };
237
- //# sourceMappingURL=actions-virtual-module-GMRXCAVN.js.map
237
+ //# sourceMappingURL=actions-virtual-module-X3CSM3AF.js.map
@@ -3,7 +3,7 @@ import "tsx/esm";
3
3
  import {
4
4
  agentCommand,
5
5
  createAgentSsrLoader
6
- } from "./chunk-NC2OPE22.js";
6
+ } from "./chunk-S46ZHINM.js";
7
7
  import "./chunk-35QIWOHT.js";
8
8
  import "./chunk-5NNFO7R7.js";
9
9
  import "./chunk-Z7CJHDLS.js";
@@ -12,4 +12,4 @@ export {
12
12
  agentCommand,
13
13
  createAgentSsrLoader
14
14
  };
15
- //# sourceMappingURL=agent-LS7I3ZQB.js.map
15
+ //# sourceMappingURL=agent-NTEVC7XA.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scanControllerModules
3
- } from "./chunk-CMCEGW3B.js";
3
+ } from "./chunk-WOANPZBT.js";
4
4
  import "./chunk-M357ILK5.js";
5
5
  import "./chunk-BPYMC7SU.js";
6
6
  import "./chunk-IRQGAC4L.js";
@@ -427,4 +427,4 @@ export {
427
427
  generateClientDts,
428
428
  shouldReemitForFile
429
429
  };
430
- //# sourceMappingURL=app-typed-client-W74QO5LH.js.map
430
+ //# sourceMappingURL=app-typed-client-KA4JFYF4.js.map
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import "tsx/esm";
3
- import "./chunk-G5JYB2LO.js";
3
+ import "./chunk-IF7CZXPW.js";
4
4
  import "./chunk-35QIWOHT.js";
5
5
  import {
6
6
  scanControllerModules
7
- } from "./chunk-CTRVTBGY.js";
7
+ } from "./chunk-L4TB6FXQ.js";
8
8
  import "./chunk-RPET332G.js";
9
9
  import "./chunk-5VLP3GQ2.js";
10
10
  import "./chunk-C27VRUVN.js";
@@ -411,4 +411,4 @@ export {
411
411
  generateClientDts,
412
412
  shouldReemitForFile
413
413
  };
414
- //# sourceMappingURL=app-typed-client-NMKJ4DR7.js.map
414
+ //# sourceMappingURL=app-typed-client-UQA2K65X.js.map
@@ -3,7 +3,7 @@ import "tsx/esm";
3
3
  import {
4
4
  describeControllerArtifacts,
5
5
  emitControllerArtifacts
6
- } from "./chunk-CLBJTDI6.js";
6
+ } from "./chunk-KZ3PXNPT.js";
7
7
  import {
8
8
  validateProjectStructure
9
9
  } from "./chunk-GEQJAFSV.js";
@@ -14,7 +14,7 @@ import {
14
14
  emitOpenApi,
15
15
  loadRoutesForOpenApi
16
16
  } from "./chunk-47VNWTL4.js";
17
- import "./chunk-CTRVTBGY.js";
17
+ import "./chunk-L4TB6FXQ.js";
18
18
  import "./chunk-C27VRUVN.js";
19
19
  import "./chunk-7UZZHCRN.js";
20
20
  import {
@@ -554,7 +554,7 @@ async function buildCommand(options) {
554
554
  `);
555
555
  }
556
556
  async function runAdapterBuild(target, config, cwd) {
557
- const { theoPluginAsync } = await import("./vite-plugin-ABJIDKE6.js");
557
+ const { theoPluginAsync } = await import("./vite-plugin-MQXUYKCN.js");
558
558
  const { default: react } = await import("@vitejs/plugin-react");
559
559
  const ctx = {
560
560
  // `react()` may return Plugin or Plugin[] depending on version; spread the
@@ -676,4 +676,4 @@ function relativize3(absPath, root) {
676
676
  export {
677
677
  buildCommand
678
678
  };
679
- //# sourceMappingURL=build-FHE72YXR.js.map
679
+ //# sourceMappingURL=build-BBPWXXC6.js.map
@@ -12,7 +12,7 @@ import {
12
12
  sendError,
13
13
  sendJson,
14
14
  validateCsrfRequest
15
- } from "./chunk-CTRVTBGY.js";
15
+ } from "./chunk-L4TB6FXQ.js";
16
16
  import {
17
17
  buildSecurityHeaders
18
18
  } from "./chunk-RPET332G.js";
@@ -2616,4 +2616,4 @@ export {
2616
2616
  CsrfReadinessStore,
2617
2617
  applySecurityHeaders
2618
2618
  };
2619
- //# sourceMappingURL=chunk-G5JYB2LO.js.map
2619
+ //# sourceMappingURL=chunk-IF7CZXPW.js.map
@@ -2,7 +2,7 @@
2
2
  import "tsx/esm";
3
3
  import {
4
4
  findControllerFiles
5
- } from "./chunk-CTRVTBGY.js";
5
+ } from "./chunk-L4TB6FXQ.js";
6
6
 
7
7
  // src/cli/commands/build/emit-controllers.ts
8
8
  import "reflect-metadata";
@@ -186,4 +186,4 @@ export {
186
186
  emitControllerArtifacts,
187
187
  describeControllerArtifacts
188
188
  };
189
- //# sourceMappingURL=chunk-CLBJTDI6.js.map
189
+ //# sourceMappingURL=chunk-KZ3PXNPT.js.map
@@ -136,7 +136,13 @@ function incomingMessageToHandlerRequest(req) {
136
136
  // src/server/http/controller-dispatch.ts
137
137
  import { readdirSync } from "fs";
138
138
  import { join } from "path";
139
- import { createDecoratorHandler, isControllerClass } from "@theokit/http";
139
+ import {
140
+ CONTROLLER_PREFIX,
141
+ createDecoratorHandler,
142
+ getMeta,
143
+ isControllerClass,
144
+ Reflector
145
+ } from "@theokit/http";
140
146
 
141
147
  // src/server/security/csrf-warn-dispatch.ts
142
148
  function dispatchCsrfWarn(payload) {
@@ -256,6 +262,16 @@ function enforceCsrf(req, mode, logger, disallowed, auditLogger) {
256
262
 
257
263
  // src/server/http/controller-dispatch.ts
258
264
  var CSRF_PROTECTED_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
265
+ var CSRF_EXEMPT_METADATA = "theokit:csrf-exempt";
266
+ function segments(value) {
267
+ return value.split("/").filter(Boolean);
268
+ }
269
+ function pathOwnedByPrefix(pathname, prefix) {
270
+ const want = segments(prefix);
271
+ const got = segments(pathname);
272
+ if (want.length === 0 || got.length < want.length) return false;
273
+ return want.every((segment, i) => got[i] === segment);
274
+ }
259
275
  function findControllerFiles(dir) {
260
276
  const found = [];
261
277
  const walk = (current) => {
@@ -296,9 +312,15 @@ async function createControllerDispatcher(opts) {
296
312
  const classes = await scanControllers(opts.controllersDir, opts.loadModule);
297
313
  if (classes.length === 0) return null;
298
314
  const handle = createDecoratorHandler({ controllers: classes, serveAgent: opts.serveAgent });
315
+ const reflector = new Reflector();
316
+ const exemptPrefixes = classes.filter((cls) => reflector.getByKey(CSRF_EXEMPT_METADATA, cls) === true).map((cls) => {
317
+ const meta = getMeta(CONTROLLER_PREFIX, cls);
318
+ return meta?.prefix ?? "";
319
+ }).filter((prefix) => prefix !== "");
299
320
  return {
300
321
  dispatch: (request) => handle(request),
301
- matches: (method, pathname) => handle.matches(method, pathname)
322
+ matches: (method, pathname) => handle.matches(method, pathname),
323
+ isCsrfExempt: (pathname) => exemptPrefixes.some((p) => pathOwnedByPrefix(pathname, p))
302
324
  };
303
325
  }
304
326
  async function writeControllerResponse(res, response) {
@@ -323,7 +345,7 @@ async function dispatchControllerRequest(args) {
323
345
  const method = (req.method ?? "GET").toUpperCase();
324
346
  const webRequest = incomingMessageToWebRequest(req);
325
347
  const pathname = new URL(webRequest.url).pathname;
326
- if (CSRF_PROTECTED_METHODS.has(method) && dispatcher.matches(method, pathname)) {
348
+ if (CSRF_PROTECTED_METHODS.has(method) && dispatcher.matches(method, pathname) && !dispatcher.isCsrfExempt(pathname)) {
327
349
  const decision = enforceCsrf(
328
350
  req,
329
351
  csrfMode,
@@ -361,4 +383,4 @@ export {
361
383
  scanControllerModules,
362
384
  dispatchControllerRequest
363
385
  };
364
- //# sourceMappingURL=chunk-CTRVTBGY.js.map
386
+ //# sourceMappingURL=chunk-L4TB6FXQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/contracts/client-safe-error.ts","../src/server/http/send-response.ts","../src/server/http/node-request.ts","../src/server/http/controller-dispatch.ts","../src/server/security/csrf-warn-dispatch.ts","../src/server/observability/audit-log.ts","../src/server/security/csrf.ts"],"sourcesContent":["import type { TheoErrorEnvelope } from './error-envelope.js'\n\n/**\n * What an error is allowed to tell the caller.\n *\n * Most error codes describe something the caller did and can fix, so their message is the useful\n * part of the response. An *internal* failure is the opposite: its message describes the server —\n * a connection string, an upstream host, a stack of internal names — and the caller can act on\n * none of it. In production it is redacted; in development it is exactly what makes the framework\n * debuggable, so it stays.\n *\n * This lives in one place because it was previously stated in two and missing from a third. The\n * Node runner redacted, the Web error builder redacted, and an exception escaping a Web handler\n * took a hand-built path that did neither — same route, same failure, more disclosure depending\n * on which transport served it. That is the \"one contract, three transports\" rule in\n * `rules/three-target-parity.md` being broken by duplication rather than by design.\n */\n\n/** Both spellings the codebase uses for \"this is our fault, and the detail is ours too\". */\nconst INTERNAL_CODES: ReadonlySet<string> = new Set(['INTERNAL_ERROR', 'INTERNAL_SERVER_ERROR'])\n\nconst GENERIC_INTERNAL_MESSAGE = 'Internal server error'\n\nfunction redacts(code: string): boolean {\n return INTERNAL_CODES.has(code) && process.env.NODE_ENV === 'production'\n}\n\n/** The message this code may carry to the caller. */\nexport function clientSafeErrorMessage(code: string, message: string): string {\n return redacts(code) ? GENERIC_INTERNAL_MESSAGE : message\n}\n\n/**\n * The envelope this code may carry to the caller.\n *\n * When it redacts, `cause`, `meta` and `ext` go with the message rather than being filtered\n * field by field: they exist to describe the failure, and the whole point is that this failure is\n * not describable to the caller. Keeping the code is what lets a client branch on it.\n */\nexport function clientSafeErrorEnvelope(envelope: TheoErrorEnvelope): TheoErrorEnvelope {\n if (!redacts(envelope.code)) return envelope\n return { code: envelope.code, message: GENERIC_INTERNAL_MESSAGE }\n}\n","import type { ServerResponse } from 'node:http'\n\nimport { clientSafeErrorMessage } from '../../core/contracts/client-safe-error.js'\nimport type { TheoTransformer } from '../transformer.js'\n\n/**\n * Canonical HTTP response helpers (T5.1 extraction).\n *\n * Moved out of execute.ts so request-pipeline stages (execute-stages.ts,\n * handle-request-error.ts, etc.) can depend on these helpers without\n * creating a cycle through execute.ts.\n *\n * Public surface re-exported from execute.ts for backward compat — every\n * existing caller of `sendError` / `sendJson` continues to work via the\n * `theokit/server` barrel.\n */\n\nexport function sendJson(\n res: ServerResponse,\n data: unknown,\n status = 200,\n transformer?: TheoTransformer,\n): void {\n // T1.2 — transformer-aware serialization. Default (no transformer) uses\n // JSON.stringify direct for backward compat.\n const body = transformer ? transformer.serialize(data) : JSON.stringify(data)\n res.writeHead(status, {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(body),\n })\n res.end(body)\n}\n\n/** Render anything that would end the log line as a visible escape, so one call logs one line. */\nfunction oneLine(value: string): string {\n return value.replace(/[\\r\\n]/g, '\\\\n')\n}\n\nexport interface SendErrorOptions {\n custom404Html?: string\n custom500Html?: string\n}\n\n/**\n * Canonical error response.\n *\n * T6.3 (PV-17): the positional 7-param signature is preserved for backward\n * compat. New call sites should use the options-bag form:\n *\n * sendError(res, { code, message, status, issues?, requestId?, options? })\n *\n * Both shapes resolve to the same implementation.\n */\nexport interface SendErrorInput {\n code: string\n message: string\n status: number\n issues?: unknown[]\n requestId?: string\n options?: SendErrorOptions\n}\n\nexport function sendError(res: ServerResponse, input: SendErrorInput): void\n/* eslint-disable-next-line max-params -- T6.3: positional overload preserved\n for backward compat (callers across cli/server still use positional). The\n options-bag overload above is the recommended path. */\nexport function sendError(\n res: ServerResponse,\n code: string,\n message: string,\n status: number,\n issues?: unknown[],\n requestId?: string,\n options?: SendErrorOptions,\n): void\n/* eslint-disable-next-line max-params -- delegates to two surface overloads above; the parameter\n count mirrors the back-compat contract, not internal complexity. The `complexity` half of this\n suppression went away when the redaction rule stopped being restated inline. */\nexport function sendError(\n res: ServerResponse,\n codeOrInput: string | SendErrorInput,\n message?: string,\n status?: number,\n issues?: unknown[],\n requestId?: string,\n options?: SendErrorOptions,\n): void {\n let code: string\n if (typeof codeOrInput === 'string') {\n code = codeOrInput\n message = message ?? ''\n status = status ?? 500\n } else {\n code = codeOrInput.code\n message = codeOrInput.message\n status = codeOrInput.status\n issues = codeOrInput.issues\n requestId = codeOrInput.requestId\n options = codeOrInput.options\n }\n const errorMessage = clientSafeErrorMessage(code, message)\n\n if (code === 'INTERNAL_ERROR') {\n // One log entry per call, whatever the message contains. An exception message can be built\n // from request data and can therefore carry a newline; unescaped, that lets a caller append\n // whatever lines it likes to the log — including a plausible entry attributed to something\n // else (CodeQL `js/log-injection`).\n console.error(`[${oneLine(requestId ?? 'no-id')}] ${oneLine(message)}`)\n }\n\n if (status === 404 && options?.custom404Html) {\n const body = options.custom404Html\n res.writeHead(404, {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Content-Length': Buffer.byteLength(body),\n })\n res.end(body)\n return\n }\n if (status === 500 && options?.custom500Html) {\n const body = options.custom500Html\n res.writeHead(500, {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Content-Length': Buffer.byteLength(body),\n })\n res.end(body)\n return\n }\n\n sendJson(\n res,\n {\n error: {\n code,\n message: errorMessage,\n ...(requestId ? { requestId } : {}),\n ...(issues ? { issues } : {}),\n },\n },\n status,\n )\n}\n\n/**\n * T5a.2 Phase G slice 4/N — Web-Standards response helpers.\n *\n * Mirror of `sendJson` + `sendError` for the Web `Request`/`Response`\n * shape. Returns a native `Response` directly instead of mutating a\n * `ServerResponse`.\n *\n * v1.0 § Phase G.\n *\n * **Difference vs IncomingMessage path:**\n * - No `Content-Length` set explicitly — the runtime computes it from\n * the body when needed. CF Workers / Bun / Deno all do this; setting\n * it manually risks conflict if the body is a stream rather than a\n * fixed string.\n * - Custom 404/500 HTML options preserved (same opts shape).\n * - `requestId` flows into the response body's error envelope AND\n * surfaces as `x-request-id` header (parity with handleWebRequestError\n * Phase G slice 3/N).\n */\nexport function buildJsonResponse(\n data: unknown,\n status = 200,\n transformer?: TheoTransformer,\n): Response {\n const body = transformer ? transformer.serialize(data) : JSON.stringify(data)\n return new Response(body, {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\nexport function buildErrorResponse(input: SendErrorInput): Response {\n const { code, message, status, issues, requestId, options } = input\n const errorMessage = clientSafeErrorMessage(code, message)\n\n if (code === 'INTERNAL_ERROR') {\n // One log entry per call, whatever the message contains. An exception message can be built\n // from request data and can therefore carry a newline; unescaped, that lets a caller append\n // whatever lines it likes to the log — including a plausible entry attributed to something\n // else (CodeQL `js/log-injection`).\n console.error(`[${oneLine(requestId ?? 'no-id')}] ${oneLine(message)}`)\n }\n\n const headers: Record<string, string> = {}\n if (requestId !== undefined) headers['x-request-id'] = requestId\n\n if (status === 404 && options?.custom404Html !== undefined) {\n headers['content-type'] = 'text/html; charset=utf-8'\n return new Response(options.custom404Html, { status: 404, headers })\n }\n if (status === 500 && options?.custom500Html !== undefined) {\n headers['content-type'] = 'text/html; charset=utf-8'\n return new Response(options.custom500Html, { status: 500, headers })\n }\n\n headers['content-type'] = 'application/json'\n const body = JSON.stringify({\n error: {\n code,\n message: errorMessage,\n ...(requestId ? { requestId } : {}),\n ...(issues ? { issues } : {}),\n },\n })\n return new Response(body, { status, headers })\n}\n","/**\n * Pure Node `IncomingMessage` → Web `Request` converters.\n *\n * Kept free of any dependency on `web-handler.js` / `execute*.js` so the\n * executor (`execute.ts`) can build the handler-facing Web `Request` without\n * pulling the Web dispatch pipeline into its import graph (ADR-0028 R3a — the\n * Node adapter is the ONLY place IncomingMessage ↔ Request conversion lives;\n * these are the primitive converters it and the executor share).\n */\nimport type { IncomingMessage } from 'node:http'\nimport { Readable } from 'node:stream'\n\n/** Pick the first usable string from Node's `string | string[] | undefined` headers. */\nfunction pickHeaderString(value: string | string[] | undefined): string | undefined {\n if (typeof value === 'string') return value\n if (Array.isArray(value)) {\n for (const v of value) if (typeof v === 'string' && v.length > 0) return v\n }\n return undefined\n}\n\n/** Web Request requires an absolute URL; synthesize one from the Host header. */\nfunction synthesizeAbsoluteUrl(req: IncomingMessage): string {\n const host = pickHeaderString(req.headers.host) ?? 'localhost'\n return `http://${host}${req.url ?? '/'}`\n}\n\n/**\n * Collapse Node's `string | string[]` headers into a Web `Headers`. Repeated\n * headers are comma-joined (not `.append`ed) because `Headers.append` creates\n * multi-value entries that behave differently on `.get()` (EC-1).\n */\nfunction nodeHeadersToWeb(req: IncomingMessage): Headers {\n const headers = new Headers()\n for (const [key, value] of Object.entries(req.headers)) {\n if (value === undefined) continue\n if (Array.isArray(value)) {\n headers.set(key, value.join(', '))\n } else {\n headers.set(key, value)\n }\n }\n return headers\n}\n\n/**\n * Build a Web `Request` from a Node `IncomingMessage`, body included. The Web\n * Request spec requires an absolute URL; we synthesize one from the Host header\n * (fallback `localhost` for test doubles).\n *\n * For methods with a body (POST/PUT/PATCH/DELETE), the Node Readable stream is\n * wrapped as a Web ReadableStream via `Readable.toWeb()` so downstream consumers\n * can call `request.json()` / `request.formData()` / `request.text()` natively.\n */\nexport function incomingMessageToWebRequest(req: IncomingMessage): Request {\n const url = synthesizeAbsoluteUrl(req)\n const headers = nodeHeadersToWeb(req)\n\n const method = (req.method ?? 'GET').toUpperCase()\n const hasBody = method !== 'GET' && method !== 'HEAD'\n\n if (!hasBody) {\n return new Request(url, { method, headers })\n }\n\n // Drain Node's Readable into a Web ReadableStream. `Readable.toWeb` is\n // available in Node 18+ (theokit's engines.node floor is 22+, so safe).\n const webStream = Readable.toWeb(req) as ReadableStream\n return new Request(url, {\n method,\n headers,\n body: webStream,\n // EC-2: Node 18+ requires `duplex: 'half'` when body is a stream.\n // The `RequestInit` type omits it (Web spec gap); cast accordingly.\n ...({ duplex: 'half' } as { duplex: 'half' }),\n })\n}\n\n/**\n * A Node request that has NOT been converted yet — method now, body only if someone claims it.\n *\n * theokit#400. `incomingMessageToWebRequest` drains the Node stream, and a stream drains once. A\n * dispatcher that converts in order to decide whether it owns a path has already spent the body on\n * every path it does not own: the next branch attaches to a readable that has already ended, waits\n * for an `'end'` that cannot fire twice, and the request hangs with no status at all.\n *\n * The fix is an ordering one, so the type encodes the ordering: a router reads `method` (free) and\n * calls `toRequest()` only after it has decided the request is its own. Passing the source instead\n * of a `Request` is what makes \"did you convert before deciding?\" answerable by reading a signature.\n *\n * `toRequest()` memoizes, because a second conversion of the same `IncomingMessage` yields a\n * Request whose body is an empty closed stream — a silent truncation, which is worse than the hang\n * it would replace.\n */\nexport interface WebRequestSource {\n /** Uppercase HTTP method. Available without touching the body. */\n readonly method: string\n /** Convert on demand. Idempotent: repeated calls return the same `Request`. */\n toRequest: () => Request\n}\n\n/** Wrap `req` as a {@link WebRequestSource} — the conversion is deferred and memoized. */\nexport function createWebRequestSource(req: IncomingMessage): WebRequestSource {\n let converted: Request | undefined\n return {\n method: (req.method ?? 'GET').toUpperCase(),\n toRequest: () => (converted ??= incomingMessageToWebRequest(req)),\n }\n}\n\n/**\n * Build the Web `Request` handed to a route handler as `ctx.request` in the\n * Node server path (dev + `theokit start`). Method + absolute URL + headers\n * only — NO body.\n *\n * Why no body: the Node executor parses the request body BEFORE the handler\n * runs and exposes the parsed value as `ctx.body` (the typed, documented body\n * API). By the time the handler is called the Node stream is already drained,\n * so re-wrapping it would yield an empty/closed stream. Handlers read the body\n * via `ctx.body`; `ctx.request` is for the Web-standard header/cookie/URL/method\n * surface (e.g. `createSessionManagerWeb.getSession(ctx.request)`).\n *\n * Per ADR-0028 R3a, handlers see a Web `Request` in every runtime — this closes\n * the gap where the Node path leaked the raw `IncomingMessage` (whose `.headers`\n * is a plain object, so `.headers.get(...)` threw for Web-standard consumers).\n */\nexport function incomingMessageToHandlerRequest(req: IncomingMessage): Request {\n return new Request(synthesizeAbsoluteUrl(req), {\n method: (req.method ?? 'GET').toUpperCase(),\n headers: nodeHeadersToWeb(req),\n })\n}\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Controller files are walked from the developer's `serverDir/controllers`\n * (a build-time config path), never from HTTP input. No injection vector.\n */\nimport { readdirSync, type Dirent } from 'node:fs'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { join } from 'node:path'\n\nimport {\n CONTROLLER_PREFIX,\n createDecoratorHandler,\n getMeta,\n isControllerClass,\n Reflector,\n type ServeAgent,\n} from '@theokit/http'\n\nimport { dispatchCsrfWarn } from '../security/csrf-warn-dispatch.js'\nimport { enforceCsrf, type DisallowedConfig } from '../security/csrf.js'\n\nimport { incomingMessageToWebRequest } from './node-request.js'\nimport { sendError } from './send-response.js'\n\n/** A decorator controller constructor (`@Controller` class). */\ntype ControllerClass = new (...args: never[]) => object\n\n/** Loads a controller module by absolute path. In dev this is Vite's `ssrLoadModule`\n * (the Task 1.1 swc transform has already compiled the parameter decorators); tests\n * inject `@theokit/http`'s `loadControllerWithSwc`. */\nexport type ControllerModuleLoader = (absPath: string) => Promise<Record<string, unknown>>\n\n/** A built controller route table exposed as a pure Web-Standard handler. */\ninterface ControllerDispatcher {\n /** `null` = no controller route matched — the host owns the miss (404 / fall-through). */\n dispatch(request: Request): Promise<Response | null>\n /** Non-executing route probe — true when a controller route owns `method` + `pathname`. */\n matches(method: string, pathname: string): boolean\n /** True when the controller owning `pathname` declared `theokit:csrf-exempt`. */\n isCsrfExempt(pathname: string): boolean\n}\n\n// State-mutating methods get CSRF, mirroring the file-route pipeline (execute.ts).\nconst CSRF_PROTECTED_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\n/**\n * A controller declaring that it authenticates by other means, so the CSRF gate has nothing to add.\n *\n * The case this exists for is a webhook. Stripe, GitHub and every other sender authenticate with an\n * HMAC over the request body — stronger than a header, and entirely unrelated to one. None of them\n * will ever send `X-Theo-Action`, so without this a webhook endpoint answers 403 to every real\n * delivery and the only escape is `csrf: 'warn'` for the whole application (theokit#535).\n *\n * DELIBERATELY separate from `theokit:public`. They answer different questions — \"may an\n * unauthenticated caller reach this?\" and \"does this route authenticate by other means?\" — and a\n * route can want the first without the second. Conflating them would lift the gate off every public\n * route in the ecosystem as a side effect of a webhook fix.\n *\n * Declared on the CONTROLLER, not the method: the granularity a webhook needs, and it avoids a\n * second path matcher alongside `handle.matches` that could drift from it.\n */\nconst CSRF_EXEMPT_METADATA = 'theokit:csrf-exempt'\n\n/**\n * Segment-wise, so `api/hooks` never matches `api/hooks-admin`.\n *\n * Split-and-filter rather than a trimming regex: `/^\\/+|\\/+$/` is quadratic on a pathname of\n * repeated slashes, and a pathname is attacker-supplied (sonarjs/slow-regex). Splitting is linear\n * and `filter(Boolean)` drops the empty segments the leading and trailing slashes produce, which is\n * all the trim was for.\n */\nfunction segments(value: string): string[] {\n return value.split('/').filter(Boolean)\n}\n\nfunction pathOwnedByPrefix(pathname: string, prefix: string): boolean {\n const want = segments(prefix)\n const got = segments(pathname)\n if (want.length === 0 || got.length < want.length) return false\n return want.every((segment, i) => got[i] === segment)\n}\n\n/** Recursively collect `*.controller.ts` files under `dir` (absolute paths). */\n/**\n * Every `*.controller.ts` under `dir`, recursively. Exported for theokit#123: the build emitter\n * must find exactly the same set the dev dispatcher does, and two independent walks would be two\n * definitions of \"a controller\" that drift.\n */\nexport function findControllerFiles(dir: string): string[] {\n const found: string[] = []\n const walk = (current: string): void => {\n let entries: Dirent[]\n try {\n entries = readdirSync(current, { withFileTypes: true })\n } catch {\n return // dir doesn't exist — no controllers\n }\n for (const entry of entries) {\n const full = join(current, entry.name)\n if (entry.isDirectory()) walk(full)\n // theokit#123 — `.mjs` alongside `.ts`. Dev walks the SOURCE tree; production walks the\n // COMPILED tree under `dist/controllers`, where the same files exist as `*.controller.mjs`.\n // One walk for both keeps a single definition of \"a controller file\"; two would drift, and a\n // file that counts in dev and not in production is exactly the dev/prod split this fixes.\n else if (entry.name.endsWith('.controller.ts') || entry.name.endsWith('.controller.mjs'))\n found.push(full)\n }\n }\n walk(dir)\n return found\n}\n\n/** A discovered controller: its source file + the loaded `@Controller` class. */\ninterface ControllerModule {\n filePath: string\n cls: ControllerClass\n /**\n * The module's full export namespace — theokit#124.\n *\n * Kept alongside the class because it is the ONLY place a `@Body(schema)` regains a name. The\n * schema on `WalkResult.bodySchema` is a runtime `z.ZodType` with no source identifier, but it is\n * the very object this module exported, so matching it back by reference identity recovers the\n * exported name the typed-client codegen needs to write `z.infer<typeof ...>`.\n *\n * Unused by the dispatch path, which needs only the class.\n */\n exports: Readonly<Record<string, unknown>>\n}\n\n/**\n * Load every `@Controller` class under `controllersDir` via the injected loader,\n * keeping each class paired with its source file (needed by the typed-client\n * codegen to emit `import type { X } from '<file>'`). Non-controller exports are\n * ignored (`isControllerClass` — reused from @theokit/http).\n */\nexport async function scanControllerModules(\n controllersDir: string,\n loadModule: ControllerModuleLoader,\n): Promise<ControllerModule[]> {\n const files = findControllerFiles(controllersDir)\n const modules: ControllerModule[] = []\n for (const filePath of files) {\n const mod = await loadModule(filePath)\n for (const exported of Object.values(mod)) {\n if (typeof exported === 'function' && isControllerClass(exported)) {\n modules.push({ filePath, cls: exported as ControllerClass, exports: mod })\n }\n }\n }\n return modules\n}\n\n/** Load every `@Controller` class under `controllersDir` (classes only). */\nasync function scanControllers(\n controllersDir: string,\n loadModule: ControllerModuleLoader,\n): Promise<ControllerClass[]> {\n const modules = await scanControllerModules(controllersDir, loadModule)\n return modules.map((m) => m.cls)\n}\n\n/**\n * Scan `controllersDir` and build a Web-Standard dispatcher over the decorator\n * controllers found. Returns `null` when the directory has no controllers, so\n * the host can skip the controller path entirely (zero cost for routes-only apps).\n *\n * Dispatch REUSES @theokit/http's `createDecoratorHandler` (match + `@Param`\n * binding + `@Body` validation + Response building) — never re-implemented (ADR-1).\n */\nexport async function createControllerDispatcher(opts: {\n controllersDir: string\n loadModule: ControllerModuleLoader\n /** M47 — serves `@Expose`-bound agent routes (theo supplies a `mountAgent`-backed impl). */\n serveAgent?: ServeAgent\n}): Promise<ControllerDispatcher | null> {\n const classes = await scanControllers(opts.controllersDir, opts.loadModule)\n if (classes.length === 0) return null\n const handle = createDecoratorHandler({ controllers: classes, serveAgent: opts.serveAgent })\n const reflector = new Reflector()\n\n // Read once at construction: the metadata cannot change between requests, and re-walking every\n // class per request would put a reflection pass on the hot path for a value that never moves.\n const exemptPrefixes = classes\n // `Reflector`, not `Reflect.getMetadata`: the global is only typed where `reflect-metadata`\n // has been imported, and this module does not import it — the dts build fails on it (TS2339).\n // `@SetMetadata` writes through the same store, so the reader is the framework's own.\n .filter((cls) => reflector.getByKey<boolean>(CSRF_EXEMPT_METADATA, cls) === true)\n .map((cls) => {\n const meta = getMeta<{ prefix?: string }>(CONTROLLER_PREFIX, cls)\n return meta?.prefix ?? ''\n })\n .filter((prefix) => prefix !== '')\n\n return {\n dispatch: (request) => handle(request),\n matches: (method, pathname) => handle.matches(method, pathname),\n isCsrfExempt: (pathname) => exemptPrefixes.some((p) => pathOwnedByPrefix(pathname, p)),\n }\n}\n\n/**\n * Write a controller's Web `Response` to a Node response, byte for byte.\n *\n * The body used to be read with `await response.text()`, under a comment asserting that controllers\n * never stream. The comment was true of this path and hid the real problem: `text()` decodes as\n * UTF-8, so every byte >= 0x80 became `U+FFFD`. A payload of all 256 byte values arrived as 512\n * bytes; a 55 296-byte MPEG from `@theokit/plugin-voice` arrived as 76 790 bytes beginning\n * `ef bf bd`. Status 200, correct content-type, plausible length — invisible until someone opens\n * the file. File routes were never affected: `executeRoute` pumps the stream, so this was a silent\n * divergence between two paths meant to be at parity.\n *\n * `arrayBuffer()` fixes it without changing anything else. This path stays BUFFERED, exactly as\n * before — a controller returning a streamed body still has it collected here, so a plugin that\n * promises progressive delivery does not get it through a controller. That is a real limitation and\n * a separate change: making it stream alters when bytes reach the client, which is behaviour beyond\n * the corruption this repairs.\n */\nasync function writeControllerResponse(res: ServerResponse, response: Response): Promise<void> {\n const headersBag: Record<string, string> = {}\n for (const [k, v] of response.headers) {\n if (k.toLowerCase() !== 'set-cookie') headersBag[k] = v\n }\n const setCookies = response.headers.getSetCookie()\n if (setCookies.length > 0) res.setHeader('Set-Cookie', setCookies)\n res.writeHead(response.status, headersBag)\n // `Buffer.from(ArrayBuffer)` views the bytes as they are. Any string in between is a decode, and\n // a decode of arbitrary bytes is a loss.\n const body = response.body ? Buffer.from(await response.arrayBuffer()) : undefined\n res.end(body !== undefined && body.length > 0 ? body : undefined)\n}\n\n/**\n * The `api-middleware` fall-through in one call: scan `controllersDir`, build the\n * dispatcher, and serve the request. Builds a body-ful Web `Request` (`@Body`\n * needs the body; the raw stream is undrained at a route miss) and enforces CSRF\n * with the SAME gate file routes use (parity). Returns `true` when a controller\n * handled it (or CSRF blocked it), `false` when there are no controllers OR none\n * matched (the host continues to its own 404). Built per-miss so controller edits\n * reflect via HMR.\n */\nexport async function dispatchControllerRequest(args: {\n controllersDir: string\n loadModule: ControllerModuleLoader\n req: IncomingMessage\n res: ServerResponse\n csrfMode: 'off' | 'warn' | 'strict'\n disallowed?: DisallowedConfig\n requestId: string\n /** M47 — serves `@Expose`-bound agent routes (mountAgent-backed); omit for routes-only apps. */\n serveAgent?: ServeAgent\n}): Promise<boolean> {\n const { req, res, csrfMode, disallowed, requestId } = args\n const dispatcher = await createControllerDispatcher({\n controllersDir: args.controllersDir,\n loadModule: args.loadModule,\n serveAgent: args.serveAgent,\n })\n if (!dispatcher) return false\n\n const method = (req.method ?? 'GET').toUpperCase()\n const webRequest = incomingMessageToWebRequest(req)\n const pathname = new URL(webRequest.url).pathname\n\n // CSRF parity: enforce ONLY when a protected-method controller route actually\n // owns this path (probe with the non-executing matcher — never double-dispatch,\n // which would run the handler + its side effects). An unrouted path falls\n // through to the host's own 404, not a 403.\n if (\n CSRF_PROTECTED_METHODS.has(method) &&\n dispatcher.matches(method, pathname) &&\n !dispatcher.isCsrfExempt(pathname)\n ) {\n const decision = enforceCsrf(\n req,\n csrfMode,\n { warn: dispatchCsrfWarn, path: req.url },\n disallowed,\n )\n if (!decision.allow) {\n sendError(\n res,\n 'CSRF_INVALID',\n decision.reason ?? 'CSRF check failed',\n 403,\n undefined,\n requestId,\n )\n return true\n }\n }\n\n const response = await dispatcher.dispatch(webRequest)\n if (response === null) return false\n await writeControllerResponse(res, response)\n return true\n}\n","/**\n * Canonical CSRF warn dispatcher (T3.3 of architecture-review-remediation-plan).\n *\n * Consolidates the duplicated `warn: (payload) => { warnOnce(...) }` closure\n * that previously appeared in both `http/execute.ts` and\n * `http/action-execute.ts`. Resolves PV-10 (DRY).\n *\n * `warnOnce` dedupes by `event:method:path` so a request loop with 1000 POSTs\n * doesn't flood logs with identical warnings. Apps grep for `event\":\"csrf.warn\"`\n * (stable event shape — see [[enforcement-cutover.md]]).\n */\nimport { warnOnce } from '../observability/logger.js'\n\ninterface CsrfWarnPayload {\n event: string\n method: string\n path?: string\n reason: string\n code?: string\n docsUrl?: string\n warnOnce?: boolean\n}\n\n/**\n * Build the warn callback that `enforceCsrf` invokes for soft-mode warnings.\n * Returned function is suitable for the `warn` field of `enforceCsrf`'s options.\n */\nexport function dispatchCsrfWarn(payload: CsrfWarnPayload): void {\n const key = `${payload.event}:${payload.method}:${payload.path ?? ''}`\n warnOnce(key, payload as unknown as Record<string, unknown>)\n}\n","/**\n * T4.1 — Audit logging interface + default JSON stdout sink.\n *\n * Per ADR D4: define the interface; ship a zero-dep default; reserve\n * adapter shapes for Postgres, File, OpenTelemetry, Sentry as follow-up\n * packages. Persistence has heavy deps (`pg`, `better-sqlite3`); we\n * keep core dep-free and let users opt in.\n *\n * Compatibility:\n * - Node / Bun / Deno / Vercel — console.log is sync, captured.\n * - Edge runtimes (CF Workers, Vercel Edge) — console.log is captured\n * but may be rate-limited by the platform. For high-volume edge audit,\n * implement a custom sink writing to a queue / HTTP endpoint.\n */\n\nexport interface AuditEvent {\n /** Domain-qualified verb. Convention: `<domain>.<verb>` (e.g. csrf.warn, session.rotated). */\n action: string\n /** Who triggered the event. Anonymous = no auth at time of event. */\n actor?: { type: 'user' | 'system' | 'anonymous'; id?: string }\n /** What was operated on (optional). */\n resource?: { type: string; id?: string }\n /** Arbitrary event-specific metadata. JSON-serializable. */\n metadata?: Record<string, unknown>\n /** ISO 8601 timestamp. If absent, sink fills in `new Date().toISOString()`. */\n timestamp?: string\n /** Optional trace id (populated by middleware from `x-trace-id`). */\n traceId?: string\n}\n\nexport interface AuditLogger {\n log(event: AuditEvent): void | Promise<void>\n}\n\n/**\n * Default sink: one JSON line per event to stdout. Sync. Never throws.\n *\n * EC: circular refs / BigInt values fall back to a placeholder line so\n * the event is still observable (action + traceId) without crashing the\n * request lifecycle.\n */\nexport class JsonStdoutSink implements AuditLogger {\n log(event: AuditEvent): void {\n const enriched = {\n level: 'audit' as const,\n ...event,\n timestamp: event.timestamp ?? new Date().toISOString(),\n }\n try {\n // eslint-disable-next-line no-console -- JsonStdoutSink IS the audit output\n console.log(JSON.stringify(enriched, jsonReplacer))\n } catch {\n // eslint-disable-next-line no-console -- fallback when payload won't serialize\n console.log(\n `{\"level\":\"audit\",\"action\":${JSON.stringify(event.action)},\"timestamp\":${JSON.stringify(enriched.timestamp)},\"note\":\"payload could not be serialized\"}`,\n )\n }\n }\n}\n\n/**\n * Replacer that walks BigInt → string. Circular ref handling is via the\n * outer try/catch (JSON.stringify throws TypeError on cycles; we drop to\n * the fallback line). We don't implement custom cycle-breaking walker\n * because the audit payload is meant to be JSON — if user metadata has\n * a cycle, the right answer is to fix the caller, not silently lose\n * the structure.\n */\nfunction jsonReplacer(_key: string, value: unknown): unknown {\n if (typeof value === 'bigint') return value.toString()\n return value\n}\n\n/**\n * No-op logger. Returned when `config.audit` is unset. Zero overhead;\n * framework wiring sites null-check before calling.\n */\nexport function createNoOpLogger(): AuditLogger {\n return {\n log() {\n // intentionally empty\n },\n }\n}\n\n/**\n * T4.2 — Safe-emit wrapper. Used by framework wiring sites (csrf.ts,\n * rate-limit.ts, session.ts) so a logger throw NEVER propagates into\n * the request handler.\n */\nexport function safeAudit(logger: AuditLogger | undefined, event: AuditEvent): void {\n if (!logger) return\n try {\n const r = logger.log(event)\n // Discard the Promise — async sinks are fire-and-forget by design.\n if (r && typeof r.then === 'function') {\n r.catch(() => {\n // swallow async sink failures — audit must never crash the request\n })\n }\n } catch {\n // swallow sync sink failures — audit must never crash the request\n }\n}\n","import type { IncomingMessage } from 'node:http'\n\nimport type { AuditLogger } from '../observability/audit-log.js'\nimport { safeAudit } from '../observability/audit-log.js'\n\n/**\n * CSRF enforcement mode.\n *\n * - `off` — skip CSRF entirely. Use only when you have another defense\n * (e.g. you don't ship session cookies, all auth is bearer).\n * - `warn` — log a structured warning when the check would fail, but\n * still serve the request. Default for 0.2.0. Migration mode.\n * - `strict` — reject failing requests with 403 + code `CSRF_INVALID`.\n * Will become the default in 0.3.0.\n */\nexport type CsrfMode = 'off' | 'warn' | 'strict'\n\n/**\n * Per-request structured logger surface. Only `warn` is used by enforceCsrf;\n * we don't require a full Logger here so callers can pass a mock or the\n * console directly.\n */\nexport interface CsrfLogger {\n warn: (payload: CsrfWarnPayload) => void\n /** Optional path the request was destined for — used for log correlation. */\n path?: string\n}\n\n/**\n * T5.1 — Rails-inspired per-route escalation.\n *\n * `routes` accepts string (exact match) or RegExp entries. When a request\n * path matches AND the request would otherwise emit a warning, the\n * `behavior` field decides what happens:\n *\n * - `'warn'` → normal warn dispatch (no-op vs default)\n * - `'raise'` → escalate to 403 regardless of global `csrf` mode\n *\n * `'raise'` never downgrades: when global mode is `'off'`, validation is\n * skipped entirely and disallowed dispatch never runs.\n */\nexport interface DisallowedConfig {\n routes: (string | RegExp)[]\n behavior: 'warn' | 'raise'\n}\n\n/**\n * Test whether `path` matches any of the supplied patterns. String\n * patterns are EXACT (trailing slash matters — use RegExp for tolerance).\n *\n * EC-5: when a RegExp carries the `/g` flag, `.test()` mutates\n * `lastIndex` and the next invocation may miss. We reset `lastIndex`\n * before each test so the matcher is a pure function.\n */\nexport function matchDisallowed(path: string, patterns: readonly (string | RegExp)[]): boolean {\n for (const p of patterns) {\n if (typeof p === 'string') {\n if (path === p) return true\n } else if (p instanceof RegExp) {\n p.lastIndex = 0\n if (p.test(path)) return true\n }\n // Neither string nor RegExp: ignore silently (defensive — the public\n // type forbids it but runtime data may slip past).\n }\n return false\n}\n\n/**\n * T2.2 — Stable cutover identifier shipped with every csrf.warn payload.\n *\n * Convention borrowed from Vite's `deprecations.ts:74` — a `code` plus a\n * `docsUrl` lets users (a) grep their logs for a single stable identifier\n * to find every csrf.warn line, and (b) click through directly to the\n * migration guide. Strings are exported constants so the analyzer (T2.3)\n * and migration guide can reference the same source of truth.\n */\nexport const CSRF_WARN_CODE = 'CSRF_STRICT_CUTOVER' as const\nexport const CSRF_WARN_DOCS_URL = 'https://theokit.dev/upgrade/csrf-strict-cutover' as const\n\n/**\n * Pick a header value, choosing the first entry when an array (Node sets\n * arrays for headers that legitimately appear multiple times). Returns\n * `''` when the header is absent or empty — callers treat `''` as \"skip\".\n */\nfunction pickHeader(value: string | string[] | undefined): string {\n if (typeof value === 'string') return value\n if (Array.isArray(value) && value.length > 0) return value[0]\n return ''\n}\n\nexport interface CsrfWarnPayload {\n event: 'csrf.warn'\n method: string\n path: string | undefined\n reason: string\n /**\n * Stable identifier for the 0.2 → 0.3 CSRF strict cutover. Always\n * `'CSRF_STRICT_CUTOVER'`. Grep-able from prod logs.\n */\n code: string\n /**\n * Link to the section of the migration guide explaining how to clear\n * this specific warning class.\n */\n docsUrl: string\n}\n\n/**\n * T5a.2 Phase B (slice 1/6): pure header-only CSRF check extracted from\n * `validateCsrf(req: IncomingMessage)` so it can be re-used by the Web-\n * Standards `validateCsrfRequest(request: Request)` sibling. Per the T5a.2\n * plan v1.0 § Phase B, header-only leaves are first to migrate. This is\n * the dual-signature pattern (anti-pattern #2 avoidance): IncomingMessage\n * consumers unchanged; new Request consumers go through the same logic\n * via the shared helper.\n *\n * Pure logic — accepts pre-extracted header values as strings or null.\n */\nfunction isCsrfValidFromHeaders(opts: {\n csrfActionHeader: string | null\n origin: string | null\n host: string | null\n}): { valid: true } | { valid: false; reason: string } {\n // 1. Custom header must be present (primary defense — simple form posts\n // cannot set custom headers, browsers gate via CORS preflight)\n if (opts.csrfActionHeader !== '1') {\n return { valid: false, reason: 'Missing X-Theo-Action header' }\n }\n\n // 2. Origin matching (secondary defense)\n if (opts.origin === null || opts.origin === '') {\n // Browsers omit Origin for same-origin requests — treat as valid\n return { valid: true }\n }\n\n if (opts.host === null || opts.host === '') {\n return { valid: true }\n }\n\n try {\n const originHost = new URL(opts.origin).host\n if (originHost !== opts.host) {\n return { valid: false, reason: `Origin ${opts.origin} does not match host ${opts.host}` }\n }\n } catch {\n return { valid: false, reason: `Invalid origin: ${opts.origin}` }\n }\n\n return { valid: true }\n}\n\nexport function validateCsrf(\n req: IncomingMessage,\n): { valid: true } | { valid: false; reason: string } {\n // IncomingMessage adapter — normalize Node header shape to the pure\n // helper's input shape (string|null).\n const action = req.headers['x-theo-action']\n const origin = req.headers.origin\n const host = req.headers.host\n\n // RFC 6454: Origin is single-valued. A caller that synthesizes an\n // IncomingMessage — an adapter, a shim, a proxy library — can hand us an\n // array, and choosing one of two conflicting origins is a decision the\n // request never authorized. The disagreement IS the rejection.\n //\n // `node:http` itself joins a repeated Origin with `, ` rather than\n // producing an array, and that string already fails to parse as a URL\n // below. This branch covers the shape the type allows and `pickHeader`\n // used to resolve silently.\n if (Array.isArray(origin)) {\n return { valid: false, reason: 'Multiple Origin headers (RFC 6454 violation)' }\n }\n\n return isCsrfValidFromHeaders({\n csrfActionHeader: typeof action === 'string' ? action : null,\n origin: origin !== undefined ? origin || null : null,\n host: host !== undefined ? pickHeader(host) || null : null,\n })\n}\n\n/**\n * T5a.2 Phase B (slice 1/6) — Web-Standards-shaped CSRF validator.\n *\n * Mirror of `validateCsrf(req: IncomingMessage)` for the Web `Request`\n * shape. Consumes `request.headers.get(name)` (native Web `Headers` API)\n * instead of `req.headers[name]` (Node `IncomingMessage` indexer). Same\n * CSRF policy + same return shape — the difference is only the input\n * extraction.\n *\n * Used by `executeWebRequest` (T5a.2 Phase A) to enforce CSRF on the\n * Web-Standards request handler entry-point.\n */\nexport function validateCsrfRequest(\n request: Request,\n): { valid: true } | { valid: false; reason: string } {\n return isCsrfValidFromHeaders({\n csrfActionHeader: request.headers.get('x-theo-action'),\n origin: request.headers.get('origin'),\n host: request.headers.get('host'),\n })\n}\n\n/**\n * Enforce CSRF policy with mode-aware behavior. Wrapper over `validateCsrf`\n * that turns the boolean valid/invalid into a request-level allow decision,\n * gated by mode + structured warning in warn mode.\n *\n * Phase 5 — CSRF warn-first (EC-1).\n */\n/**\n * Dispatch the csrf.warn payload to both the structured logger and the\n * audit sink (when configured). Extracted from `enforceCsrf` to keep that\n * function's complexity within ceiling.\n */\nfunction dispatchCsrfWarn(\n req: IncomingMessage,\n reason: string,\n logger: CsrfLogger | undefined,\n auditLogger: AuditLogger | undefined,\n pathFallback = '',\n): void {\n const payload: CsrfWarnPayload = {\n event: 'csrf.warn',\n method: req.method ?? 'UNKNOWN',\n path: logger?.path ?? pathFallback,\n reason,\n code: CSRF_WARN_CODE,\n docsUrl: CSRF_WARN_DOCS_URL,\n }\n logger?.warn(payload)\n // `metadata` is typed as Record<string, unknown>; CsrfWarnPayload is a\n // structurally-equivalent shape but lacks the index signature.\n safeAudit(auditLogger, {\n action: 'csrf.warn',\n actor: { type: 'anonymous' },\n metadata: { ...payload },\n })\n}\n\nexport function enforceCsrf(\n req: IncomingMessage,\n mode: CsrfMode,\n logger?: CsrfLogger,\n disallowed?: DisallowedConfig,\n auditLogger?: AuditLogger,\n): { allow: boolean; reason?: string } {\n if (mode === 'off') {\n // `off` short-circuits before disallowed dispatch — users who set\n // csrf: 'off' globally have explicitly turned validation off, and\n // disallowed must never re-introduce it. The escape hatch is to\n // set csrf: 'warn' and use disallowed for surgical strict pockets.\n return { allow: true }\n }\n\n const check = validateCsrf(req)\n if (check.valid) {\n return { allow: true }\n }\n\n // T5.1 — disallowed dispatch: when the failing request matches a\n // disallowed pattern AND behavior is 'raise', escalate to 403 even if\n // global mode is 'warn'. Strict mode would 403 anyway, so the branch\n // is a no-op there.\n if (disallowed?.behavior === 'raise') {\n const path = logger?.path ?? req.url ?? ''\n if (matchDisallowed(path, disallowed.routes)) {\n return { allow: false, reason: check.reason }\n }\n }\n\n if (mode === 'warn') {\n // T2.1: emit via warnOnce by default — callers can override via the\n // injected logger.warn (tests, custom log routers).\n // T2.2: include the stable cutover code + docsUrl so logs are\n // grep-able and click-through-able.\n dispatchCsrfWarn(req, check.reason, logger, auditLogger)\n return { allow: true, reason: check.reason }\n }\n\n // strict — 403 the request, AND emit a warn payload so the dev (and\n // devtools UI) sees WHY it was blocked + the docsUrl to fix it.\n // Without this, strict-mode users get a silent 403 with no context.\n dispatchCsrfWarn(req, check.reason, logger, auditLogger)\n return { allow: false, reason: check.reason }\n}\n"],"mappings":"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","names":["dispatchCsrfWarn"]}
@@ -12,7 +12,7 @@ import {
12
12
  serveMatchedAuxRoute,
13
13
  serveThroughPluginLifecycle,
14
14
  setupSsrDevMiddleware
15
- } from "./chunk-OKDCBENR.js";
15
+ } from "./chunk-VJRPOUKM.js";
16
16
  import {
17
17
  resolveTheoRootDir
18
18
  } from "./chunk-FDOOBTXJ.js";
@@ -38,7 +38,7 @@ import {
38
38
  mountAgent,
39
39
  readAgentPolicy,
40
40
  writeWebResponseToServerResponse
41
- } from "./chunk-G5JYB2LO.js";
41
+ } from "./chunk-IF7CZXPW.js";
42
42
  import {
43
43
  getApprovalRegistry,
44
44
  resolveProvider
@@ -48,7 +48,7 @@ import {
48
48
  dispatchControllerRequest,
49
49
  incomingMessageToHandlerRequest,
50
50
  sendError
51
- } from "./chunk-CTRVTBGY.js";
51
+ } from "./chunk-L4TB6FXQ.js";
52
52
  import {
53
53
  TRACE_HEADER,
54
54
  createObservabilityPluginFromConfig,
@@ -2179,19 +2179,19 @@ async function theoPluginAsync(rootOrOptions) {
2179
2179
  })
2180
2180
  );
2181
2181
  }
2182
- const { appTypedClientPlugin } = await import("./app-typed-client-NMKJ4DR7.js");
2182
+ const { appTypedClientPlugin } = await import("./app-typed-client-UQA2K65X.js");
2183
2183
  const appClientPlugin = appTypedClientPlugin({
2184
2184
  cwd: projectRoot,
2185
2185
  serverDir: serverDirAbs,
2186
2186
  distDir: resolve5(projectRoot, ".theokit")
2187
2187
  });
2188
- const { actionsVirtualModule } = await import("./actions-virtual-module-XAWZOWSY.js");
2188
+ const { actionsVirtualModule } = await import("./actions-virtual-module-GMVT4ZDV.js");
2189
2189
  const actionsPlugin = actionsVirtualModule({
2190
2190
  serverDir: serverDirAbs,
2191
2191
  distDir: resolve5(projectRoot, ".theokit")
2192
2192
  });
2193
2193
  const { agentsTypedClientPlugin } = await import("./agents-typed-client-D4OA2TXB.js");
2194
- const { generateManifest } = await import("./internal-api-732K3H75.js");
2194
+ const { generateManifest } = await import("./internal-api-LDPEUWNZ.js");
2195
2195
  const agentsClientPlugin = agentsTypedClientPlugin({
2196
2196
  projectRoot,
2197
2197
  agentsDir: agentsDirName,
@@ -2208,7 +2208,7 @@ async function theoPluginAsync(rootOrOptions) {
2208
2208
  const controllerTransformPlugin = controllerSwcTransformPlugin({
2209
2209
  serverDir: serverDirAbs
2210
2210
  });
2211
- const { serverOnlyImportBoundary } = await import("./server-boundary-B6U4DRRH.js");
2211
+ const { serverOnlyImportBoundary } = await import("./server-boundary-LXV2W3LA.js");
2212
2212
  const serverBoundaryPlugin = serverOnlyImportBoundary({
2213
2213
  projectRoot,
2214
2214
  serverDir: serverDirAbs
@@ -2383,4 +2383,4 @@ export {
2383
2383
  theoPluginAsync,
2384
2384
  theoPlugin
2385
2385
  };
2386
- //# sourceMappingURL=chunk-KEDRCI2L.js.map
2386
+ //# sourceMappingURL=chunk-N2OXYOV6.js.map
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-SSYDEKWF.js";
8
8
  import {
9
9
  dispatchControllerRequest
10
- } from "./chunk-CMCEGW3B.js";
10
+ } from "./chunk-WOANPZBT.js";
11
11
  import {
12
12
  RUN_ID_HEADER,
13
13
  SSE_BASE_HEADERS,
@@ -3484,19 +3484,19 @@ async function theoPluginAsync(rootOrOptions) {
3484
3484
  })
3485
3485
  );
3486
3486
  }
3487
- const { appTypedClientPlugin } = await import("./app-typed-client-W74QO5LH.js");
3487
+ const { appTypedClientPlugin } = await import("./app-typed-client-KA4JFYF4.js");
3488
3488
  const appClientPlugin = appTypedClientPlugin({
3489
3489
  cwd: projectRoot,
3490
3490
  serverDir: serverDirAbs,
3491
3491
  distDir: resolve10(projectRoot, ".theokit")
3492
3492
  });
3493
- const { actionsVirtualModule } = await import("./actions-virtual-module-GMRXCAVN.js");
3493
+ const { actionsVirtualModule } = await import("./actions-virtual-module-X3CSM3AF.js");
3494
3494
  const actionsPlugin = actionsVirtualModule({
3495
3495
  serverDir: serverDirAbs,
3496
3496
  distDir: resolve10(projectRoot, ".theokit")
3497
3497
  });
3498
3498
  const { agentsTypedClientPlugin } = await import("./agents-typed-client-PVUZU65B.js");
3499
- const { generateManifest } = await import("./internal-api-EVJSGGO3.js");
3499
+ const { generateManifest } = await import("./internal-api-VY5LXKVZ.js");
3500
3500
  const agentsClientPlugin = agentsTypedClientPlugin({
3501
3501
  projectRoot,
3502
3502
  agentsDir: agentsDirName,
@@ -3513,7 +3513,7 @@ async function theoPluginAsync(rootOrOptions) {
3513
3513
  const controllerTransformPlugin = controllerSwcTransformPlugin({
3514
3514
  serverDir: serverDirAbs
3515
3515
  });
3516
- const { serverOnlyImportBoundary } = await import("./server-boundary-QRVK3OIJ.js");
3516
+ const { serverOnlyImportBoundary } = await import("./server-boundary-6EM7WWNH.js");
3517
3517
  const serverBoundaryPlugin = serverOnlyImportBoundary({
3518
3518
  projectRoot,
3519
3519
  serverDir: serverDirAbs
@@ -3696,4 +3696,4 @@ export {
3696
3696
  theoPluginAsync,
3697
3697
  theoPlugin
3698
3698
  };
3699
- //# sourceMappingURL=chunk-Q6HS3NFG.js.map
3699
+ //# sourceMappingURL=chunk-NLBJ27DJ.js.map
@@ -178,7 +178,7 @@ async function agentCommand(name, message, deps = {}) {
178
178
  async function createAgentSsrLoader(projectRoot) {
179
179
  const { createServer } = await import("vite");
180
180
  const react = (await import("@vitejs/plugin-react")).default;
181
- const { theoPluginAsync } = await import("./vite-plugin-ABJIDKE6.js");
181
+ const { theoPluginAsync } = await import("./vite-plugin-MQXUYKCN.js");
182
182
  const { loadConfig } = await import("./load-config-BKUAMGO5.js");
183
183
  const config = await loadConfig(projectRoot);
184
184
  const theoPlugins = await theoPluginAsync({
@@ -212,4 +212,4 @@ export {
212
212
  agentCommand,
213
213
  createAgentSsrLoader
214
214
  };
215
- //# sourceMappingURL=chunk-NC2OPE22.js.map
215
+ //# sourceMappingURL=chunk-S46ZHINM.js.map
@@ -25,14 +25,14 @@ import {
25
25
  parseAgentRequestBody,
26
26
  readAgentPolicy,
27
27
  subjectFromContext
28
- } from "./chunk-G5JYB2LO.js";
28
+ } from "./chunk-IF7CZXPW.js";
29
29
  import {
30
30
  getApprovalRegistry
31
31
  } from "./chunk-35QIWOHT.js";
32
32
  import {
33
33
  sendError,
34
34
  validateCsrfRequest
35
- } from "./chunk-CTRVTBGY.js";
35
+ } from "./chunk-L4TB6FXQ.js";
36
36
  import {
37
37
  generateNonce
38
38
  } from "./chunk-RPET332G.js";
@@ -867,4 +867,4 @@ export {
867
867
  applyNonceToInlineScripts,
868
868
  setupSsrDevMiddleware
869
869
  };
870
- //# sourceMappingURL=chunk-OKDCBENR.js.map
870
+ //# sourceMappingURL=chunk-VJRPOUKM.js.map
@@ -10,8 +10,24 @@ import {
10
10
  // src/server/http/controller-dispatch.ts
11
11
  import { readdirSync } from "node:fs";
12
12
  import { join } from "node:path";
13
- import { createDecoratorHandler, isControllerClass } from "@theokit/http";
13
+ import {
14
+ CONTROLLER_PREFIX,
15
+ createDecoratorHandler,
16
+ getMeta,
17
+ isControllerClass,
18
+ Reflector
19
+ } from "@theokit/http";
14
20
  var CSRF_PROTECTED_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
21
+ var CSRF_EXEMPT_METADATA = "theokit:csrf-exempt";
22
+ function segments(value) {
23
+ return value.split("/").filter(Boolean);
24
+ }
25
+ function pathOwnedByPrefix(pathname, prefix) {
26
+ const want = segments(prefix);
27
+ const got = segments(pathname);
28
+ if (want.length === 0 || got.length < want.length) return false;
29
+ return want.every((segment, i) => got[i] === segment);
30
+ }
15
31
  function findControllerFiles(dir) {
16
32
  const found = [];
17
33
  const walk = (current) => {
@@ -52,9 +68,15 @@ async function createControllerDispatcher(opts) {
52
68
  const classes = await scanControllers(opts.controllersDir, opts.loadModule);
53
69
  if (classes.length === 0) return null;
54
70
  const handle = createDecoratorHandler({ controllers: classes, serveAgent: opts.serveAgent });
71
+ const reflector = new Reflector();
72
+ const exemptPrefixes = classes.filter((cls) => reflector.getByKey(CSRF_EXEMPT_METADATA, cls) === true).map((cls) => {
73
+ const meta = getMeta(CONTROLLER_PREFIX, cls);
74
+ return meta?.prefix ?? "";
75
+ }).filter((prefix) => prefix !== "");
55
76
  return {
56
77
  dispatch: (request) => handle(request),
57
- matches: (method, pathname) => handle.matches(method, pathname)
78
+ matches: (method, pathname) => handle.matches(method, pathname),
79
+ isCsrfExempt: (pathname) => exemptPrefixes.some((p) => pathOwnedByPrefix(pathname, p))
58
80
  };
59
81
  }
60
82
  async function writeControllerResponse(res, response) {
@@ -79,7 +101,7 @@ async function dispatchControllerRequest(args) {
79
101
  const method = (req.method ?? "GET").toUpperCase();
80
102
  const webRequest = incomingMessageToWebRequest(req);
81
103
  const pathname = new URL(webRequest.url).pathname;
82
- if (CSRF_PROTECTED_METHODS.has(method) && dispatcher.matches(method, pathname)) {
104
+ if (CSRF_PROTECTED_METHODS.has(method) && dispatcher.matches(method, pathname) && !dispatcher.isCsrfExempt(pathname)) {
83
105
  const decision = enforceCsrf(
84
106
  req,
85
107
  csrfMode,
@@ -108,4 +130,4 @@ export {
108
130
  scanControllerModules,
109
131
  dispatchControllerRequest
110
132
  };
111
- //# sourceMappingURL=chunk-CMCEGW3B.js.map
133
+ //# sourceMappingURL=chunk-WOANPZBT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/http/controller-dispatch.ts"],"mappings":"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","names":[]}
package/dist/cli/index.js CHANGED
@@ -15,12 +15,12 @@ function cliVersion() {
15
15
  // src/cli/index.ts
16
16
  var cli = cac("theokit");
17
17
  cli.command("dev", "Start development server").option("--port <port>", "Port number").action(async (options) => {
18
- const { devCommand } = await import("../dev-WDKROZBS.js");
18
+ const { devCommand } = await import("../dev-2SNAEDZH.js");
19
19
  await devCommand({ port: options.port ? Number(options.port) : void 0 });
20
20
  });
21
21
  cli.command("build", "Build for production").option("--target <target>", "Deploy target (node, vercel, cloudflare)").action(async (options) => {
22
22
  try {
23
- const { buildCommand } = await import("../build-FHE72YXR.js");
23
+ const { buildCommand } = await import("../build-BBPWXXC6.js");
24
24
  await buildCommand({ target: options.target });
25
25
  } catch (err) {
26
26
  const msg = err instanceof Error ? err.message : String(err);
@@ -32,7 +32,7 @@ cli.command("build", "Build for production").option("--target <target>", "Deploy
32
32
  });
33
33
  cli.command("start", "Start production server").option("--port <port>", "Port number").action(async (options) => {
34
34
  try {
35
- const { startCommand } = await import("../start-UAMJ5TSC.js");
35
+ const { startCommand } = await import("../start-ZFT6OVMF.js");
36
36
  await startCommand({ port: options.port ? Number(options.port) : void 0 });
37
37
  } catch (err) {
38
38
  const msg = err instanceof Error ? err.message : String(err);
@@ -44,7 +44,7 @@ cli.command("start", "Start production server").option("--port <port>", "Port nu
44
44
  });
45
45
  cli.command("preview", "Build for production, then serve it \u2014 one step (B-030)").option("--port <port>", "Port number").option("--target <target>", "Deploy target (node, vercel, cloudflare)").action(async (options) => {
46
46
  try {
47
- const { previewCommand } = await import("../preview-VFIFKVP4.js");
47
+ const { previewCommand } = await import("../preview-YBYAPNQW.js");
48
48
  await previewCommand({
49
49
  port: options.port ? Number(options.port) : void 0,
50
50
  target: options.target
@@ -77,7 +77,7 @@ cli.command(
77
77
  "Run an agent in the terminal (stream + tool calls + approval)"
78
78
  ).action(async (name, message) => {
79
79
  try {
80
- const { agentCommand } = await import("../agent-LS7I3ZQB.js");
80
+ const { agentCommand } = await import("../agent-NTEVC7XA.js");
81
81
  const { sawError } = await agentCommand(name, message);
82
82
  if (sawError) process.exit(1);
83
83
  } catch (err) {
@@ -108,7 +108,7 @@ cli.command("agent sessions gc", "Collect old transcripts for this project (dry
108
108
  });
109
109
  cli.command("mcp <agent>", "Serve an agent as an MCP server over stdio (for desktop MCP clients)").action(async (agent) => {
110
110
  try {
111
- const { mcpCommand } = await import("../mcp-PBUCHHAS.js");
111
+ const { mcpCommand } = await import("../mcp-HNR2EH4K.js");
112
112
  await mcpCommand(agent);
113
113
  } catch (err) {
114
114
  const msg = err instanceof Error ? err.message : String(err);
@@ -11,15 +11,15 @@ import {
11
11
  } from "./chunk-KTMQUYED.js";
12
12
  import {
13
13
  theoPluginAsync
14
- } from "./chunk-KEDRCI2L.js";
15
- import "./chunk-OKDCBENR.js";
14
+ } from "./chunk-N2OXYOV6.js";
15
+ import "./chunk-VJRPOUKM.js";
16
16
  import "./chunk-2CVV6CNN.js";
17
17
  import "./chunk-3PWQQWT6.js";
18
18
  import "./chunk-FDOOBTXJ.js";
19
19
  import "./chunk-47MM2JUK.js";
20
- import "./chunk-G5JYB2LO.js";
20
+ import "./chunk-IF7CZXPW.js";
21
21
  import "./chunk-35QIWOHT.js";
22
- import "./chunk-CTRVTBGY.js";
22
+ import "./chunk-L4TB6FXQ.js";
23
23
  import "./chunk-RPET332G.js";
24
24
  import "./chunk-5VLP3GQ2.js";
25
25
  import "./chunk-C27VRUVN.js";
@@ -123,4 +123,4 @@ export {
123
123
  devCommand,
124
124
  startDevServer
125
125
  };
126
- //# sourceMappingURL=dev-WDKROZBS.js.map
126
+ //# sourceMappingURL=dev-2SNAEDZH.js.map
package/dist/index.js CHANGED
@@ -8,10 +8,10 @@ import {
8
8
  scanRoutes,
9
9
  theoConfigSchema,
10
10
  theoPlugin
11
- } from "./chunk-Q6HS3NFG.js";
11
+ } from "./chunk-NLBJ27DJ.js";
12
12
  import "./chunk-7NRFZBAL.js";
13
13
  import "./chunk-SSYDEKWF.js";
14
- import "./chunk-CMCEGW3B.js";
14
+ import "./chunk-WOANPZBT.js";
15
15
  import "./chunk-M357ILK5.js";
16
16
  import "./chunk-OIGJOYX6.js";
17
17
  import "./chunk-U3SPBLJF.js";
@@ -16,7 +16,7 @@ import {
16
16
  handleCspReport,
17
17
  handleCsrfReadiness,
18
18
  mountAgent
19
- } from "./chunk-G5JYB2LO.js";
19
+ } from "./chunk-IF7CZXPW.js";
20
20
  import {
21
21
  resolveProvider
22
22
  } from "./chunk-35QIWOHT.js";
@@ -24,7 +24,7 @@ import {
24
24
  dispatchControllerRequest,
25
25
  incomingMessageToHandlerRequest,
26
26
  sendError
27
- } from "./chunk-CTRVTBGY.js";
27
+ } from "./chunk-L4TB6FXQ.js";
28
28
  import {
29
29
  generateNonce
30
30
  } from "./chunk-RPET332G.js";
@@ -87,4 +87,4 @@ export {
87
87
  scanWebSocketRoutes,
88
88
  sendError
89
89
  };
90
- //# sourceMappingURL=internal-api-732K3H75.js.map
90
+ //# sourceMappingURL=internal-api-LDPEUWNZ.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  dispatchControllerRequest
3
- } from "./chunk-CMCEGW3B.js";
3
+ } from "./chunk-WOANPZBT.js";
4
4
  import {
5
5
  mountAgent
6
6
  } from "./chunk-M357ILK5.js";
@@ -107,4 +107,4 @@ export {
107
107
  scanWebSocketRoutes,
108
108
  sendError
109
109
  };
110
- //# sourceMappingURL=internal-api-EVJSGGO3.js.map
110
+ //# sourceMappingURL=internal-api-VY5LXKVZ.js.map
@@ -2,7 +2,7 @@
2
2
  import "tsx/esm";
3
3
  import {
4
4
  createAgentSsrLoader
5
- } from "./chunk-NC2OPE22.js";
5
+ } from "./chunk-S46ZHINM.js";
6
6
  import {
7
7
  extractAppResources,
8
8
  handleMcpJsonRpc
@@ -75,4 +75,4 @@ async function mcpCommand(name, deps = {}) {
75
75
  export {
76
76
  mcpCommand
77
77
  };
78
- //# sourceMappingURL=mcp-PBUCHHAS.js.map
78
+ //# sourceMappingURL=mcp-HNR2EH4K.js.map
@@ -4,8 +4,8 @@ import "tsx/esm";
4
4
  // src/cli/commands/preview.ts
5
5
  async function defaultSteps() {
6
6
  const [{ buildCommand }, { startCommand }] = await Promise.all([
7
- import("./build-FHE72YXR.js"),
8
- import("./start-UAMJ5TSC.js")
7
+ import("./build-BBPWXXC6.js"),
8
+ import("./start-ZFT6OVMF.js")
9
9
  ]);
10
10
  return {
11
11
  build: (opts) => buildCommand(opts),
@@ -20,4 +20,4 @@ async function previewCommand(options = {}, steps) {
20
20
  export {
21
21
  previewCommand
22
22
  };
23
- //# sourceMappingURL=preview-VFIFKVP4.js.map
23
+ //# sourceMappingURL=preview-YBYAPNQW.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  resolveTheoRootDir
3
3
  } from "./chunk-7NRFZBAL.js";
4
- import "./chunk-CMCEGW3B.js";
4
+ import "./chunk-WOANPZBT.js";
5
5
  import "./chunk-M357ILK5.js";
6
6
  import "./chunk-BPYMC7SU.js";
7
7
  import "./chunk-IRQGAC4L.js";
@@ -147,4 +147,4 @@ export {
147
147
  ServerOnlyImportError,
148
148
  serverOnlyImportBoundary
149
149
  };
150
- //# sourceMappingURL=server-boundary-QRVK3OIJ.js.map
150
+ //# sourceMappingURL=server-boundary-6EM7WWNH.js.map
@@ -3,9 +3,9 @@ import "tsx/esm";
3
3
  import {
4
4
  resolveTheoRootDir
5
5
  } from "./chunk-FDOOBTXJ.js";
6
- import "./chunk-G5JYB2LO.js";
6
+ import "./chunk-IF7CZXPW.js";
7
7
  import "./chunk-35QIWOHT.js";
8
- import "./chunk-CTRVTBGY.js";
8
+ import "./chunk-L4TB6FXQ.js";
9
9
  import "./chunk-RPET332G.js";
10
10
  import "./chunk-5VLP3GQ2.js";
11
11
  import "./chunk-C27VRUVN.js";
@@ -131,4 +131,4 @@ export {
131
131
  ServerOnlyImportError,
132
132
  serverOnlyImportBoundary
133
133
  };
134
- //# sourceMappingURL=server-boundary-B6U4DRRH.js.map
134
+ //# sourceMappingURL=server-boundary-LXV2W3LA.js.map
@@ -2,7 +2,7 @@
2
2
  import "tsx/esm";
3
3
  import {
4
4
  CONTROLLER_MANIFEST_FILE
5
- } from "./chunk-CLBJTDI6.js";
5
+ } from "./chunk-KZ3PXNPT.js";
6
6
  import {
7
7
  parseCookieHeader
8
8
  } from "./chunk-V7MTRM6Q.js";
@@ -25,7 +25,7 @@ import {
25
25
  resolveTransformer,
26
26
  serveMatchedAuxRoute,
27
27
  serveThroughPluginLifecycle
28
- } from "./chunk-OKDCBENR.js";
28
+ } from "./chunk-VJRPOUKM.js";
29
29
  import "./chunk-2CVV6CNN.js";
30
30
  import {
31
31
  findRootDiv
@@ -41,7 +41,7 @@ import {
41
41
  mountAgent,
42
42
  readAgentPolicy,
43
43
  writeWebResponseToServerResponse
44
- } from "./chunk-G5JYB2LO.js";
44
+ } from "./chunk-IF7CZXPW.js";
45
45
  import {
46
46
  getApprovalRegistry,
47
47
  resolveProvider
@@ -50,7 +50,7 @@ import {
50
50
  createWebRequestSource,
51
51
  dispatchControllerRequest,
52
52
  sendError
53
- } from "./chunk-CTRVTBGY.js";
53
+ } from "./chunk-L4TB6FXQ.js";
54
54
  import {
55
55
  buildSecurityHeaders,
56
56
  generateNonce
@@ -1381,4 +1381,4 @@ export {
1381
1381
  resolveSsrEntry,
1382
1382
  startCommand
1383
1383
  };
1384
- //# sourceMappingURL=start-UAMJ5TSC.js.map
1384
+ //# sourceMappingURL=start-ZFT6OVMF.js.map
@@ -5,10 +5,10 @@ import {
5
5
  defineTheoIntegration,
6
6
  theoPlugin,
7
7
  theoPluginAsync
8
- } from "../chunk-Q6HS3NFG.js";
8
+ } from "../chunk-NLBJ27DJ.js";
9
9
  import "../chunk-7NRFZBAL.js";
10
10
  import "../chunk-SSYDEKWF.js";
11
- import "../chunk-CMCEGW3B.js";
11
+ import "../chunk-WOANPZBT.js";
12
12
  import "../chunk-M357ILK5.js";
13
13
  import "../chunk-OIGJOYX6.js";
14
14
  import "../chunk-U3SPBLJF.js";
@@ -7,15 +7,15 @@ import {
7
7
  defineTheoIntegration,
8
8
  theoPlugin,
9
9
  theoPluginAsync
10
- } from "./chunk-KEDRCI2L.js";
11
- import "./chunk-OKDCBENR.js";
10
+ } from "./chunk-N2OXYOV6.js";
11
+ import "./chunk-VJRPOUKM.js";
12
12
  import "./chunk-2CVV6CNN.js";
13
13
  import "./chunk-3PWQQWT6.js";
14
14
  import "./chunk-FDOOBTXJ.js";
15
15
  import "./chunk-47MM2JUK.js";
16
- import "./chunk-G5JYB2LO.js";
16
+ import "./chunk-IF7CZXPW.js";
17
17
  import "./chunk-35QIWOHT.js";
18
- import "./chunk-CTRVTBGY.js";
18
+ import "./chunk-L4TB6FXQ.js";
19
19
  import "./chunk-RPET332G.js";
20
20
  import "./chunk-5VLP3GQ2.js";
21
21
  import "./chunk-C27VRUVN.js";
@@ -40,4 +40,4 @@ export {
40
40
  theoPlugin,
41
41
  theoPluginAsync
42
42
  };
43
- //# sourceMappingURL=vite-plugin-ABJIDKE6.js.map
43
+ //# sourceMappingURL=vite-plugin-MQXUYKCN.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theokit",
3
- "version": "0.57.0",
3
+ "version": "0.58.0",
4
4
  "description": "The TheoKit web framework — file-based routing, typed server surfaces, the Vite plugin, the CLI and the deploy adapters, around agents served from agents/*.ts.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -149,8 +149,8 @@
149
149
  "tsx": "^4.22.4",
150
150
  "typescript": "^5.9.3",
151
151
  "vite": "^7.0.0",
152
+ "@theokit/http": "^1.1.2",
152
153
  "@theokit/agents": "^12.1.0",
153
- "@theokit/http": "^1.1.1",
154
154
  "@theokit/presenter": "^0.8.0"
155
155
  },
156
156
  "peerDependencies": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/server/http/controller-dispatch.ts"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/contracts/client-safe-error.ts","../src/server/http/send-response.ts","../src/server/http/node-request.ts","../src/server/http/controller-dispatch.ts","../src/server/security/csrf-warn-dispatch.ts","../src/server/observability/audit-log.ts","../src/server/security/csrf.ts"],"sourcesContent":["import type { TheoErrorEnvelope } from './error-envelope.js'\n\n/**\n * What an error is allowed to tell the caller.\n *\n * Most error codes describe something the caller did and can fix, so their message is the useful\n * part of the response. An *internal* failure is the opposite: its message describes the server —\n * a connection string, an upstream host, a stack of internal names — and the caller can act on\n * none of it. In production it is redacted; in development it is exactly what makes the framework\n * debuggable, so it stays.\n *\n * This lives in one place because it was previously stated in two and missing from a third. The\n * Node runner redacted, the Web error builder redacted, and an exception escaping a Web handler\n * took a hand-built path that did neither — same route, same failure, more disclosure depending\n * on which transport served it. That is the \"one contract, three transports\" rule in\n * `rules/three-target-parity.md` being broken by duplication rather than by design.\n */\n\n/** Both spellings the codebase uses for \"this is our fault, and the detail is ours too\". */\nconst INTERNAL_CODES: ReadonlySet<string> = new Set(['INTERNAL_ERROR', 'INTERNAL_SERVER_ERROR'])\n\nconst GENERIC_INTERNAL_MESSAGE = 'Internal server error'\n\nfunction redacts(code: string): boolean {\n return INTERNAL_CODES.has(code) && process.env.NODE_ENV === 'production'\n}\n\n/** The message this code may carry to the caller. */\nexport function clientSafeErrorMessage(code: string, message: string): string {\n return redacts(code) ? GENERIC_INTERNAL_MESSAGE : message\n}\n\n/**\n * The envelope this code may carry to the caller.\n *\n * When it redacts, `cause`, `meta` and `ext` go with the message rather than being filtered\n * field by field: they exist to describe the failure, and the whole point is that this failure is\n * not describable to the caller. Keeping the code is what lets a client branch on it.\n */\nexport function clientSafeErrorEnvelope(envelope: TheoErrorEnvelope): TheoErrorEnvelope {\n if (!redacts(envelope.code)) return envelope\n return { code: envelope.code, message: GENERIC_INTERNAL_MESSAGE }\n}\n","import type { ServerResponse } from 'node:http'\n\nimport { clientSafeErrorMessage } from '../../core/contracts/client-safe-error.js'\nimport type { TheoTransformer } from '../transformer.js'\n\n/**\n * Canonical HTTP response helpers (T5.1 extraction).\n *\n * Moved out of execute.ts so request-pipeline stages (execute-stages.ts,\n * handle-request-error.ts, etc.) can depend on these helpers without\n * creating a cycle through execute.ts.\n *\n * Public surface re-exported from execute.ts for backward compat — every\n * existing caller of `sendError` / `sendJson` continues to work via the\n * `theokit/server` barrel.\n */\n\nexport function sendJson(\n res: ServerResponse,\n data: unknown,\n status = 200,\n transformer?: TheoTransformer,\n): void {\n // T1.2 — transformer-aware serialization. Default (no transformer) uses\n // JSON.stringify direct for backward compat.\n const body = transformer ? transformer.serialize(data) : JSON.stringify(data)\n res.writeHead(status, {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(body),\n })\n res.end(body)\n}\n\n/** Render anything that would end the log line as a visible escape, so one call logs one line. */\nfunction oneLine(value: string): string {\n return value.replace(/[\\r\\n]/g, '\\\\n')\n}\n\nexport interface SendErrorOptions {\n custom404Html?: string\n custom500Html?: string\n}\n\n/**\n * Canonical error response.\n *\n * T6.3 (PV-17): the positional 7-param signature is preserved for backward\n * compat. New call sites should use the options-bag form:\n *\n * sendError(res, { code, message, status, issues?, requestId?, options? })\n *\n * Both shapes resolve to the same implementation.\n */\nexport interface SendErrorInput {\n code: string\n message: string\n status: number\n issues?: unknown[]\n requestId?: string\n options?: SendErrorOptions\n}\n\nexport function sendError(res: ServerResponse, input: SendErrorInput): void\n/* eslint-disable-next-line max-params -- T6.3: positional overload preserved\n for backward compat (callers across cli/server still use positional). The\n options-bag overload above is the recommended path. */\nexport function sendError(\n res: ServerResponse,\n code: string,\n message: string,\n status: number,\n issues?: unknown[],\n requestId?: string,\n options?: SendErrorOptions,\n): void\n/* eslint-disable-next-line max-params -- delegates to two surface overloads above; the parameter\n count mirrors the back-compat contract, not internal complexity. The `complexity` half of this\n suppression went away when the redaction rule stopped being restated inline. */\nexport function sendError(\n res: ServerResponse,\n codeOrInput: string | SendErrorInput,\n message?: string,\n status?: number,\n issues?: unknown[],\n requestId?: string,\n options?: SendErrorOptions,\n): void {\n let code: string\n if (typeof codeOrInput === 'string') {\n code = codeOrInput\n message = message ?? ''\n status = status ?? 500\n } else {\n code = codeOrInput.code\n message = codeOrInput.message\n status = codeOrInput.status\n issues = codeOrInput.issues\n requestId = codeOrInput.requestId\n options = codeOrInput.options\n }\n const errorMessage = clientSafeErrorMessage(code, message)\n\n if (code === 'INTERNAL_ERROR') {\n // One log entry per call, whatever the message contains. An exception message can be built\n // from request data and can therefore carry a newline; unescaped, that lets a caller append\n // whatever lines it likes to the log — including a plausible entry attributed to something\n // else (CodeQL `js/log-injection`).\n console.error(`[${oneLine(requestId ?? 'no-id')}] ${oneLine(message)}`)\n }\n\n if (status === 404 && options?.custom404Html) {\n const body = options.custom404Html\n res.writeHead(404, {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Content-Length': Buffer.byteLength(body),\n })\n res.end(body)\n return\n }\n if (status === 500 && options?.custom500Html) {\n const body = options.custom500Html\n res.writeHead(500, {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Content-Length': Buffer.byteLength(body),\n })\n res.end(body)\n return\n }\n\n sendJson(\n res,\n {\n error: {\n code,\n message: errorMessage,\n ...(requestId ? { requestId } : {}),\n ...(issues ? { issues } : {}),\n },\n },\n status,\n )\n}\n\n/**\n * T5a.2 Phase G slice 4/N — Web-Standards response helpers.\n *\n * Mirror of `sendJson` + `sendError` for the Web `Request`/`Response`\n * shape. Returns a native `Response` directly instead of mutating a\n * `ServerResponse`.\n *\n * v1.0 § Phase G.\n *\n * **Difference vs IncomingMessage path:**\n * - No `Content-Length` set explicitly — the runtime computes it from\n * the body when needed. CF Workers / Bun / Deno all do this; setting\n * it manually risks conflict if the body is a stream rather than a\n * fixed string.\n * - Custom 404/500 HTML options preserved (same opts shape).\n * - `requestId` flows into the response body's error envelope AND\n * surfaces as `x-request-id` header (parity with handleWebRequestError\n * Phase G slice 3/N).\n */\nexport function buildJsonResponse(\n data: unknown,\n status = 200,\n transformer?: TheoTransformer,\n): Response {\n const body = transformer ? transformer.serialize(data) : JSON.stringify(data)\n return new Response(body, {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\nexport function buildErrorResponse(input: SendErrorInput): Response {\n const { code, message, status, issues, requestId, options } = input\n const errorMessage = clientSafeErrorMessage(code, message)\n\n if (code === 'INTERNAL_ERROR') {\n // One log entry per call, whatever the message contains. An exception message can be built\n // from request data and can therefore carry a newline; unescaped, that lets a caller append\n // whatever lines it likes to the log — including a plausible entry attributed to something\n // else (CodeQL `js/log-injection`).\n console.error(`[${oneLine(requestId ?? 'no-id')}] ${oneLine(message)}`)\n }\n\n const headers: Record<string, string> = {}\n if (requestId !== undefined) headers['x-request-id'] = requestId\n\n if (status === 404 && options?.custom404Html !== undefined) {\n headers['content-type'] = 'text/html; charset=utf-8'\n return new Response(options.custom404Html, { status: 404, headers })\n }\n if (status === 500 && options?.custom500Html !== undefined) {\n headers['content-type'] = 'text/html; charset=utf-8'\n return new Response(options.custom500Html, { status: 500, headers })\n }\n\n headers['content-type'] = 'application/json'\n const body = JSON.stringify({\n error: {\n code,\n message: errorMessage,\n ...(requestId ? { requestId } : {}),\n ...(issues ? { issues } : {}),\n },\n })\n return new Response(body, { status, headers })\n}\n","/**\n * Pure Node `IncomingMessage` → Web `Request` converters.\n *\n * Kept free of any dependency on `web-handler.js` / `execute*.js` so the\n * executor (`execute.ts`) can build the handler-facing Web `Request` without\n * pulling the Web dispatch pipeline into its import graph (ADR-0028 R3a — the\n * Node adapter is the ONLY place IncomingMessage ↔ Request conversion lives;\n * these are the primitive converters it and the executor share).\n */\nimport type { IncomingMessage } from 'node:http'\nimport { Readable } from 'node:stream'\n\n/** Pick the first usable string from Node's `string | string[] | undefined` headers. */\nfunction pickHeaderString(value: string | string[] | undefined): string | undefined {\n if (typeof value === 'string') return value\n if (Array.isArray(value)) {\n for (const v of value) if (typeof v === 'string' && v.length > 0) return v\n }\n return undefined\n}\n\n/** Web Request requires an absolute URL; synthesize one from the Host header. */\nfunction synthesizeAbsoluteUrl(req: IncomingMessage): string {\n const host = pickHeaderString(req.headers.host) ?? 'localhost'\n return `http://${host}${req.url ?? '/'}`\n}\n\n/**\n * Collapse Node's `string | string[]` headers into a Web `Headers`. Repeated\n * headers are comma-joined (not `.append`ed) because `Headers.append` creates\n * multi-value entries that behave differently on `.get()` (EC-1).\n */\nfunction nodeHeadersToWeb(req: IncomingMessage): Headers {\n const headers = new Headers()\n for (const [key, value] of Object.entries(req.headers)) {\n if (value === undefined) continue\n if (Array.isArray(value)) {\n headers.set(key, value.join(', '))\n } else {\n headers.set(key, value)\n }\n }\n return headers\n}\n\n/**\n * Build a Web `Request` from a Node `IncomingMessage`, body included. The Web\n * Request spec requires an absolute URL; we synthesize one from the Host header\n * (fallback `localhost` for test doubles).\n *\n * For methods with a body (POST/PUT/PATCH/DELETE), the Node Readable stream is\n * wrapped as a Web ReadableStream via `Readable.toWeb()` so downstream consumers\n * can call `request.json()` / `request.formData()` / `request.text()` natively.\n */\nexport function incomingMessageToWebRequest(req: IncomingMessage): Request {\n const url = synthesizeAbsoluteUrl(req)\n const headers = nodeHeadersToWeb(req)\n\n const method = (req.method ?? 'GET').toUpperCase()\n const hasBody = method !== 'GET' && method !== 'HEAD'\n\n if (!hasBody) {\n return new Request(url, { method, headers })\n }\n\n // Drain Node's Readable into a Web ReadableStream. `Readable.toWeb` is\n // available in Node 18+ (theokit's engines.node floor is 22+, so safe).\n const webStream = Readable.toWeb(req) as ReadableStream\n return new Request(url, {\n method,\n headers,\n body: webStream,\n // EC-2: Node 18+ requires `duplex: 'half'` when body is a stream.\n // The `RequestInit` type omits it (Web spec gap); cast accordingly.\n ...({ duplex: 'half' } as { duplex: 'half' }),\n })\n}\n\n/**\n * A Node request that has NOT been converted yet — method now, body only if someone claims it.\n *\n * theokit#400. `incomingMessageToWebRequest` drains the Node stream, and a stream drains once. A\n * dispatcher that converts in order to decide whether it owns a path has already spent the body on\n * every path it does not own: the next branch attaches to a readable that has already ended, waits\n * for an `'end'` that cannot fire twice, and the request hangs with no status at all.\n *\n * The fix is an ordering one, so the type encodes the ordering: a router reads `method` (free) and\n * calls `toRequest()` only after it has decided the request is its own. Passing the source instead\n * of a `Request` is what makes \"did you convert before deciding?\" answerable by reading a signature.\n *\n * `toRequest()` memoizes, because a second conversion of the same `IncomingMessage` yields a\n * Request whose body is an empty closed stream — a silent truncation, which is worse than the hang\n * it would replace.\n */\nexport interface WebRequestSource {\n /** Uppercase HTTP method. Available without touching the body. */\n readonly method: string\n /** Convert on demand. Idempotent: repeated calls return the same `Request`. */\n toRequest: () => Request\n}\n\n/** Wrap `req` as a {@link WebRequestSource} — the conversion is deferred and memoized. */\nexport function createWebRequestSource(req: IncomingMessage): WebRequestSource {\n let converted: Request | undefined\n return {\n method: (req.method ?? 'GET').toUpperCase(),\n toRequest: () => (converted ??= incomingMessageToWebRequest(req)),\n }\n}\n\n/**\n * Build the Web `Request` handed to a route handler as `ctx.request` in the\n * Node server path (dev + `theokit start`). Method + absolute URL + headers\n * only — NO body.\n *\n * Why no body: the Node executor parses the request body BEFORE the handler\n * runs and exposes the parsed value as `ctx.body` (the typed, documented body\n * API). By the time the handler is called the Node stream is already drained,\n * so re-wrapping it would yield an empty/closed stream. Handlers read the body\n * via `ctx.body`; `ctx.request` is for the Web-standard header/cookie/URL/method\n * surface (e.g. `createSessionManagerWeb.getSession(ctx.request)`).\n *\n * Per ADR-0028 R3a, handlers see a Web `Request` in every runtime — this closes\n * the gap where the Node path leaked the raw `IncomingMessage` (whose `.headers`\n * is a plain object, so `.headers.get(...)` threw for Web-standard consumers).\n */\nexport function incomingMessageToHandlerRequest(req: IncomingMessage): Request {\n return new Request(synthesizeAbsoluteUrl(req), {\n method: (req.method ?? 'GET').toUpperCase(),\n headers: nodeHeadersToWeb(req),\n })\n}\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Controller files are walked from the developer's `serverDir/controllers`\n * (a build-time config path), never from HTTP input. No injection vector.\n */\nimport { readdirSync, type Dirent } from 'node:fs'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { join } from 'node:path'\n\nimport { createDecoratorHandler, isControllerClass, type ServeAgent } from '@theokit/http'\n\nimport { dispatchCsrfWarn } from '../security/csrf-warn-dispatch.js'\nimport { enforceCsrf, type DisallowedConfig } from '../security/csrf.js'\n\nimport { incomingMessageToWebRequest } from './node-request.js'\nimport { sendError } from './send-response.js'\n\n/** A decorator controller constructor (`@Controller` class). */\ntype ControllerClass = new (...args: never[]) => object\n\n/** Loads a controller module by absolute path. In dev this is Vite's `ssrLoadModule`\n * (the Task 1.1 swc transform has already compiled the parameter decorators); tests\n * inject `@theokit/http`'s `loadControllerWithSwc`. */\nexport type ControllerModuleLoader = (absPath: string) => Promise<Record<string, unknown>>\n\n/** A built controller route table exposed as a pure Web-Standard handler. */\ninterface ControllerDispatcher {\n /** `null` = no controller route matched — the host owns the miss (404 / fall-through). */\n dispatch(request: Request): Promise<Response | null>\n /** Non-executing route probe — true when a controller route owns `method` + `pathname`. */\n matches(method: string, pathname: string): boolean\n}\n\n// State-mutating methods get CSRF, mirroring the file-route pipeline (execute.ts).\nconst CSRF_PROTECTED_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\n/** Recursively collect `*.controller.ts` files under `dir` (absolute paths). */\n/**\n * Every `*.controller.ts` under `dir`, recursively. Exported for theokit#123: the build emitter\n * must find exactly the same set the dev dispatcher does, and two independent walks would be two\n * definitions of \"a controller\" that drift.\n */\nexport function findControllerFiles(dir: string): string[] {\n const found: string[] = []\n const walk = (current: string): void => {\n let entries: Dirent[]\n try {\n entries = readdirSync(current, { withFileTypes: true })\n } catch {\n return // dir doesn't exist — no controllers\n }\n for (const entry of entries) {\n const full = join(current, entry.name)\n if (entry.isDirectory()) walk(full)\n // theokit#123 — `.mjs` alongside `.ts`. Dev walks the SOURCE tree; production walks the\n // COMPILED tree under `dist/controllers`, where the same files exist as `*.controller.mjs`.\n // One walk for both keeps a single definition of \"a controller file\"; two would drift, and a\n // file that counts in dev and not in production is exactly the dev/prod split this fixes.\n else if (entry.name.endsWith('.controller.ts') || entry.name.endsWith('.controller.mjs'))\n found.push(full)\n }\n }\n walk(dir)\n return found\n}\n\n/** A discovered controller: its source file + the loaded `@Controller` class. */\ninterface ControllerModule {\n filePath: string\n cls: ControllerClass\n /**\n * The module's full export namespace — theokit#124.\n *\n * Kept alongside the class because it is the ONLY place a `@Body(schema)` regains a name. The\n * schema on `WalkResult.bodySchema` is a runtime `z.ZodType` with no source identifier, but it is\n * the very object this module exported, so matching it back by reference identity recovers the\n * exported name the typed-client codegen needs to write `z.infer<typeof ...>`.\n *\n * Unused by the dispatch path, which needs only the class.\n */\n exports: Readonly<Record<string, unknown>>\n}\n\n/**\n * Load every `@Controller` class under `controllersDir` via the injected loader,\n * keeping each class paired with its source file (needed by the typed-client\n * codegen to emit `import type { X } from '<file>'`). Non-controller exports are\n * ignored (`isControllerClass` — reused from @theokit/http).\n */\nexport async function scanControllerModules(\n controllersDir: string,\n loadModule: ControllerModuleLoader,\n): Promise<ControllerModule[]> {\n const files = findControllerFiles(controllersDir)\n const modules: ControllerModule[] = []\n for (const filePath of files) {\n const mod = await loadModule(filePath)\n for (const exported of Object.values(mod)) {\n if (typeof exported === 'function' && isControllerClass(exported)) {\n modules.push({ filePath, cls: exported as ControllerClass, exports: mod })\n }\n }\n }\n return modules\n}\n\n/** Load every `@Controller` class under `controllersDir` (classes only). */\nasync function scanControllers(\n controllersDir: string,\n loadModule: ControllerModuleLoader,\n): Promise<ControllerClass[]> {\n const modules = await scanControllerModules(controllersDir, loadModule)\n return modules.map((m) => m.cls)\n}\n\n/**\n * Scan `controllersDir` and build a Web-Standard dispatcher over the decorator\n * controllers found. Returns `null` when the directory has no controllers, so\n * the host can skip the controller path entirely (zero cost for routes-only apps).\n *\n * Dispatch REUSES @theokit/http's `createDecoratorHandler` (match + `@Param`\n * binding + `@Body` validation + Response building) — never re-implemented (ADR-1).\n */\nexport async function createControllerDispatcher(opts: {\n controllersDir: string\n loadModule: ControllerModuleLoader\n /** M47 — serves `@Expose`-bound agent routes (theo supplies a `mountAgent`-backed impl). */\n serveAgent?: ServeAgent\n}): Promise<ControllerDispatcher | null> {\n const classes = await scanControllers(opts.controllersDir, opts.loadModule)\n if (classes.length === 0) return null\n const handle = createDecoratorHandler({ controllers: classes, serveAgent: opts.serveAgent })\n return {\n dispatch: (request) => handle(request),\n matches: (method, pathname) => handle.matches(method, pathname),\n }\n}\n\n/**\n * Write a controller's Web `Response` to a Node response, byte for byte.\n *\n * The body used to be read with `await response.text()`, under a comment asserting that controllers\n * never stream. The comment was true of this path and hid the real problem: `text()` decodes as\n * UTF-8, so every byte >= 0x80 became `U+FFFD`. A payload of all 256 byte values arrived as 512\n * bytes; a 55 296-byte MPEG from `@theokit/plugin-voice` arrived as 76 790 bytes beginning\n * `ef bf bd`. Status 200, correct content-type, plausible length — invisible until someone opens\n * the file. File routes were never affected: `executeRoute` pumps the stream, so this was a silent\n * divergence between two paths meant to be at parity.\n *\n * `arrayBuffer()` fixes it without changing anything else. This path stays BUFFERED, exactly as\n * before — a controller returning a streamed body still has it collected here, so a plugin that\n * promises progressive delivery does not get it through a controller. That is a real limitation and\n * a separate change: making it stream alters when bytes reach the client, which is behaviour beyond\n * the corruption this repairs.\n */\nasync function writeControllerResponse(res: ServerResponse, response: Response): Promise<void> {\n const headersBag: Record<string, string> = {}\n for (const [k, v] of response.headers) {\n if (k.toLowerCase() !== 'set-cookie') headersBag[k] = v\n }\n const setCookies = response.headers.getSetCookie()\n if (setCookies.length > 0) res.setHeader('Set-Cookie', setCookies)\n res.writeHead(response.status, headersBag)\n // `Buffer.from(ArrayBuffer)` views the bytes as they are. Any string in between is a decode, and\n // a decode of arbitrary bytes is a loss.\n const body = response.body ? Buffer.from(await response.arrayBuffer()) : undefined\n res.end(body !== undefined && body.length > 0 ? body : undefined)\n}\n\n/**\n * The `api-middleware` fall-through in one call: scan `controllersDir`, build the\n * dispatcher, and serve the request. Builds a body-ful Web `Request` (`@Body`\n * needs the body; the raw stream is undrained at a route miss) and enforces CSRF\n * with the SAME gate file routes use (parity). Returns `true` when a controller\n * handled it (or CSRF blocked it), `false` when there are no controllers OR none\n * matched (the host continues to its own 404). Built per-miss so controller edits\n * reflect via HMR.\n */\nexport async function dispatchControllerRequest(args: {\n controllersDir: string\n loadModule: ControllerModuleLoader\n req: IncomingMessage\n res: ServerResponse\n csrfMode: 'off' | 'warn' | 'strict'\n disallowed?: DisallowedConfig\n requestId: string\n /** M47 — serves `@Expose`-bound agent routes (mountAgent-backed); omit for routes-only apps. */\n serveAgent?: ServeAgent\n}): Promise<boolean> {\n const { req, res, csrfMode, disallowed, requestId } = args\n const dispatcher = await createControllerDispatcher({\n controllersDir: args.controllersDir,\n loadModule: args.loadModule,\n serveAgent: args.serveAgent,\n })\n if (!dispatcher) return false\n\n const method = (req.method ?? 'GET').toUpperCase()\n const webRequest = incomingMessageToWebRequest(req)\n const pathname = new URL(webRequest.url).pathname\n\n // CSRF parity: enforce ONLY when a protected-method controller route actually\n // owns this path (probe with the non-executing matcher — never double-dispatch,\n // which would run the handler + its side effects). An unrouted path falls\n // through to the host's own 404, not a 403.\n if (CSRF_PROTECTED_METHODS.has(method) && dispatcher.matches(method, pathname)) {\n const decision = enforceCsrf(\n req,\n csrfMode,\n { warn: dispatchCsrfWarn, path: req.url },\n disallowed,\n )\n if (!decision.allow) {\n sendError(\n res,\n 'CSRF_INVALID',\n decision.reason ?? 'CSRF check failed',\n 403,\n undefined,\n requestId,\n )\n return true\n }\n }\n\n const response = await dispatcher.dispatch(webRequest)\n if (response === null) return false\n await writeControllerResponse(res, response)\n return true\n}\n","/**\n * Canonical CSRF warn dispatcher (T3.3 of architecture-review-remediation-plan).\n *\n * Consolidates the duplicated `warn: (payload) => { warnOnce(...) }` closure\n * that previously appeared in both `http/execute.ts` and\n * `http/action-execute.ts`. Resolves PV-10 (DRY).\n *\n * `warnOnce` dedupes by `event:method:path` so a request loop with 1000 POSTs\n * doesn't flood logs with identical warnings. Apps grep for `event\":\"csrf.warn\"`\n * (stable event shape — see [[enforcement-cutover.md]]).\n */\nimport { warnOnce } from '../observability/logger.js'\n\ninterface CsrfWarnPayload {\n event: string\n method: string\n path?: string\n reason: string\n code?: string\n docsUrl?: string\n warnOnce?: boolean\n}\n\n/**\n * Build the warn callback that `enforceCsrf` invokes for soft-mode warnings.\n * Returned function is suitable for the `warn` field of `enforceCsrf`'s options.\n */\nexport function dispatchCsrfWarn(payload: CsrfWarnPayload): void {\n const key = `${payload.event}:${payload.method}:${payload.path ?? ''}`\n warnOnce(key, payload as unknown as Record<string, unknown>)\n}\n","/**\n * T4.1 — Audit logging interface + default JSON stdout sink.\n *\n * Per ADR D4: define the interface; ship a zero-dep default; reserve\n * adapter shapes for Postgres, File, OpenTelemetry, Sentry as follow-up\n * packages. Persistence has heavy deps (`pg`, `better-sqlite3`); we\n * keep core dep-free and let users opt in.\n *\n * Compatibility:\n * - Node / Bun / Deno / Vercel — console.log is sync, captured.\n * - Edge runtimes (CF Workers, Vercel Edge) — console.log is captured\n * but may be rate-limited by the platform. For high-volume edge audit,\n * implement a custom sink writing to a queue / HTTP endpoint.\n */\n\nexport interface AuditEvent {\n /** Domain-qualified verb. Convention: `<domain>.<verb>` (e.g. csrf.warn, session.rotated). */\n action: string\n /** Who triggered the event. Anonymous = no auth at time of event. */\n actor?: { type: 'user' | 'system' | 'anonymous'; id?: string }\n /** What was operated on (optional). */\n resource?: { type: string; id?: string }\n /** Arbitrary event-specific metadata. JSON-serializable. */\n metadata?: Record<string, unknown>\n /** ISO 8601 timestamp. If absent, sink fills in `new Date().toISOString()`. */\n timestamp?: string\n /** Optional trace id (populated by middleware from `x-trace-id`). */\n traceId?: string\n}\n\nexport interface AuditLogger {\n log(event: AuditEvent): void | Promise<void>\n}\n\n/**\n * Default sink: one JSON line per event to stdout. Sync. Never throws.\n *\n * EC: circular refs / BigInt values fall back to a placeholder line so\n * the event is still observable (action + traceId) without crashing the\n * request lifecycle.\n */\nexport class JsonStdoutSink implements AuditLogger {\n log(event: AuditEvent): void {\n const enriched = {\n level: 'audit' as const,\n ...event,\n timestamp: event.timestamp ?? new Date().toISOString(),\n }\n try {\n // eslint-disable-next-line no-console -- JsonStdoutSink IS the audit output\n console.log(JSON.stringify(enriched, jsonReplacer))\n } catch {\n // eslint-disable-next-line no-console -- fallback when payload won't serialize\n console.log(\n `{\"level\":\"audit\",\"action\":${JSON.stringify(event.action)},\"timestamp\":${JSON.stringify(enriched.timestamp)},\"note\":\"payload could not be serialized\"}`,\n )\n }\n }\n}\n\n/**\n * Replacer that walks BigInt → string. Circular ref handling is via the\n * outer try/catch (JSON.stringify throws TypeError on cycles; we drop to\n * the fallback line). We don't implement custom cycle-breaking walker\n * because the audit payload is meant to be JSON — if user metadata has\n * a cycle, the right answer is to fix the caller, not silently lose\n * the structure.\n */\nfunction jsonReplacer(_key: string, value: unknown): unknown {\n if (typeof value === 'bigint') return value.toString()\n return value\n}\n\n/**\n * No-op logger. Returned when `config.audit` is unset. Zero overhead;\n * framework wiring sites null-check before calling.\n */\nexport function createNoOpLogger(): AuditLogger {\n return {\n log() {\n // intentionally empty\n },\n }\n}\n\n/**\n * T4.2 — Safe-emit wrapper. Used by framework wiring sites (csrf.ts,\n * rate-limit.ts, session.ts) so a logger throw NEVER propagates into\n * the request handler.\n */\nexport function safeAudit(logger: AuditLogger | undefined, event: AuditEvent): void {\n if (!logger) return\n try {\n const r = logger.log(event)\n // Discard the Promise — async sinks are fire-and-forget by design.\n if (r && typeof r.then === 'function') {\n r.catch(() => {\n // swallow async sink failures — audit must never crash the request\n })\n }\n } catch {\n // swallow sync sink failures — audit must never crash the request\n }\n}\n","import type { IncomingMessage } from 'node:http'\n\nimport type { AuditLogger } from '../observability/audit-log.js'\nimport { safeAudit } from '../observability/audit-log.js'\n\n/**\n * CSRF enforcement mode.\n *\n * - `off` — skip CSRF entirely. Use only when you have another defense\n * (e.g. you don't ship session cookies, all auth is bearer).\n * - `warn` — log a structured warning when the check would fail, but\n * still serve the request. Default for 0.2.0. Migration mode.\n * - `strict` — reject failing requests with 403 + code `CSRF_INVALID`.\n * Will become the default in 0.3.0.\n */\nexport type CsrfMode = 'off' | 'warn' | 'strict'\n\n/**\n * Per-request structured logger surface. Only `warn` is used by enforceCsrf;\n * we don't require a full Logger here so callers can pass a mock or the\n * console directly.\n */\nexport interface CsrfLogger {\n warn: (payload: CsrfWarnPayload) => void\n /** Optional path the request was destined for — used for log correlation. */\n path?: string\n}\n\n/**\n * T5.1 — Rails-inspired per-route escalation.\n *\n * `routes` accepts string (exact match) or RegExp entries. When a request\n * path matches AND the request would otherwise emit a warning, the\n * `behavior` field decides what happens:\n *\n * - `'warn'` → normal warn dispatch (no-op vs default)\n * - `'raise'` → escalate to 403 regardless of global `csrf` mode\n *\n * `'raise'` never downgrades: when global mode is `'off'`, validation is\n * skipped entirely and disallowed dispatch never runs.\n */\nexport interface DisallowedConfig {\n routes: (string | RegExp)[]\n behavior: 'warn' | 'raise'\n}\n\n/**\n * Test whether `path` matches any of the supplied patterns. String\n * patterns are EXACT (trailing slash matters — use RegExp for tolerance).\n *\n * EC-5: when a RegExp carries the `/g` flag, `.test()` mutates\n * `lastIndex` and the next invocation may miss. We reset `lastIndex`\n * before each test so the matcher is a pure function.\n */\nexport function matchDisallowed(path: string, patterns: readonly (string | RegExp)[]): boolean {\n for (const p of patterns) {\n if (typeof p === 'string') {\n if (path === p) return true\n } else if (p instanceof RegExp) {\n p.lastIndex = 0\n if (p.test(path)) return true\n }\n // Neither string nor RegExp: ignore silently (defensive — the public\n // type forbids it but runtime data may slip past).\n }\n return false\n}\n\n/**\n * T2.2 — Stable cutover identifier shipped with every csrf.warn payload.\n *\n * Convention borrowed from Vite's `deprecations.ts:74` — a `code` plus a\n * `docsUrl` lets users (a) grep their logs for a single stable identifier\n * to find every csrf.warn line, and (b) click through directly to the\n * migration guide. Strings are exported constants so the analyzer (T2.3)\n * and migration guide can reference the same source of truth.\n */\nexport const CSRF_WARN_CODE = 'CSRF_STRICT_CUTOVER' as const\nexport const CSRF_WARN_DOCS_URL = 'https://theokit.dev/upgrade/csrf-strict-cutover' as const\n\n/**\n * Pick a header value, choosing the first entry when an array (Node sets\n * arrays for headers that legitimately appear multiple times). Returns\n * `''` when the header is absent or empty — callers treat `''` as \"skip\".\n */\nfunction pickHeader(value: string | string[] | undefined): string {\n if (typeof value === 'string') return value\n if (Array.isArray(value) && value.length > 0) return value[0]\n return ''\n}\n\nexport interface CsrfWarnPayload {\n event: 'csrf.warn'\n method: string\n path: string | undefined\n reason: string\n /**\n * Stable identifier for the 0.2 → 0.3 CSRF strict cutover. Always\n * `'CSRF_STRICT_CUTOVER'`. Grep-able from prod logs.\n */\n code: string\n /**\n * Link to the section of the migration guide explaining how to clear\n * this specific warning class.\n */\n docsUrl: string\n}\n\n/**\n * T5a.2 Phase B (slice 1/6): pure header-only CSRF check extracted from\n * `validateCsrf(req: IncomingMessage)` so it can be re-used by the Web-\n * Standards `validateCsrfRequest(request: Request)` sibling. Per the T5a.2\n * plan v1.0 § Phase B, header-only leaves are first to migrate. This is\n * the dual-signature pattern (anti-pattern #2 avoidance): IncomingMessage\n * consumers unchanged; new Request consumers go through the same logic\n * via the shared helper.\n *\n * Pure logic — accepts pre-extracted header values as strings or null.\n */\nfunction isCsrfValidFromHeaders(opts: {\n csrfActionHeader: string | null\n origin: string | null\n host: string | null\n}): { valid: true } | { valid: false; reason: string } {\n // 1. Custom header must be present (primary defense — simple form posts\n // cannot set custom headers, browsers gate via CORS preflight)\n if (opts.csrfActionHeader !== '1') {\n return { valid: false, reason: 'Missing X-Theo-Action header' }\n }\n\n // 2. Origin matching (secondary defense)\n if (opts.origin === null || opts.origin === '') {\n // Browsers omit Origin for same-origin requests — treat as valid\n return { valid: true }\n }\n\n if (opts.host === null || opts.host === '') {\n return { valid: true }\n }\n\n try {\n const originHost = new URL(opts.origin).host\n if (originHost !== opts.host) {\n return { valid: false, reason: `Origin ${opts.origin} does not match host ${opts.host}` }\n }\n } catch {\n return { valid: false, reason: `Invalid origin: ${opts.origin}` }\n }\n\n return { valid: true }\n}\n\nexport function validateCsrf(\n req: IncomingMessage,\n): { valid: true } | { valid: false; reason: string } {\n // IncomingMessage adapter — normalize Node header shape to the pure\n // helper's input shape (string|null).\n const action = req.headers['x-theo-action']\n const origin = req.headers.origin\n const host = req.headers.host\n\n // RFC 6454: Origin is single-valued. A caller that synthesizes an\n // IncomingMessage — an adapter, a shim, a proxy library — can hand us an\n // array, and choosing one of two conflicting origins is a decision the\n // request never authorized. The disagreement IS the rejection.\n //\n // `node:http` itself joins a repeated Origin with `, ` rather than\n // producing an array, and that string already fails to parse as a URL\n // below. This branch covers the shape the type allows and `pickHeader`\n // used to resolve silently.\n if (Array.isArray(origin)) {\n return { valid: false, reason: 'Multiple Origin headers (RFC 6454 violation)' }\n }\n\n return isCsrfValidFromHeaders({\n csrfActionHeader: typeof action === 'string' ? action : null,\n origin: origin !== undefined ? origin || null : null,\n host: host !== undefined ? pickHeader(host) || null : null,\n })\n}\n\n/**\n * T5a.2 Phase B (slice 1/6) — Web-Standards-shaped CSRF validator.\n *\n * Mirror of `validateCsrf(req: IncomingMessage)` for the Web `Request`\n * shape. Consumes `request.headers.get(name)` (native Web `Headers` API)\n * instead of `req.headers[name]` (Node `IncomingMessage` indexer). Same\n * CSRF policy + same return shape — the difference is only the input\n * extraction.\n *\n * Used by `executeWebRequest` (T5a.2 Phase A) to enforce CSRF on the\n * Web-Standards request handler entry-point.\n */\nexport function validateCsrfRequest(\n request: Request,\n): { valid: true } | { valid: false; reason: string } {\n return isCsrfValidFromHeaders({\n csrfActionHeader: request.headers.get('x-theo-action'),\n origin: request.headers.get('origin'),\n host: request.headers.get('host'),\n })\n}\n\n/**\n * Enforce CSRF policy with mode-aware behavior. Wrapper over `validateCsrf`\n * that turns the boolean valid/invalid into a request-level allow decision,\n * gated by mode + structured warning in warn mode.\n *\n * Phase 5 — CSRF warn-first (EC-1).\n */\n/**\n * Dispatch the csrf.warn payload to both the structured logger and the\n * audit sink (when configured). Extracted from `enforceCsrf` to keep that\n * function's complexity within ceiling.\n */\nfunction dispatchCsrfWarn(\n req: IncomingMessage,\n reason: string,\n logger: CsrfLogger | undefined,\n auditLogger: AuditLogger | undefined,\n pathFallback = '',\n): void {\n const payload: CsrfWarnPayload = {\n event: 'csrf.warn',\n method: req.method ?? 'UNKNOWN',\n path: logger?.path ?? pathFallback,\n reason,\n code: CSRF_WARN_CODE,\n docsUrl: CSRF_WARN_DOCS_URL,\n }\n logger?.warn(payload)\n // `metadata` is typed as Record<string, unknown>; CsrfWarnPayload is a\n // structurally-equivalent shape but lacks the index signature.\n safeAudit(auditLogger, {\n action: 'csrf.warn',\n actor: { type: 'anonymous' },\n metadata: { ...payload },\n })\n}\n\nexport function enforceCsrf(\n req: IncomingMessage,\n mode: CsrfMode,\n logger?: CsrfLogger,\n disallowed?: DisallowedConfig,\n auditLogger?: AuditLogger,\n): { allow: boolean; reason?: string } {\n if (mode === 'off') {\n // `off` short-circuits before disallowed dispatch — users who set\n // csrf: 'off' globally have explicitly turned validation off, and\n // disallowed must never re-introduce it. The escape hatch is to\n // set csrf: 'warn' and use disallowed for surgical strict pockets.\n return { allow: true }\n }\n\n const check = validateCsrf(req)\n if (check.valid) {\n return { allow: true }\n }\n\n // T5.1 — disallowed dispatch: when the failing request matches a\n // disallowed pattern AND behavior is 'raise', escalate to 403 even if\n // global mode is 'warn'. Strict mode would 403 anyway, so the branch\n // is a no-op there.\n if (disallowed?.behavior === 'raise') {\n const path = logger?.path ?? req.url ?? ''\n if (matchDisallowed(path, disallowed.routes)) {\n return { allow: false, reason: check.reason }\n }\n }\n\n if (mode === 'warn') {\n // T2.1: emit via warnOnce by default — callers can override via the\n // injected logger.warn (tests, custom log routers).\n // T2.2: include the stable cutover code + docsUrl so logs are\n // grep-able and click-through-able.\n dispatchCsrfWarn(req, check.reason, logger, auditLogger)\n return { allow: true, reason: check.reason }\n }\n\n // strict — 403 the request, AND emit a warn payload so the dev (and\n // devtools UI) sees WHY it was blocked + the docsUrl to fix it.\n // Without this, strict-mode users get a silent 403 with no context.\n dispatchCsrfWarn(req, check.reason, logger, auditLogger)\n return { allow: false, reason: check.reason }\n}\n"],"mappings":"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","names":["dispatchCsrfWarn"]}