theokit 0.5.1 → 0.5.3

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 (135) hide show
  1. package/dist/{actions-virtual-module-IY46EEEX.js → actions-virtual-module-DL74V6SR.js} +2 -2
  2. package/dist/actions-virtual-module-DL74V6SR.js.map +1 -0
  3. package/dist/{actions-virtual-module-XNHDNCZ6.js → actions-virtual-module-ZDD54ZZE.js} +2 -2
  4. package/dist/actions-virtual-module-ZDD54ZZE.js.map +1 -0
  5. package/dist/{app-typed-client-OUJO3V5J.js → app-typed-client-4GIKUKRT.js} +4 -4
  6. package/dist/app-typed-client-4GIKUKRT.js.map +1 -0
  7. package/dist/{app-typed-client-YOMWSA3F.js → app-typed-client-4M4NVSK3.js} +3 -3
  8. package/dist/app-typed-client-4M4NVSK3.js.map +1 -0
  9. package/dist/{aws-lambda-GKSTX6HD.js → aws-lambda-SCRGTGF5.js} +5 -5
  10. package/dist/aws-lambda-SCRGTGF5.js.map +1 -0
  11. package/dist/{build-2WZ6XXVY.js → build-R74EU6BP.js} +11 -11
  12. package/dist/build-R74EU6BP.js.map +1 -0
  13. package/dist/{bun-ISHSNFIO.js → bun-FURRCQMS.js} +7 -7
  14. package/dist/bun-FURRCQMS.js.map +1 -0
  15. package/dist/{chunk-FI7MPTJW.js → chunk-3IOFTG3L.js} +1 -1
  16. package/dist/chunk-3IOFTG3L.js.map +1 -0
  17. package/dist/{chunk-U6TPSW4L.js → chunk-DUKXQNM7.js} +2 -2
  18. package/dist/{chunk-4S2UCILN.js → chunk-FGOX37NB.js} +8 -8
  19. package/dist/chunk-FGOX37NB.js.map +1 -0
  20. package/dist/{chunk-MR7OLIC6.js → chunk-GPF64ENM.js} +2 -2
  21. package/dist/{chunk-4S6YHNG2.js → chunk-GUUWDMR2.js} +2 -2
  22. package/dist/chunk-GUUWDMR2.js.map +1 -0
  23. package/dist/{chunk-WEGALZEU.js → chunk-HDA6M7P4.js} +3 -3
  24. package/dist/{chunk-K4M64WD6.js → chunk-IZQN5CDS.js} +4 -4
  25. package/dist/chunk-IZQN5CDS.js.map +1 -0
  26. package/dist/{chunk-M2EY7KDR.js → chunk-JD7AQNJS.js} +15 -15
  27. package/dist/chunk-JD7AQNJS.js.map +1 -0
  28. package/dist/{chunk-5Z2727GV.js → chunk-KJLECZWQ.js} +1 -1
  29. package/dist/chunk-KJLECZWQ.js.map +1 -0
  30. package/dist/{chunk-LC5PYNJP.js → chunk-MWYSRZI4.js} +7 -7
  31. package/dist/chunk-MWYSRZI4.js.map +1 -0
  32. package/dist/{chunk-VBZCVFSA.js → chunk-P2RS3WWY.js} +2 -2
  33. package/dist/{chunk-R7OC6UDK.js → chunk-P3SGM4NR.js} +1 -1
  34. package/dist/chunk-P3SGM4NR.js.map +1 -0
  35. package/dist/{chunk-ABVITXFH.js → chunk-S74FECKW.js} +1 -1
  36. package/dist/{chunk-ABVITXFH.js.map → chunk-S74FECKW.js.map} +1 -1
  37. package/dist/{chunk-COXLEZCN.js → chunk-UVHRD33F.js} +1 -1
  38. package/dist/chunk-UVHRD33F.js.map +1 -0
  39. package/dist/{chunk-27LWMYNK.js → chunk-X32KQH5C.js} +2 -2
  40. package/dist/chunk-X32KQH5C.js.map +1 -0
  41. package/dist/{chunk-2F4FROEQ.js → chunk-X3VNROZ7.js} +17 -17
  42. package/dist/chunk-X3VNROZ7.js.map +1 -0
  43. package/dist/{chunk-Y4H3JNVK.js → chunk-YB5DJJ4U.js} +1 -1
  44. package/dist/chunk-YB5DJJ4U.js.map +1 -0
  45. package/dist/cli/index.js +8 -8
  46. package/dist/{cloudflare-OJGJ7R2D.js → cloudflare-IZ6VJ2OU.js} +7 -7
  47. package/dist/cloudflare-IZ6VJ2OU.js.map +1 -0
  48. package/dist/{deno-deploy-BHWKFTOW.js → deno-deploy-O73NBXIW.js} +6 -6
  49. package/dist/deno-deploy-O73NBXIW.js.map +1 -0
  50. package/dist/{dev-U4UDZFDB.js → dev-ZU46C5AZ.js} +8 -8
  51. package/dist/{dev-emit-HHP2XJXE.js → dev-emit-66FCOS7V.js} +4 -4
  52. package/dist/{dev-emit-5VPRCHOD.js → dev-emit-JBVINGHU.js} +3 -3
  53. package/dist/devtools/entry.js +5 -4
  54. package/dist/devtools/entry.js.map +1 -1
  55. package/dist/{docker-EZDA5FT7.js → docker-AZSJFLP2.js} +3 -3
  56. package/dist/docker-AZSJFLP2.js.map +1 -0
  57. package/dist/{index-DWWEdFh5.d.ts → index-CuHICqb9.d.ts} +2 -2
  58. package/dist/index.d.ts +1 -1
  59. package/dist/index.js +3 -3
  60. package/dist/{info-47VB62MV.js → info-CORLCPEJ.js} +4 -4
  61. package/dist/{netlify-GSNSJGMN.js → netlify-O7G3RLK4.js} +3 -3
  62. package/dist/netlify-O7G3RLK4.js.map +1 -0
  63. package/dist/{node-6I5B6RL3.js → node-LXPQLMVB.js} +3 -3
  64. package/dist/{openapi-NFLSNNVQ.js → openapi-TEOUOIJ2.js} +7 -7
  65. package/dist/registry-KW4F4D2L.js +25 -0
  66. package/dist/{routes-3A4XGIEJ.js → routes-GAXVWJUK.js} +6 -6
  67. package/dist/{schema-KZVOV333.js → schema-Z5VJ2I76.js} +3 -3
  68. package/dist/server/cron/index.js +2 -2
  69. package/dist/server/http/index.d.ts +1 -1
  70. package/dist/server/http/index.js +2 -2
  71. package/dist/server/index.d.ts +3 -3
  72. package/dist/server/index.js +18 -11
  73. package/dist/server/index.js.map +1 -1
  74. package/dist/server/jobs/index.d.ts +1 -1
  75. package/dist/server/jobs/index.js +2 -2
  76. package/dist/server/scan/index.d.ts +1 -1
  77. package/dist/server/scan/index.js +2 -2
  78. package/dist/{services-json-Z2QNGVPW.js → services-json-6SHWMYJH.js} +1 -1
  79. package/dist/services-json-6SHWMYJH.js.map +1 -0
  80. package/dist/{services-typed-client-T7M5VPBP.js → services-typed-client-ASZL7FQV.js} +2 -2
  81. package/dist/{start-QDRSRNXK.js → start-HGUNNF5X.js} +10 -10
  82. package/dist/start-HGUNNF5X.js.map +1 -0
  83. package/dist/{static-QI5NQT2X.js → static-EO73I4C7.js} +6 -6
  84. package/dist/static-EO73I4C7.js.map +1 -0
  85. package/dist/{theo-cloud-RSQCBNXL.js → theo-cloud-HBCYEODQ.js} +2 -2
  86. package/dist/theo-cloud-HBCYEODQ.js.map +1 -0
  87. package/dist/{vercel-TKPZK62A.js → vercel-GSFDMF7K.js} +4 -4
  88. package/dist/vercel-GSFDMF7K.js.map +1 -0
  89. package/dist/vite-plugin/index.js +3 -3
  90. package/dist/{vite-plugin-CR4JDFUQ.js → vite-plugin-TX5H4MB6.js} +8 -8
  91. package/package.json +1 -1
  92. package/dist/actions-virtual-module-IY46EEEX.js.map +0 -1
  93. package/dist/actions-virtual-module-XNHDNCZ6.js.map +0 -1
  94. package/dist/app-typed-client-OUJO3V5J.js.map +0 -1
  95. package/dist/app-typed-client-YOMWSA3F.js.map +0 -1
  96. package/dist/aws-lambda-GKSTX6HD.js.map +0 -1
  97. package/dist/build-2WZ6XXVY.js.map +0 -1
  98. package/dist/bun-ISHSNFIO.js.map +0 -1
  99. package/dist/chunk-27LWMYNK.js.map +0 -1
  100. package/dist/chunk-2F4FROEQ.js.map +0 -1
  101. package/dist/chunk-4S2UCILN.js.map +0 -1
  102. package/dist/chunk-4S6YHNG2.js.map +0 -1
  103. package/dist/chunk-5Z2727GV.js.map +0 -1
  104. package/dist/chunk-COXLEZCN.js.map +0 -1
  105. package/dist/chunk-FI7MPTJW.js.map +0 -1
  106. package/dist/chunk-K4M64WD6.js.map +0 -1
  107. package/dist/chunk-LC5PYNJP.js.map +0 -1
  108. package/dist/chunk-M2EY7KDR.js.map +0 -1
  109. package/dist/chunk-R7OC6UDK.js.map +0 -1
  110. package/dist/chunk-Y4H3JNVK.js.map +0 -1
  111. package/dist/cloudflare-OJGJ7R2D.js.map +0 -1
  112. package/dist/deno-deploy-BHWKFTOW.js.map +0 -1
  113. package/dist/docker-EZDA5FT7.js.map +0 -1
  114. package/dist/netlify-GSNSJGMN.js.map +0 -1
  115. package/dist/registry-QHEZ3BWB.js +0 -25
  116. package/dist/services-json-Z2QNGVPW.js.map +0 -1
  117. package/dist/start-QDRSRNXK.js.map +0 -1
  118. package/dist/static-QI5NQT2X.js.map +0 -1
  119. package/dist/theo-cloud-RSQCBNXL.js.map +0 -1
  120. package/dist/vercel-TKPZK62A.js.map +0 -1
  121. /package/dist/{chunk-U6TPSW4L.js.map → chunk-DUKXQNM7.js.map} +0 -0
  122. /package/dist/{chunk-MR7OLIC6.js.map → chunk-GPF64ENM.js.map} +0 -0
  123. /package/dist/{chunk-WEGALZEU.js.map → chunk-HDA6M7P4.js.map} +0 -0
  124. /package/dist/{chunk-VBZCVFSA.js.map → chunk-P2RS3WWY.js.map} +0 -0
  125. /package/dist/{dev-U4UDZFDB.js.map → dev-ZU46C5AZ.js.map} +0 -0
  126. /package/dist/{dev-emit-HHP2XJXE.js.map → dev-emit-66FCOS7V.js.map} +0 -0
  127. /package/dist/{dev-emit-5VPRCHOD.js.map → dev-emit-JBVINGHU.js.map} +0 -0
  128. /package/dist/{info-47VB62MV.js.map → info-CORLCPEJ.js.map} +0 -0
  129. /package/dist/{node-6I5B6RL3.js.map → node-LXPQLMVB.js.map} +0 -0
  130. /package/dist/{openapi-NFLSNNVQ.js.map → openapi-TEOUOIJ2.js.map} +0 -0
  131. /package/dist/{registry-QHEZ3BWB.js.map → registry-KW4F4D2L.js.map} +0 -0
  132. /package/dist/{routes-3A4XGIEJ.js.map → routes-GAXVWJUK.js.map} +0 -0
  133. /package/dist/{schema-KZVOV333.js.map → schema-Z5VJ2I76.js.map} +0 -0
  134. /package/dist/{services-typed-client-T7M5VPBP.js.map → services-typed-client-ASZL7FQV.js.map} +0 -0
  135. /package/dist/{vite-plugin-CR4JDFUQ.js.map → vite-plugin-TX5H4MB6.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/config/schema.ts","../src/config/schemas/header-safe.ts","../src/config/schemas/rate-limit.ts","../src/config/schemas/upload.ts","../src/config/schemas/logging.ts","../src/config/schemas/cache.ts","../src/config/schemas/security.ts","../src/config/schemas/storage.ts"],"sourcesContent":["import { z } from 'zod'\n\nimport { servicesConfigSchema } from '../services/index.js'\n\nimport {\n cacheSchema,\n loggingSchema,\n rateLimitSchema,\n securitySchema,\n storageSchema,\n uploadSchema,\n type FormatErrorContext,\n type FormatErrorHook,\n} from './schemas/index.js'\n\n// Re-export per-concern schemas + types so downstream consumers\n// (`adapters/*`, vite-plugin, generators, tests) keep their existing\n// imports valid. Per plan T2.3 — pure structural split; zero behavior\n// change at the call site.\nexport {\n cacheSchema,\n corsSchema,\n disallowedConfigSchema,\n headerSafeString,\n loggingSchema,\n rateLimitSchema,\n securityHeadersSchema,\n securitySchema,\n storageSchema,\n uploadSchema,\n} from './schemas/index.js'\n\nexport type { FormatErrorContext, FormatErrorHook, StorageConfig } from './schemas/index.js'\n\n/**\n * Root `theo.config.ts` schema — composer assembled from the per-concern\n * primitives re-exported above.\n *\n * Embedded blocks that exist ONLY as part of the root config (`agents`,\n * `ui`, `devtools`, `jobs`, `openapi`) stay inline below. They have no\n * external consumers; splitting them would create lonely files (M5\n * smell) without comprehension benefit.\n */\nexport const theoConfigSchema = z\n .object({\n /**\n * Plan v1.2 T2.1 — project identifier propagated into\n * `.theo/services.json` v2 `project` field. DNS-1123 compatible (drives\n * Gitea repo + ArgoCD App naming on TheoCloud). When omitted, the build\n * emits services.json v1 with the legacy `services-bundle` fallback\n * (ADR D10) and logs a deprecation warning.\n */\n name: z\n .string()\n .max(63)\n .refine(\n // DNS-1123 equivalent expressed as explicit single-char anchors so\n // security/detect-unsafe-regex stays clean (no backtracking).\n (value) =>\n value.length > 0 &&\n /^[a-z0-9-]+$/u.test(value) &&\n !value.startsWith('-') &&\n !value.endsWith('-'),\n 'name must match DNS-1123 (lowercase alphanumeric+hyphens, 1-63 chars, no leading/trailing hyphen)',\n )\n .optional(),\n appDir: z.string().default('app'),\n serverDir: z.string().default('server'),\n /**\n * T2.2 / EC-4 — Build output directory. Must be a relative path inside\n * the project root. Refused absolute or parent-relative paths to prevent\n * `cleanOutDir` from wiping arbitrary locations (defense-in-depth on\n * top of cleanOutDir's runtime EC-3 guard).\n */\n distDir: z\n .string()\n .default('.theo')\n .refine(\n (d) => !/^([A-Za-z]:)?[/\\\\]/.test(d) && !d.startsWith('..'),\n 'distDir must be a relative path inside the project root (e.g., \".theo\")',\n ),\n /**\n * Agent runtime configuration.\n *\n * - `maxRegistries` (T2.3, legacy): dev-mode cleanup of stale\n * `.theokit/agents/<id>/` dirs by mtime. Now superseded by SDK's\n * native registry GC but kept for backward-compat.\n * - `registry` (Phase 6, Production-Readiness #2): forwarded to\n * `Agent.registry.configure()` lazily on first request.\n * * `maxAgents` — LRU cap. MUST be ≥ max-concurrent-active-conversations\n * (EC-17 DOCUMENT): with maxAgents:1 and 2 concurrent chats, LRU evicts\n * mid-stream → SDK aborts with code:'aborted'. Default 100 covers\n * indie/small-team; tune up for high-traffic.\n * * `idleTimeoutMs` — auto-evict idle agents (default 30 min). 0 disables\n * idle eviction (LRU only).\n */\n agents: z\n .object({\n maxRegistries: z.number().int().positive().default(100),\n registry: z\n .object({\n /** LRU cap — MUST be ≥ max-concurrent-active-conversations. */\n maxAgents: z.number().int().positive().max(10_000).default(100),\n /** Idle eviction window in ms. 0 disables idle eviction (LRU only). */\n idleTimeoutMs: z\n .number()\n .int()\n .nonnegative()\n .default(30 * 60_000),\n })\n .optional(),\n })\n .optional(),\n port: z.number().int().min(1).max(65535).default(3000),\n ssr: z.boolean().default(false),\n /** When true (and ssr === true), use renderToPipeableStream with progressive\n * shell flush instead of single-shot renderToString. Opt-in for streaming\n * SSR; default false preserves the current behavior. */\n ssrStreaming: z.boolean().default(false),\n rateLimit: rateLimitSchema.optional(),\n upload: uploadSchema.optional(),\n logging: loggingSchema.optional(),\n security: securitySchema.optional(),\n serialization: z.enum(['json', 'superjson']).default('json'),\n // Plugins are validated structurally at runtime by createPluginRunnerFromConfig.\n // Zod only checks the shape minimally (must be array). Type-level safety is\n // provided through defineConfig at the user surface.\n plugins: z.array(z.unknown()).optional(),\n /** Enable client-side batching of theoFetch calls and the\n * /api/__theo_batch__ server endpoint. */\n batching: z\n .union([z.boolean(), z.object({ max: z.number().int().positive().optional() })])\n .optional(),\n /** T4.1 — Audit log. When `logger` is provided, framework events\n * (csrf.warn, rate-limit.exceeded, session.rotated, csp.violation) are\n * emitted to it. Default: noop. */\n audit: z\n .object({\n logger: z.unknown().optional(),\n })\n .optional(),\n /** TheoUI auto-wire (T2.1). `false` = opt-out; object = explicit theme/fonts;\n * undefined = enabled when @theokit/ui is detected in node_modules. */\n ui: z\n .union([\n z.literal(false),\n z.object({\n theme: z.enum(['violet-forge', 'noir', 'paper']).optional(),\n fonts: z.enum(['bundled', 'cdn']).optional(),\n }),\n ])\n .optional(),\n /**\n * Cache subsystem (caching-and-revalidation-plan).\n * Default `undefined` keeps caching disabled (backward compatible).\n * Pass `cache: {}` to opt in with defaults.\n */\n cache: cacheSchema.optional(),\n /**\n * Devtools overlay (Phase 0.4.0+ — see docs/plans/devtools-plan.md).\n *\n * - `undefined` (default): devtools auto-injects in `pnpm dev`, NEVER in `vite build`.\n * - `false`: devtools disabled entirely (Vite plugin skips injection even in dev).\n * - `{ ... }`: devtools enabled with explicit defaults (position, theme).\n *\n * Tree-shaken to noop in prod via the dual-export pattern in\n * `packages/theo/src/devtools/index.ts` (EC-17 positive prod check).\n */\n devtools: z\n .union([\n z.literal(false),\n z.object({\n position: z.enum(['top-left', 'top-right', 'bottom-left', 'bottom-right']).optional(),\n theme: z.enum(['light', 'dark', 'system']).optional(),\n }),\n ])\n .optional(),\n /**\n * Informative list of deploy adapters the app supports. Does NOT\n * trigger per-build translations — only the `--target` CLI flag does\n * (EC-201 / ADR D2). If `config.adapters` includes a target NOT\n * matching `--target`, `theokit build` emits a cross-reference note.\n */\n adapters: z.array(z.string()).optional(),\n /**\n * Extra package names to pre-bundle via Vite's `optimizeDeps.include`.\n *\n * Framework auto-includes `@theokit/ui` and `lucide-react` when present.\n * Apps adding plugin peer-deps that rely on a runtime `import('<pkg>')`\n * (dynamic specifier) — e.g. `@theokit/plugin-canvas` consumers\n * installing `mermaid` for Mermaid SVG rendering — must declare those\n * here so Vite can resolve the literal in dev mode without\n * \"Failed to resolve module specifier\" errors. Production builds use\n * tsup/esbuild bundling and are not affected.\n *\n * Example:\n * viteOptimizeDeps: ['mermaid']\n */\n viteOptimizeDeps: z.array(z.string()).optional(),\n /**\n * Jobs backend (ADR D3 + T2.1). When configured, every request gets\n * `ctx.queue.enqueue` auto-wired via the outbox lifecycle. Pass a\n * `JobBackend` instance (InMemoryJobBackend, PostgresJobBackend, etc.).\n */\n jobs: z\n .object({\n backend: z.custom<unknown>((v) => typeof v === 'object' && v !== null),\n })\n .optional(),\n /**\n * StorageManager configuration (T1.1 / ADR-0007).\n *\n * Declares Postgres servers + databases + Redis servers. Consumed by\n * `getStorageManager().configure()` at boot via `start.ts`.\n *\n * Default `undefined` means manager exists but is not configured — adapters\n * relying on the manager (PostgresJobBackend.fromStorageManager, etc.) will\n * throw with an actionable error on first use.\n */\n storage: storageSchema.optional(),\n /**\n * Wave 2 — Polyglot services orchestration (T1.1 / ADR-0012/0013/0014/0015).\n *\n * Declarative external sidecar processes (Python FastAPI / Node Hono)\n * that boot alongside the TheoKit TS app. Empty `services: {}` is the\n * default and preserves Wave 1 behavior.\n */\n services: servicesConfigSchema,\n /**\n * G2 — OpenAPI emit (build-time only).\n *\n * When present, `theokit build` writes a generated `openapi.json` from\n * every `defineRoute()` body/query/params Zod schema. `undefined` keeps\n * the framework backward-compatible (no emit). Pass `openapi: {}` to\n * opt in with defaults.\n *\n * Defaults: spec 3.1.0 · servers `http://localhost:3000` · title\n * \"TheoKit App\" · version \"0.0.0\" · outDir \".theo\" (next to dist).\n *\n * The env var `THEOKIT_OPENAPI_SERVERS` (CSV of URLs) overrides\n * `servers[].url` at emit time without rebuilding the config.\n */\n openapi: z\n .object({\n servers: z\n .array(\n z.object({\n url: z.string().url(),\n description: z.string().optional(),\n }),\n )\n .default([{ url: 'http://localhost:3000', description: 'Local development' }]),\n specVersion: z.enum(['3.1.0', '3.0.3']).default('3.1.0'),\n title: z.string().default('TheoKit App'),\n version: z.string().default('0.0.0'),\n outDir: z.string().default('.theo'),\n })\n .optional(),\n /**\n * G5 T1.3 — error envelope transformer hook (blueprint ADR D3).\n *\n * When present, runs at framework error-boundary time to enrich the\n * envelope with consumer-defined extensions (`hint`, custom telemetry\n * tags, etc.) BEFORE the envelope is serialized to the client. The\n * function signature is preserved via the explicit `FormatErrorHook`\n * type — TS inference flows from `theo.config.ts` into `@theo/client`\n * codegen and `@theokit/ui` AgentErrorCard consumers.\n *\n * Use `z.custom<FormatErrorHook>(...)` because Zod's built-in\n * `z.function()` discards its TS signature after parse; `z.custom`\n * with a function-typed runtime guard preserves the call-site type\n * without trading off Zod runtime validation.\n */\n formatError: z\n .custom<FormatErrorHook>((val) => typeof val === 'function', {\n message: 'formatError must be a function',\n })\n .optional(),\n })\n // EC-2 fix: cross-config refine — no service may share TheoKit's web port.\n .refine((cfg) => !Object.values(cfg.services).some((s) => s.port === cfg.port), {\n message: 'service.port collides with TheoKit web port — change one of them',\n path: ['services'],\n })\n\nexport type TheoConfig = z.infer<typeof theoConfigSchema>\n\nexport type OpenApiConfig = NonNullable<TheoConfig['openapi']>\n\n// Silence unused-import warnings for type-only re-exports — TS strips at compile,\n// but exports above re-introduce them for downstream consumers.\nexport type _FormatErrorContext = FormatErrorContext\n","import { z } from 'zod'\n\n/**\n * EC-3 — CWE-113 HTTP Response Splitting mitigation.\n *\n * Every string that becomes a header value must reject CR/LF. Apps that\n * derive header values from untrusted input (feature flags, tenant config,\n * URL params) would otherwise let attackers inject Set-Cookie / Location\n * headers via `\\r\\n`. Apply this refinement to every header-bound string\n * field: permissionsPolicy, csp, hsts, referrerPolicy, CORS allow/expose.\n */\nexport const headerSafeString = z\n .string()\n .refine((s) => !/[\\r\\n]/.test(s), { message: 'Header value must not contain CR/LF' })\n","import { z } from 'zod'\n\n/**\n * Base bucket config — legacy shape preserved for backwards compatibility.\n */\nconst baseRateLimitSchema = z.object({\n windowMs: z.number().min(1),\n max: z.number().int().min(1),\n})\n\n/**\n * T2.2 — Per-route + per-user rate limit. The new shape adds `routes`\n * (path map), `keyBy`, and `cookieName`. The legacy flat shape is still\n * accepted via the union — see `createRouteRateLimiter` for normalization.\n */\nexport const rateLimitSchema = z.union([\n baseRateLimitSchema,\n z.object({\n default: baseRateLimitSchema.optional(),\n routes: z.record(z.string(), baseRateLimitSchema).optional(),\n keyBy: z\n .union([\n z.enum(['ip', 'session', 'user']),\n z.function({ input: z.tuple([z.unknown()]), output: z.string() }),\n ])\n .optional(),\n cookieName: z.string().min(1).optional(),\n }),\n])\n","import { z } from 'zod'\n\nexport const uploadSchema = z.object({\n maxFileSize: z\n .number()\n .min(1)\n .default(10 * 1024 * 1024), // 10MB\n maxFiles: z.number().int().min(1).default(10),\n maxFieldSize: z\n .number()\n .min(1)\n .default(1 * 1024 * 1024), // 1MB\n})\n","import { z } from 'zod'\n\nexport const loggingSchema = z.object({\n level: z.enum(['debug', 'info', 'warn', 'error', 'silent']).default('info'),\n})\n","import { z } from 'zod'\n\n/**\n * Cache subsystem config (caching-and-revalidation-plan).\n * Default `cache: undefined` keeps the framework backward-compatible\n * (no engine initialized → no cache primitives available).\n *\n * Setting `cache: {}` opts in with all defaults.\n */\nconst routeRuleSchema = z.object({\n maxAge: z.number().nonnegative().finite().optional(),\n swr: z.number().nonnegative().finite().optional(),\n tags: z.array(z.string()).optional(),\n})\n\nexport const cacheSchema = z.object({\n enabled: z.boolean().default(true),\n /** 'memory' uses InMemoryCacheAdapter; otherwise pass a custom adapter instance. */\n storage: z.union([z.literal('memory'), z.custom<unknown>()]).default('memory'),\n maxEntries: z.number().int().positive().default(1000),\n defaults: z\n .object({\n maxAge: z.number().nonnegative().finite().default(1),\n swr: z.number().nonnegative().finite().optional(),\n cacheErrors: z.boolean().default(false),\n })\n .default({ maxAge: 1, cacheErrors: false }),\n keyDerivation: z\n .object({\n excludeQuery: z.array(z.string()).optional(),\n sortQuery: z.boolean().default(true),\n lowercaseHost: z.boolean().default(true),\n })\n .default({ sortQuery: true, lowercaseHost: true }),\n routeRules: z.record(z.string(), routeRuleSchema).optional(),\n})\n","import { z } from 'zod'\n\nimport { headerSafeString } from './header-safe.js'\n\n/**\n * Phase 5 — CSRF warn-first (EC-1).\n *\n * 0.2.0 default: `warn`. Existing apps keep working but get structured\n * warnings about every state-mutating request that does not carry the\n * `X-Theo-Action: 1` header. 0.3.0 will flip the default to `strict`.\n *\n * Set explicitly to `strict` to opt into the future default early,\n * or `off` to disable CSRF entirely (only valid when you have another\n * defense — bearer auth, no session cookies, etc).\n */\n\n/**\n * Phase 6 — Default security headers (D4 / EC-2).\n *\n * 0.2.0 defaults:\n * - CSP in `report-only` mode (EC-2: don't break existing apps)\n * - X-Frame-Options: DENY · X-Content-Type-Options: nosniff\n * - Referrer-Policy: strict-origin-when-cross-origin\n * - HSTS in production only (no TLS on localhost)\n *\n * Users override individual headers, swap CSP to `enforce`, or disable\n * CSP entirely (`csp: false` / `cspMode: 'off'`).\n */\nexport const securityHeadersSchema = z.object({\n csp: z.union([headerSafeString, z.literal(false)]).optional(),\n // T6.1 — default flipped from 'report-only' to 'enforce' for 0.3.0.\n // Users who want the old behaviour set `cspMode: 'report-only'`\n // explicitly. See docs/migrating/0.2-to-0.3.md.\n cspMode: z.enum(['enforce', 'report-only', 'off']).default('enforce'),\n hsts: z.union([headerSafeString, z.literal(false)]).optional(),\n frameOptions: z.enum(['DENY', 'SAMEORIGIN']).default('DENY'),\n contentTypeOptions: z.literal('nosniff').default('nosniff'),\n referrerPolicy: headerSafeString.default('strict-origin-when-cross-origin'),\n /**\n * T1.1 — Permissions-Policy directive string. EC-3-refined: rejects CR/LF.\n * Pass `false` to suppress the header.\n */\n permissionsPolicy: z.union([headerSafeString, z.literal(false)]).optional(),\n})\n\n/**\n * T5.1 — Disallowed-routes escalation pattern (Rails-inspired).\n *\n * `routes` accepts string (exact match — trailing slash matters) or\n * RegExp entries. Matched routes that would otherwise emit `csrf.warn`\n * dispatch through `disallowedBehavior` instead:\n * - `'warn'` : no-op vs the default warn-mode behavior\n * - `'raise'` : escalate to 403, even when global `csrf` mode is 'warn'\n *\n * Use to roll out strict mode per-route (e.g., flip /api/auth/* first)\n * without committing the entire surface to strict at once.\n */\nexport const disallowedConfigSchema = z.object({\n routes: z.array(z.union([z.string(), z.instanceof(RegExp)])),\n behavior: z.enum(['warn', 'raise']).default('raise'),\n})\n\n/**\n * T1.2 — CORS configuration.\n *\n * `origins` accepts a single value (`'*'`, string, RegExp, callback) OR an\n * array of (string | RegExp). The spec-violating `origins: '*'` +\n * `credentials: true` combination is rejected at parse time (browsers\n * ignore wildcards when credentials are sent).\n *\n * EC-3 — `allowedHeaders` and `exposedHeaders` entries go through the\n * header-safe refinement (CR/LF rejected — CWE-113 mitigation).\n */\nexport const corsSchema = z\n .object({\n origins: z.union([\n z.literal('*'),\n headerSafeString,\n z.instanceof(RegExp),\n z.array(z.union([headerSafeString, z.instanceof(RegExp)])),\n z.function({ input: z.tuple([z.string()]), output: z.boolean() }),\n ]),\n methods: z\n .array(z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']))\n .optional(),\n allowedHeaders: z.array(headerSafeString).optional(),\n exposedHeaders: z.array(headerSafeString).optional(),\n credentials: z.boolean().default(false),\n maxAge: z.number().int().min(0).max(86400).default(600),\n })\n .refine((c) => !(c.origins === '*' && c.credentials), {\n message: 'CORS spec forbids origins:\"*\" with credentials:true — browsers ignore the wildcard',\n })\n\nexport const securitySchema = z.object({\n // T6.1 — default flipped from 'warn' to 'strict' for 0.3.0. Apps that\n // grep their warn-mode logs from 0.2.x already know which endpoints\n // break; opt back into 'warn' globally OR use disallowedRoutes for\n // surgical migration. See docs/migrating/0.2-to-0.3.md.\n csrf: z.enum(['off', 'warn', 'strict']).default('strict'),\n headers: securityHeadersSchema.optional(),\n /** T5.1 — per-route escalation (Rails disallowed_warnings pattern). */\n disallowed: disallowedConfigSchema.optional(),\n /** T1.2 — Cross-Origin Resource Sharing. Single global config; runs first in pipeline. */\n cors: corsSchema.optional(),\n})\n","import { z } from 'zod'\n\n/**\n * StorageManager schemas (T1.1 / ADR-0007 D4).\n *\n * `theo.config.ts > storage` declares Postgres servers (credentials), the\n * databases on each server (TheoCloud / managed PG / self-host), and Redis\n * servers. The runtime `StorageManager` consumes this block; adapters\n * (PostgresJobBackend, PostgresConversationStorage, etc.) request pools by\n * database name and never see raw credentials at the call site.\n *\n * EC-1 (DOCUMENT in concept doc T4.1): Zod default strip-unknown mode\n * silently drops typo keys (`databasees` instead of `databases`). The\n * concept doc warns users to use exact names; runtime errors are still\n * actionable (`Database \"X\" not configured`).\n *\n * EC-2 (DEFERRED to use-time): `databases.X.server` references a key in\n * `servers`. Cross-field validation is intentionally NOT enforced at parse\n * time — `StorageManager.usePostgres()` throws an actionable error on the\n * first call with the dangling reference, matching Rails / Nitro behavior.\n */\nconst tlsConfigSchema = z.object({\n rejectUnauthorized: z.boolean().optional(),\n caCert: z.string().optional(),\n clientCert: z.string().optional(),\n clientKey: z.string().optional(),\n})\n\nconst serverConfigSchema = z.object({\n host: z.string().min(1),\n port: z.number().int().positive().max(65535).optional(),\n user: z.string().min(1),\n /** Password may be the empty string (some providers permit it). */\n password: z.string(),\n tls: tlsConfigSchema.optional(),\n})\n\nconst postgresPoolConfigSchema = z.object({\n min: z.number().int().nonnegative().optional(),\n max: z.number().int().positive().optional(),\n connectionTimeoutMillis: z.number().int().positive().optional(),\n idleTimeoutMillis: z.number().int().nonnegative().optional(),\n})\n\nconst postgresDatabaseConfigSchema = z.object({\n /** References a key in `storage.servers`. Resolved at `usePostgres()` call time. */\n server: z.string().min(1),\n database: z.string().min(1),\n pool: postgresPoolConfigSchema.optional(),\n})\n\nconst redisServerConfigSchema = serverConfigSchema.extend({\n db: z.number().int().nonnegative().optional(),\n maxRetriesPerRequest: z.number().int().nonnegative().optional(),\n})\n\nexport const storageSchema = z.object({\n servers: z.record(z.string(), serverConfigSchema).optional(),\n databases: z.record(z.string(), postgresDatabaseConfigSchema).optional(),\n redis: z.record(z.string(), redisServerConfigSchema).optional(),\n})\n\nexport type StorageConfig = z.infer<typeof storageSchema>\n"],"mappings":"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","names":["z","z","z","z","z","z","z","z","z"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/server/jobs/define-job.ts","../src/server/jobs/job-backend.ts","../src/server/jobs/job-backend-memory.ts","../src/server/jobs/job-backend-postgres.ts","../src/server/jobs/job-runner.ts","../src/server/jobs/job-manifest.ts","../src/server/jobs/job-scan.ts"],"sourcesContent":["import type { JobDefinition, JobOptions } from './job-types.js'\n\nconst NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/\n\nfunction validateName(name: string): void {\n if (typeof name !== 'string' || !NAME_RE.test(name)) {\n throw new Error(\n `defineJob: invalid name \"${name}\". ` +\n 'Must be 1-64 chars, lowercase alphanumeric + hyphen, starting with [a-z0-9].',\n )\n }\n}\n\n/**\n * Declare a background job. Pure identity helper — no registration side\n * effect; the build-time scanner (T2.3) discovers definitions by walking\n * `server/jobs/` and emits `.theo/jobs.json`.\n *\n * Per ADR-0003, handler returns `void` (or `Promise<void>`); no workflow\n * API. To run another job after this one, call `ctx.queue.enqueue` from\n * the handler.\n *\n * @example\n * ```ts\n * // server/jobs/process-document.ts\n * import { defineJob } from 'theokit/server'\n * import { z } from 'zod'\n *\n * export default defineJob('process-document', {\n * input: z.object({ documentId: z.string() }),\n * maxAttempts: 3,\n * async handler({ input, traceId }) {\n * // process input.documentId\n * },\n * })\n * ```\n */\nexport function defineJob<TInput = unknown>(\n name: string,\n options: JobOptions<TInput>,\n): JobDefinition<TInput> {\n validateName(name)\n return {\n name,\n maxAttempts: options.maxAttempts ?? 1,\n hasInputSchema: options.input !== undefined,\n handler: options.handler,\n inputSchema: options.input,\n }\n}\n","/**\n * Neutral `JobBackend` interface per ADR-0002.\n *\n * Implementations ship in core:\n * - `InMemoryJobBackend` (T2.2) — dev + tests; no external deps\n * - `PostgresJobBackend` (T3.1) — production; requires `pg` peer dep\n *\n * Community packages (`@theokit/jobs-redis`, `@theokit/jobs-sqs`, etc.)\n * implement this same interface to plug in alternate substrates without\n * modifying TheoKit core.\n */\n\nexport interface JobEnqueueInput {\n /** The job name (matches a `defineJob` declaration). */\n name: string\n /** Validated input (already passed through Zod or equivalent at the queue-client layer). */\n input: unknown\n /** Optional idempotency key for at-most-once dispatch within TTL. */\n idempotencyKey?: string\n /** Optional delay before the job becomes available for dispatch. */\n delaySeconds?: number\n /** W3C Trace Context propagation per ADR/R0.5.9. */\n traceparent?: string\n /** Maximum dispatch attempts (read from JobDefinition by the queue client). */\n maxAttempts?: number\n}\n\nexport interface JobLease {\n readonly jobId: string\n readonly name: string\n readonly input: unknown\n readonly attempts: number\n readonly maxAttempts: number\n readonly traceparent?: string\n readonly lockExpiresAt: Date\n}\n\nexport interface JobBackend {\n /** Human-readable name for logging (e.g., \"memory\", \"postgres\"). */\n readonly name: string\n\n /** Persist a job for later dispatch. Returns the generated jobId. */\n enqueue(input: JobEnqueueInput): Promise<{ jobId: string }>\n\n /** Worker loop polls for the next available leases. */\n dequeue(opts: { batchSize?: number; lockSeconds?: number }): Promise<JobLease[]>\n\n /** Mark a job complete (success). */\n ack(jobId: string): Promise<void>\n\n /** Mark a job failed; backend decides retry vs DLQ via attempts. */\n nack(jobId: string, opts: { error: string; nonRetryable?: boolean }): Promise<void>\n\n /** Optional: return existing jobId if `key` was enqueued within `ttlSeconds`. */\n idempotency?(key: string, ttlSeconds: number): Promise<{ jobId: string } | null>\n}\n\n/**\n * Thrown from a job handler to opt out of retry policy. The backend MUST\n * nack with `nonRetryable: true` and permanently remove the job.\n */\nexport class NonRetryableError extends Error {\n readonly code = 'NON_RETRYABLE'\n constructor(message: string) {\n super(message)\n this.name = 'NonRetryableError'\n }\n}\n","// T5a.1a — Web Standards migration (leaf-first slice). Web Crypto's\n// `crypto.randomUUID()` is available on globalThis in every supported\n// runtime per ADR-0028 (Node 22+, CF Workers, Bun, Deno, browsers).\n// No node:crypto import needed.\n\nimport type { JobBackend, JobEnqueueInput, JobLease } from './job-backend.js'\n\n/**\n * In-memory job backend for dev + tests + single-instance prototypes.\n *\n * Zero external dependencies. Storage is two Maps:\n * - `pending: Map<jobId, PendingEntry>` — all enqueued jobs (locked or not)\n * - `idempotencyMap: Map<key, { jobId; expiresAt }>` — dedup window\n *\n * Per ADR-0002: this is one of two first-party backends shipped in core.\n * Production deploys plug in PostgresJobBackend (T3.1).\n *\n * EC-104 (jobs-crons-webhooks-cost-tracking-plan): registers a\n * `process.on('beforeExit')` handler that clears pending dispatch timers\n * and logs a warning with the dropped count. Failure mode is visible,\n * not silent. `destroy()` removes the listener for test isolation.\n */\n\nexport interface InMemoryJobBackendOptions {\n /** Maximum pending entries before oldest is dropped + warning logged. */\n maxPending?: number\n}\n\ninterface PendingEntry {\n jobId: string\n name: string\n input: unknown\n attempts: number\n maxAttempts: number\n traceparent?: string\n availableAt: number\n lockExpiresAt: number | null\n dispatchTimer: NodeJS.Timeout | null\n}\n\ninterface IdempotencyEntry {\n jobId: string\n expiresAt: number\n}\n\nconst DEFAULT_MAX_PENDING = 10_000\n\nexport class InMemoryJobBackend implements JobBackend {\n readonly name = 'memory'\n readonly #pending = new Map<string, PendingEntry>()\n readonly #idempotency = new Map<string, IdempotencyEntry>()\n readonly #maxPending: number\n #beforeExitHandler: (() => void) | null = null\n #destroyed = false\n\n constructor(opts: InMemoryJobBackendOptions = {}) {\n this.#maxPending = opts.maxPending ?? DEFAULT_MAX_PENDING\n // EC-104 — register graceful shutdown handler. We keep a reference\n // so `destroy()` can remove it (test isolation; avoid leaking\n // listeners across vitest test files).\n this.#beforeExitHandler = (): void => {\n this.#cleanupOnShutdown()\n }\n process.on('beforeExit', this.#beforeExitHandler)\n }\n\n async enqueue(input: JobEnqueueInput): Promise<{ jobId: string }> {\n // Overflow eviction (visible failure, not silent leak)\n if (this.#pending.size >= this.#maxPending) {\n const oldestKey = this.#pending.keys().next().value\n if (oldestKey) {\n const oldest = this.#pending.get(oldestKey)\n if (oldest?.dispatchTimer) clearTimeout(oldest.dispatchTimer)\n this.#pending.delete(oldestKey)\n }\n\n console.warn(\n `[theokit:jobs:memory] pending overflow at ${this.#maxPending} — dropped oldest. ` +\n 'Switch to PostgresJobBackend for production.',\n )\n }\n\n // Idempotency dedup\n if (input.idempotencyKey) {\n const existing = this.#idempotency.get(input.idempotencyKey)\n const now = Date.now()\n if (existing && existing.expiresAt > now) {\n return { jobId: existing.jobId }\n }\n }\n\n const jobId = globalThis.crypto.randomUUID()\n const now = Date.now()\n const availableAt = now + (input.delaySeconds ?? 0) * 1000\n const entry: PendingEntry = {\n jobId,\n name: input.name,\n input: input.input,\n attempts: 0,\n maxAttempts: input.maxAttempts ?? 1,\n traceparent: input.traceparent,\n availableAt,\n lockExpiresAt: null,\n dispatchTimer: null,\n }\n this.#pending.set(jobId, entry)\n\n if (input.idempotencyKey) {\n // Default TTL window matches typical idempotency expectations;\n // explicit idempotency(key, ttl) check overrides per caller.\n this.#idempotency.set(input.idempotencyKey, {\n jobId,\n expiresAt: now + 60_000,\n })\n }\n\n return Promise.resolve({ jobId })\n }\n\n async dequeue(opts: { batchSize?: number; lockSeconds?: number }): Promise<JobLease[]> {\n const batchSize = opts.batchSize ?? 1\n const lockSeconds = opts.lockSeconds ?? 30\n const now = Date.now()\n\n const leases: JobLease[] = []\n for (const entry of this.#pending.values()) {\n if (leases.length >= batchSize) break\n // Skip if locked + lock still valid\n if (entry.lockExpiresAt !== null && entry.lockExpiresAt > now) continue\n // Skip if not yet available (delay)\n if (entry.availableAt > now) continue\n\n entry.lockExpiresAt = now + lockSeconds * 1000\n entry.attempts += 1\n leases.push({\n jobId: entry.jobId,\n name: entry.name,\n input: entry.input,\n attempts: entry.attempts,\n maxAttempts: entry.maxAttempts,\n traceparent: entry.traceparent,\n lockExpiresAt: new Date(entry.lockExpiresAt),\n })\n }\n return Promise.resolve(leases)\n }\n\n async ack(jobId: string): Promise<void> {\n const entry = this.#pending.get(jobId)\n if (entry?.dispatchTimer) clearTimeout(entry.dispatchTimer)\n this.#pending.delete(jobId)\n return Promise.resolve()\n }\n\n async nack(jobId: string, opts: { error: string; nonRetryable?: boolean }): Promise<void> {\n const entry = this.#pending.get(jobId)\n if (!entry) return Promise.resolve()\n if (opts.nonRetryable || entry.attempts >= entry.maxAttempts) {\n if (entry.dispatchTimer) clearTimeout(entry.dispatchTimer)\n this.#pending.delete(jobId)\n } else {\n // Release lock — becomes available again\n entry.lockExpiresAt = null\n }\n return Promise.resolve()\n }\n\n idempotency(key: string, ttlSeconds: number): Promise<{ jobId: string } | null> {\n const now = Date.now()\n const existing = this.#idempotency.get(key)\n if (!existing || existing.expiresAt <= now) {\n return Promise.resolve(null)\n }\n // Renew if caller's ttl differs\n existing.expiresAt = Math.max(existing.expiresAt, now + ttlSeconds * 1000)\n return Promise.resolve({ jobId: existing.jobId })\n }\n\n /**\n * Remove the beforeExit listener and clear all pending timers.\n * Idempotent — safe to call multiple times. Use in test teardown to\n * avoid leaking listeners across vitest test files.\n */\n destroy(): void {\n if (this.#destroyed) return\n this.#destroyed = true\n if (this.#beforeExitHandler) {\n process.off('beforeExit', this.#beforeExitHandler)\n this.#beforeExitHandler = null\n }\n for (const entry of this.#pending.values()) {\n if (entry.dispatchTimer) clearTimeout(entry.dispatchTimer)\n }\n }\n\n /** EC-104 test hook — invokes the shutdown handler synchronously. */\n triggerBeforeExitForTest(): void {\n this.#cleanupOnShutdown()\n }\n\n #cleanupOnShutdown(): void {\n const droppedCount = this.#pending.size\n for (const entry of this.#pending.values()) {\n if (entry.dispatchTimer) clearTimeout(entry.dispatchTimer)\n }\n if (droppedCount > 0) {\n console.warn(\n `[theokit:jobs:memory] ${droppedCount} jobs dropped on shutdown — ` +\n 'use PostgresJobBackend for durability across restarts.',\n )\n }\n }\n}\n","import type { PostgresFactory, StorageManager } from '../storage/storage-manager.js'\nimport type { PoolLike } from '../storage/storage-types.js'\n\nimport type { JobBackend, JobEnqueueInput, JobLease } from './job-backend.js'\n\n/**\n * Postgres-backed `JobBackend` per ADR-0002 (T3.1).\n *\n * Uses the Graphile Worker pattern:\n * - Single `theokit_jobs` table.\n * - Dequeue via `SELECT ... FOR UPDATE SKIP LOCKED` (concurrent workers\n * never race for the same job).\n * - Idempotency via UNIQUE INDEX on (name, idempotency_key).\n *\n * `pg` is an OPTIONAL peer dependency — the caller provides their own\n * `pg.Pool` (or compatible). This avoids forcing every TheoKit user to\n * install Postgres tooling for in-memory dev work.\n *\n * Pool size recommendation (EC-108): set `connectionTimeoutMillis` to\n * a finite value (e.g., 5000) so a saturated pool errors instead of\n * deadlocking. Suggested pool size: `min(workerConcurrency * 1.5, 20)`.\n *\n * @see https://github.com/graphile/worker (Postgres SKIP LOCKED reference)\n * @see docs/adr/0002-job-backend-interface-neutral-contract.md\n */\n\n/**\n * `PoolLike` moved to `../storage/storage-types.ts` (T0.2 / ADR-0007 D7) to\n * deduplicate the shape across multiple adapters. Re-exported here so existing\n * `import { PoolLike } from 'theokit/server'` (which routes through the jobs\n * barrel today) continues to compile unchanged.\n */\nexport type { PoolLike } from '../storage/storage-types.js'\n\nexport interface PostgresJobBackendOptions {\n pool: PoolLike\n /** Table name override (default `theokit_jobs`). */\n tableName?: string\n}\n\nconst DEFAULT_TABLE = 'theokit_jobs'\n\nexport class PostgresJobBackend implements JobBackend {\n readonly name = 'postgres'\n readonly #pool: PoolLike\n readonly #table: string\n\n constructor(opts: PostgresJobBackendOptions) {\n // Runtime guard for users who pass `pool: undefined` despite the type;\n // helps surface \"you forgot to install pg\" early with an actionable\n // error rather than a cryptic TypeError later.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!opts.pool) {\n throw new Error(\n 'PostgresJobBackend requires a `pool` option (pg.Pool or compatible). ' +\n 'Install pg via `pnpm add pg` and pass a configured Pool instance.',\n )\n }\n this.#pool = opts.pool\n this.#table = opts.tableName ?? DEFAULT_TABLE\n }\n\n /**\n * T2.1 — Construct a `PostgresJobBackend` using a pool managed by the\n * shared `StorageManager`. The manager caches the pool per `dbName`, so\n * repeated calls (in the same process) re-use the same connection pool.\n *\n * Equivalent to:\n * ```ts\n * const pool = manager.usePostgres(dbName, factory)\n * new PostgresJobBackend({ pool, ...options })\n * ```\n *\n * Errors thrown by the manager (unknown dbName, dangling server reference,\n * manager already disposed) propagate to the caller with actionable text.\n *\n * @see ADR-0007\n */\n static fromStorageManager(\n manager: StorageManager,\n dbName: string,\n factory: PostgresFactory,\n options?: Omit<PostgresJobBackendOptions, 'pool'>,\n ): PostgresJobBackend {\n const pool = manager.usePostgres(dbName, factory)\n return new PostgresJobBackend({ pool, ...options })\n }\n\n /**\n * Create or upgrade the schema. Idempotent (uses IF NOT EXISTS).\n * Call once at process start; safe to re-run on every deploy.\n */\n async migrate(): Promise<void> {\n const t = this.#table\n await this.#pool.query(`\n CREATE TABLE IF NOT EXISTS ${t} (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n input JSONB NOT NULL,\n attempts INT NOT NULL DEFAULT 0,\n max_attempts INT NOT NULL DEFAULT 1,\n available_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n locked_until TIMESTAMPTZ,\n traceparent TEXT,\n idempotency_key TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n )\n `)\n await this.#pool.query(`\n CREATE UNIQUE INDEX IF NOT EXISTS ${t}_idempotency_key_idx\n ON ${t} (name, idempotency_key)\n WHERE idempotency_key IS NOT NULL\n `)\n await this.#pool.query(`\n CREATE INDEX IF NOT EXISTS ${t}_available_at_idx\n ON ${t} (available_at)\n WHERE locked_until IS NULL\n `)\n }\n\n async enqueue(input: JobEnqueueInput): Promise<{ jobId: string }> {\n const id = randomId()\n const delaySeconds = input.delaySeconds ?? 0\n const t = this.#table\n\n // Idempotency: explicit SELECT-then-INSERT. The UNIQUE INDEX is the\n // safety net under race conditions (we surface the conflict by\n // re-selecting on insert-error). This 2-step path is pg-mem compatible\n // AND works on real Postgres.\n if (input.idempotencyKey) {\n const existing = await this.#pool.query<{ id: string }>(\n `SELECT id FROM ${t} WHERE name = $1 AND idempotency_key = $2 LIMIT 1`,\n [input.name, input.idempotencyKey],\n )\n if (existing.rows.length > 0) {\n return { jobId: existing.rows[0].id }\n }\n }\n\n await this.#pool.query(\n `INSERT INTO ${t} (id, name, input, max_attempts, available_at, traceparent, idempotency_key)\n VALUES ($1, $2, $3::jsonb, $4, NOW() + ($5 || ' seconds')::interval, $6, $7)`,\n [\n id,\n input.name,\n JSON.stringify(input.input),\n input.maxAttempts ?? 1,\n delaySeconds.toString(),\n input.traceparent ?? null,\n input.idempotencyKey ?? null,\n ],\n )\n return { jobId: id }\n }\n\n async dequeue(opts: { batchSize?: number; lockSeconds?: number }): Promise<JobLease[]> {\n const t = this.#table\n const batchSize = opts.batchSize ?? 1\n const lockSeconds = opts.lockSeconds ?? 30\n\n // Two-step pattern (pg-mem compatible AND real-Postgres compatible):\n // 1. SELECT FOR UPDATE SKIP LOCKED to claim candidate IDs.\n // 2. UPDATE in-place to mark lock_until + bump attempts.\n // On real Postgres, the FOR UPDATE SKIP LOCKED prevents concurrent\n // workers from seeing the same rows. On pg-mem the lock is silently\n // ignored, but pg-mem serializes all queries through one in-memory\n // db so the test still verifies single-claim semantics.\n const candidates = await this.#pool.query<{ id: string }>(\n `SELECT id FROM ${t}\n WHERE (locked_until IS NULL OR locked_until < NOW())\n AND available_at <= NOW()\n ORDER BY available_at ASC\n LIMIT $1\n FOR UPDATE SKIP LOCKED`,\n [batchSize],\n )\n\n if (candidates.rows.length === 0) return []\n\n const ids = candidates.rows.map((r) => r.id)\n // Build a parameterized IN clause: ($2, $3, ...). pg-mem's UPDATE\n // does not support `ANY($1::text[])`, so we materialize the list.\n // For batch sizes we expect (typically ≤100), this is fine.\n const placeholders = ids.map((_, i) => `$${i + 2}`).join(', ')\n\n const r = await this.#pool.query<{\n id: string\n name: string\n input: unknown\n attempts: number\n max_attempts: number\n traceparent: string | null\n locked_until: Date | string\n }>(\n `UPDATE ${t}\n SET locked_until = NOW() + ($1 || ' seconds')::interval,\n attempts = attempts + 1\n WHERE id IN (${placeholders})\n RETURNING id, name, input, attempts, max_attempts, traceparent, locked_until`,\n [lockSeconds.toString(), ...ids],\n )\n\n return r.rows.map((row) => ({\n jobId: row.id,\n name: row.name,\n input: row.input,\n attempts: row.attempts,\n maxAttempts: row.max_attempts,\n traceparent: row.traceparent ?? undefined,\n lockExpiresAt:\n row.locked_until instanceof Date ? row.locked_until : new Date(row.locked_until),\n }))\n }\n\n async ack(jobId: string): Promise<void> {\n await this.#pool.query(`DELETE FROM ${this.#table} WHERE id = $1`, [jobId])\n }\n\n async nack(jobId: string, opts: { error: string; nonRetryable?: boolean }): Promise<void> {\n const t = this.#table\n if (opts.nonRetryable) {\n await this.#pool.query(`DELETE FROM ${t} WHERE id = $1`, [jobId])\n return\n }\n // Check attempts >= maxAttempts → permanent removal.\n const r = await this.#pool.query<{\n attempts: number\n max_attempts: number\n }>(`SELECT attempts, max_attempts FROM ${t} WHERE id = $1`, [jobId])\n if (r.rows.length === 0) return\n const { attempts, max_attempts } = r.rows[0]\n if (attempts >= max_attempts) {\n await this.#pool.query(`DELETE FROM ${t} WHERE id = $1`, [jobId])\n } else {\n // Release lock — next dequeue picks it up again.\n await this.#pool.query(`UPDATE ${t} SET locked_until = NULL WHERE id = $1`, [jobId])\n }\n }\n\n async idempotency(key: string, _ttlSeconds: number): Promise<{ jobId: string } | null> {\n // The UNIQUE INDEX is the source of truth. Look for any non-deleted\n // row with this idempotency_key.\n const r = await this.#pool.query<{ id: string }>(\n `SELECT id FROM ${this.#table} WHERE idempotency_key = $1 LIMIT 1`,\n [key],\n )\n return r.rows.length > 0 ? { jobId: r.rows[0].id } : null\n }\n}\n\nfunction randomId(): string {\n // Use crypto.randomUUID if available; this is process-local, not DB-generated.\n // The DB UNIQUE constraint is on (name, idempotency_key), not on id, so\n // collisions across processes are functionally impossible.\n return globalThis.crypto.randomUUID()\n}\n","import {\n extractTraceContext,\n generateNewTraceContext,\n} from '../observability/trace-context-propagation.js'\n\nimport { type JobBackend, type JobLease, NonRetryableError } from './job-backend.js'\nimport type { JobContext, JobDefinition } from './job-types.js'\n\n/**\n * Job runner — pulls leases from a backend and invokes the matching\n * handler. Each invocation wires up `JobContext` with:\n * - `traceId` extracted from the lease's `traceparent` header (W3C\n * Trace Context per R0.5.9), OR a fresh generated trace_id when\n * the upstream had no traceparent OR it was malformed.\n * - `attempt` from the lease.\n * - `signal` that aborts when the lease times out.\n *\n * `tick()` runs one dequeue+dispatch cycle and returns when all leases\n * have been ack'd or nack'd. Production deploys call `tick()` in a loop\n * with backoff (the loop is outside this module's scope).\n *\n * EC-6 (trace continuity edge): when a lease has no traceparent OR a\n * malformed one, the handler still gets a valid traceId (generated) —\n * never null/undefined. The job MAY itself enqueue child jobs that\n * continue the new trace_id.\n */\n\nexport interface JobRunner {\n /** Run one dequeue+dispatch cycle. */\n tick(opts?: { batchSize?: number; lockSeconds?: number }): Promise<number>\n}\n\nexport function createJobRunner(\n backend: JobBackend,\n definitions: readonly JobDefinition[],\n): JobRunner {\n const byName = new Map<string, JobDefinition>()\n for (const def of definitions) {\n byName.set(def.name, def)\n }\n\n const runOne = async (lease: JobLease): Promise<void> => {\n const def = byName.get(lease.name)\n if (!def) {\n // Job name not registered in this runner's definition set. Nack\n // permanently — the lease can't be processed by this runner.\n await backend.nack(lease.jobId, {\n error: `No job definition for \"${lease.name}\"`,\n nonRetryable: true,\n })\n return\n }\n\n // Resolve trace ID from lease's traceparent, or fall back to fresh.\n let traceId: string\n if (lease.traceparent) {\n const headers = new Headers({ traceparent: lease.traceparent })\n const ctx = extractTraceContext(headers)\n traceId = ctx?.trace_id ?? generateNewTraceContext().trace_id\n } else {\n traceId = generateNewTraceContext().trace_id\n }\n\n // Optional Zod validation on input.\n let parsedInput: unknown = lease.input\n if (def.inputSchema) {\n try {\n parsedInput = def.inputSchema.parse(lease.input)\n } catch (err) {\n await backend.nack(lease.jobId, {\n error: `Input validation failed: ${err instanceof Error ? err.message : String(err)}`,\n nonRetryable: true,\n })\n return\n }\n }\n\n const abortController = new AbortController()\n // Auto-abort when lease expires\n const lockMs = lease.lockExpiresAt.getTime() - Date.now()\n const lockTimer = setTimeout(\n () => {\n abortController.abort()\n },\n Math.max(0, lockMs),\n )\n\n const jobCtx: JobContext = {\n traceId,\n input: parsedInput,\n signal: abortController.signal,\n attempt: lease.attempts,\n }\n\n try {\n await def.handler(jobCtx)\n await backend.ack(lease.jobId)\n } catch (err) {\n const isNonRetryable = err instanceof NonRetryableError\n await backend.nack(lease.jobId, {\n error: err instanceof Error ? err.message : String(err),\n nonRetryable: isNonRetryable,\n })\n } finally {\n clearTimeout(lockTimer)\n }\n }\n\n return {\n async tick(opts) {\n const leases = await backend.dequeue({\n batchSize: opts?.batchSize ?? 10,\n lockSeconds: opts?.lockSeconds ?? 30,\n })\n // Process leases sequentially — caller controls concurrency via\n // batchSize and tick frequency.\n for (const lease of leases) {\n await runOne(lease)\n }\n return leases.length\n },\n }\n}\n","import { writeAtomic } from '../_internal/atomic-write.js'\n\nimport type { JobNode } from './job-scan.js'\n\nexport const JOB_MANIFEST_SCHEMA_VERSION = 1 as const\n\nexport interface JobManifestEntry {\n readonly name: string\n readonly filePath: string\n readonly maxAttempts: number\n readonly hasInputSchema: boolean\n}\n\nexport interface JobManifest {\n readonly schemaVersion: typeof JOB_MANIFEST_SCHEMA_VERSION\n readonly generatedAt: string\n readonly jobs: readonly JobManifestEntry[]\n}\n\nexport function buildJobManifest(nodes: readonly JobNode[], projectRoot?: string): JobManifest {\n const jobs: JobManifestEntry[] = nodes.map((n) => ({\n name: n.name,\n filePath: projectRoot ? relativize(n.filePath, projectRoot) : n.filePath,\n maxAttempts: n.maxAttempts,\n hasInputSchema: n.hasInputSchema,\n }))\n return {\n schemaVersion: JOB_MANIFEST_SCHEMA_VERSION,\n generatedAt: new Date().toISOString(),\n jobs,\n }\n}\n\nexport function writeJobManifest(\n path: string,\n input: readonly JobNode[] | JobManifest,\n projectRoot?: string,\n): void {\n const manifest: JobManifest = isManifest(input) ? input : buildJobManifest(input, projectRoot)\n writeAtomic(path, JSON.stringify(manifest, null, 2))\n}\n\nfunction isManifest(value: unknown): value is JobManifest {\n return typeof value === 'object' && value !== null && 'schemaVersion' in value && 'jobs' in value\n}\n\nfunction relativize(absPath: string, root: string): string {\n if (absPath.startsWith(root)) {\n const trimmed = absPath.slice(root.length)\n return trimmed.startsWith('/') ? trimmed.slice(1) : trimmed\n }\n return absPath\n}\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time scanner: caller-controlled directory paths only.\n */\nimport { existsSync } from 'node:fs'\nimport { basename } from 'node:path'\n\nimport { importUserModule } from '../../config/import-user-module.js'\nimport { walkSourceFiles } from '../_internal/scan-walker.js'\n\nimport type { JobDefinition } from './job-types.js'\n\nexport interface JobNode {\n readonly name: string\n readonly filePath: string\n readonly maxAttempts: number\n readonly hasInputSchema: boolean\n}\n\nexport class DuplicateJobNameError extends Error {\n readonly code = 'DUPLICATE_JOB_NAME'\n constructor(\n public readonly jobName: string,\n public readonly filePaths: readonly string[],\n ) {\n super(\n `Duplicate job name \"${jobName}\" defined in: ${filePaths.join(', ')}. ` +\n 'Job names must be unique across server/jobs/.',\n )\n this.name = 'DuplicateJobNameError'\n }\n}\n\nconst JOB_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.mjs'])\n\ninterface JobModule {\n default?: unknown\n}\n\nfunction isJobDefinition(value: unknown): value is JobDefinition {\n if (typeof value !== 'object' || value === null) return false\n const def = value as Record<string, unknown>\n return (\n typeof def.name === 'string' &&\n typeof def.handler === 'function' &&\n typeof def.maxAttempts === 'number' &&\n typeof def.hasInputSchema === 'boolean'\n )\n}\n\nexport async function scanJobs(jobsDir: string): Promise<JobNode[]> {\n if (!existsSync(jobsDir)) return []\n\n const filePaths: string[] = []\n walkSourceFiles(jobsDir, { extensions: JOB_EXTENSIONS }, (p) => {\n const base = basename(p)\n if (base.startsWith('_') || base.startsWith('.')) return\n filePaths.push(p)\n })\n\n const nodes: JobNode[] = []\n for (const filePath of filePaths) {\n let mod: JobModule\n try {\n mod = await importUserModule(filePath)\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err)\n throw new Error(`Failed to import job file \"${filePath}\": ${reason}`)\n }\n const exported = mod.default\n if (!isJobDefinition(exported)) {\n throw new Error(\n `Job file \"${filePath}\" is missing a valid default export. ` +\n 'Expected `export default defineJob(name, { handler })`.',\n )\n }\n nodes.push({\n name: exported.name,\n filePath,\n maxAttempts: exported.maxAttempts,\n hasInputSchema: exported.hasInputSchema,\n })\n }\n\n const byName = new Map<string, string[]>()\n for (const node of nodes) {\n const bucket = byName.get(node.name) ?? []\n bucket.push(node.filePath)\n byName.set(node.name, bucket)\n }\n for (const [name, paths] of byName) {\n if (paths.length > 1) {\n throw new DuplicateJobNameError(name, paths)\n }\n }\n\n return nodes.sort((a, b) => a.name.localeCompare(b.name))\n}\n"],"mappings":"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","names":["now","r"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/contracts/action-protocol.ts","../src/server/http/send-response.ts","../src/server/http/middleware-runner.ts","../src/server/security/csrf-warn-dispatch.ts","../src/server/http/execute-stages.ts","../src/server/http/execute.ts","../src/server/http/form-data-to-object.ts","../src/server/http/handle-request-error.ts","../src/server/http/serialize-action-result.ts","../src/server/http/action-execute.ts","../src/server/http/batch-handler.ts","../src/server/http/cors.ts","../src/server/http/trace-context.ts"],"sourcesContent":["/**\n * Action protocol — cross-boundary contract for `defineAction` + `useAction`.\n *\n * Per plan g3-server-actions-and-useaction v1.2 § Phase 0 / T0.1, ADRs D1 (encoding)\n * + D6 (full dot-notation path) + D7 (PII mask) — types and runtime referenced\n * by server (`server/http/action-execute.ts`, `server/define/define-action.ts`)\n * and client (`@theokit/react/useAction`, `vite-plugin/actions-virtual-module`).\n *\n * Lives in `core/contracts/` per architecture.md v3.1 exception — cross-module\n * deep imports ALLOWED for this file. Core depends on NOTHING intra-monorepo;\n * `extractUniversalIssues` is implemented INLINE (NOT imported from `@theokit/sdk`)\n * to honor dep-cruiser `core-depends-on-nothing` invariant (EC-1 absorbed).\n *\n * Field-key convention (D6 + EC-13): dot-notation full path. Nested objects use\n * `'user.address.zip'`; array indices use `'items.0.name'` (NOT bracket\n * `'items[0].name'`). Root-level errors (path = []) use empty string `''`\n * (EC-7). Consumers needing bracket notation can write a small `dotToBracket`\n * helper in userland.\n */\n\nimport type { TheoErrorCode, TheoErrorEnvelope, ValidationFieldsExt } from './error-envelope.js'\n\n/**\n * HTTP-status mapping per IANA HTTP Status Code Registry (subset relevant to\n * action surface). VALIDATION_ERROR → 422 is preferred over Astro's BAD_REQUEST/400\n * (more semantic for input-shape mismatch). PAYLOAD_TOO_LARGE → 413 covers EC-3\n * (response-side size limit; request-side reuses standard 413).\n */\nexport type ActionErrorCode =\n | 'VALIDATION_ERROR'\n | 'BAD_REQUEST'\n | 'UNAUTHORIZED'\n | 'FORBIDDEN'\n | 'NOT_FOUND'\n | 'METHOD_NOT_ALLOWED'\n | 'CONFLICT'\n | 'CONTENT_TOO_LARGE'\n | 'PAYLOAD_TOO_LARGE'\n | 'UNSUPPORTED_MEDIA_TYPE'\n | 'TOO_MANY_REQUESTS'\n | 'INTERNAL_SERVER_ERROR'\n\nconst CODE_TO_STATUS: Record<ActionErrorCode, number> = {\n VALIDATION_ERROR: 422,\n BAD_REQUEST: 400,\n UNAUTHORIZED: 401,\n FORBIDDEN: 403,\n NOT_FOUND: 404,\n METHOD_NOT_ALLOWED: 405,\n CONFLICT: 409,\n CONTENT_TOO_LARGE: 413,\n PAYLOAD_TOO_LARGE: 413,\n UNSUPPORTED_MEDIA_TYPE: 415,\n TOO_MANY_REQUESTS: 429,\n INTERNAL_SERVER_ERROR: 500,\n}\n\nconst STATUS_TO_CODE: Record<number, ActionErrorCode> = {\n 400: 'BAD_REQUEST',\n 401: 'UNAUTHORIZED',\n 403: 'FORBIDDEN',\n 404: 'NOT_FOUND',\n 405: 'METHOD_NOT_ALLOWED',\n 409: 'CONFLICT',\n 413: 'PAYLOAD_TOO_LARGE',\n 415: 'UNSUPPORTED_MEDIA_TYPE',\n 422: 'VALIDATION_ERROR',\n 429: 'TOO_MANY_REQUESTS',\n 500: 'INTERNAL_SERVER_ERROR',\n}\n\n/**\n * Universal zod issue shape covering BOTH v3 (`z.ZodIssue`) and v4 (`$ZodIssue`).\n * Duck-typed minimum surface: `{path, message}`. Implemented per EC-1 absorbed —\n * consumers chain may ship either zod major; constructor accepts `unknown` and\n * normalizes via `extractUniversalIssues`.\n */\nexport interface UniversalZodIssue {\n readonly path: readonly (string | number)[]\n readonly message: string\n readonly code?: string\n}\n\n/**\n * `ActionError` — general server-side action failure. Discriminator `type` is\n * literal `'TheoActionError'`; used by `ActionError.fromJson` to distinguish\n * from `ActionInputError`.\n */\nexport class ActionError extends Error {\n // Discriminator widened to the full union so subclasses can narrow to their\n // specific literal (TypeScript would otherwise reject the override). Concrete\n // values are still always exact literals at runtime.\n readonly type: 'TheoActionError' | 'TheoActionInputError' = 'TheoActionError'\n readonly code: ActionErrorCode\n readonly status: number\n\n constructor(params: { code: ActionErrorCode; message?: string; stack?: string }) {\n super(params.message ?? params.code)\n this.code = params.code\n this.status = ActionError.codeToStatus(params.code)\n if (params.stack) {\n this.stack = params.stack\n }\n }\n\n /**\n * G5 T2.4 — canonical envelope view of the action error. Maps the G3\n * ActionErrorCode to a canonical TheoErrorCode (VALIDATION_ERROR ↔\n * UNPROCESSABLE_ENTITY, CONTENT_TOO_LARGE ↔ PAYLOAD_TOO_LARGE) so consumer\n * UI / SDK code can switch on the unified envelope.\n *\n * Subclasses override to populate `ext` (see `ActionInputError.envelope`).\n */\n get envelope(): TheoErrorEnvelope {\n return {\n code: ActionError.toTheoErrorCode(this.code),\n message: this.message,\n }\n }\n\n /**\n * Translate G3 ActionErrorCode → canonical TheoErrorCode (blueprint\n * Recommendations § \"G3 ActionError becomes inaugural envelope user\").\n */\n static toTheoErrorCode(code: ActionErrorCode): TheoErrorCode {\n if (code === 'VALIDATION_ERROR') return 'UNPROCESSABLE_ENTITY'\n if (code === 'CONTENT_TOO_LARGE') return 'PAYLOAD_TOO_LARGE'\n // The other ActionErrorCode members (BAD_REQUEST/UNAUTHORIZED/FORBIDDEN/\n // NOT_FOUND/METHOD_NOT_ALLOWED/CONFLICT/PAYLOAD_TOO_LARGE/\n // UNSUPPORTED_MEDIA_TYPE/TOO_MANY_REQUESTS/INTERNAL_SERVER_ERROR) are\n // identical strings in TheoErrorCode. Narrow via a type predicate\n // instead of assertion.\n return code\n }\n\n static codeToStatus(code: ActionErrorCode): number {\n return CODE_TO_STATUS[code]\n }\n\n static statusToCode(status: number): ActionErrorCode {\n return STATUS_TO_CODE[status] ?? 'INTERNAL_SERVER_ERROR'\n }\n\n /**\n * Parse a serialized error JSON back into the typed class hierarchy.\n * Distinguishes `TheoActionInputError` (with `issues` array) from\n * `TheoActionError` via the `type` discriminator. Falls back to\n * `INTERNAL_SERVER_ERROR` for malformed bodies (non-object, missing\n * `type`, unknown `code`).\n */\n static fromJson(body: unknown): ActionError {\n if (typeof body !== 'object' || body === null) {\n return new ActionError({ code: 'INTERNAL_SERVER_ERROR' })\n }\n const obj = body as Record<string, unknown>\n if (obj.type === 'TheoActionInputError' && Array.isArray(obj.issues)) {\n return new ActionInputError(obj.issues as unknown[])\n }\n if (\n obj.type === 'TheoActionError' &&\n typeof obj.code === 'string' &&\n obj.code in CODE_TO_STATUS\n ) {\n return new ActionError({\n code: obj.code as ActionErrorCode,\n message: typeof obj.message === 'string' ? obj.message : undefined,\n })\n }\n return new ActionError({ code: 'INTERNAL_SERVER_ERROR' })\n }\n}\n\n/**\n * `ActionInputError` — validation failure with field-level mapping.\n *\n * `fields` is auto-derived from `issues`: for each issue, key = full\n * dot-notation path (`'user.address.zip'`; root-level `path:[]` → `''`;\n * array indices as numeric segments → `'items.0.name'`). Multiple messages\n * for the same key accumulate; duplicate (path, message) tuples are deduped.\n */\nexport class ActionInputError extends ActionError {\n override readonly type = 'TheoActionInputError' as const\n readonly issues: readonly UniversalZodIssue[]\n readonly fields: Record<string, string[]>\n\n constructor(rawIssues: unknown) {\n super({ code: 'VALIDATION_ERROR', message: 'Validation failed' })\n this.issues = extractUniversalIssues(rawIssues)\n this.fields = buildFieldsMap(this.issues)\n }\n\n /**\n * G5 T2.4 — envelope view with ValidationFieldsExt populated from .fields.\n * UI consumers can `switch (env.code)` on `UNPROCESSABLE_ENTITY` and read\n * `(env.ext as ValidationFieldsExt).fields` for field-level rendering.\n */\n override get envelope(): TheoErrorEnvelope<ValidationFieldsExt> {\n return {\n code: 'UNPROCESSABLE_ENTITY',\n message: this.message,\n ext: { fields: this.fields },\n }\n }\n}\n\nfunction buildFieldsMap(issues: readonly UniversalZodIssue[]): Record<string, string[]> {\n const fields: Record<string, string[]> = {}\n const seen = new Set<string>()\n for (const issue of issues) {\n // EC-7: root path → empty string key. EC-8: numeric segments preserved as strings.\n const key = issue.path.length === 0 ? '' : issue.path.join('.')\n const dedupeKey = `${key}\u0000${issue.message}`\n if (seen.has(dedupeKey)) continue\n seen.add(dedupeKey)\n const bucket = fields[key] ?? []\n bucket.push(issue.message)\n fields[key] = bucket\n }\n return fields\n}\n\n/**\n * Normalize zod v3 (`z.ZodIssue`) and v4 (`$ZodIssue`) raw issues to\n * `UniversalZodIssue[]` (EC-1 absorbed). Duck-typed on `{path, message}` —\n * does NOT import zod types (`core/contracts/` depends on no intra-monorepo\n * + zod is consumer-controlled across the boundary).\n *\n * Silently skips entries missing required fields or shape mismatches; returns\n * empty array on non-array input.\n */\nexport function extractUniversalIssues(raw: unknown): UniversalZodIssue[] {\n if (!Array.isArray(raw)) return []\n const out: UniversalZodIssue[] = []\n for (const entry of raw) {\n if (typeof entry !== 'object' || entry === null) continue\n const obj = entry as Record<string, unknown>\n if (!Array.isArray(obj.path)) continue\n if (typeof obj.message !== 'string') continue\n // Path entries must be string or number\n const path: (string | number)[] = []\n let pathValid = true\n for (const seg of obj.path) {\n if (typeof seg === 'string' || typeof seg === 'number') {\n path.push(seg)\n } else {\n pathValid = false\n break\n }\n }\n if (!pathValid) continue\n out.push({\n path,\n message: obj.message,\n code: typeof obj.code === 'string' ? obj.code : undefined,\n })\n }\n return out\n}\n\n/**\n * Type guard for `ActionError` (and its subclass `ActionInputError`). True\n * iff the value is an instance of `ActionError`. Use `isInputError` to\n * narrow specifically to validation failures.\n */\nexport function isActionError(value: unknown): value is ActionError {\n return value instanceof ActionError\n}\n\n/**\n * Type guard narrowing to `ActionInputError`. Requires `instanceof` check\n * (not duck-typing on `type`) to prevent attacker-controlled JSON from\n * being mistaken for a real error instance.\n */\nexport function isInputError(value: unknown): value is ActionInputError {\n return value instanceof ActionInputError\n}\n\n/**\n * `ActionResult<TData, TError>` — discriminated union returned by client-side\n * action invocation. Either `{data, error: undefined}` (success) or\n * `{data: undefined, error}` (failure). Mirrors Astro `SafeResult` shape.\n */\nexport type ActionResult<TData = unknown, TError extends ActionError = ActionError> =\n | { data: TData; error: undefined }\n | { data: undefined; error: TError }\n\n/**\n * `SerializedActionResult` — wire-shape emitted by `serializeActionResult`\n * in `server/http/serialize-action-result.ts` (T1.3). Discriminator `type`\n * distinguishes data (devalue-encoded), error (JSON), and empty (204) cases.\n */\nexport type SerializedActionResult =\n | {\n type: 'data'\n status: number\n contentType: 'application/json+devalue'\n body: string\n }\n | {\n type: 'error'\n status: number\n contentType: 'application/json'\n body: string\n }\n | { type: 'empty'; status: 204 }\n\n/**\n * `ActionManifestEntry` — per-action metadata emitted by `action-scan.ts`\n * (T1.4) into `.theo/actions-manifest.json`. Consumed by virtual module\n * `@theo/actions` (T3.1) and G4 devtools \"Actions\" tab (T5.1).\n */\nexport interface ActionManifestEntry {\n readonly name: string\n readonly filePath: string\n readonly urlPath: string\n readonly accept: 'form' | 'json'\n readonly hasInput: boolean\n}\n","import type { ServerResponse } from 'node:http'\n\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\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, complexity -- delegates to two\n surface overloads above; the branch density mirrors the back-compat\n contract, not internal complexity. */\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 =\n code === 'INTERNAL_ERROR' && process.env.NODE_ENV === 'production'\n ? 'Internal server error'\n : message\n\n if (code === 'INTERNAL_ERROR') {\n console.error(`[${requestId ?? 'no-id'}] ${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 * Per `docs/plans/t5a2-incoming-message-to-request-shape-refactor-plan.md`\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 =\n code === 'INTERNAL_ERROR' && process.env.NODE_ENV === 'production'\n ? 'Internal server error'\n : message\n\n if (code === 'INTERNAL_ERROR') {\n console.error(`[${requestId ?? 'no-id'}] ${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","/* eslint-disable security/detect-non-literal-fs-filename --\n * Middleware runner. Checks for `serverDir/middleware.ts` + `context.ts`,\n * cached by CR-017. Paths are derived from `serverDir` (cwd-derived). No\n * HTTP input.\n */\nimport { existsSync } from 'node:fs'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { join } from 'node:path'\n\nimport { scanMiddlewares } from '../scan/middleware-scan.js'\nimport type { LoadModule } from '../scan/module-loader.js'\n\nexport interface MiddlewareResult {\n ctx: unknown\n aborted: boolean\n}\n\n// CR-017 fix: in dev `existsSync` + `scanMiddlewares` ran on EVERY request,\n// turning a constant filesystem read into per-request overhead. We cache\n// the scan result by serverDir. In prod the same scan should be done once\n// at boot — `theo build` already emits a manifest, but the dev path uses\n// this runtime cache as a defense-in-depth. The cache is invalidated by\n// process restart (Vite HMR replaces the module, clearing this map).\ninterface MiddlewareCacheEntry {\n singleFilePath: string\n singleFileExists: boolean\n dirMiddlewares: string[]\n}\nconst middlewareCache = new Map<string, MiddlewareCacheEntry>()\n\nexport function _resetMiddlewareCacheForTests(): void {\n middlewareCache.clear()\n}\n\n// Middleware default-export contract: a function (req, res, next).\ntype MiddlewareFn = (\n req: IncomingMessage,\n res: ServerResponse,\n next: () => void,\n) => void | Promise<void>\ntype ContextFactory = (args: { request: IncomingMessage; response: ServerResponse }) => unknown // Promise<unknown> is structurally `unknown`; one arm covers both.\n\nfunction getCachedScan(serverDir: string): MiddlewareCacheEntry {\n let cached = middlewareCache.get(serverDir)\n if (!cached) {\n const singleFilePath = join(serverDir, 'middleware.ts')\n cached = {\n singleFilePath,\n singleFileExists: existsSync(singleFilePath),\n dirMiddlewares: scanMiddlewares(serverDir),\n }\n middlewareCache.set(serverDir, cached)\n }\n return cached\n}\n\nasync function runOneMiddleware(\n mw: MiddlewareFn,\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<{ nextCalled: boolean }> {\n // Object-held flag avoids TS narrowing `let nextCalled = false` to the\n // literal `false`, which would make `!nextCalled` an \"always-truthy\"\n // condition under control-flow analysis.\n const state = { nextCalled: false }\n await mw(req, res, () => {\n state.nextCalled = true\n })\n return state\n}\n\nexport async function runMiddlewareAndContext(\n req: IncomingMessage,\n res: ServerResponse,\n loadModule: LoadModule,\n serverDir: string,\n): Promise<MiddlewareResult> {\n const { singleFilePath, singleFileExists, dirMiddlewares } = getCachedScan(serverDir)\n const dirExists = dirMiddlewares.length > 0\n\n // 1. Ambiguity check — both file and directory is a configuration error\n if (singleFileExists && dirExists) {\n throw new Error(\n 'Ambiguous middleware configuration: found both server/middleware.ts and server/middleware/ directory. ' +\n 'Use one or the other, not both.',\n )\n }\n\n // 2. Run middleware chain from directory\n if (dirExists) {\n for (const mwPath of dirMiddlewares) {\n const mod = await loadModule(mwPath)\n const mw = mod.default as MiddlewareFn | undefined\n if (typeof mw !== 'function') continue\n\n const { nextCalled } = await runOneMiddleware(mw, req, res)\n if (!nextCalled || res.writableEnded) {\n return { ctx: {}, aborted: true }\n }\n }\n }\n\n // 3. Run single middleware file (backward compat)\n if (singleFileExists) {\n const mod = await loadModule(singleFilePath)\n const mw = mod.default as MiddlewareFn | undefined\n if (typeof mw === 'function') {\n const { nextCalled } = await runOneMiddleware(mw, req, res)\n if (!nextCalled || res.writableEnded) {\n return { ctx: {}, aborted: true }\n }\n }\n }\n\n // 4. Create context (if exists)\n let ctx: unknown = {}\n const contextPath = join(serverDir, 'context.ts')\n if (existsSync(contextPath)) {\n const mod = await loadModule(contextPath)\n const createContext = mod.createContext as ContextFactory | undefined\n if (typeof createContext === 'function') {\n ctx = await createContext({ request: req, response: res })\n }\n }\n\n return { ctx, aborted: false }\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\nexport interface 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","import type { IncomingMessage, ServerResponse } from 'node:http'\n\nimport { parseRequestBody } from '../body-parser.js'\n\nimport { sendError } from './send-response.js'\n\n/**\n * T5.1 (partial Pipeline extraction) — request-parsing + Zod-validation\n * stages extracted from the executeRoute monolith.\n *\n * Each stage returns `{ ok: true, ...data }` on success OR `{ ok: false }`\n * on short-circuit (in which case the stage already sent an error response).\n *\n * Per EC-5 (plugin hook ordering) and EC-15 (AsyncLocalStorage), these\n * stages do NOT invoke plugin hooks — the orchestrator (`executeRoute`)\n * keeps that responsibility in one place.\n */\n\nexport type StageResult<T> = { ok: true; data: T } | { ok: false }\n\n/**\n * Parse URL query + request body (multipart/form-data or JSON).\n * On parse failure, sends 400 or 415 (Unsupported Content-Type) + returns\n * `{ ok: false }`. Otherwise returns `{ query, body }`.\n */\nexport async function parseQueryAndBody(\n req: IncomingMessage,\n res: ServerResponse,\n requestId: string | undefined,\n): Promise<StageResult<{ query: Record<string, string>; body: unknown }>> {\n // Query\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`)\n const query: Record<string, string> = Object.fromEntries(url.searchParams)\n\n // Body (JSON + multipart/form-data)\n let body: unknown\n try {\n const parsed = await parseRequestBody(req)\n if (parsed.json !== undefined) {\n body = parsed.json\n } else if (parsed.files.length > 0 || Object.keys(parsed.fields).length > 0) {\n body = { ...parsed.fields, _files: parsed.files }\n } else {\n body = undefined\n }\n } catch (err) {\n const message = (err as Error).message\n const status = message.includes('Unsupported Content-Type') ? 415 : 400\n sendError(res, 'VALIDATION_ERROR', message, status, undefined, requestId)\n return { ok: false }\n }\n\n return { ok: true, data: { query, body } }\n}\n\n/**\n * Validate query, body, params against the route's Zod schemas (when present).\n * On validation failure, sends 400 with Zod issues + returns `{ ok: false }`.\n * On success, returns the (possibly-transformed) values from `schema.safeParse(...).data`.\n */\ninterface ZodLike {\n safeParse: (value: unknown) => {\n success: boolean\n data?: unknown\n error?: { issues: unknown[] }\n }\n}\nconst isZodLike = (value: unknown): value is ZodLike =>\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { safeParse?: unknown }).safeParse === 'function'\n\nexport function runZodValidation(\n routeConfig: Record<string, unknown>,\n res: ServerResponse,\n requestId: string | undefined,\n input: {\n query: Record<string, string>\n body: unknown\n params: Record<string, string>\n },\n): StageResult<{ query: Record<string, string>; body: unknown; params: Record<string, string> }> {\n const { query, params } = input\n let { body } = input\n\n if (isZodLike(routeConfig.query)) {\n const result = routeConfig.query.safeParse(query)\n if (!result.success) {\n sendError(\n res,\n 'VALIDATION_ERROR',\n 'Invalid query parameters',\n 400,\n result.error?.issues,\n requestId,\n )\n return { ok: false }\n }\n Object.assign(query, result.data)\n }\n\n if (isZodLike(routeConfig.body)) {\n const result = routeConfig.body.safeParse(body)\n if (!result.success) {\n sendError(\n res,\n 'VALIDATION_ERROR',\n 'Invalid request body',\n 400,\n result.error?.issues,\n requestId,\n )\n return { ok: false }\n }\n body = result.data\n }\n\n if (isZodLike(routeConfig.params)) {\n const result = routeConfig.params.safeParse(params)\n if (!result.success) {\n sendError(\n res,\n 'VALIDATION_ERROR',\n 'Invalid route parameters',\n 400,\n result.error?.issues,\n requestId,\n )\n return { ok: false }\n }\n Object.assign(params, result.data)\n }\n\n return { ok: true, data: { query, body, params } }\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http'\n\nimport { AuthRequiredError } from '../auth/auth.js'\nimport { DuplicateContextKeyError } from '../jobs/duplicate-context-key-error.js'\nimport { createOutbox } from '../jobs/outbox.js'\nimport { createOutboxDispatcher, createQueueClient } from '../jobs/queue-client.js'\nimport { warnOnce } from '../observability/logger.js'\nimport type { PluginContext } from '../plugin-types.js'\nimport type { PluginRunner } from '../plugins/plugin-runner.js'\nimport { dispatchCsrfWarn } from '../security/csrf-warn-dispatch.js'\nimport { enforceCsrf } from '../security/csrf.js'\n\nimport type { ExecuteRouteContext } from './execute-context.js'\nimport { parseQueryAndBody, runZodValidation } from './execute-stages.js'\nimport { runMiddlewareAndContext } from './middleware-runner.js'\nimport { sendError, sendJson } from './send-response.js'\n\n// CSRF policy applies to every state-mutating method, including DELETE.\nconst CSRF_PROTECTED_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\n// T5.1: sendJson + sendError + SendErrorOptions moved to send-response.ts\n// to break the execute ↔ execute-stages cycle. Re-exported below for\n// backward compat.\nexport { sendJson, sendError } from './send-response.js'\nexport type { SendErrorOptions, SendErrorInput } from './send-response.js'\n// T3.1 — ExecuteRouteContext (ADR-0016)\nexport type { ExecuteRouteContext } from './execute-context.js'\n\ninterface StreamPipeCtx {\n buildPluginCtx: (ctxObj: Record<string, unknown>) => PluginContext\n ctx: Record<string, unknown>\n method: string\n pluginRunner: PluginRunner | undefined\n requestId: string | undefined\n routePath: string\n}\n\n/**\n * Pipe a Web Standard ReadableStream into a Node ServerResponse. Stream\n * errors after headers are sent cannot change the response status, but\n * MUST be logged + reported (CR-004 fix). Extracted from `executeRoute`\n * to keep that function's nesting under the max-depth ceiling.\n */\nasync function pipeWebStreamToResponse(\n body: ReadableStream<Uint8Array>,\n res: ServerResponse,\n ctx: StreamPipeCtx,\n): Promise<void> {\n const reader = body.getReader()\n try {\n let done = false\n while (!done) {\n const chunk = await reader.read()\n done = chunk.done\n if (!done) res.write(chunk.value)\n }\n } catch (streamErr) {\n warnOnce(`stream-error:${ctx.routePath}:${ctx.method}`, {\n event: 'stream.error',\n requestId: ctx.requestId ?? 'no-id',\n route: ctx.routePath,\n method: ctx.method,\n message: streamErr instanceof Error ? streamErr.message : String(streamErr),\n })\n if (ctx.pluginRunner) {\n try {\n await ctx.pluginRunner.runOnError(ctx.buildPluginCtx(ctx.ctx), streamErr)\n } catch {\n // onError plugins must never destabilize the response close.\n }\n }\n } finally {\n try {\n reader.releaseLock()\n } catch {\n /* lock may already be released by abort */\n }\n }\n}\n\n// sendError moved to send-response.ts (T5.1) — re-exported above.\n\n// CR-007/Knip cleanup: `parseBody` (legacy JSON-only parser) was exported\n// but had no remaining consumers — the route pipeline uses\n// `parseRequestBody` (multipart + JSON) directly. Removed to shrink the\n// public surface and remove the duplicated METHODS_WITH_BODY constant.\n\n// eslint-disable-next-line max-lines-per-function, sonarjs/cognitive-complexity, complexity -- `executeRoute` is the framework's central request pipeline; its body length + branch density mirror the actual request lifecycle. T3.1 / ADR-0016 retired `max-params` (context object replaces 12 positional args). Branch complexity remains intentional: the request lifecycle has irreducible state machine arms (CSRF stage → Zod validate → middleware → handler → stream/JSON response). Remaining helpers `runCsrfStage`, `parseQueryAndBody`, `runHandlerStage`, etc. were extracted in earlier waves.\nexport async function executeRoute(ctx: ExecuteRouteContext): Promise<void> {\n // T3.1 — destructure the context with defaults applied\n const {\n route,\n method,\n params,\n req,\n res,\n loadModule,\n serverDir,\n requestId,\n pluginRunner,\n transformer,\n csrfMode = 'strict',\n disallowed,\n jobBackend,\n } = ctx\n const buildPluginCtx = (ctxObj: Record<string, unknown>): PluginContext => ({\n request: req,\n response: res,\n ctx: ctxObj,\n requestId: requestId ?? 'no-id',\n })\n\n // T1.2 — emit x-theo-transformer header when a non-default transformer is in use.\n // 'json' is treated as default (no header); any named transformer emits.\n if (transformer && transformer.name !== 'json') {\n res.setHeader('x-theo-transformer', transformer.name)\n }\n\n try {\n // T4.2 — onRequest hook (runs before middleware)\n let ctx: Record<string, unknown> = {}\n if (pluginRunner) {\n pluginRunner.applyDecorations(ctx)\n const onReqResult = await pluginRunner.runOnRequest(buildPluginCtx(ctx))\n if (onReqResult.shortCircuited) return\n }\n\n // Run middleware + context pipeline\n if (serverDir) {\n const result = await runMiddlewareAndContext(req, res, loadModule, serverDir)\n if (result.aborted) return\n ctx = (result.ctx ?? {}) as Record<string, unknown>\n // Re-apply decorations on top of middleware-produced ctx so plugin\n // decorations win when middleware did not set the same key.\n if (pluginRunner) pluginRunner.applyDecorations(ctx)\n }\n\n // T2.1 — wire ctx.queue + outbox lifecycle when jobBackend is configured.\n // EC-202: throw on collision instead of silent override.\n if (jobBackend) {\n if (ctx.queue !== undefined) {\n throw new DuplicateContextKeyError('queue', {\n reason:\n 'A plugin or middleware already decorated ctx.queue; choose a different key OR remove jobs.backend from theo.config.ts.',\n })\n }\n const outbox = createOutbox()\n const queueClient = createQueueClient(jobBackend, outbox)\n ctx.queue = queueClient\n\n // Discard on abort or 4xx (handler throws cascade to 500 via catch\n // below, where statusCode is already >= 400).\n res.on('close', () => {\n if (!res.writableFinished) outbox.discard()\n })\n // Flush on commit, only when response indicates success.\n res.on('finish', () => {\n if (res.statusCode >= 400) {\n outbox.discard()\n return\n }\n void outbox.flush(createOutboxDispatcher(jobBackend))\n })\n }\n\n const mod = await loadModule(route.filePath)\n const routeConfig = mod[method]\n\n if (!routeConfig) {\n sendError(\n res,\n 'METHOD_NOT_ALLOWED',\n `Method ${method} not allowed`,\n 405,\n undefined,\n requestId,\n )\n return\n }\n\n const handler =\n typeof routeConfig === 'function'\n ? routeConfig\n : (routeConfig as Record<string, unknown>).handler\n if (typeof handler !== 'function') {\n sendError(res, 'INTERNAL_ERROR', 'Route handler is not a function', 500, undefined, requestId)\n return\n }\n\n // Phase 5 — CSRF enforcement (warn-first default; strict in 0.3.0).\n // Skips: safe methods (GET/HEAD/OPTIONS), per-route opt-out (`csrf: false`),\n // and bare function exports (legacy style — no opt-out hook available).\n const routeOptOut =\n typeof routeConfig === 'object' && (routeConfig as { csrf?: unknown }).csrf === false\n if (CSRF_PROTECTED_METHODS.has(method) && !routeOptOut) {\n const decision = enforceCsrf(\n req,\n csrfMode,\n {\n // T3.3 DRY — see security/csrf-warn-dispatch.ts\n warn: dispatchCsrfWarn,\n path: req.url,\n },\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\n }\n }\n\n // T5.1 — extracted stages (parseQueryAndBody + runZodValidation).\n // Each stage either succeeds (returns parsed data) OR short-circuits\n // (sends the error response inline + returns ok:false).\n const rc = routeConfig as Record<string, unknown>\n\n const parseResult = await parseQueryAndBody(req, res, requestId)\n if (!parseResult.ok) return\n const { query } = parseResult.data\n let { body } = parseResult.data\n\n const validationResult = runZodValidation(rc, res, requestId, { query, body, params })\n if (!validationResult.ok) return\n body = validationResult.data.body\n\n // T4.3 — preHandler hook (after Zod validation, before handler)\n if (pluginRunner) {\n const preResult = await pluginRunner.runPreHandler(buildPluginCtx(ctx))\n if (preResult.shortCircuited) return\n }\n\n // Execute handler. `handler` is structurally typed as `unknown` at\n // this point (came out of a duck-typed module). Cast through a narrow\n // type so the call is properly typed.\n type RouteHandlerCallable = (args: {\n query: Record<string, string>\n body: unknown\n params: Record<string, string>\n request: IncomingMessage\n ctx: Record<string, unknown>\n }) => unknown\n const callableHandler = handler as RouteHandlerCallable\n const handlerResult = await callableHandler({ query, body, params, request: req, ctx })\n\n // Handle result\n if (handlerResult === undefined || handlerResult === null) {\n sendJson(res, null, (rc.status as number | undefined) ?? 204, transformer)\n if (pluginRunner) await pluginRunner.runOnResponse(buildPluginCtx(ctx))\n return\n }\n\n if (handlerResult instanceof Response) {\n // `Object.fromEntries(Headers)` collapses multi-valued headers like\n // `Set-Cookie` to a single string. Set Set-Cookie via setHeader array\n // overload BEFORE writeHead (writeHead flushes headers; later setHeader\n // is a no-op or throws). Then writeHead with the remaining singletons.\n const headersBag: Record<string, string> = {}\n for (const [k, v] of handlerResult.headers) {\n if (k.toLowerCase() !== 'set-cookie') headersBag[k] = v\n }\n const setCookies = handlerResult.headers.getSetCookie()\n if (setCookies.length > 0) {\n res.setHeader('Set-Cookie', setCookies)\n }\n res.writeHead(handlerResult.status, headersBag)\n\n if (handlerResult.body) {\n await pipeWebStreamToResponse(handlerResult.body, res, {\n buildPluginCtx,\n ctx,\n method,\n pluginRunner,\n requestId,\n routePath: route.routePath,\n })\n }\n\n res.end()\n if (pluginRunner) await pluginRunner.runOnResponse(buildPluginCtx(ctx))\n return\n }\n\n sendJson(res, handlerResult, (rc.status as number | undefined) ?? 200, transformer)\n if (pluginRunner) await pluginRunner.runOnResponse(buildPluginCtx(ctx))\n } catch (err) {\n // T4.4 — onError hook (runs before default error response)\n if (pluginRunner) {\n // Best-effort: capture a ctx snapshot for plugins. Decorations were\n // applied to local `ctx` above, but if the error came before that, we\n // pass an empty ctx — the request/response are what matters here.\n const errCtxObj: Record<string, unknown> = {}\n pluginRunner.applyDecorations(errCtxObj)\n await pluginRunner.runOnError(buildPluginCtx(errCtxObj), err)\n // EC-9: response may already have been ended by an onError hook\n if (res.writableEnded) {\n // Still run onResponse but mark inErrorPath to prevent recursion\n await pluginRunner.runOnResponse(buildPluginCtx(errCtxObj), { inErrorPath: true })\n return\n }\n }\n // Duck-type the auth error: under Vite dev / vitest the module-loader\n // can produce a duplicate `AuthRequiredError` class identity, so\n // `instanceof` returns false even though the thrown value carries the\n // expected `code` + `status` fields. We fall back to a shape check.\n const isAuthError =\n err instanceof AuthRequiredError ||\n (err !== null &&\n typeof err === 'object' &&\n (err as { code?: unknown }).code === 'AUTH_REQUIRED' &&\n (err as { status?: unknown }).status === 401)\n\n if (isAuthError) {\n const authErr = err as { code: string; message: string; status: number }\n sendError(res, authErr.code, authErr.message, authErr.status, undefined, requestId)\n if (pluginRunner) {\n const errCtxObj: Record<string, unknown> = {}\n pluginRunner.applyDecorations(errCtxObj)\n await pluginRunner.runOnResponse(buildPluginCtx(errCtxObj), { inErrorPath: true })\n }\n return\n }\n sendError(\n res,\n 'INTERNAL_ERROR',\n err instanceof Error && err.message ? err.message : 'Internal server error',\n 500,\n undefined,\n requestId,\n )\n if (pluginRunner) {\n const errCtxObj: Record<string, unknown> = {}\n pluginRunner.applyDecorations(errCtxObj)\n await pluginRunner.runOnResponse(buildPluginCtx(errCtxObj), { inErrorPath: true })\n }\n }\n}\n","/**\n * FormData → ZodObject coercion driven by the declared schema.\n *\n * Per plan g3-server-actions-and-useaction v1.2 § Phase 1 / T1.3 + Astro\n * runtime/server.ts:323-397 pattern (adapted to zod v3 shape access via\n * `_def.shape()`). FormData entries arrive as strings (or File for binary);\n * we walk the schema field-by-field and coerce to the declared zod type\n * before letting `safeParse` finalize validation.\n *\n * Supports: ZodString, ZodNumber, ZodBoolean, ZodArray (via getAll), nested\n * ZodObject (via dot-notation prefix), ZodOptional / ZodNullable / ZodDefault\n * wrappers. Skips: ZodDiscriminatedUnion + ZodPipe + ZodIntersection (deferred\n * to consumer's safeParse fallback — values returned as-is, zod will validate\n * or coerce).\n */\nimport { z } from 'zod'\n\n/**\n * Walk the declared ZodObject schema and pull values from `formData`,\n * coercing per-field. Returns a plain object suitable for `schema.safeParse`.\n *\n * `prefix` is used for recursive nested-object resolution: a nested field at\n * `user.address.zip` is read from formData key `user.address.zip` (top-level\n * call uses empty prefix).\n */\nexport function formDataToObject(\n formData: FormData,\n schema: z.ZodObject<z.ZodRawShape>,\n prefix = '',\n): Record<string, unknown> {\n const shape = schema._def.shape()\n const out: Record<string, unknown> = {}\n\n for (const [key, rawValidator] of Object.entries(shape)) {\n const fullKey = prefix + key\n const validator = unwrapWrappers(rawValidator)\n\n if (validator instanceof z.ZodObject) {\n // Recurse for nested object types\n const nestedPrefix = `${fullKey}.`\n const hasNestedKeys = [...formData.keys()].some((k) => k.startsWith(nestedPrefix))\n if (hasNestedKeys) {\n out[key] = formDataToObject(formData, validator as z.ZodObject<z.ZodRawShape>, nestedPrefix)\n continue\n }\n // No nested keys present — apply default / nullable / undefined semantics\n out[key] = unwrapMissingDefault(rawValidator)\n continue\n }\n\n if (validator instanceof z.ZodArray) {\n const values = formData.getAll(fullKey)\n out[key] = coerceArrayElements(values, validator as z.ZodArray<z.ZodTypeAny>)\n continue\n }\n\n if (validator instanceof z.ZodBoolean) {\n out[key] = coerceBoolean(formData, fullKey)\n continue\n }\n\n // Scalar (string / number / etc.)\n if (formData.has(fullKey)) {\n const raw = formData.get(fullKey)\n out[key] = coerceScalar(raw, validator)\n } else {\n out[key] = unwrapMissingDefault(rawValidator)\n }\n }\n\n return out\n}\n\n/** Strip ZodOptional / ZodNullable / ZodDefault to reach the inner validator. */\nfunction unwrapWrappers(validator: z.ZodTypeAny): z.ZodTypeAny {\n let inner: z.ZodTypeAny = validator\n while (\n inner instanceof z.ZodOptional ||\n inner instanceof z.ZodNullable ||\n inner instanceof z.ZodDefault\n ) {\n inner = (inner._def as { innerType: z.ZodTypeAny }).innerType\n }\n return inner\n}\n\n/**\n * What to return when a field is missing from FormData:\n * - ZodDefault → the default value (evaluated if function)\n * - ZodNullable → null\n * - ZodOptional → undefined\n * - else → undefined (consumer safeParse will likely error)\n */\nfunction unwrapMissingDefault(validator: z.ZodTypeAny): unknown {\n let cursor: z.ZodTypeAny = validator\n while (\n cursor instanceof z.ZodOptional ||\n cursor instanceof z.ZodNullable ||\n cursor instanceof z.ZodDefault\n ) {\n if (cursor instanceof z.ZodDefault) {\n const def = (cursor._def as { defaultValue: unknown }).defaultValue\n return typeof def === 'function' ? (def as () => unknown)() : def\n }\n if (cursor instanceof z.ZodNullable) return null\n cursor = (cursor._def as { innerType: z.ZodTypeAny }).innerType\n }\n return undefined\n}\n\nfunction coerceScalar(raw: FormDataEntryValue | null, validator: z.ZodTypeAny): unknown {\n if (raw === null) return undefined\n if (validator instanceof z.ZodNumber) {\n return typeof raw === 'string' ? Number(raw) : raw\n }\n // String, File, default — pass through (zod will validate File via .instanceof if used)\n return raw\n}\n\nfunction coerceArrayElements(\n values: FormDataEntryValue[],\n arrayValidator: z.ZodArray<z.ZodTypeAny>,\n): unknown[] {\n const elementType = unwrapWrappers(arrayValidator._def.type)\n if (elementType instanceof z.ZodNumber) {\n return values.map((v) => (typeof v === 'string' ? Number(v) : v))\n }\n if (elementType instanceof z.ZodBoolean) {\n return values.map((v) => {\n if (v === 'true') return true\n if (v === 'false') return false\n return Boolean(v)\n })\n }\n // String / File / unknown — pass through\n return values\n}\n\nfunction coerceBoolean(formData: FormData, key: string): boolean | undefined {\n if (!formData.has(key)) return undefined\n const val = formData.get(key)\n if (val === 'true') return true\n if (val === 'false') return false\n // Presence with truthy non-boolean string (e.g., HTML checkbox \"on\") → true\n return Boolean(val)\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http'\n\nimport { AuthRequiredError } from '../auth/auth.js'\nimport type { PluginContext } from '../plugin-types.js'\nimport type { PluginRunner } from '../plugins/plugin-runner.js'\n\nimport { sendError } from './send-response.js'\n\n/**\n * Canonical error handler for HTTP request pipelines (T3.4 of\n * architecture-review-remediation-plan, PV-9 DRY).\n *\n * Replaces the duplicated catch-block logic in `executeRoute` (1 site) and\n * `executeAction` (1 site, via `handleActionError`). Both code paths now\n * share this implementation.\n *\n * Behavior contract (preserved from previous inline catches):\n * - Plugin `onError` fires first (swallowed if it throws — never amplify failure).\n * - `AuthRequiredError` is detected via `instanceof` AND a duck-type shape\n * check (`code === 'AUTH_REQUIRED' && status === 401`) — required because\n * under Vite dev / vitest the module-loader can produce a duplicate\n * AuthRequiredError class identity, breaking `instanceof`.\n * - `onResponse({ inErrorPath: true })` always fires at the end (swallowed\n * if it throws — EC-9).\n */\nexport interface HandleRequestErrorCtx {\n req: IncomingMessage\n res: ServerResponse\n requestId: string | undefined\n pluginRunner: PluginRunner | undefined\n buildPluginCtx: (ctxObj: Record<string, unknown>) => PluginContext\n}\n\nexport async function handleRequestError(err: unknown, c: HandleRequestErrorCtx): Promise<void> {\n // 1. onError hook — swallowed on failure (EC-9 — never amplify)\n if (c.pluginRunner) {\n const errCtxObj: Record<string, unknown> = {}\n c.pluginRunner.applyDecorations(errCtxObj)\n try {\n await c.pluginRunner.runOnError(c.buildPluginCtx(errCtxObj), err)\n } catch {\n // onError handlers must never destabilize the response.\n }\n // EC-9: response may already have been ended by an onError hook\n if (c.res.writableEnded) {\n try {\n await c.pluginRunner.runOnResponse(c.buildPluginCtx(errCtxObj), {\n inErrorPath: true,\n })\n } catch {\n // Containment (same as above).\n }\n return\n }\n }\n\n // 2. Auth error detection (instanceof + duck-type fallback)\n const isAuthError =\n err instanceof AuthRequiredError ||\n (err !== null &&\n typeof err === 'object' &&\n (err as { code?: unknown }).code === 'AUTH_REQUIRED' &&\n (err as { status?: unknown }).status === 401)\n\n if (isAuthError) {\n const authErr = err as { code: string; message: string; status: number }\n sendError(c.res, authErr.code, authErr.message, authErr.status, undefined, c.requestId)\n } else {\n sendError(\n c.res,\n 'INTERNAL_ERROR',\n err instanceof Error ? err.message : 'Internal server error',\n 500,\n undefined,\n c.requestId,\n )\n }\n\n // 3. onResponse(inErrorPath) — swallowed on failure (EC-9)\n if (c.pluginRunner) {\n const errCtxObj: Record<string, unknown> = {}\n c.pluginRunner.applyDecorations(errCtxObj)\n try {\n await c.pluginRunner.runOnResponse(c.buildPluginCtx(errCtxObj), {\n inErrorPath: true,\n })\n } catch {\n // Containment.\n }\n }\n}\n\n/**\n * T5a.2 Phase G slice 3/N — Web-Standards request error handler.\n *\n * Mirror of `handleRequestError(err, c: HandleRequestErrorCtx)` for the\n * Web `Request` shape. Returns a native `Response` directly instead of\n * mutating `res`. Same behavioral contract:\n *\n * 1. Auth-error detection via `instanceof AuthRequiredError` PLUS a\n * duck-type fallback (`code === 'AUTH_REQUIRED' && status === 401`)\n * — needed because Vite dev / vitest can produce duplicate class\n * identities, breaking `instanceof`.\n * 2. Envelope-shaped JSON body using `serverErrorToEnvelope` (G5 D3\n * boundary translation).\n * 3. Status code derived from envelope.code via `envelopeCodeToStatus`\n * mirror (already implemented inline in web-handler.ts; this slice\n * uses the same mapping table).\n *\n * **Difference vs IncomingMessage path:** the Web path's plugin runner\n * orchestration lives in `executeWebRequest`'s `runWithHooks` /\n * `runErrorHooks` (Phase G slice 1/N) — `handleWebRequestError` is the\n * leaf that builds the error Response WITHOUT touching plugin hooks.\n * Callers compose: `executeWebRequest` (or future Web execute pipeline)\n * catches a throw, calls `handleWebRequestError(err, { requestId })` to\n * build the Response, then routes through onError hooks separately.\n */\nexport interface HandleWebRequestErrorCtx {\n requestId?: string\n}\n\nexport async function handleWebRequestError(\n err: unknown,\n ctx: HandleWebRequestErrorCtx = {},\n): Promise<Response> {\n // Lazy import to avoid pulling the full envelope translator into the\n // bundle for consumers who don't hit the error path. Same dynamic-\n // import pattern as web-handler.ts's parseBodyFull.\n const { serverErrorToEnvelope } = await import('../../core/contracts/server-error-to-envelope.js')\n\n // 1. Auth-error detection (instanceof + duck-type fallback — same as\n // IncomingMessage path for cross-module class-identity safety).\n const isAuthError =\n err instanceof AuthRequiredError ||\n (err !== null &&\n typeof err === 'object' &&\n (err as { code?: unknown }).code === 'AUTH_REQUIRED' &&\n (err as { status?: unknown }).status === 401)\n\n if (isAuthError) {\n const authErr = err as { code?: string; message?: string; status?: number }\n return new Response(\n JSON.stringify({\n code: authErr.code ?? 'AUTH_REQUIRED',\n message: authErr.message ?? 'Authentication required',\n }),\n {\n status: authErr.status ?? 401,\n headers: {\n 'content-type': 'application/json',\n ...(ctx.requestId !== undefined ? { 'x-request-id': ctx.requestId } : {}),\n },\n },\n )\n }\n\n // 2. Envelope translation for everything else (G5 D3 boundary).\n const envelope = serverErrorToEnvelope(err)\n return new Response(JSON.stringify(envelope), {\n status: envelopeCodeToHttpStatus(envelope.code),\n headers: {\n 'content-type': 'application/json',\n ...(ctx.requestId !== undefined ? { 'x-request-id': ctx.requestId } : {}),\n },\n })\n}\n\n/**\n * T5a.2 Phase G slice 3/N — internal HTTP-status mapper for envelope codes.\n * Mirror of the inline `envelopeCodeToStatus` table in web-handler.ts;\n * duplicated to avoid a circular dep between handle-request-error and\n * web-handler. The two tables MUST stay in sync — Phase G slice 4/N may\n * consolidate them into a shared core/contracts module.\n */\nfunction envelopeCodeToHttpStatus(code: string): number {\n switch (code) {\n case 'BAD_REQUEST':\n return 400\n case 'UNAUTHORIZED':\n return 401\n case 'FORBIDDEN':\n return 403\n case 'NOT_FOUND':\n return 404\n case 'METHOD_NOT_ALLOWED':\n return 405\n case 'PAYLOAD_TOO_LARGE':\n return 413\n case 'UNPROCESSABLE_ENTITY':\n return 422\n case 'TOO_MANY_REQUESTS':\n case 'RATE_LIMITED':\n return 429\n case 'BAD_GATEWAY':\n return 502\n case 'SERVICE_UNAVAILABLE':\n return 503\n case 'GATEWAY_TIMEOUT':\n return 504\n case 'INTERNAL_SERVER_ERROR':\n default:\n return 500\n }\n}\n","/**\n * Server-side action result serializer.\n *\n * Per plan g3-server-actions-and-useaction v1.2 § Phase 1 / T1.3 + ADR D1.\n * Wraps `devalue.stringify` for success payloads (preserves Date/Set/URL/bigint\n * roundtrip) and JSON-encodes error envelopes for `ActionError` /\n * `ActionInputError`. Returns a `SerializedActionResult` discriminated union\n * consumed by the HTTP send layer (`action-execute.ts`).\n *\n * EC absorbed:\n * - EC-3 (response side): `responseBodySizeLimit` default 5 MB; throws\n * `ActionError({code:'PAYLOAD_TOO_LARGE'})` when the serialized body\n * exceeds the limit. Prevents handler-returns-100MB DoS.\n */\nimport { stringify as devalueStringify } from 'devalue'\n\nimport {\n ActionError,\n ActionInputError,\n type ActionResult,\n type SerializedActionResult,\n} from '../../core/contracts/action-protocol.js'\n\nconst DEFAULT_RESPONSE_BODY_SIZE_LIMIT = 5 * 1024 * 1024 // 5 MB\n\nexport interface SerializeOptions {\n /**\n * Maximum allowed length of the serialized response body in BYTES (UTF-8\n * length of `body` string is checked, not the JS string length). Default\n * 5 MB. Configurable via `defineConfig({security:{responseBodySizeLimit}})`.\n */\n responseBodySizeLimit?: number\n}\n\n/**\n * Serialize an action result for the wire.\n *\n * - Success with data: `application/json+devalue` body via devalue.stringify\n * with a `URL` reviver for round-tripping URL instances.\n * - Success with `undefined` data: status 204 empty.\n * - Error: `application/json` JSON body with type discriminator\n * (`TheoActionError` or `TheoActionInputError`) + code + message\n * (+ fields/issues for input errors).\n *\n * Throws `ActionError({code:'PAYLOAD_TOO_LARGE'})` if the serialized body\n * exceeds the configured size limit (EC-3).\n *\n * Throws on `Response` instance as data (Astro guard — handler must return\n * plain JSON-serializable values, not Web Response objects).\n */\nexport function serializeActionResult(\n result: ActionResult,\n options: SerializeOptions = {},\n): SerializedActionResult {\n const limit = options.responseBodySizeLimit ?? DEFAULT_RESPONSE_BODY_SIZE_LIMIT\n\n if (result.error !== undefined) {\n const body =\n result.error instanceof ActionInputError\n ? JSON.stringify({\n type: result.error.type,\n code: result.error.code,\n message: result.error.message,\n issues: result.error.issues,\n fields: result.error.fields,\n })\n : JSON.stringify({\n type: result.error.type,\n code: result.error.code,\n message: result.error.message,\n })\n if (Buffer.byteLength(body, 'utf8') > limit) {\n throw new ActionError({\n code: 'PAYLOAD_TOO_LARGE',\n message: `Serialized error body exceeds limit (${limit} bytes)`,\n })\n }\n return {\n type: 'error',\n status: result.error.status,\n contentType: 'application/json',\n body,\n }\n }\n\n if (result.data === undefined) {\n return { type: 'empty', status: 204 }\n }\n\n if (result.data instanceof Response) {\n throw new ActionError({\n code: 'INTERNAL_SERVER_ERROR',\n message: 'Action handler cannot serialize Response objects — return plain data instead',\n })\n }\n\n let body: string\n try {\n body = devalueStringify(result.data, {\n URL: (value: unknown) => value instanceof URL && value.href,\n })\n } catch (e) {\n throw new ActionError({\n code: 'INTERNAL_SERVER_ERROR',\n message: `Action data not serializable: ${e instanceof Error ? e.message : String(e)}`,\n })\n }\n\n if (Buffer.byteLength(body, 'utf8') > limit) {\n throw new ActionError({\n code: 'PAYLOAD_TOO_LARGE',\n message: `Serialized response body exceeds limit (${limit} bytes)`,\n })\n }\n\n return {\n type: 'data',\n status: 200,\n contentType: 'application/json+devalue',\n body,\n }\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http'\n\nimport type { z } from 'zod'\n\nimport {\n ActionError,\n ActionInputError,\n type ActionErrorCode,\n} from '../../core/contracts/action-protocol.js'\nimport { parseRequestBody, type ParsedBody } from '../body-parser.js'\nimport type { PluginContext } from '../plugin-types.js'\nimport type { PluginRunner } from '../plugins/plugin-runner.js'\nimport type { LoadModule } from '../scan/module-loader.js'\nimport { dispatchCsrfWarn } from '../security/csrf-warn-dispatch.js'\nimport { enforceCsrf, type CsrfMode, type DisallowedConfig } from '../security/csrf.js'\n\nimport { sendError } from './execute.js'\nimport { formDataToObject } from './form-data-to-object.js'\nimport { handleRequestError } from './handle-request-error.js'\nimport { runMiddlewareAndContext } from './middleware-runner.js'\nimport { serializeActionResult } from './serialize-action-result.js'\n\n// Universal dev gate — same IIFE pattern as track-agent-run (EC-11 tree-shake).\n// Both checks are statically replaceable by bundlers in prod build, so the\n// devtools dispatcher import is eliminated from prod bundles entirely.\nconst __IS_DEV = (() => {\n try {\n return (import.meta as { env?: { DEV?: boolean } }).env?.DEV === true\n } catch {\n return process.env.NODE_ENV !== 'production'\n }\n})()\n\n// Minimal Zod-shaped contract — we only need `safeParse`, not the whole API.\ninterface ZodLike {\n safeParse: (value: unknown) => {\n success: boolean\n data?: unknown\n error?: { issues: z.ZodIssue[] }\n }\n}\n\n// Shape of a `defineAction` export. Anchored by structural typing — we do\n// not import the action factory's return type to avoid module cycles, but\n// we do reject inputs that fail the structural test.\ninterface ActionConfig {\n input: ZodLike\n handler: (params: { input: unknown; ctx: unknown }) => unknown\n csrf?: false\n /** T1.3 sub-C: wire-protocol accept mode (defaults to 'json' when omitted). */\n accept?: 'form' | 'json'\n}\n\nfunction isActionConfig(value: unknown): value is ActionConfig {\n if (typeof value !== 'object' || value === null) return false\n const candidate = value as Record<string, unknown>\n if (typeof candidate.handler !== 'function') return false\n const input = candidate.input as ZodLike | undefined\n return typeof input?.safeParse === 'function'\n}\n\nexport interface ExecuteActionOptions {\n filePath: string\n exportName: string\n req: IncomingMessage\n res: ServerResponse\n loadModule: LoadModule\n serverDir?: string\n requestId?: string\n pluginRunner?: PluginRunner\n csrfMode?: CsrfMode\n disallowed?: DisallowedConfig\n}\n\n// Backwards-compatible positional signature; the options-shape is the new\n// preferred entry point and what the framework uses internally.\n// eslint-disable-next-line max-params -- public API surface; existing callers pass positional args\nexport async function executeAction(\n filePath: string,\n exportName: string,\n req: IncomingMessage,\n res: ServerResponse,\n loadModule: LoadModule,\n serverDir?: string,\n requestId?: string,\n pluginRunner?: PluginRunner,\n csrfMode: CsrfMode = 'strict',\n disallowed?: DisallowedConfig,\n): Promise<void> {\n return executeActionWithOptions({\n filePath,\n exportName,\n req,\n res,\n loadModule,\n serverDir,\n requestId,\n pluginRunner,\n csrfMode,\n disallowed,\n })\n}\n\nasync function loadActionConfig(\n loadModule: LoadModule,\n filePath: string,\n exportName: string,\n res: ServerResponse,\n requestId: string | undefined,\n): Promise<ActionConfig | null> {\n const mod = await loadModule(filePath)\n const exportedValue = mod[exportName]\n if (!isActionConfig(exportedValue)) {\n sendError(res, 'NOT_FOUND', `Action \"${exportName}\" not found`, 404, undefined, requestId)\n return null\n }\n return exportedValue\n}\n\ninterface CsrfActionCtx {\n actionConfig: ActionConfig\n csrfMode: CsrfMode\n disallowed: DisallowedConfig | undefined\n requestId: string | undefined\n}\n\nfunction enforceCsrfForAction(\n req: IncomingMessage,\n res: ServerResponse,\n ctx: CsrfActionCtx,\n): boolean {\n if (ctx.actionConfig.csrf === false) return true\n const decision = enforceCsrf(\n req,\n ctx.csrfMode,\n {\n // T3.3 DRY — canonical dispatcher\n warn: dispatchCsrfWarn,\n path: req.url,\n },\n ctx.disallowed,\n )\n if (decision.allow) return true\n sendError(\n res,\n 'CSRF_INVALID',\n decision.reason ?? 'CSRF check failed',\n 403,\n undefined,\n ctx.requestId,\n )\n return false\n}\n\ninterface ActionPipeline {\n ctx: Record<string, unknown>\n buildPluginCtx: (ctxObj: Record<string, unknown>) => PluginContext\n pluginRunner: PluginRunner | undefined\n serverDir: string | undefined\n loadModule: LoadModule\n req: IncomingMessage\n res: ServerResponse\n}\n\nasync function runPreHandlerPipeline(p: ActionPipeline): Promise<boolean> {\n if (p.serverDir) {\n const result = await runMiddlewareAndContext(p.req, p.res, p.loadModule, p.serverDir)\n if (result.aborted) return false\n Object.assign(p.ctx, (result.ctx ?? {}) as Record<string, unknown>)\n p.pluginRunner?.applyDecorations(p.ctx)\n }\n if (p.pluginRunner) {\n const preResult = await p.pluginRunner.runPreHandler(p.buildPluginCtx(p.ctx))\n if (preResult.shortCircuited) return false\n }\n return true\n}\n\nasync function readActionBody(\n req: IncomingMessage,\n res: ServerResponse,\n requestId: string | undefined,\n actionConfig: ActionConfig,\n): Promise<{ ok: true; body: unknown } | { ok: false }> {\n try {\n const parsed = await parseRequestBody(req)\n const accept = actionConfig.accept ?? 'json'\n let body: unknown\n if (accept === 'form') {\n body = formDataToObject(\n synthesizeFormData(parsed),\n actionConfig.input as unknown as z.ZodObject<z.ZodRawShape>,\n )\n } else if (parsed.json !== undefined) {\n body = parsed.json\n } else {\n body = parsed.fields\n }\n return { ok: true, body }\n } catch (err) {\n sendError(res, 'VALIDATION_ERROR', (err as Error).message, 400, undefined, requestId)\n return { ok: false }\n }\n}\n\n/**\n * Build a FormData instance from the body-parser's `ParsedBody`. Fields become\n * string entries; files become Blob entries with the original filename. Used\n * only when `accept === 'form'` to feed `formDataToObject`.\n */\nfunction synthesizeFormData(parsed: ParsedBody): FormData {\n const fd = new FormData()\n for (const [name, value] of Object.entries(parsed.fields)) {\n fd.append(name, value)\n }\n for (const file of parsed.files) {\n const blob = new Blob([file.buffer as unknown as ArrayBuffer], {\n type: file.mimeType,\n })\n fd.append(file.fieldname, blob, file.filename)\n }\n return fd\n}\n\n/**\n * Write a serialized ActionResult to the response. Sets content-type, status,\n * and body. Returns void; caller must not write further after invoking.\n */\nfunction writeSerialized(\n res: ServerResponse,\n serialized: ReturnType<typeof serializeActionResult>,\n): void {\n if (serialized.type === 'empty') {\n res.statusCode = serialized.status\n res.end()\n return\n }\n res.statusCode = serialized.status\n res.setHeader('Content-Type', serialized.contentType)\n res.end(serialized.body)\n}\n\n/**\n * Dev-only telemetry: dispatch ACTION_CALL_ADD so the devtools Actions tab\n * (T5.1) can render this call. Tree-shaken in prod via __IS_DEV guard.\n * Mirrors trackAgentRun pattern. Never throws (swallow + log).\n */\nasync function emitActionCallTelemetry(\n name: string,\n startedAt: number,\n input: unknown,\n outcome: { status: 'success'; output: unknown } | { status: 'error'; error: ActionError },\n): Promise<void> {\n if (!__IS_DEV) return\n try {\n const mod = (await import('../../devtools/bridge/dispatcher.js')) as {\n dispatcher: {\n onActionCall: (r: {\n id: string\n timestamp: number\n name: string\n input: unknown\n output?: unknown\n error?: { code: string; message: string; fields?: Record<string, string[]> }\n durationMs: number\n status: 'success' | 'error'\n }) => void\n }\n }\n mod.dispatcher.onActionCall({\n // eslint-disable-next-line sonarjs/pseudo-random -- non-secret correlation id\n id: `act-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`,\n timestamp: startedAt,\n name,\n input,\n output: outcome.status === 'success' ? outcome.output : undefined,\n error:\n outcome.status === 'error'\n ? {\n code: outcome.error.code,\n message: outcome.error.message,\n fields: outcome.error instanceof ActionInputError ? outcome.error.fields : undefined,\n }\n : undefined,\n durationMs: Date.now() - startedAt,\n status: outcome.status,\n })\n } catch {\n // devtools dispatcher missing in some prod-like bundle — silently skip\n }\n}\n\nasync function executeActionWithOptions(opts: ExecuteActionOptions): Promise<void> {\n const {\n filePath,\n exportName,\n req,\n res,\n loadModule,\n serverDir,\n requestId,\n pluginRunner,\n csrfMode = 'strict',\n disallowed,\n } = opts\n\n const buildPluginCtx = (ctxObj: Record<string, unknown>): PluginContext => ({\n request: req,\n response: res,\n ctx: ctxObj,\n requestId: requestId ?? 'no-id',\n })\n\n let ctx: Record<string, unknown> = {}\n\n try {\n // 1. Only POST.\n if ((req.method ?? 'GET').toUpperCase() !== 'POST') {\n sendError(res, 'METHOD_NOT_ALLOWED', 'Actions only accept POST', 405, undefined, requestId)\n return\n }\n\n // 2. Plugin onRequest hook (parity with executeRoute).\n if (pluginRunner) {\n pluginRunner.applyDecorations(ctx)\n const onReqResult = await pluginRunner.runOnRequest(buildPluginCtx(ctx))\n if (onReqResult.shortCircuited) return\n }\n\n // 3. Load module + locate action export.\n const actionConfig = await loadActionConfig(loadModule, filePath, exportName, res, requestId)\n if (!actionConfig) return\n\n // 4. CSRF enforcement\n if (!enforceCsrfForAction(req, res, { actionConfig, csrfMode, disallowed, requestId })) {\n return\n }\n\n // 5+6. Middleware + context pipeline, then plugin preHandler.\n const pipeline: ActionPipeline = {\n ctx,\n buildPluginCtx,\n pluginRunner,\n serverDir,\n loadModule,\n req,\n res,\n }\n if (!(await runPreHandlerPipeline(pipeline))) return\n ctx = pipeline.ctx\n\n // 7. Parse body (supports JSON and multipart/form-data).\n const bodyOutcome = await readActionBody(req, res, requestId, actionConfig)\n if (!bodyOutcome.ok) return\n\n // T1.3 sub-C action name derived from exportName for telemetry.\n const actionName = exportName === 'default' ? deriveActionNameFromPath(filePath) : exportName\n const startedAt = Date.now()\n\n // 8. Validate input with Zod — failure → ActionInputError envelope (T0.1 + ADR D6).\n const result = actionConfig.input.safeParse(bodyOutcome.body)\n if (!result.success) {\n const inputErr = new ActionInputError(result.error?.issues ?? [])\n writeSerialized(res, serializeActionResult({ data: undefined, error: inputErr }))\n await emitActionCallTelemetry(actionName, startedAt, bodyOutcome.body, {\n status: 'error',\n error: inputErr,\n })\n return\n }\n\n await runActionHandler({\n actionConfig,\n actionName,\n startedAt,\n input: result.data,\n ctx,\n res,\n pluginRunner,\n buildPluginCtx,\n })\n } catch (err) {\n await handleActionError(err, { req, res, ctx, requestId, pluginRunner, buildPluginCtx })\n }\n}\n\n/** Derive a stable display name from filePath when handler exported as default. */\nfunction deriveActionNameFromPath(filePath: string): string {\n const base = filePath.split(/[\\\\/]/).pop() ?? 'unknown'\n return base.replace(/\\.[jt]sx?$/, '')\n}\n\n/**\n * Duck-typed guard for ActionError-shaped throws. Necessary because Vite SSR\n * may load multiple module copies of `core/contracts/action-protocol.ts` when\n * the fixture resolves `theokit/server` via a different path than the runtime\n * import — `err instanceof ActionError` then fails even though the user\n * legitimately threw `new ActionError(...)`.\n *\n * Server-only: only invoked on handler-thrown errors (trust boundary already\n * inside the action runtime). Not safe to use on client-parsed JSON — for\n * that path keep `isActionError` (the instanceof guard) from action-protocol.\n */\nfunction isActionErrorLike(err: unknown): err is ActionError {\n if (err === null || typeof err !== 'object') return false\n const obj = err as Record<string, unknown>\n return (\n (obj.type === 'TheoActionError' || obj.type === 'TheoActionInputError') &&\n typeof obj.code === 'string' &&\n typeof obj.status === 'number'\n )\n}\n\ninterface HandlerCtx {\n actionConfig: ActionConfig\n actionName: string\n startedAt: number\n input: unknown\n ctx: Record<string, unknown>\n res: ServerResponse\n pluginRunner: PluginRunner | undefined\n buildPluginCtx: (ctxObj: Record<string, unknown>) => PluginContext\n}\n\nasync function runActionHandler(args: HandlerCtx): Promise<void> {\n try {\n const handlerResult = await args.actionConfig.handler({ input: args.input, ctx: args.ctx })\n // T1.3 sub-C — wire devalue serialization (ADR D1). Undefined → 204; data → 200 + json+devalue.\n writeSerialized(args.res, serializeActionResult({ data: handlerResult, error: undefined }))\n if (args.pluginRunner) {\n await args.pluginRunner.runOnResponse(args.buildPluginCtx(args.ctx))\n }\n await emitActionCallTelemetry(args.actionName, args.startedAt, args.input, {\n status: 'success',\n output: handlerResult,\n })\n } catch (err) {\n // Handler-thrown ActionError → use the typed envelope; other throws bubble\n // up to executeActionWithOptions for handleActionError fallback chain.\n // Duck-type the `type` discriminator (not instanceof): Vite SSR can load\n // multiple module copies of action-protocol.ts when fixture and runtime\n // resolve different paths; `err instanceof ActionError` then fails even\n // when the handler legitimately threw via `new ActionError(...)`.\n if (isActionErrorLike(err)) {\n writeSerialized(args.res, serializeActionResult({ data: undefined, error: err }))\n await emitActionCallTelemetry(args.actionName, args.startedAt, args.input, {\n status: 'error',\n error: err,\n })\n return\n }\n // Wrap unknown throws as INTERNAL_SERVER_ERROR with the original message\n // preserved for telemetry; production error handler still consumes via\n // handleActionError fallback (preserves AuthRequiredError duck-type).\n const wrapped = new ActionError({\n code: 'INTERNAL_SERVER_ERROR' satisfies ActionErrorCode,\n message: err instanceof Error ? err.message : 'Action handler threw',\n })\n await emitActionCallTelemetry(args.actionName, args.startedAt, args.input, {\n status: 'error',\n error: wrapped,\n })\n throw err\n }\n}\n\ninterface ActionErrorCtx {\n req: IncomingMessage\n res: ServerResponse\n ctx: Record<string, unknown>\n requestId: string | undefined\n pluginRunner: PluginRunner | undefined\n buildPluginCtx: (ctxObj: Record<string, unknown>) => PluginContext\n}\n\n// T3.4 (PV-9 DRY): delegate to the shared `handleRequestError` helper.\n// Adds the duck-type AuthRequiredError fallback (latent bug fix — was\n// missing from action-execute, present in execute).\nasync function handleActionError(err: unknown, c: ActionErrorCtx): Promise<void> {\n return handleRequestError(err, {\n req: c.req,\n res: c.res,\n requestId: c.requestId,\n pluginRunner: c.pluginRunner,\n buildPluginCtx: c.buildPluginCtx,\n })\n}\n","/**\n * T1.4 — Server-side batch handler.\n *\n * Receives `{ requests: [...] }` POSTed to `/api/__theo_batch__` and returns\n * `{ results: [...] }` with per-item error isolation. EC-2: items cannot\n * override auth/forwarded headers (would be a session-bypass vector).\n */\n\nimport { z } from 'zod'\n\nexport const STRIPPED_HEADERS = [\n 'authorization',\n 'cookie',\n 'x-forwarded-for',\n 'x-forwarded-host',\n 'x-forwarded-proto',\n 'x-real-ip',\n 'host',\n] as const\n\nexport const BATCH_PATH = '/api/__theo_batch__'\nconst DEFAULT_MAX_BATCH = 32\n\nexport class BatchPathConflictError extends Error {\n constructor(path: string) {\n super(\n `Server route conflicts with reserved batch path ${path}. ` +\n `Rename the route or disable batching in theo.config.ts.`,\n )\n this.name = 'BatchPathConflictError'\n }\n}\n\nconst batchRequestSchema = z.object({\n path: z.string().min(1),\n method: z.string().min(1),\n query: z.record(z.string(), z.unknown()).optional(),\n body: z.unknown().optional(),\n headers: z.record(z.string(), z.string()).optional(),\n})\n\nconst batchPayloadSchema = z.object({\n requests: z.array(batchRequestSchema).min(1),\n})\n\nexport type BatchRequestItem = z.infer<typeof batchRequestSchema>\nexport type BatchPayload = z.infer<typeof batchPayloadSchema>\n\nexport type BatchExecuteFn = (\n req: BatchRequestItem,\n) => Promise<{ data: unknown } | { error: { message: string; code?: string } }>\n\nexport interface HandleBatchOptions {\n execute: BatchExecuteFn\n /** Max number of items per batch. Default 32. */\n max?: number\n /** Outer-request headers that override per-item headers in STRIPPED_HEADERS. */\n outerHeaders?: Record<string, string>\n}\n\nexport type BatchResultItem = { data: unknown } | { error: { message: string; code?: string } }\n\nexport interface BatchResponse {\n results: BatchResultItem[]\n}\n\n/**\n * Sanitize a per-item header object: keys in STRIPPED_HEADERS are removed,\n * then the outer request's values for those keys are layered on top so that\n * downstream middlewares see the real session/auth headers.\n */\nfunction sanitizeItemHeaders(\n itemHeaders: Record<string, string> | undefined,\n outerHeaders: Record<string, string> | undefined,\n): Record<string, string> {\n const out: Record<string, string> = {}\n if (itemHeaders) {\n for (const [k, v] of Object.entries(itemHeaders)) {\n const lower = k.toLowerCase()\n if (!(STRIPPED_HEADERS as readonly string[]).includes(lower)) {\n out[lower] = v\n }\n }\n }\n if (outerHeaders) {\n for (const stripped of STRIPPED_HEADERS) {\n // `Object.hasOwn` defends against missing keys without triggering\n // `no-unnecessary-condition` (TypeScript types `outerHeaders` keys\n // as defined; `hasOwn` keeps the conditional honest at runtime).\n if (Object.hasOwn(outerHeaders, stripped)) {\n out[stripped] = outerHeaders[stripped]\n }\n }\n }\n return out\n}\n\n/**\n * Validate + execute a batch request.\n *\n * CR-028 fix: previously the signature was `payload: BatchPayload`,\n * suggesting the caller had already validated. In reality `api-middleware`\n * passes `JSON.parse(rawBody) as BatchPayload` — an unsafe cast over raw\n * HTTP input. We widen the input type to `unknown` so the type system\n * forces validation, then run Zod once at this single trust boundary.\n */\nexport async function handleBatchRequest(\n payload: unknown,\n options: HandleBatchOptions,\n): Promise<BatchResponse> {\n const parsed = batchPayloadSchema.parse(payload)\n const max = options.max ?? DEFAULT_MAX_BATCH\n if (parsed.requests.length > max) {\n throw new Error(`Batch size ${parsed.requests.length} exceeds max ${max}`)\n }\n\n const results: BatchResultItem[] = []\n for (const item of parsed.requests) {\n const sanitized: BatchRequestItem = {\n ...item,\n headers: sanitizeItemHeaders(item.headers, options.outerHeaders),\n }\n try {\n const r = await options.execute(sanitized)\n results.push(r)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n results.push({ error: { message } })\n }\n }\n return { results }\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http'\n\n/**\n * T1.2 — CORS middleware.\n *\n * Single global middleware that runs FIRST in the request pipeline:\n * CORS preflight → rate limit → CSRF → security headers → handler\n *\n * Preflight (`OPTIONS` with `Access-Control-Request-Method`) is handled\n * by `handlePreflight`, which short-circuits the response with 204 +\n * Access-Control-* headers.\n *\n * Non-preflight requests pass through; `applyHeaders` adds\n * `Access-Control-Allow-Origin` (echoing the request's Origin),\n * `Access-Control-Expose-Headers`, and `Access-Control-Allow-Credentials`.\n *\n * Per ADR D3: preflight responses are deterministic and only the matched\n * origin is echoed back (never `'*'` when credentials are enabled —\n * required by the CORS spec).\n */\n\n// `'*'` is conceptually distinct from a regular host string (it means\n// \"any origin\"), but TypeScript-wise it is just `string`, so we collapse\n// the union and document the wildcard via the type's name. Callers should\n// still pass the literal `'*'` to mean \"allow anywhere\" for readability.\nexport type CorsOrigin =\n | string\n | RegExp\n | readonly (string | RegExp)[]\n | ((origin: string) => boolean)\n\nexport interface CorsConfig {\n origins: CorsOrigin\n methods?: readonly ('GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD')[]\n allowedHeaders?: readonly string[]\n exposedHeaders?: readonly string[]\n credentials?: boolean\n maxAge?: number\n}\n\nexport interface CorsHandler {\n handlePreflight(req: IncomingMessage, res: ServerResponse): boolean\n applyHeaders(req: IncomingMessage, res: ServerResponse): void\n}\n\nconst DEFAULT_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'] as const\nconst DEFAULT_ALLOWED_HEADERS = ['Content-Type', 'X-Theo-Action', 'Authorization'] as const\nconst DEFAULT_MAX_AGE = 600\n\n/**\n * Read the request Origin header. Per Node typing, it can be a string,\n * string[] (proxy doubled), or undefined. We take the FIRST non-empty\n * value (consistent with `csrf.ts:121-122`).\n */\nfunction readOrigin(req: IncomingMessage): string | undefined {\n const raw = req.headers.origin\n if (raw === undefined) return undefined\n if (Array.isArray(raw)) {\n return raw.find((v): v is string => typeof v === 'string' && v.length > 0)\n }\n return raw\n}\n\n/**\n * Test whether `origin` is allowed by `allowed`. Pure function — no I/O.\n *\n * EC-8: callback variants that throw are fail-closed (deny). Without\n * this, a transient datastore outage would silently widen CORS during\n * the failure window.\n */\nexport function matchesOrigin(origin: string, allowed: CorsOrigin): boolean {\n if (allowed === '*') return true\n if (typeof allowed === 'string') return origin === allowed\n if (allowed instanceof RegExp) {\n allowed.lastIndex = 0\n return allowed.test(origin)\n }\n if (Array.isArray(allowed)) {\n for (const entry of allowed) {\n if (typeof entry === 'string' && origin === entry) return true\n if (entry instanceof RegExp) {\n entry.lastIndex = 0\n if (entry.test(origin)) return true\n }\n }\n return false\n }\n if (typeof allowed === 'function') {\n // EC-8: fail-closed on any throw\n try {\n return allowed(origin)\n } catch {\n return false\n }\n }\n return false\n}\n\nexport function createCorsHandler(config: CorsConfig): CorsHandler {\n const methods = (config.methods ?? DEFAULT_METHODS).slice()\n const allowedHeaders = (config.allowedHeaders ?? DEFAULT_ALLOWED_HEADERS).slice()\n const maxAge = String(config.maxAge ?? DEFAULT_MAX_AGE)\n const credentials = config.credentials === true\n\n return {\n handlePreflight(req, res) {\n if (req.method !== 'OPTIONS') return false\n const acMethod = req.headers['access-control-request-method']\n if (!acMethod) return false\n const origin = readOrigin(req)\n if (!origin) return false\n\n if (!matchesOrigin(origin, config.origins)) {\n res.statusCode = 403\n res.end()\n return true\n }\n\n // Echo the matched origin (NEVER '*' when credentials are enabled —\n // browsers reject wildcard responses with credentials per CORS spec).\n res.setHeader('Access-Control-Allow-Origin', origin)\n res.setHeader('Access-Control-Allow-Methods', methods.join(', '))\n res.setHeader('Access-Control-Allow-Headers', allowedHeaders.join(', '))\n res.setHeader('Access-Control-Max-Age', maxAge)\n // Mark the response as origin-varying so caches don't poison\n res.setHeader('Vary', 'Origin')\n if (credentials) res.setHeader('Access-Control-Allow-Credentials', 'true')\n res.statusCode = 204\n res.end()\n return true\n },\n\n applyHeaders(req, res) {\n const origin = readOrigin(req)\n if (!origin) return\n if (!matchesOrigin(origin, config.origins)) return\n\n res.setHeader('Access-Control-Allow-Origin', origin)\n res.setHeader('Vary', 'Origin')\n if (credentials) res.setHeader('Access-Control-Allow-Credentials', 'true')\n if (config.exposedHeaders && config.exposedHeaders.length > 0) {\n res.setHeader('Access-Control-Expose-Headers', config.exposedHeaders.join(', '))\n }\n },\n }\n}\n\n/**\n * T5a.2 Phase B slice 5/6 — Web-Standards CORS handler.\n *\n * Mirror of `createCorsHandler(config): CorsHandler` for the Web `Request`\n * shape. Same `CorsConfig`, same `matchesOrigin` logic (pure helper\n * already), same security guarantees (echo matched origin only — never\n * `'*'` when credentials enabled; EC-8 fail-closed on callback throw).\n *\n * Signature differences vs the IncomingMessage pair:\n * - `handlePreflightRequest(request, config): Response | null` — returns\n * `Response` when this is a CORS preflight; `null` when not. Caller\n * short-circuits accordingly (same control-flow semantic as boolean).\n * - `applyCorsHeaders(request, target, config): void` — mutates a\n * `Headers` instance in place. Caller passes the headers they're\n * building for their Response.\n */\nexport interface CorsWebHandler {\n handlePreflightRequest(request: Request): Response | null\n applyCorsHeaders(request: Request, target: Headers): void\n}\n\nexport function createCorsWebHandler(config: CorsConfig): CorsWebHandler {\n const methods = (config.methods ?? DEFAULT_METHODS).slice()\n const allowedHeaders = (config.allowedHeaders ?? DEFAULT_ALLOWED_HEADERS).slice()\n const maxAge = String(config.maxAge ?? DEFAULT_MAX_AGE)\n const credentials = config.credentials === true\n\n return {\n handlePreflightRequest(request) {\n if (request.method !== 'OPTIONS') return null\n const acMethod = request.headers.get('access-control-request-method')\n if (acMethod === null || acMethod.length === 0) return null\n const origin = request.headers.get('origin')\n if (origin === null || origin.length === 0) return null\n\n if (!matchesOrigin(origin, config.origins)) {\n return new Response(null, { status: 403 })\n }\n\n // Echo matched origin only (security: never `'*'` when credentials\n // enabled — browsers reject per CORS spec).\n const headers = new Headers({\n 'Access-Control-Allow-Origin': origin,\n 'Access-Control-Allow-Methods': methods.join(', '),\n 'Access-Control-Allow-Headers': allowedHeaders.join(', '),\n 'Access-Control-Max-Age': maxAge,\n Vary: 'Origin',\n })\n if (credentials) headers.set('Access-Control-Allow-Credentials', 'true')\n return new Response(null, { status: 204, headers })\n },\n\n applyCorsHeaders(request, target) {\n const origin = request.headers.get('origin')\n if (origin === null || origin.length === 0) return\n if (!matchesOrigin(origin, config.origins)) return\n\n target.set('Access-Control-Allow-Origin', origin)\n target.set('Vary', 'Origin')\n if (credentials) target.set('Access-Control-Allow-Credentials', 'true')\n if (config.exposedHeaders !== undefined && config.exposedHeaders.length > 0) {\n target.set('Access-Control-Expose-Headers', config.exposedHeaders.join(', '))\n }\n },\n }\n}\n","// T5a.1b — Web Crypto migration. randomUUID() moved to globalThis.crypto.\n// IncomingMessage stays as a type-only import (runtime-clean — TS erases at\n// build); full IncomingMessage→Request boundary migration deferred to a\n// later T5a.1c+ slice per ADR-0028 incremental leaf-first sequence.\nimport type { IncomingMessage } from 'node:http'\n\n/**\n * Phase 7 — Observability: traceId propagation (D7).\n *\n * Extract a stable identifier from incoming requests so a single value\n * correlates the client request, every server log line, the response\n * envelope, and any downstream span. Precedence:\n *\n * 1. `traceparent` (W3C Trace Context — `00-{32-hex}-{16-hex}-{flags}`)\n * 2. `x-request-id` (Heroku / GCP / generic proxy header)\n * 3. Generated UUID (fresh per request)\n *\n * UUIDs are accepted as trace identifiers by every major vendor that\n * does not enforce strict 32-hex (Datadog, Honeycomb, Sentry, Logflare,\n * Axiom, etc). We don't need ULIDs to ship this surface.\n */\n\nexport const TRACE_HEADER = 'x-trace-id'\nexport const TRACE_PARENT_HEADER = 'traceparent'\nconst REQUEST_ID_HEADER = 'x-request-id'\n\n// W3C Trace Context: 00-<trace-id 32 hex>-<span-id 16 hex>-<flags 2 hex>\nconst TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/\n\n/**\n * Parse a W3C Trace Context `traceparent` header value. Returns the\n * 32-hex trace-id when valid (and not the reserved all-zeros), else\n * `null`.\n */\nexport function parseTraceparent(value: string): string | null {\n if (!value) return null\n const m = TRACEPARENT_RE.exec(value)\n if (!m) return null\n const traceId = m[1]\n // W3C: trace-id of all zeroes is invalid by spec\n if (/^0+$/.test(traceId)) return null\n return traceId\n}\n\n/**\n * Pick the first string value out of an IncomingMessage header. Node\n * collapses repeated headers into arrays; proxies sometimes do this for\n * `x-request-id`. Empty strings count as absent.\n */\nfunction pickHeader(value: string | string[] | undefined): string | null {\n if (Array.isArray(value)) {\n for (const v of value) {\n if (typeof v === 'string' && v.length > 0) return v\n }\n return null\n }\n if (typeof value === 'string' && value.length > 0) return value\n return null\n}\n\n/**\n * T5a.2 Phase C slice 1/2 — pure traceId resolution from pre-extracted\n * header values. Shared between IncomingMessage and Web Request wrappers.\n */\nfunction resolveTraceIdFromHeaders(traceparent: string | null, requestId: string | null): string {\n if (traceparent !== null) {\n const parsed = parseTraceparent(traceparent)\n if (parsed !== null) return parsed\n }\n if (requestId !== null) return requestId\n return globalThis.crypto.randomUUID()\n}\n\n/**\n * Resolve the request's traceId following the precedence above.\n */\nexport function extractTraceId(req: IncomingMessage): string {\n return resolveTraceIdFromHeaders(\n pickHeader(req.headers[TRACE_PARENT_HEADER]),\n pickHeader(req.headers[REQUEST_ID_HEADER]),\n )\n}\n\n/**\n * T5a.2 Phase C slice 1/2 — Web-Standards-shaped traceId resolver.\n *\n * Mirror of `extractTraceId(req: IncomingMessage)` for the Web `Request`\n * shape. Same precedence (`traceparent` → `x-request-id` → generated\n * UUID). Uses `request.headers.get(name)` (native Web `Headers` API)\n * instead of the Node indexer.\n *\n * **Multi-value note:** Web `Headers` collapses repeated headers into a\n * single comma-separated string at parse. The IncomingMessage path's\n * `pickHeader` \"first non-empty value\" semantic is naturally satisfied\n * because there's no array to pick from on the Web side — `.get()`\n * returns the comma-joined value, which for `traceparent` / `x-request-id`\n * is treated as a single string anyway (both headers are conventionally\n * single-valued).\n */\nexport function extractTraceIdFromRequest(request: Request): string {\n return resolveTraceIdFromHeaders(\n request.headers.get(TRACE_PARENT_HEADER),\n request.headers.get(REQUEST_ID_HEADER),\n )\n}\n"],"mappings":"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","names":["ctx","body","z"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/server/scan/action-scan.ts","../src/server/_internal/scan-walker.ts"],"sourcesContent":["/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time scanner: walks `serverDir/actions/` derived from cwd.\n * No HTTP input ever reaches these fs calls.\n */\nimport { existsSync, readFileSync, statSync } from 'node:fs'\nimport { extname, join, relative } from 'node:path'\n\nimport { walkSourceFiles } from '../_internal/scan-walker.js'\n\nexport interface ActionNode {\n filePath: string\n actionPath: string\n}\n\n/**\n * Enriched manifest entry per plan g3-server-actions-and-useaction v1.2\n * § Phase 1 / T1.4 + ADR D4. Consumed by virtual module `@theo/actions`\n * (T3.1) and G4 devtools \"Actions\" tab (T5.1).\n */\nexport interface ActionManifestEntry {\n name: string\n filePath: string\n urlPath: string\n accept: 'form' | 'json'\n hasInput: boolean\n /**\n * P#4 plugin-forms shared-schema convention (per plan p4-plugin-forms v1.1 T1.1).\n * When present, points to an isomorphic schema file at\n * `<serverDir>/actions/schemas/<basename>.ts` exporting `export const schema = z.object(...)`.\n * Virtual module emits `import {schema} from '<schemaFilePath>'` + attaches as\n * `actions.X.__zodSchema` so client-side <TheoForm> can drive zodResolver.\n * Undefined when convention not followed (graceful degrade — TheoForm still\n * works via explicit `schema={...}` prop escape hatch).\n */\n schemaFilePath?: string\n}\n\n/**\n * EC-2: structured error for scan-time defects (name collision, reserved\n * identifier, etc.). Throw at scan time to fail loud — silent shadowing is\n * a security/correctness footgun.\n */\nexport class ActionScanError extends Error {\n readonly code: 'NAME_COLLISION' | 'RESERVED_NAME'\n readonly conflictingPaths: readonly string[]\n\n constructor(\n code: 'NAME_COLLISION' | 'RESERVED_NAME',\n message: string,\n conflictingPaths: readonly string[],\n ) {\n super(message)\n // T4.1 fix: identify class by name so serverErrorToEnvelope boundary\n // translator can route via class-name lookup (without this, runtime\n // `err.name` defaults to 'Error' and the meta.name diagnostic is wrong).\n this.name = 'ActionScanError'\n this.code = code\n this.conflictingPaths = conflictingPaths\n }\n}\n\nconst ACTION_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])\nconst TEST_FILE_RE = /\\.(test|spec)\\.(ts|tsx|js|jsx)$/\nconst RESERVED_NAMES = new Set(['index', 'constructor', '__proto__', 'prototype', 'hasOwnProperty'])\n\n/**\n * Backward-compatible: original simple-shape scanner used by existing\n * consumers. Preserved verbatim; new consumers should call\n * `scanServerActionsEnriched`.\n */\nexport function scanServerActions(serverDir: string): ActionNode[] {\n const actionsDir = join(serverDir, 'actions')\n if (!existsSync(actionsDir) || !statSync(actionsDir).isDirectory()) {\n return []\n }\n\n const results: ActionNode[] = []\n walkSourceFiles(actionsDir, { extensions: ACTION_EXTENSIONS }, (absPath) => {\n if (TEST_FILE_RE.test(absPath)) return\n let rel = relative(actionsDir, absPath)\n rel = rel.replace(/\\\\/g, '/')\n rel = rel.slice(0, -extname(rel).length)\n results.push({\n filePath: absPath,\n actionPath: rel,\n })\n })\n return results\n}\n\n/**\n * Enriched scan: light AST detection of `accept: 'form'|'json'` + `input:`\n * presence via regex-after-comment-stripping (EC-9). Throws ActionScanError\n * on file/dir name collision (EC-2) or reserved JS identifier names.\n *\n * Output `ActionManifestEntry[]` is sorted by `name` for deterministic\n * `.theo/actions-manifest.json` emission.\n */\nexport function scanServerActionsEnriched(serverDir: string): ActionManifestEntry[] {\n const actionsDir = join(serverDir, 'actions')\n if (!existsSync(actionsDir) || !statSync(actionsDir).isDirectory()) {\n return []\n }\n\n const seenNames = new Set<string>()\n const entries: ActionManifestEntry[] = []\n\n // Collect first; collision-check after the full walk.\n walkSourceFiles(actionsDir, { extensions: ACTION_EXTENSIONS }, (absPath) => {\n if (TEST_FILE_RE.test(absPath)) return\n const rel = relative(actionsDir, absPath).replace(/\\\\/g, '/')\n // P#4 plugin-forms — `schemas/` subdir holds isomorphic zod schemas\n // for the shared-schema convention (T1.1). NOT executable actions; skip.\n if (rel.startsWith('schemas/')) return\n const name = rel.slice(0, -extname(rel).length)\n\n const basename = name.includes('/') ? (name.split('/').pop() ?? name) : name\n if (RESERVED_NAMES.has(basename)) {\n throw new ActionScanError(\n 'RESERVED_NAME',\n `Reserved JS identifier \"${basename}\" cannot be an action name (${absPath})`,\n [absPath],\n )\n }\n // File-level collision (one file appearing twice) is impossible via the\n // walker; per-export collision check happens inside the export loop below.\n const source = readFileSync(absPath, 'utf8')\n const stripped = stripComments(source)\n const accept = /\\baccept\\s*:\\s*['\"]form['\"]/.test(stripped) ? 'form' : 'json'\n const hasInput = /\\binput\\s*:\\s*z\\./.test(stripped) || /\\binput\\s*:\\s*\\w+\\(/.test(stripped)\n\n // T7.1 wire fix — extract exports so the urlPath includes the second\n // segment required by action-middleware (`/api/__actions/<file>/<export>`).\n // Each named action export becomes its own manifest entry.\n // Proxy key on the EXPORT name (so consumers write `actions.saveMemory(input)`).\n // URL keeps the runtime 2-segment shape `/api/__actions/<file>/<export>`\n // expected by action-middleware. Cross-file export collisions become a\n // scan error to surface the ambiguity early.\n const exportNames = extractActionExportNames(stripped)\n // P#4 plugin-forms shared-schema convention: check for\n // `<actionsDir>/schemas/<basename>.ts` (or .tsx/.js/.jsx).\n // Skip when actions live in subdirs (`schemas/` is flat by convention).\n const schemaFilePath = name.includes('/') ? undefined : detectSchemaFile(actionsDir, basename)\n for (const exportName of exportNames) {\n const proxyKey = exportName === 'default' ? name : exportName\n if (seenNames.has(proxyKey)) {\n throw new ActionScanError(\n 'NAME_COLLISION',\n `Duplicate action proxy key \"${proxyKey}\" (two files export the same name)`,\n [absPath],\n )\n }\n seenNames.add(proxyKey)\n const entry: ActionManifestEntry = {\n name: proxyKey,\n filePath: absPath,\n urlPath: `/api/__actions/${name}/${exportName}`,\n accept,\n hasInput,\n }\n if (schemaFilePath !== undefined) {\n entry.schemaFilePath = schemaFilePath\n }\n entries.push(entry)\n }\n })\n\n // EC-2: detect file vs dir collisions (e.g., foo.ts AND foo/bar.ts).\n // After walk completes, any name that has children prefixed `name/` triggers collision.\n for (const entry of entries) {\n const childPrefix = `${entry.name}/`\n const conflictingChild = entries.find((other) => other.name.startsWith(childPrefix))\n if (conflictingChild) {\n throw new ActionScanError(\n 'NAME_COLLISION',\n `Action \"${entry.name}\" conflicts with directory of same name containing \"${conflictingChild.name}\"`,\n [entry.filePath, conflictingChild.filePath],\n )\n }\n }\n\n entries.sort((a, b) => {\n if (a.name < b.name) return -1\n if (a.name > b.name) return 1\n return 0\n })\n return entries\n}\n\n/**\n * Strip JavaScript line + block comments from source. Simple state machine\n * (does not parse strings — false positives if a comment marker appears\n * inside a string literal, but that's an acceptable trade-off for v1 vs\n * full AST parse).\n */\n/**\n * Extract action export names via regex over comment-stripped source.\n * Matches `export const <name> = defineAction(...)`, `export default\n * defineAction(...)`, and `export function <name>(...)` forms. Returns\n * `['default']` when no named action exports are found (best-effort fallback\n * for default-export shapes the regex misses).\n */\nfunction extractActionExportNames(stripped: string): string[] {\n const names = new Set<string>()\n const namedRe = /\\bexport\\s+(?:const|let|var|function\\*?)\\s+([a-zA-Z_$][\\w$]*)\\s*[=(]/g\n let m: RegExpExecArray | null\n while ((m = namedRe.exec(stripped)) !== null) {\n const name = m[1]\n if (typeof name === 'string' && name.length > 0) names.add(name)\n }\n if (/\\bexport\\s+default\\s+defineAction\\b/.test(stripped)) {\n names.add('default')\n }\n if (names.size === 0) names.add('default')\n return [...names]\n}\n\n/**\n * P#4 plugin-forms shared-schema convention helper (per plan p4-plugin-forms v1.1 T1.1).\n * Returns the resolved path to `<actionsDir>/schemas/<basename>.<ext>` if it exists.\n * Tries `.ts`, `.tsx`, `.js`, `.jsx` in that order. Returns undefined when no match.\n */\nfunction detectSchemaFile(actionsDir: string, basename: string): string | undefined {\n const schemasDir = join(actionsDir, 'schemas')\n if (!existsSync(schemasDir)) return undefined\n for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {\n const candidate = join(schemasDir, `${basename}${ext}`)\n if (existsSync(candidate)) return candidate\n }\n return undefined\n}\n\nfunction stripComments(source: string): string {\n let out = ''\n let i = 0\n while (i < source.length) {\n const ch = source[i]\n const next = source[i + 1]\n if (ch === '/' && next === '/') {\n // Line comment: skip until newline\n while (i < source.length && source[i] !== '\\n') i++\n continue\n }\n if (ch === '/' && next === '*') {\n // Block comment: skip until */\n i += 2\n while (i < source.length - 1 && !(source[i] === '*' && source[i + 1] === '/')) i++\n i += 2\n continue\n }\n out += ch\n i++\n }\n return out\n}\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time scanner: walks directories derived from cwd.\n * No HTTP input ever reaches these fs calls.\n */\nimport { readdirSync } from 'node:fs'\nimport { extname, join, resolve } from 'node:path'\n\n/**\n * Options for walkSourceFiles.\n *\n * Sequential by design — callers (route precedence) depend on insertion\n * order (EC-19 documented decision). Async-callback support is implicit\n * via JavaScript's serial await loop.\n *\n * Tested on macOS + Linux. Windows long-path support not validated (EC-20).\n */\nexport interface WalkOptions {\n /** File extensions to include (e.g., new Set(['.ts', '.tsx'])). */\n extensions: ReadonlySet<string>\n /** Skip directories whose name starts with any of these (default: ['_', '.']). */\n skipPrefixes?: readonly string[]\n}\n\n/**\n * Recursively walk `root`, invoking `onFile(absPath)` for every file matching\n * `opts.extensions`. Directories whose name starts with any `skipPrefixes`\n * char are skipped (defaults to `_` and `.`).\n *\n * Replaces 3 near-identical recursive walkers in scan.ts, action-scan.ts,\n * ws-scan.ts (PV-3 — DRY consolidation). Resolves T3.1 of\n * architecture-review-remediation-plan.\n *\n * Symlink loops are NOT tracked — callers must avoid them or pre-resolve\n * via `fs.realpath`. EC-11 documented but not implemented (rare in dev).\n */\nexport function walkSourceFiles(\n root: string,\n opts: WalkOptions,\n onFile: (absPath: string) => void,\n): void {\n const skipPrefixes = opts.skipPrefixes ?? ['_', '.']\n const visit = (dir: string): void => {\n let entries\n try {\n entries = readdirSync(dir, { withFileTypes: true })\n } catch {\n // Silently skip unreadable directories — caller controls discoverability\n return\n }\n for (const entry of entries) {\n const fullPath = join(dir, entry.name)\n if (entry.isDirectory() && !skipPrefixes.some((p) => entry.name.startsWith(p))) {\n visit(fullPath)\n } else if (entry.isFile() && opts.extensions.has(extname(entry.name))) {\n onFile(resolve(fullPath))\n }\n }\n }\n visit(root)\n}\n"],"mappings":";;;;AAIA,SAAS,YAAY,cAAc,gBAAgB;AACnD,SAAS,WAAAA,UAAS,QAAAC,OAAM,gBAAgB;;;ACDxC,SAAS,mBAAmB;AAC5B,SAAS,SAAS,MAAM,eAAe;AA8BhC,SAAS,gBACd,MACA,MACA,QACM;AACN,QAAM,eAAe,KAAK,gBAAgB,CAAC,KAAK,GAAG;AACnD,QAAM,QAAQ,CAAC,QAAsB;AACnC,QAAI;AACJ,QAAI;AACF,gBAAU,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC;AAAA,IACpD,QAAQ;AAEN;AAAA,IACF;AACA,eAAW,SAAS,SAAS;AAC3B,YAAM,WAAW,KAAK,KAAK,MAAM,IAAI;AACrC,UAAI,MAAM,YAAY,KAAK,CAAC,aAAa,KAAK,CAAC,MAAM,MAAM,KAAK,WAAW,CAAC,CAAC,GAAG;AAC9E,cAAM,QAAQ;AAAA,MAChB,WAAW,MAAM,OAAO,KAAK,KAAK,WAAW,IAAI,QAAQ,MAAM,IAAI,CAAC,GAAG;AACrE,eAAO,QAAQ,QAAQ,CAAC;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AACA,QAAM,IAAI;AACZ;;;ADjBO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAChC;AAAA,EACA;AAAA,EAET,YACE,MACA,SACA,kBACA;AACA,UAAM,OAAO;AAIb,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,mBAAmB;AAAA,EAC1B;AACF;AAEA,IAAM,oBAAoB,oBAAI,IAAI,CAAC,OAAO,QAAQ,OAAO,MAAM,CAAC;AAChE,IAAM,eAAe;AACrB,IAAM,iBAAiB,oBAAI,IAAI,CAAC,SAAS,eAAe,aAAa,aAAa,gBAAgB,CAAC;AAO5F,SAAS,kBAAkB,WAAiC;AACjE,QAAM,aAAaC,MAAK,WAAW,SAAS;AAC5C,MAAI,CAAC,WAAW,UAAU,KAAK,CAAC,SAAS,UAAU,EAAE,YAAY,GAAG;AAClE,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,UAAwB,CAAC;AAC/B,kBAAgB,YAAY,EAAE,YAAY,kBAAkB,GAAG,CAAC,YAAY;AAC1E,QAAI,aAAa,KAAK,OAAO,EAAG;AAChC,QAAI,MAAM,SAAS,YAAY,OAAO;AACtC,UAAM,IAAI,QAAQ,OAAO,GAAG;AAC5B,UAAM,IAAI,MAAM,GAAG,CAACC,SAAQ,GAAG,EAAE,MAAM;AACvC,YAAQ,KAAK;AAAA,MACX,UAAU;AAAA,MACV,YAAY;AAAA,IACd,CAAC;AAAA,EACH,CAAC;AACD,SAAO;AACT;AAUO,SAAS,0BAA0B,WAA0C;AAClF,QAAM,aAAaD,MAAK,WAAW,SAAS;AAC5C,MAAI,CAAC,WAAW,UAAU,KAAK,CAAC,SAAS,UAAU,EAAE,YAAY,GAAG;AAClE,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,YAAY,oBAAI,IAAY;AAClC,QAAM,UAAiC,CAAC;AAGxC,kBAAgB,YAAY,EAAE,YAAY,kBAAkB,GAAG,CAAC,YAAY;AAC1E,QAAI,aAAa,KAAK,OAAO,EAAG;AAChC,UAAM,MAAM,SAAS,YAAY,OAAO,EAAE,QAAQ,OAAO,GAAG;AAG5D,QAAI,IAAI,WAAW,UAAU,EAAG;AAChC,UAAM,OAAO,IAAI,MAAM,GAAG,CAACC,SAAQ,GAAG,EAAE,MAAM;AAE9C,UAAM,WAAW,KAAK,SAAS,GAAG,IAAK,KAAK,MAAM,GAAG,EAAE,IAAI,KAAK,OAAQ;AACxE,QAAI,eAAe,IAAI,QAAQ,GAAG;AAChC,YAAM,IAAI;AAAA,QACR;AAAA,QACA,2BAA2B,QAAQ,+BAA+B,OAAO;AAAA,QACzE,CAAC,OAAO;AAAA,MACV;AAAA,IACF;AAGA,UAAM,SAAS,aAAa,SAAS,MAAM;AAC3C,UAAM,WAAW,cAAc,MAAM;AACrC,UAAM,SAAS,8BAA8B,KAAK,QAAQ,IAAI,SAAS;AACvE,UAAM,WAAW,oBAAoB,KAAK,QAAQ,KAAK,sBAAsB,KAAK,QAAQ;AAS1F,UAAM,cAAc,yBAAyB,QAAQ;AAIrD,UAAM,iBAAiB,KAAK,SAAS,GAAG,IAAI,SAAY,iBAAiB,YAAY,QAAQ;AAC7F,eAAW,cAAc,aAAa;AACpC,YAAM,WAAW,eAAe,YAAY,OAAO;AACnD,UAAI,UAAU,IAAI,QAAQ,GAAG;AAC3B,cAAM,IAAI;AAAA,UACR;AAAA,UACA,+BAA+B,QAAQ;AAAA,UACvC,CAAC,OAAO;AAAA,QACV;AAAA,MACF;AACA,gBAAU,IAAI,QAAQ;AACtB,YAAM,QAA6B;AAAA,QACjC,MAAM;AAAA,QACN,UAAU;AAAA,QACV,SAAS,kBAAkB,IAAI,IAAI,UAAU;AAAA,QAC7C;AAAA,QACA;AAAA,MACF;AACA,UAAI,mBAAmB,QAAW;AAChC,cAAM,iBAAiB;AAAA,MACzB;AACA,cAAQ,KAAK,KAAK;AAAA,IACpB;AAAA,EACF,CAAC;AAID,aAAW,SAAS,SAAS;AAC3B,UAAM,cAAc,GAAG,MAAM,IAAI;AACjC,UAAM,mBAAmB,QAAQ,KAAK,CAAC,UAAU,MAAM,KAAK,WAAW,WAAW,CAAC;AACnF,QAAI,kBAAkB;AACpB,YAAM,IAAI;AAAA,QACR;AAAA,QACA,WAAW,MAAM,IAAI,uDAAuD,iBAAiB,IAAI;AAAA,QACjG,CAAC,MAAM,UAAU,iBAAiB,QAAQ;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AAEA,UAAQ,KAAK,CAAC,GAAG,MAAM;AACrB,QAAI,EAAE,OAAO,EAAE,KAAM,QAAO;AAC5B,QAAI,EAAE,OAAO,EAAE,KAAM,QAAO;AAC5B,WAAO;AAAA,EACT,CAAC;AACD,SAAO;AACT;AAeA,SAAS,yBAAyB,UAA4B;AAC5D,QAAM,QAAQ,oBAAI,IAAY;AAC9B,QAAM,UAAU;AAChB,MAAI;AACJ,UAAQ,IAAI,QAAQ,KAAK,QAAQ,OAAO,MAAM;AAC5C,UAAM,OAAO,EAAE,CAAC;AAChB,QAAI,OAAO,SAAS,YAAY,KAAK,SAAS,EAAG,OAAM,IAAI,IAAI;AAAA,EACjE;AACA,MAAI,sCAAsC,KAAK,QAAQ,GAAG;AACxD,UAAM,IAAI,SAAS;AAAA,EACrB;AACA,MAAI,MAAM,SAAS,EAAG,OAAM,IAAI,SAAS;AACzC,SAAO,CAAC,GAAG,KAAK;AAClB;AAOA,SAAS,iBAAiB,YAAoB,UAAsC;AAClF,QAAM,aAAaD,MAAK,YAAY,SAAS;AAC7C,MAAI,CAAC,WAAW,UAAU,EAAG,QAAO;AACpC,aAAW,OAAO,CAAC,OAAO,QAAQ,OAAO,MAAM,GAAG;AAChD,UAAM,YAAYA,MAAK,YAAY,GAAG,QAAQ,GAAG,GAAG,EAAE;AACtD,QAAI,WAAW,SAAS,EAAG,QAAO;AAAA,EACpC;AACA,SAAO;AACT;AAEA,SAAS,cAAc,QAAwB;AAC7C,MAAI,MAAM;AACV,MAAI,IAAI;AACR,SAAO,IAAI,OAAO,QAAQ;AACxB,UAAM,KAAK,OAAO,CAAC;AACnB,UAAM,OAAO,OAAO,IAAI,CAAC;AACzB,QAAI,OAAO,OAAO,SAAS,KAAK;AAE9B,aAAO,IAAI,OAAO,UAAU,OAAO,CAAC,MAAM,KAAM;AAChD;AAAA,IACF;AACA,QAAI,OAAO,OAAO,SAAS,KAAK;AAE9B,WAAK;AACL,aAAO,IAAI,OAAO,SAAS,KAAK,EAAE,OAAO,CAAC,MAAM,OAAO,OAAO,IAAI,CAAC,MAAM,KAAM;AAC/E,WAAK;AACL;AAAA,IACF;AACA,WAAO;AACP;AAAA,EACF;AACA,SAAO;AACT;","names":["extname","join","join","extname"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/server/http/error-pages.ts","../src/server/http/static.ts"],"sourcesContent":["/**\n * T2.4 — Custom error pages loader.\n *\n * Loads optional `.theo/client/404.html` and `500.html` from disk so adapters\n * (Node, CF, Vercel, Bun, Netlify, AWS Lambda, Deno) can pass them to the\n * shared `sendError` pipeline.\n *\n * EC-9: caps file size at 1MB. Files beyond the limit are skipped with a\n * console.warn and the default JSON error path is used.\n */\n\n/* eslint-disable security/detect-non-literal-fs-filename --\n * Loads `.theo/client/404.html` and `500.html` from a directory `dir` that\n * is always the build output of the current project. The names are fixed\n * literals (`'404.html'` / `'500.html'`) — no HTTP input.\n */\nimport { existsSync, readFileSync, statSync } from 'node:fs'\nimport { resolve } from 'node:path'\n\nexport const MAX_ERROR_HTML_BYTES = 1_048_576 // 1MB\n\nexport interface CustomErrorPages {\n custom404Html?: string\n custom500Html?: string\n}\n\nfunction loadIfSafe(dir: string, name: string): string | undefined {\n const path = resolve(dir, name)\n if (!existsSync(path)) return undefined\n try {\n const stat = statSync(path)\n if (stat.size > MAX_ERROR_HTML_BYTES) {\n console.warn(\n `[theokit] Custom error page ${name} is ${stat.size} bytes (max ${MAX_ERROR_HTML_BYTES}); skipping.`,\n )\n return undefined\n }\n return readFileSync(path, 'utf-8')\n } catch (err) {\n console.warn(`[theokit] Failed to read ${name}: ${(err as Error).message}`)\n return undefined\n }\n}\n\n/**\n * Load `.theo/client/{404,500}.html` from the given client directory.\n * Returns object with both fields when present; missing pages remain undefined.\n */\nexport function loadCustomErrorPages(clientDir: string): CustomErrorPages {\n return {\n custom404Html: loadIfSafe(clientDir, '404.html'),\n custom500Html: loadIfSafe(clientDir, '500.html'),\n }\n}\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Static-file server. The URL path IS user-controlled, BUT before any fs\n * call we resolve to absolute and reject with 403 if the resolved path\n * escapes `clientDir` (see EC-1 guard at line below). The guard is\n * authoritative; the rule cannot see it.\n */\nimport { existsSync, readFileSync, statSync } from 'node:fs'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { resolve, extname } from 'node:path'\n\nconst MIME_TYPES: Record<string, string> = {\n '.html': 'text/html',\n '.js': 'application/javascript',\n '.mjs': 'application/javascript',\n '.css': 'text/css',\n '.json': 'application/json',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.txt': 'text/plain',\n '.map': 'application/json',\n}\n\nexport function serveStaticFile(\n req: IncomingMessage,\n res: ServerResponse,\n clientDir: string,\n): boolean {\n const urlPath = (req.url ?? '/').split('?')[0]\n\n // Path traversal prevention (EC-1)\n const filePath = resolve(clientDir, '.' + urlPath)\n if (!filePath.startsWith(clientDir)) {\n res.writeHead(403)\n res.end('Forbidden')\n return true\n }\n\n if (!existsSync(filePath)) return false\n\n const stat = statSync(filePath)\n if (!stat.isFile()) return false\n\n const ext = extname(filePath)\n const contentType = MIME_TYPES[ext] ?? 'application/octet-stream'\n const content = readFileSync(filePath)\n\n res.writeHead(200, {\n 'Content-Type': contentType,\n 'Content-Length': content.length,\n })\n res.end(content)\n return true\n}\n"],"mappings":";AAgBA,SAAS,YAAY,cAAc,gBAAgB;AACnD,SAAS,eAAe;AAEjB,IAAM,uBAAuB;AAOpC,SAAS,WAAW,KAAa,MAAkC;AACjE,QAAM,OAAO,QAAQ,KAAK,IAAI;AAC9B,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO;AAC9B,MAAI;AACF,UAAM,OAAO,SAAS,IAAI;AAC1B,QAAI,KAAK,OAAO,sBAAsB;AACpC,cAAQ;AAAA,QACN,+BAA+B,IAAI,OAAO,KAAK,IAAI,eAAe,oBAAoB;AAAA,MACxF;AACA,aAAO;AAAA,IACT;AACA,WAAO,aAAa,MAAM,OAAO;AAAA,EACnC,SAAS,KAAK;AACZ,YAAQ,KAAK,4BAA4B,IAAI,KAAM,IAAc,OAAO,EAAE;AAC1E,WAAO;AAAA,EACT;AACF;AAMO,SAAS,qBAAqB,WAAqC;AACxE,SAAO;AAAA,IACL,eAAe,WAAW,WAAW,UAAU;AAAA,IAC/C,eAAe,WAAW,WAAW,UAAU;AAAA,EACjD;AACF;;;AC/CA,SAAS,cAAAA,aAAY,gBAAAC,eAAc,YAAAC,iBAAgB;AAEnD,SAAS,WAAAC,UAAS,eAAe;AAEjC,IAAM,aAAqC;AAAA,EACzC,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AACV;AAEO,SAAS,gBACd,KACA,KACA,WACS;AACT,QAAM,WAAW,IAAI,OAAO,KAAK,MAAM,GAAG,EAAE,CAAC;AAG7C,QAAM,WAAWA,SAAQ,WAAW,MAAM,OAAO;AACjD,MAAI,CAAC,SAAS,WAAW,SAAS,GAAG;AACnC,QAAI,UAAU,GAAG;AACjB,QAAI,IAAI,WAAW;AACnB,WAAO;AAAA,EACT;AAEA,MAAI,CAACH,YAAW,QAAQ,EAAG,QAAO;AAElC,QAAM,OAAOE,UAAS,QAAQ;AAC9B,MAAI,CAAC,KAAK,OAAO,EAAG,QAAO;AAE3B,QAAM,MAAM,QAAQ,QAAQ;AAC5B,QAAM,cAAc,WAAW,GAAG,KAAK;AACvC,QAAM,UAAUD,cAAa,QAAQ;AAErC,MAAI,UAAU,KAAK;AAAA,IACjB,gBAAgB;AAAA,IAChB,kBAAkB,QAAQ;AAAA,EAC5B,CAAC;AACD,MAAI,IAAI,OAAO;AACf,SAAO;AACT;","names":["existsSync","readFileSync","statSync","resolve"]}