veryfront 0.1.73 → 0.1.75

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 (120) hide show
  1. package/esm/cli/commands/knowledge/command-help.d.ts.map +1 -1
  2. package/esm/cli/commands/knowledge/command-help.js +3 -1
  3. package/esm/cli/commands/knowledge/command.d.ts +34 -5
  4. package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
  5. package/esm/cli/commands/knowledge/command.js +151 -22
  6. package/esm/cli/commands/knowledge/parser-source.d.ts.map +1 -1
  7. package/esm/cli/commands/knowledge/parser-source.js +110 -5
  8. package/esm/deno.d.ts +2 -0
  9. package/esm/deno.js +3 -1
  10. package/esm/src/data/data-fetcher.d.ts +11 -1
  11. package/esm/src/data/data-fetcher.d.ts.map +1 -1
  12. package/esm/src/data/data-fetcher.js +5 -2
  13. package/esm/src/data/index.d.ts +1 -1
  14. package/esm/src/data/index.d.ts.map +1 -1
  15. package/esm/src/data/server-data-fetcher.d.ts +14 -1
  16. package/esm/src/data/server-data-fetcher.d.ts.map +1 -1
  17. package/esm/src/data/server-data-fetcher.js +49 -3
  18. package/esm/src/rendering/orchestrator/lifecycle.d.ts +4 -0
  19. package/esm/src/rendering/orchestrator/lifecycle.d.ts.map +1 -1
  20. package/esm/src/rendering/orchestrator/lifecycle.js +8 -0
  21. package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
  22. package/esm/src/rendering/orchestrator/pipeline.js +6 -1
  23. package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts +26 -1
  24. package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts.map +1 -1
  25. package/esm/src/rendering/orchestrator/ssr-orchestrator.js +77 -1
  26. package/esm/src/routing/api/handler.d.ts.map +1 -1
  27. package/esm/src/routing/api/handler.js +6 -2
  28. package/esm/src/routing/api/route-executor.d.ts +8 -2
  29. package/esm/src/routing/api/route-executor.d.ts.map +1 -1
  30. package/esm/src/routing/api/route-executor.js +131 -3
  31. package/esm/src/security/deno-permissions.d.ts +6 -0
  32. package/esm/src/security/deno-permissions.d.ts.map +1 -1
  33. package/esm/src/security/deno-permissions.js +10 -0
  34. package/esm/src/security/sandbox/project-worker.d.ts +61 -0
  35. package/esm/src/security/sandbox/project-worker.d.ts.map +1 -0
  36. package/esm/src/security/sandbox/project-worker.js +318 -0
  37. package/esm/src/security/sandbox/worker-permissions.d.ts +30 -0
  38. package/esm/src/security/sandbox/worker-permissions.d.ts.map +1 -0
  39. package/esm/src/security/sandbox/worker-permissions.js +60 -0
  40. package/esm/src/security/sandbox/worker-pool.d.ts +87 -0
  41. package/esm/src/security/sandbox/worker-pool.d.ts.map +1 -0
  42. package/esm/src/security/sandbox/worker-pool.js +356 -0
  43. package/esm/src/security/sandbox/worker-types.d.ts +165 -0
  44. package/esm/src/security/sandbox/worker-types.d.ts.map +1 -0
  45. package/esm/src/security/sandbox/worker-types.js +17 -0
  46. package/esm/src/server/handlers/request/ssr/ssr.handler.d.ts +2 -0
  47. package/esm/src/server/handlers/request/ssr/ssr.handler.d.ts.map +1 -1
  48. package/esm/src/server/handlers/request/ssr/ssr.handler.js +6 -2
  49. package/esm/src/server/project-env/storage.d.ts +6 -0
  50. package/esm/src/server/project-env/storage.d.ts.map +1 -1
  51. package/esm/src/server/project-env/storage.js +8 -0
  52. package/esm/src/server/runtime-handler/adapter-factory.d.ts +3 -0
  53. package/esm/src/server/runtime-handler/adapter-factory.d.ts.map +1 -1
  54. package/esm/src/server/runtime-handler/adapter-factory.js +6 -5
  55. package/esm/src/server/runtime-handler/index.d.ts +33 -0
  56. package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
  57. package/esm/src/server/runtime-handler/index.js +103 -37
  58. package/esm/src/server/runtime-handler/local-project-discovery.d.ts +32 -4
  59. package/esm/src/server/runtime-handler/local-project-discovery.d.ts.map +1 -1
  60. package/esm/src/server/runtime-handler/local-project-discovery.js +46 -16
  61. package/esm/src/server/runtime-handler/project-isolation.d.ts +5 -0
  62. package/esm/src/server/runtime-handler/project-isolation.d.ts.map +1 -1
  63. package/esm/src/server/runtime-handler/project-isolation.js +44 -0
  64. package/esm/src/server/services/rendering/ssr.service.d.ts +19 -1
  65. package/esm/src/server/services/rendering/ssr.service.d.ts.map +1 -1
  66. package/esm/src/server/services/rendering/ssr.service.js +9 -1
  67. package/esm/src/server/shared/renderer/adapter.d.ts +25 -0
  68. package/esm/src/server/shared/renderer/adapter.d.ts.map +1 -1
  69. package/esm/src/server/shared/renderer/adapter.js +83 -10
  70. package/esm/src/server/shared/renderer/index.d.ts +1 -1
  71. package/esm/src/server/shared/renderer/index.d.ts.map +1 -1
  72. package/esm/src/server/shared/renderer/index.js +1 -1
  73. package/esm/src/server/shared/renderer/memory/pressure.d.ts +7 -0
  74. package/esm/src/server/shared/renderer/memory/pressure.d.ts.map +1 -1
  75. package/esm/src/server/shared/renderer/memory/pressure.js +7 -0
  76. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts +4 -4
  77. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts.map +1 -1
  78. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.js +15 -15
  79. package/esm/src/utils/index.d.ts +10 -1
  80. package/esm/src/utils/index.d.ts.map +1 -1
  81. package/esm/src/utils/index.js +9 -1
  82. package/esm/src/utils/logger/index.d.ts +1 -1
  83. package/esm/src/utils/logger/index.d.ts.map +1 -1
  84. package/esm/src/utils/logger/index.js +1 -1
  85. package/esm/src/utils/logger/logger.d.ts +14 -0
  86. package/esm/src/utils/logger/logger.d.ts.map +1 -1
  87. package/esm/src/utils/logger/logger.js +17 -0
  88. package/esm/src/workflow/claude-code/tool.d.ts +5 -5
  89. package/package.json +4 -1
  90. package/src/cli/commands/knowledge/command-help.ts +3 -1
  91. package/src/cli/commands/knowledge/command.ts +180 -22
  92. package/src/cli/commands/knowledge/parser-source.ts +110 -5
  93. package/src/deno.js +3 -1
  94. package/src/src/data/data-fetcher.ts +18 -2
  95. package/src/src/data/index.ts +1 -1
  96. package/src/src/data/server-data-fetcher.ts +78 -3
  97. package/src/src/rendering/orchestrator/lifecycle.ts +11 -0
  98. package/src/src/rendering/orchestrator/pipeline.ts +7 -2
  99. package/src/src/rendering/orchestrator/ssr-orchestrator.ts +119 -0
  100. package/src/src/routing/api/handler.ts +16 -3
  101. package/src/src/routing/api/route-executor.ts +222 -1
  102. package/src/src/security/deno-permissions.ts +11 -0
  103. package/src/src/security/sandbox/project-worker.ts +416 -0
  104. package/src/src/security/sandbox/worker-permissions.ts +74 -0
  105. package/src/src/security/sandbox/worker-pool.ts +451 -0
  106. package/src/src/security/sandbox/worker-types.ts +209 -0
  107. package/src/src/server/handlers/request/ssr/ssr.handler.ts +11 -2
  108. package/src/src/server/project-env/storage.ts +9 -0
  109. package/src/src/server/runtime-handler/adapter-factory.ts +13 -5
  110. package/src/src/server/runtime-handler/index.ts +132 -39
  111. package/src/src/server/runtime-handler/local-project-discovery.ts +51 -17
  112. package/src/src/server/runtime-handler/project-isolation.ts +53 -0
  113. package/src/src/server/services/rendering/ssr.service.ts +34 -3
  114. package/src/src/server/shared/renderer/adapter.ts +107 -8
  115. package/src/src/server/shared/renderer/index.ts +7 -1
  116. package/src/src/server/shared/renderer/memory/pressure.ts +8 -0
  117. package/src/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.ts +18 -12
  118. package/src/src/utils/index.ts +11 -0
  119. package/src/src/utils/logger/index.ts +1 -0
  120. package/src/src/utils/logger/logger.ts +34 -0
@@ -38,6 +38,8 @@ export interface LifecycleOptions {
38
38
  projectId?: string;
39
39
  /** Content source identifier for cache isolation (branch or release) */
40
40
  contentSourceId?: string;
41
+ /** Injectable factory for testing — bypasses real service construction */
42
+ servicesFactory?: (adapter: RuntimeAdapter) => RendererServices;
41
43
  }
42
44
 
43
45
  export interface RendererServices {
@@ -62,6 +64,7 @@ export class RendererLifecycle {
62
64
  private contentSourceId?: string;
63
65
  private services?: RendererServices;
64
66
  private adapter!: RuntimeAdapter;
67
+ private servicesFactory?: (adapter: RuntimeAdapter) => RendererServices;
65
68
 
66
69
  constructor(options: LifecycleOptions) {
67
70
  this.configManager = options.configManager;
@@ -69,6 +72,7 @@ export class RendererLifecycle {
69
72
  this.moduleServerUrl = options.moduleServerUrl;
70
73
  this.projectId = options.projectId;
71
74
  this.contentSourceId = options.contentSourceId;
75
+ this.servicesFactory = options.servicesFactory;
72
76
  }
73
77
 
74
78
  async initialize(): Promise<RendererServices> {
@@ -83,6 +87,13 @@ export class RendererLifecycle {
83
87
  this.adapter = await runtime.get();
84
88
  }
85
89
 
90
+ // Allow tests to bypass the full service graph construction
91
+ if (this.servicesFactory) {
92
+ this.services = this.servicesFactory(this.adapter);
93
+ logger.debug("Renderer services initialized via injected factory");
94
+ return this.services;
95
+ }
96
+
86
97
  const projectDir = this.configManager.getProjectDir();
87
98
  const mode = this.configManager.getMode();
88
99
  const debugMode = this.configManager.isDebugMode();
@@ -36,7 +36,7 @@ import type { PageResolver } from "../page-resolution/index.js";
36
36
  import type { LayoutOrchestrator } from "./layout.js";
37
37
  import type { SSROrchestrator } from "./ssr-orchestrator.js";
38
38
  import type { PageDataResponse, RenderOptions, RenderResult } from "./types.js";
39
- import { DataFetcher } from "../../data/index.js";
39
+ import { DataFetcher, type FetchDataOptions } from "../../data/index.js";
40
40
  import type { DataContext, PageWithData } from "../../data/types.js";
41
41
  import { clearSSRModuleCacheForProject } from "../../modules/react-loader/index.js";
42
42
  import { setupSSRGlobals } from "../ssr-globals.js";
@@ -303,8 +303,13 @@ export class RenderPipeline {
303
303
  Promise.all(
304
304
  dataJobs.map(async (job) => {
305
305
  try {
306
+ const jobPath = (job as LoadedModule & { path?: string }).path;
307
+ const fetchOptions: FetchDataOptions = {
308
+ modulePath: jobPath,
309
+ projectDir: this.config.projectDir,
310
+ };
306
311
  const result = await this.dataFetcher
307
- .fetchData(job.mod as PageWithData, dataContext, this.config.mode);
312
+ .fetchData(job.mod as PageWithData, dataContext, this.config.mode, fetchOptions);
308
313
  return { ...job, result, error: null as Error | null };
309
314
  } catch (error) {
310
315
  return { ...job, result: null, error: error as Error };
@@ -10,6 +10,8 @@ import { computeHash } from "../utils/index.js";
10
10
  import type { HTMLGenerationContext, HTMLGenerator } from "./html.js";
11
11
  import type { RenderOptions } from "./types.js";
12
12
  import { runWithHeadCollector } from "../../react/head-collector.js";
13
+ import { getWorkerPool, isSSRIsolationEnabled } from "../../security/sandbox/worker-pool.js";
14
+ import type { WorkerResponse } from "../../security/sandbox/worker-types.js";
13
15
 
14
16
  const logger = rendererLogger.component("ssr-orchestrator");
15
17
 
@@ -27,6 +29,24 @@ export interface SSRRenderingResult {
27
29
  ssrHash: string;
28
30
  }
29
31
 
32
+ /**
33
+ * Options for isolated SSR rendering through the Worker pool.
34
+ * When provided and SSR isolation is enabled, the rendering happens
35
+ * in a per-project Worker instead of the main process.
36
+ */
37
+ export interface SSRIsolationOptions {
38
+ /** Temp file path for the page component module */
39
+ pageModulePath: string;
40
+ /** Ordered layout module temp paths (innermost → outermost) */
41
+ layoutModulePaths: string[];
42
+ /** Page component props */
43
+ pageProps: Record<string, unknown>;
44
+ /** Layout props (one entry per layout, matching layoutModulePaths order) */
45
+ layoutProps: Record<string, unknown>[];
46
+ /** Project directory for worker scoping */
47
+ projectDir: string;
48
+ }
49
+
30
50
  function getElementTypeName(el: React.ReactElement | null | undefined): string {
31
51
  if (!el?.type) return "unknown";
32
52
  if (typeof el.type === "string") return el.type;
@@ -46,7 +66,18 @@ export class SSROrchestrator {
46
66
  pageElement: React.ReactElement,
47
67
  generationContext: Omit<HTMLGenerationContext, "html" | "ssrHash">,
48
68
  options?: RenderOptions,
69
+ isolationOptions?: SSRIsolationOptions,
49
70
  ): Promise<SSRRenderingResult> {
71
+ // Isolated SSR path: render in per-project Worker
72
+ if (
73
+ isSSRIsolationEnabled() &&
74
+ isolationOptions?.pageModulePath &&
75
+ isolationOptions?.projectDir
76
+ ) {
77
+ return this.performIsolatedSSR(generationContext, options, isolationOptions);
78
+ }
79
+
80
+ // Default path: render in main process
50
81
  logger.debug("performSSRRendering called", {
51
82
  elementType: getElementTypeName(pageElement),
52
83
  hasChildren: !!(pageElement.props as Record<string, unknown>)?.children,
@@ -133,6 +164,94 @@ export class SSROrchestrator {
133
164
  };
134
165
  }
135
166
 
167
+ /**
168
+ * Perform SSR rendering in an isolated per-project Worker.
169
+ *
170
+ * The Worker imports user modules from their temp file paths,
171
+ * constructs the React element tree, and renders to HTML.
172
+ * For streaming, the Worker sends chunks via postMessage.
173
+ */
174
+ private async performIsolatedSSR(
175
+ generationContext: Omit<HTMLGenerationContext, "html" | "ssrHash">,
176
+ options: RenderOptions | undefined,
177
+ isolation: SSRIsolationOptions,
178
+ ): Promise<SSRRenderingResult> {
179
+ const wantsStream = options?.delivery === "stream";
180
+ const pool = getWorkerPool();
181
+ const requestId = dntShim.crypto.randomUUID();
182
+
183
+ return withSpan(
184
+ "ssr.isolated_render",
185
+ async () => {
186
+ const worker = pool.getOrCreateWorker(isolation.projectDir, [isolation.projectDir]);
187
+
188
+ if (wantsStream) {
189
+ // Streaming mode: get a ReadableStream of chunks from the Worker
190
+ const stream = worker.executeStream({
191
+ type: "render-ssr",
192
+ id: requestId,
193
+ pageModulePath: isolation.pageModulePath,
194
+ layoutModulePaths: isolation.layoutModulePaths,
195
+ pageProps: isolation.pageProps,
196
+ layoutProps: isolation.layoutProps,
197
+ delivery: "stream",
198
+ });
199
+
200
+ const ssrHash = `stream-isolated-${Date.now()}`;
201
+
202
+ // Generate HTML stream using the framework's HTML generator
203
+ const finalStream = await this.config.htmlGenerator.generateHTMLStream(stream, {
204
+ ...generationContext,
205
+ ssrHash,
206
+ options: { ...generationContext.options, ...options },
207
+ collectedHead: undefined,
208
+ });
209
+
210
+ return { fullHtml: "", finalStream, ssrHash };
211
+ }
212
+
213
+ // String mode: render to HTML in Worker, get result back
214
+ const workerResponse: WorkerResponse = await worker.execute({
215
+ type: "render-ssr",
216
+ id: requestId,
217
+ pageModulePath: isolation.pageModulePath,
218
+ layoutModulePaths: isolation.layoutModulePaths,
219
+ pageProps: isolation.pageProps,
220
+ layoutProps: isolation.layoutProps,
221
+ delivery: "string",
222
+ });
223
+
224
+ if (workerResponse.type === "error") {
225
+ const err = new Error(workerResponse.error.message);
226
+ err.name = workerResponse.error.name;
227
+ throw err;
228
+ }
229
+
230
+ if (workerResponse.type !== "ssr-result") {
231
+ throw new Error(`Unexpected worker response type: ${workerResponse.type}`);
232
+ }
233
+
234
+ const html = workerResponse.html;
235
+ const ssrHash = await computeHash(html);
236
+
237
+ const fullHtml = await this.config.htmlGenerator.generateFullHTML({
238
+ ...generationContext,
239
+ html,
240
+ ssrHash,
241
+ options: { ...generationContext.options, ...options },
242
+ collectedHead: undefined,
243
+ });
244
+
245
+ return { fullHtml, finalStream: null, ssrHash };
246
+ },
247
+ {
248
+ "ssr.isolated": true,
249
+ "ssr.wants_stream": wantsStream,
250
+ "ssr.project_dir": isolation.projectDir,
251
+ },
252
+ );
253
+ }
254
+
136
255
  private createStream(html: string): ReadableStream | null {
137
256
  try {
138
257
  return new dntShim.Response(html).body ?? null;
@@ -13,7 +13,7 @@ import { ApiRouteMatcher, type RouteMatch } from "./api-route-matcher.js";
13
13
  import type { APIRoute } from "./module-loader/types.js";
14
14
  import { loadHandlerModule } from "./module-loader/loader.js";
15
15
  import { discoverAppRoutes, discoverPagesRoutes } from "./route-discovery.js";
16
- import { executeAppRoute, executePagesRoute } from "./route-executor.js";
16
+ import { executeAppRoute, executePagesRoute, type ExecuteRouteOptions } from "./route-executor.js";
17
17
  import { withSpan } from "../../observability/tracing/otlp-setup.js";
18
18
 
19
19
  /** Max entries in the loaded-handler LRU cache */
@@ -188,9 +188,22 @@ export class APIRouteHandler {
188
188
  // Note: Cannot use path-based detection (/app/) as projectDir may be '/app' in production
189
189
  const isAppRoute = /\/route\.(ts|js|tsx|jsx)$/.test(match.route.page);
190
190
 
191
+ const isolationOptions: ExecuteRouteOptions = {
192
+ modulePath: match.route.page,
193
+ projectDir: this.projectDir,
194
+ };
195
+
191
196
  const response = isAppRoute
192
- ? await executeAppRoute(handler, request, match, pathname, adapter)
193
- : await executePagesRoute(handler, request, match, pathname, adapter, this.projectDir);
197
+ ? await executeAppRoute(handler, request, match, pathname, adapter, isolationOptions)
198
+ : await executePagesRoute(
199
+ handler,
200
+ request,
201
+ match,
202
+ pathname,
203
+ adapter,
204
+ this.projectDir,
205
+ isolationOptions,
206
+ );
194
207
 
195
208
  const corsResponse = await applyCORSHeaders({
196
209
  request,
@@ -1,6 +1,6 @@
1
1
  import * as dntShim from "../../../_dnt.shims.js";
2
2
  import type { FileSystemAdapter, RuntimeAdapter } from "../../platform/adapters/base.js";
3
- import { createContext, normalizeParams } from "./context-builder.js";
3
+ import { createContext, normalizeParams, parseCookies } from "./context-builder.js";
4
4
  import type { RouteMatch } from "./api-route-matcher.js";
5
5
  import { createError, toError } from "../../errors/veryfront-error.js";
6
6
  import type {
@@ -20,6 +20,16 @@ import { errorToRFC9457Response } from "../../errors/middleware/http-error-bound
20
20
  import { serverLogger as logger } from "../../utils/index.js";
21
21
  import { isDevelopment as isDevelopmentEnv } from "../../build/config/environment.js";
22
22
  import type { HandlerContext } from "../../types/index.js";
23
+ import {
24
+ getWorkerPool,
25
+ isWorkerIsolationEnabled,
26
+ } from "../../security/sandbox/worker-pool.js";
27
+ import type {
28
+ SerializedRequest,
29
+ SerializedResponse,
30
+ WorkerResponse,
31
+ } from "../../security/sandbox/worker-types.js";
32
+ import { getProjectEnvSnapshot } from "../../server/project-env/storage.js";
23
33
 
24
34
  function isDevelopment(adapter: RuntimeAdapter): boolean {
25
35
  const env = adapter.env.get("MODE") ??
@@ -115,13 +125,206 @@ function toHeadResponse(response: dntShim.Response): dntShim.Response {
115
125
  return new dntShim.Response(null, { status: response.status, headers: response.headers });
116
126
  }
117
127
 
128
+ // ---------------------------------------------------------------------------
129
+ // Worker Isolation Helpers
130
+ // ---------------------------------------------------------------------------
131
+
132
+ async function serializeRequest(request: dntShim.Request): Promise<SerializedRequest> {
133
+ const body = request.body ? new Uint8Array(await request.arrayBuffer()) : null;
134
+ return {
135
+ url: request.url,
136
+ method: request.method,
137
+ headers: [...request.headers.entries()],
138
+ body,
139
+ };
140
+ }
141
+
142
+ function deserializeResponse(s: SerializedResponse): dntShim.Response {
143
+ return new dntShim.Response(s.body as dntShim.BodyInit | null, {
144
+ status: s.status,
145
+ statusText: s.statusText,
146
+ headers: s.headers,
147
+ });
148
+ }
149
+
150
+ function workerResponseToResponse(
151
+ workerResponse: WorkerResponse,
152
+ pathname: string,
153
+ adapter: RuntimeAdapter,
154
+ ): dntShim.Response {
155
+ if (workerResponse.type === "error") {
156
+ const { error } = workerResponse;
157
+ logger.error(`API route error in ${pathname} (worker):`, error.message);
158
+
159
+ // If the worker serialized RFC 9457 fields, return them directly
160
+ // to preserve the original status code, type, and detail.
161
+ if (error.status && error.type) {
162
+ return dntShim.Response.json(
163
+ {
164
+ type: error.type,
165
+ title: error.name,
166
+ status: error.status,
167
+ detail: error.detail ?? error.message,
168
+ instance: pathname,
169
+ },
170
+ { status: error.status },
171
+ );
172
+ }
173
+
174
+ const ctx = { isLocalProject: isDevelopment(adapter) } as HandlerContext;
175
+ const req = new dntShim.Request(`http://localhost${pathname}`);
176
+ const err = new Error(error.message);
177
+ err.name = error.name;
178
+ return errorToRFC9457Response(err, ctx, req);
179
+ }
180
+
181
+ if (workerResponse.type === "result") {
182
+ return deserializeResponse(workerResponse.response);
183
+ }
184
+
185
+ // data-result type is not expected in API route execution
186
+ throw new Error(`Unexpected worker response type: ${workerResponse.type}`);
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Isolated Execution (Worker Path)
191
+ // ---------------------------------------------------------------------------
192
+
193
+ function executeAppRouteIsolated(
194
+ modulePath: string,
195
+ request: dntShim.Request,
196
+ match: RouteMatch,
197
+ pathname: string,
198
+ adapter: RuntimeAdapter,
199
+ projectDir: string,
200
+ ): Promise<dntShim.Response> {
201
+ const method = request.method.toUpperCase() as HTTPMethod;
202
+
203
+ return withSpan(
204
+ "api.executeAppRoute.isolated",
205
+ async () => {
206
+ try {
207
+ const pool = getWorkerPool();
208
+ const serialized = await serializeRequest(request);
209
+
210
+ const workerResponse = await pool.execute(
211
+ projectDir,
212
+ [projectDir],
213
+ {
214
+ type: "execute-app-route",
215
+ id: dntShim.crypto.randomUUID(),
216
+ modulePath,
217
+ method,
218
+ request: serialized,
219
+ params: match.params,
220
+ projectDir,
221
+ projectEnv: getProjectEnvSnapshot(),
222
+ },
223
+ );
224
+
225
+ const response = workerResponseToResponse(workerResponse, pathname, adapter);
226
+ return method === "HEAD" ? toHeadResponse(response) : response;
227
+ } catch (error) {
228
+ return handleAPIError(error, pathname, adapter);
229
+ }
230
+ },
231
+ {
232
+ "http.method": method,
233
+ "http.path": pathname,
234
+ "api.route.pattern": match.route.pattern,
235
+ "api.isolated": true,
236
+ },
237
+ );
238
+ }
239
+
240
+ function executePagesRouteIsolated(
241
+ modulePath: string,
242
+ request: dntShim.Request,
243
+ match: RouteMatch,
244
+ pathname: string,
245
+ adapter: RuntimeAdapter,
246
+ projectDir: string,
247
+ ): Promise<dntShim.Response> {
248
+ const method = request.method as string;
249
+
250
+ return withSpan(
251
+ "api.executePagesRoute.isolated",
252
+ async () => {
253
+ try {
254
+ const pool = getWorkerPool();
255
+ const body = request.body ? new Uint8Array(await request.arrayBuffer()) : null;
256
+
257
+ const workerResponse = await pool.execute(
258
+ projectDir,
259
+ [projectDir],
260
+ {
261
+ type: "execute-pages-route",
262
+ id: dntShim.crypto.randomUUID(),
263
+ modulePath,
264
+ method,
265
+ context: {
266
+ url: request.url,
267
+ method: request.method,
268
+ headers: [...request.headers.entries()],
269
+ body,
270
+ params: match.params,
271
+ cookies: parseCookies(request.headers.get("cookie") ?? ""),
272
+ },
273
+ projectDir,
274
+ projectEnv: getProjectEnvSnapshot(),
275
+ },
276
+ );
277
+
278
+ return workerResponseToResponse(workerResponse, pathname, adapter);
279
+ } catch (error) {
280
+ return handleAPIError(error, pathname, adapter);
281
+ }
282
+ },
283
+ {
284
+ "http.method": method,
285
+ "http.path": pathname,
286
+ "api.route.pattern": match.route.pattern,
287
+ "api.isolated": true,
288
+ },
289
+ );
290
+ }
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // Public API
294
+ // ---------------------------------------------------------------------------
295
+
296
+ export interface ExecuteRouteOptions {
297
+ /** Absolute path to the handler module on disk (for isolated execution) */
298
+ modulePath?: string;
299
+ /** Project directory (for isolated execution scope) */
300
+ projectDir?: string;
301
+ }
302
+
118
303
  export function executeAppRoute(
119
304
  handler: APIRoute,
120
305
  request: dntShim.Request,
121
306
  match: RouteMatch,
122
307
  pathname: string,
123
308
  adapter: RuntimeAdapter,
309
+ options?: ExecuteRouteOptions,
124
310
  ): Promise<dntShim.Response> {
311
+ // Isolated path: execute in per-project Worker, fall back to main process on error
312
+ if (
313
+ isWorkerIsolationEnabled() &&
314
+ options?.modulePath &&
315
+ options?.projectDir
316
+ ) {
317
+ return executeAppRouteIsolated(
318
+ options.modulePath,
319
+ request,
320
+ match,
321
+ pathname,
322
+ adapter,
323
+ options.projectDir,
324
+ );
325
+ }
326
+
327
+ // Default path: execute in main process (existing behavior)
125
328
  const method = request.method.toUpperCase() as HTTPMethod;
126
329
 
127
330
  return withSpan(
@@ -158,7 +361,25 @@ export function executePagesRoute(
158
361
  pathname: string,
159
362
  adapter: RuntimeAdapter,
160
363
  projectDir?: string,
364
+ options?: ExecuteRouteOptions,
161
365
  ): Promise<dntShim.Response> {
366
+ // Isolated path: execute in per-project Worker, fall back to main process on error
367
+ if (
368
+ isWorkerIsolationEnabled() &&
369
+ options?.modulePath &&
370
+ (options?.projectDir ?? projectDir)
371
+ ) {
372
+ return executePagesRouteIsolated(
373
+ options.modulePath,
374
+ request,
375
+ match,
376
+ pathname,
377
+ adapter,
378
+ options.projectDir ?? projectDir!,
379
+ );
380
+ }
381
+
382
+ // Default path: execute in main process (existing behavior)
162
383
  const method = request.method as keyof APIRoute;
163
384
 
164
385
  return withSpan(
@@ -41,3 +41,14 @@ export const BUILD_HELPER_PERMISSIONS = [
41
41
  "--allow-write",
42
42
  "--allow-env",
43
43
  ] as const;
44
+
45
+ /**
46
+ * RENDER_WORKER — Per-project Worker for isolated code execution.
47
+ * Read-only filesystem (transformed modules), network (data fetchers),
48
+ * env (API keys and config). No subprocess/ffi/sys.
49
+ */
50
+ export const RENDER_WORKER_PERMISSIONS = [
51
+ "--allow-read",
52
+ "--allow-net",
53
+ "--allow-env",
54
+ ] as const;