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
@@ -0,0 +1,451 @@
1
+ /**
2
+ * Worker Pool Manager
3
+ *
4
+ * Manages a pool of per-project Deno Workers for tenant-isolated code execution.
5
+ * Uses LRU eviction when the pool exceeds its capacity, idle timeout for
6
+ * cleanup, and health checks for reliability.
7
+ *
8
+ * @module security/sandbox/worker-pool
9
+ */
10
+ import * as dntShim from "../../../_dnt.shims.js";
11
+
12
+
13
+ import { serverLogger } from "../../utils/index.js";
14
+ import { getEnvBoolean, getEnvNumber, unrefTimer } from "../../platform/compat/process.js";
15
+ import { withSpan } from "../../observability/tracing/otlp-setup.js";
16
+ import { SECURITY_VIOLATION } from "../../errors/index.js";
17
+ import { ProjectWorker } from "./project-worker.js";
18
+ import { buildWorkerPermissions } from "./worker-permissions.js";
19
+ import type { WorkerPoolConfig, WorkerRequest, WorkerResponse } from "./worker-types.js";
20
+ import { DEFAULT_WORKER_POOL_CONFIG } from "./worker-types.js";
21
+
22
+ const logger = serverLogger.component("worker-pool");
23
+
24
+ interface PoolEntry {
25
+ worker: ProjectWorker;
26
+ lastAccessedAt: number;
27
+ createdAt: number;
28
+ }
29
+
30
+ export class WorkerPool {
31
+ private pool = new Map<string, PoolEntry>();
32
+ private recycling = new Set<string>();
33
+ private config: WorkerPoolConfig;
34
+
35
+ private cleanupInterval: ReturnType<typeof dntShim.setInterval> | undefined;
36
+ private healthCheckInterval: ReturnType<typeof dntShim.setInterval> | undefined;
37
+
38
+ constructor(config: Partial<WorkerPoolConfig> = {}) {
39
+ this.config = { ...DEFAULT_WORKER_POOL_CONFIG, ...config };
40
+ this.startCleanup();
41
+ this.startHealthChecks();
42
+ }
43
+
44
+ /**
45
+ * Get or create a worker for the given project.
46
+ */
47
+ getOrCreateWorker(projectId: string, readPaths: string[]): ProjectWorker {
48
+ const existing = this.pool.get(projectId);
49
+ if (
50
+ existing && existing.worker.status !== "crashed" && existing.worker.status !== "terminated"
51
+ ) {
52
+ existing.lastAccessedAt = Date.now();
53
+ return existing.worker;
54
+ }
55
+
56
+ // If an existing entry is crashed/terminated, clean it up
57
+ if (existing) {
58
+ existing.worker.terminate();
59
+ this.pool.delete(projectId);
60
+ }
61
+
62
+ // Evict LRU if at capacity
63
+ this.evictIfNeeded();
64
+
65
+ const permissions = buildWorkerPermissions(readPaths);
66
+ const worker = new ProjectWorker({
67
+ projectId,
68
+ permissions,
69
+ requestTimeoutMs: this.config.requestTimeoutMs,
70
+ });
71
+
72
+ worker.start();
73
+
74
+ const now = Date.now();
75
+ this.pool.set(projectId, {
76
+ worker,
77
+ lastAccessedAt: now,
78
+ createdAt: now,
79
+ });
80
+
81
+ logger.debug("Worker created", {
82
+ projectId,
83
+ poolSize: this.pool.size,
84
+ });
85
+
86
+ return worker;
87
+ }
88
+
89
+ /**
90
+ * Execute a request in a project worker. Convenience method that
91
+ * combines getOrCreateWorker + execute.
92
+ */
93
+ execute(
94
+ projectId: string,
95
+ readPaths: string[],
96
+ request: WorkerRequest,
97
+ ): Promise<WorkerResponse> {
98
+ // Validate modulePath is within allowed read paths (defense-in-depth)
99
+ if ("modulePath" in request && request.modulePath) {
100
+ const modulePath = request.modulePath;
101
+ const isAllowed = readPaths.some((p) => modulePath.startsWith(p));
102
+ if (!isAllowed) {
103
+ return Promise.reject(
104
+ SECURITY_VIOLATION.create({
105
+ detail:
106
+ `Module path "${modulePath}" is outside allowed read paths for project "${projectId}"`,
107
+ }),
108
+ );
109
+ }
110
+ }
111
+
112
+ return withSpan(
113
+ "workerPool.execute",
114
+ async () => {
115
+ const worker = this.getOrCreateWorker(projectId, readPaths);
116
+
117
+ // Check if worker should be recycled (request count or age)
118
+ const entry = this.pool.get(projectId);
119
+ const shouldRecycle = worker.requestCount >= this.config.maxRequestsPerWorker ||
120
+ (entry && Date.now() - entry.createdAt > this.config.maxWorkerAgeMs);
121
+
122
+ if (shouldRecycle && !this.recycling.has(projectId)) {
123
+ this.recycling.add(projectId);
124
+ try {
125
+ logger.debug("Recycling worker", {
126
+ projectId,
127
+ requestCount: worker.requestCount,
128
+ ageMs: entry ? Date.now() - entry.createdAt : 0,
129
+ reason: worker.requestCount >= this.config.maxRequestsPerWorker
130
+ ? "request_count"
131
+ : "age",
132
+ });
133
+ this.evictWorker(projectId);
134
+ const fresh = this.getOrCreateWorker(projectId, readPaths);
135
+ return fresh.execute(request);
136
+ } finally {
137
+ this.recycling.delete(projectId);
138
+ }
139
+ }
140
+
141
+ return worker.execute(request);
142
+ },
143
+ { "workerPool.projectId": projectId },
144
+ );
145
+ }
146
+
147
+ /**
148
+ * Evict a specific project's worker.
149
+ */
150
+ evictWorker(projectId: string): void {
151
+ const entry = this.pool.get(projectId);
152
+ if (!entry) return;
153
+
154
+ entry.worker.terminate();
155
+ this.pool.delete(projectId);
156
+
157
+ logger.debug("Worker evicted", { projectId, poolSize: this.pool.size });
158
+ }
159
+
160
+ /**
161
+ * Get pool statistics for monitoring.
162
+ */
163
+ getStats(): {
164
+ poolSize: number;
165
+ maxPoolSize: number;
166
+ memoryBudgetMb: number;
167
+ workers: Record<string, {
168
+ status: string;
169
+ requestCount: number;
170
+ hasPending: boolean;
171
+ idleMs: number;
172
+ ageMs: number;
173
+ }>;
174
+ } {
175
+ const workers: Record<string, {
176
+ status: string;
177
+ requestCount: number;
178
+ hasPending: boolean;
179
+ idleMs: number;
180
+ ageMs: number;
181
+ }> = {};
182
+ const now = Date.now();
183
+
184
+ for (const [id, entry] of this.pool) {
185
+ workers[id] = {
186
+ status: entry.worker.status,
187
+ requestCount: entry.worker.requestCount,
188
+ hasPending: entry.worker.hasPendingRequests,
189
+ idleMs: now - entry.lastAccessedAt,
190
+ ageMs: now - entry.createdAt,
191
+ };
192
+ }
193
+
194
+ return {
195
+ poolSize: this.pool.size,
196
+ maxPoolSize: this.config.maxPoolSize,
197
+ memoryBudgetMb: this.config.memoryBudgetMb,
198
+ workers,
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Get aggregate metrics suitable for Prometheus exposition.
204
+ */
205
+ getMetrics(): {
206
+ /** Current number of active workers */
207
+ workerPoolSize: number;
208
+ /** Number of workers at capacity (max pool size) */
209
+ workerPoolCapacity: number;
210
+ /** Total requests processed across all workers */
211
+ totalRequestsProcessed: number;
212
+ /** Number of workers with pending requests (busy) */
213
+ busyWorkers: number;
214
+ /** Number of crashed workers (cleaned up at next health check) */
215
+ crashedWorkers: number;
216
+ } {
217
+ let totalRequests = 0;
218
+ let busy = 0;
219
+ let crashed = 0;
220
+
221
+ for (const [, entry] of this.pool) {
222
+ totalRequests += entry.worker.requestCount;
223
+ if (entry.worker.hasPendingRequests) busy++;
224
+ if (entry.worker.status === "crashed") crashed++;
225
+ }
226
+
227
+ return {
228
+ workerPoolSize: this.pool.size,
229
+ workerPoolCapacity: this.config.maxPoolSize,
230
+ totalRequestsProcessed: totalRequests,
231
+ busyWorkers: busy,
232
+ crashedWorkers: crashed,
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Shutdown the pool. Terminates all workers and stops timers.
238
+ */
239
+ shutdown(): void {
240
+ if (this.cleanupInterval) clearInterval(this.cleanupInterval);
241
+ if (this.healthCheckInterval) clearInterval(this.healthCheckInterval);
242
+
243
+ for (const [, entry] of this.pool) {
244
+ entry.worker.terminate();
245
+ }
246
+
247
+ this.pool.clear();
248
+ logger.debug("Worker pool shut down");
249
+ }
250
+
251
+ // -----------------------------------------------------------------------
252
+ // Private — Cleanup & Eviction
253
+ // -----------------------------------------------------------------------
254
+
255
+ private startCleanup(): void {
256
+ // Run idle eviction every 30 seconds
257
+ this.cleanupInterval = dntShim.setInterval(() => {
258
+ this.evictIdleWorkers();
259
+ }, 30_000);
260
+
261
+ unrefTimer(this.cleanupInterval);
262
+ }
263
+
264
+ private startHealthChecks(): void {
265
+ this.healthCheckInterval = dntShim.setInterval(() => {
266
+ void this.checkHealth();
267
+ }, this.config.healthCheckIntervalMs);
268
+
269
+ unrefTimer(this.healthCheckInterval);
270
+ }
271
+
272
+ private evictIdleWorkers(): void {
273
+ const now = Date.now();
274
+
275
+ for (const [projectId, entry] of this.pool) {
276
+ const idleTime = now - entry.lastAccessedAt;
277
+
278
+ if (idleTime > this.config.idleTimeoutMs && !entry.worker.hasPendingRequests) {
279
+ entry.worker.terminate();
280
+ this.pool.delete(projectId);
281
+
282
+ logger.debug("Evicted idle worker", {
283
+ projectId,
284
+ idleMs: idleTime,
285
+ poolSize: this.pool.size,
286
+ });
287
+ }
288
+ }
289
+ }
290
+
291
+ private evictIfNeeded(): void {
292
+ if (this.pool.size < this.config.maxPoolSize) return;
293
+
294
+ // Find the LRU entry that has no pending requests
295
+ let lruId: string | null = null;
296
+ let lruTime = Infinity;
297
+
298
+ for (const [projectId, entry] of this.pool) {
299
+ if (!entry.worker.hasPendingRequests && entry.lastAccessedAt < lruTime) {
300
+ lruTime = entry.lastAccessedAt;
301
+ lruId = projectId;
302
+ }
303
+ }
304
+
305
+ if (lruId) {
306
+ this.evictWorker(lruId);
307
+ } else {
308
+ // All workers have pending requests — force evict the oldest
309
+ for (const [projectId, entry] of this.pool) {
310
+ if (entry.lastAccessedAt < lruTime) {
311
+ lruTime = entry.lastAccessedAt;
312
+ lruId = projectId;
313
+ }
314
+ }
315
+ if (lruId) this.evictWorker(lruId);
316
+ }
317
+ }
318
+
319
+ private async checkHealth(): Promise<void> {
320
+ for (const [projectId, entry] of this.pool) {
321
+ if (entry.worker.status === "crashed" || entry.worker.status === "terminated") {
322
+ this.pool.delete(projectId);
323
+ continue;
324
+ }
325
+
326
+ const healthy = await entry.worker.isHealthy();
327
+ if (!healthy) {
328
+ logger.warn("Worker failed health check", { projectId });
329
+ entry.worker.terminate();
330
+ this.pool.delete(projectId);
331
+ }
332
+ }
333
+
334
+ // Evict oldest workers when under memory pressure
335
+ this.evictUnderMemoryPressure();
336
+ }
337
+
338
+ /**
339
+ * Evict workers when the process is under memory pressure.
340
+ * Uses the global heap stats — if heap usage is above a threshold,
341
+ * evict idle workers starting with the oldest to free memory.
342
+ */
343
+ private evictUnderMemoryPressure(): void {
344
+ // Lazy import to avoid circular deps — this is only called during health checks
345
+ try {
346
+ // deno-lint-ignore no-explicit-any
347
+ const { getHeapStats } = (dntShim.dntGlobalThis as any).__veryfront_heap_stats ?? {};
348
+ if (!getHeapStats) return;
349
+
350
+ const { heapUsedPercent } = getHeapStats();
351
+ if (heapUsedPercent < 70) return; // Only act above 70%
352
+
353
+ // Sort workers by last access time (oldest first)
354
+ const entries = [...this.pool.entries()]
355
+ .filter(([, e]) => !e.worker.hasPendingRequests)
356
+ .sort(([, a], [, b]) => a.lastAccessedAt - b.lastAccessedAt);
357
+
358
+ // Evict up to 25% of idle workers
359
+ const toEvict = Math.max(1, Math.ceil(entries.length * 0.25));
360
+ for (let i = 0; i < toEvict && i < entries.length; i++) {
361
+ const projectId = entries[i]![0];
362
+ this.evictWorker(projectId);
363
+ logger.debug("Evicted worker due to memory pressure", {
364
+ projectId,
365
+ heapUsedPercent,
366
+ poolSize: this.pool.size,
367
+ });
368
+ }
369
+ } catch {
370
+ // getHeapStats may not be available in all environments
371
+ }
372
+ }
373
+ }
374
+
375
+ // ---------------------------------------------------------------------------
376
+ // Singleton & Feature Flag
377
+ // ---------------------------------------------------------------------------
378
+
379
+ // Cache feature flag results to avoid env lookups on every request
380
+ let _flagsResolved = false;
381
+ let _apiIsolation = false;
382
+ let _dataIsolation = false;
383
+ let _ssrIsolation = false;
384
+
385
+ function resolveFlags(): void {
386
+ if (_flagsResolved) return;
387
+ const master = getEnvBoolean("WORKER_ISOLATION_ENABLED", false);
388
+ _apiIsolation = master && getEnvBoolean("WORKER_ISOLATION_API", false);
389
+ _dataIsolation = master && getEnvBoolean("WORKER_ISOLATION_DATA", false);
390
+ _ssrIsolation = master && getEnvBoolean("WORKER_ISOLATION_SSR", false);
391
+ _flagsResolved = true;
392
+ }
393
+
394
+ /**
395
+ * Whether worker isolation is enabled for API routes.
396
+ * Controlled by WORKER_ISOLATION_API=1 (or WORKER_ISOLATION_ENABLED=1 as master switch).
397
+ */
398
+ export function isWorkerIsolationEnabled(): boolean {
399
+ resolveFlags();
400
+ return _apiIsolation;
401
+ }
402
+
403
+ /**
404
+ * Whether worker isolation is enabled for data fetchers (getServerData).
405
+ * Controlled by WORKER_ISOLATION_DATA=1 (requires WORKER_ISOLATION_ENABLED=1).
406
+ */
407
+ export function isDataIsolationEnabled(): boolean {
408
+ resolveFlags();
409
+ return _dataIsolation;
410
+ }
411
+
412
+ /**
413
+ * Whether worker isolation is enabled for SSR rendering.
414
+ * Controlled by WORKER_ISOLATION_SSR=1 (requires WORKER_ISOLATION_ENABLED=1).
415
+ */
416
+ export function isSSRIsolationEnabled(): boolean {
417
+ resolveFlags();
418
+ return _ssrIsolation;
419
+ }
420
+
421
+ /** Lazy singleton — created on first use when isolation is enabled */
422
+ let _pool: WorkerPool | null = null;
423
+
424
+ export function getWorkerPool(): WorkerPool {
425
+ if (!_pool) {
426
+ _pool = new WorkerPool({
427
+ maxPoolSize: getEnvNumber("WORKER_MAX_POOL_SIZE") ?? DEFAULT_WORKER_POOL_CONFIG.maxPoolSize,
428
+ idleTimeoutMs: getEnvNumber("WORKER_IDLE_TIMEOUT_MS") ??
429
+ DEFAULT_WORKER_POOL_CONFIG.idleTimeoutMs,
430
+ requestTimeoutMs: getEnvNumber("WORKER_REQUEST_TIMEOUT_MS") ??
431
+ DEFAULT_WORKER_POOL_CONFIG.requestTimeoutMs,
432
+ maxRequestsPerWorker: getEnvNumber("WORKER_MAX_REQUESTS_PER_WORKER") ??
433
+ DEFAULT_WORKER_POOL_CONFIG.maxRequestsPerWorker,
434
+ maxWorkerAgeMs: getEnvNumber("WORKER_MAX_AGE_MS") ??
435
+ DEFAULT_WORKER_POOL_CONFIG.maxWorkerAgeMs,
436
+ memoryBudgetMb: getEnvNumber("WORKER_MEMORY_BUDGET_MB") ??
437
+ DEFAULT_WORKER_POOL_CONFIG.memoryBudgetMb,
438
+ });
439
+ }
440
+ return _pool;
441
+ }
442
+
443
+ /** Reset the singleton and cached flags — for testing only */
444
+ export function __resetPoolForTests(): void {
445
+ _pool?.shutdown();
446
+ _pool = null;
447
+ _flagsResolved = false;
448
+ _apiIsolation = false;
449
+ _dataIsolation = false;
450
+ _ssrIsolation = false;
451
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Worker Isolation Types
3
+ *
4
+ * Shared type definitions for the worker isolation system.
5
+ * Used by both the main process and worker script.
6
+ *
7
+ * @module security/sandbox/worker-types
8
+ */
9
+
10
+ /**
11
+ * Serialized request data that can cross the Worker boundary via postMessage.
12
+ * We cannot send a full Request object (it's not structured-cloneable),
13
+ * so we extract the essential fields.
14
+ */
15
+ export interface SerializedRequest {
16
+ url: string;
17
+ method: string;
18
+ headers: [string, string][];
19
+ body: Uint8Array | null;
20
+ }
21
+
22
+ /**
23
+ * Serialized API context for Pages Router routes.
24
+ */
25
+ export interface SerializedPagesContext {
26
+ url: string;
27
+ method: string;
28
+ headers: [string, string][];
29
+ body: Uint8Array | null;
30
+ params: Record<string, string | string[]>;
31
+ cookies: Record<string, string>;
32
+ }
33
+
34
+ /**
35
+ * Serialized response data that can cross the Worker boundary.
36
+ */
37
+ export interface SerializedResponse {
38
+ status: number;
39
+ statusText: string;
40
+ headers: [string, string][];
41
+ body: Uint8Array | null;
42
+ }
43
+
44
+ /**
45
+ * Serialized error for cross-boundary transport.
46
+ */
47
+ export interface SerializedError {
48
+ message: string;
49
+ name: string;
50
+ stack?: string;
51
+ /** RFC 9457 fields if the error originated from VFError */
52
+ type?: string;
53
+ status?: number;
54
+ detail?: string;
55
+ }
56
+
57
+ /**
58
+ * Serialized DataContext for data fetcher isolation.
59
+ * Request and URL are not structured-cloneable, so we serialize them.
60
+ */
61
+ export interface SerializedDataContext {
62
+ params: Record<string, string | string[]>;
63
+ /** URLSearchParams.toString() */
64
+ query: string;
65
+ request: SerializedRequest;
66
+ /** URL.toString() */
67
+ url: string;
68
+ }
69
+
70
+ /**
71
+ * Serialized DataResult — plain JSON, fully structured-cloneable.
72
+ */
73
+ export interface SerializedDataResult {
74
+ props?: unknown;
75
+ redirect?: { destination: string; permanent?: boolean };
76
+ notFound?: boolean;
77
+ revalidate?: number | false;
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Worker Request / Response Protocol
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export type WorkerRequest =
85
+ | ExecuteAppRouteRequest
86
+ | ExecutePagesRouteRequest
87
+ | FetchDataRequest
88
+ | RenderSSRRequest;
89
+
90
+ export interface ExecuteAppRouteRequest {
91
+ type: "execute-app-route";
92
+ id: string;
93
+ modulePath: string;
94
+ method: string;
95
+ request: SerializedRequest;
96
+ params: Record<string, string | string[]>;
97
+ projectDir: string;
98
+ /** Per-project env var overlay for multi-tenant proxy mode */
99
+ projectEnv?: Record<string, string>;
100
+ }
101
+
102
+ export interface ExecutePagesRouteRequest {
103
+ type: "execute-pages-route";
104
+ id: string;
105
+ modulePath: string;
106
+ method: string;
107
+ context: SerializedPagesContext;
108
+ projectDir: string;
109
+ /** Per-project env var overlay for multi-tenant proxy mode */
110
+ projectEnv?: Record<string, string>;
111
+ }
112
+
113
+ export interface FetchDataRequest {
114
+ type: "fetch-data";
115
+ id: string;
116
+ modulePath: string;
117
+ context: SerializedDataContext;
118
+ }
119
+
120
+ export interface RenderSSRRequest {
121
+ type: "render-ssr";
122
+ id: string;
123
+ /** Temp file path for the page component module */
124
+ pageModulePath: string;
125
+ /** Ordered layout module temp paths (innermost → outermost) */
126
+ layoutModulePaths: string[];
127
+ /** Page component props (JSON-serializable) */
128
+ pageProps: Record<string, unknown>;
129
+ /** Layout props keyed by layout index (matching layoutModulePaths order) */
130
+ layoutProps: Record<string, unknown>[];
131
+ /** Rendering delivery mode */
132
+ delivery: "string" | "stream";
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Streaming SSR Protocol
137
+ // ---------------------------------------------------------------------------
138
+
139
+ export interface WorkerStreamChunk {
140
+ type: "stream-chunk";
141
+ id: string;
142
+ chunk: Uint8Array;
143
+ }
144
+
145
+ export interface WorkerStreamEnd {
146
+ type: "stream-end";
147
+ id: string;
148
+ }
149
+
150
+ export type WorkerResponse =
151
+ | WorkerResultResponse
152
+ | WorkerDataResultResponse
153
+ | WorkerSSRResultResponse
154
+ | WorkerErrorResponse;
155
+
156
+ export interface WorkerSSRResultResponse {
157
+ type: "ssr-result";
158
+ id: string;
159
+ html: string;
160
+ }
161
+
162
+ export interface WorkerResultResponse {
163
+ type: "result";
164
+ id: string;
165
+ response: SerializedResponse;
166
+ }
167
+
168
+ export interface WorkerDataResultResponse {
169
+ type: "data-result";
170
+ id: string;
171
+ result: SerializedDataResult;
172
+ }
173
+
174
+ export interface WorkerErrorResponse {
175
+ type: "error";
176
+ id: string;
177
+ error: SerializedError;
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Worker Pool Configuration
182
+ // ---------------------------------------------------------------------------
183
+
184
+ export interface WorkerPoolConfig {
185
+ /** Maximum number of concurrent workers (default: 20) */
186
+ maxPoolSize: number;
187
+ /** Idle timeout before evicting a worker (default: 300_000 = 5 minutes) */
188
+ idleTimeoutMs: number;
189
+ /** Per-request timeout inside the worker (default: 30_000) */
190
+ requestTimeoutMs: number;
191
+ /** Health check interval (default: 30_000) */
192
+ healthCheckIntervalMs: number;
193
+ /** Maximum requests before recycling a worker (default: 1000) */
194
+ maxRequestsPerWorker: number;
195
+ /** Maximum age of a worker in ms before recycling (default: 600_000 = 10 minutes) */
196
+ maxWorkerAgeMs: number;
197
+ /** Per-worker memory budget in MB (default: 64). Workers exceeding this are evicted. */
198
+ memoryBudgetMb: number;
199
+ }
200
+
201
+ export const DEFAULT_WORKER_POOL_CONFIG: WorkerPoolConfig = {
202
+ maxPoolSize: 20,
203
+ idleTimeoutMs: 300_000,
204
+ requestTimeoutMs: 30_000,
205
+ healthCheckIntervalMs: 30_000,
206
+ maxRequestsPerWorker: 1_000,
207
+ maxWorkerAgeMs: 600_000,
208
+ memoryBudgetMb: 64,
209
+ };
@@ -26,7 +26,11 @@ import { serverLogger } from "../../../../utils/index.js";
26
26
  import { endRequest, startRequest } from "../../../../utils/index.js";
27
27
  import { tryNotFoundFallback } from "./not-found-fallback.js";
28
28
  import { tryErrorPageFallback } from "./error-page-fallback.js";
29
- import { type SSRRenderResult, SSRService } from "../../../services/rendering/ssr.service.js";
29
+ import {
30
+ type SSRRenderResult,
31
+ SSRService,
32
+ type SSRServiceLike,
33
+ } from "../../../services/rendering/ssr.service.js";
30
34
  import { ErrorPages } from "../../../utils/error-html.js";
31
35
  import { buildSSRResponse } from "./ssr-response-builder.js";
32
36
 
@@ -62,7 +66,12 @@ export class SSRHandler extends BaseHandler {
62
66
  patterns: [{ pattern: /^(?!\/_).*/, method: ["GET", "HEAD"] }],
63
67
  };
64
68
 
65
- private ssrService = new SSRService();
69
+ private ssrService: SSRServiceLike;
70
+
71
+ constructor(ssrService?: SSRServiceLike) {
72
+ super();
73
+ this.ssrService = ssrService ?? new SSRService();
74
+ }
66
75
 
67
76
  handle(req: dntShim.Request, ctx: HandlerContext): Promise<HandlerResult> {
68
77
  const url = new URL(req.url);
@@ -38,6 +38,15 @@ export function isProjectEnvActive(): boolean {
38
38
  return projectEnvStorage.getStore() !== undefined;
39
39
  }
40
40
 
41
+ /**
42
+ * Get a snapshot of the current project env overlay.
43
+ * Returns undefined if no overlay is active.
44
+ * Used to forward env vars to isolated workers in proxy mode.
45
+ */
46
+ export function getProjectEnvSnapshot(): Record<string, string> | undefined {
47
+ return projectEnvStorage.getStore();
48
+ }
49
+
41
50
  // Register on globalThis so process.ts can access without circular imports.
42
51
  // process.ts is low-level (platform/compat), project-env is high-level (server/).
43
52
  (dntShim.dntGlobalThis as Record<string, unknown>).__vfProjectEnvGetter = getProjectEnv;