veryfront 0.1.74 → 0.1.76

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 (122) hide show
  1. package/README.md +2 -0
  2. package/esm/cli/commands/files/command.d.ts +3 -3
  3. package/esm/cli/commands/knowledge/command.d.ts +2 -0
  4. package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
  5. package/esm/cli/commands/knowledge/command.js +64 -1
  6. package/esm/deno.d.ts +7 -0
  7. package/esm/deno.js +13 -6
  8. package/esm/src/data/data-fetcher.d.ts +11 -1
  9. package/esm/src/data/data-fetcher.d.ts.map +1 -1
  10. package/esm/src/data/data-fetcher.js +5 -2
  11. package/esm/src/data/index.d.ts +1 -1
  12. package/esm/src/data/index.d.ts.map +1 -1
  13. package/esm/src/data/server-data-fetcher.d.ts +14 -1
  14. package/esm/src/data/server-data-fetcher.d.ts.map +1 -1
  15. package/esm/src/data/server-data-fetcher.js +65 -3
  16. package/esm/src/jobs/index.d.ts +34 -0
  17. package/esm/src/jobs/index.d.ts.map +1 -0
  18. package/esm/src/jobs/index.js +33 -0
  19. package/esm/src/jobs/jobs-client.d.ts +134 -0
  20. package/esm/src/jobs/jobs-client.d.ts.map +1 -0
  21. package/esm/src/jobs/jobs-client.js +218 -0
  22. package/esm/src/jobs/schemas.d.ts +1304 -0
  23. package/esm/src/jobs/schemas.d.ts.map +1 -0
  24. package/esm/src/jobs/schemas.js +159 -0
  25. package/esm/src/platform/adapters/veryfront-api-client/retry-handler.d.ts +4 -0
  26. package/esm/src/platform/adapters/veryfront-api-client/retry-handler.d.ts.map +1 -1
  27. package/esm/src/platform/adapters/veryfront-api-client/retry-handler.js +12 -6
  28. package/esm/src/proxy/handler.d.ts.map +1 -1
  29. package/esm/src/proxy/handler.js +21 -21
  30. package/esm/src/rendering/orchestrator/lifecycle.d.ts +4 -0
  31. package/esm/src/rendering/orchestrator/lifecycle.d.ts.map +1 -1
  32. package/esm/src/rendering/orchestrator/lifecycle.js +8 -0
  33. package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
  34. package/esm/src/rendering/orchestrator/pipeline.js +6 -1
  35. package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts +26 -1
  36. package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts.map +1 -1
  37. package/esm/src/rendering/orchestrator/ssr-orchestrator.js +77 -1
  38. package/esm/src/routing/api/handler.d.ts.map +1 -1
  39. package/esm/src/routing/api/handler.js +6 -2
  40. package/esm/src/routing/api/route-executor.d.ts +8 -2
  41. package/esm/src/routing/api/route-executor.d.ts.map +1 -1
  42. package/esm/src/routing/api/route-executor.js +158 -3
  43. package/esm/src/security/deno-permissions.d.ts +7 -1
  44. package/esm/src/security/deno-permissions.d.ts.map +1 -1
  45. package/esm/src/security/deno-permissions.js +12 -1
  46. package/esm/src/security/sandbox/project-worker.d.ts +61 -0
  47. package/esm/src/security/sandbox/project-worker.d.ts.map +1 -0
  48. package/esm/src/security/sandbox/project-worker.js +318 -0
  49. package/esm/src/security/sandbox/worker-permissions.d.ts +30 -0
  50. package/esm/src/security/sandbox/worker-permissions.d.ts.map +1 -0
  51. package/esm/src/security/sandbox/worker-permissions.js +63 -0
  52. package/esm/src/security/sandbox/worker-pool.d.ts +87 -0
  53. package/esm/src/security/sandbox/worker-pool.d.ts.map +1 -0
  54. package/esm/src/security/sandbox/worker-pool.js +359 -0
  55. package/esm/src/security/sandbox/worker-types.d.ts +167 -0
  56. package/esm/src/security/sandbox/worker-types.d.ts.map +1 -0
  57. package/esm/src/security/sandbox/worker-types.js +19 -0
  58. package/esm/src/server/handlers/request/internal-tasks-list.handler.d.ts +11 -0
  59. package/esm/src/server/handlers/request/internal-tasks-list.handler.d.ts.map +1 -0
  60. package/esm/src/server/handlers/request/internal-tasks-list.handler.js +72 -0
  61. package/esm/src/server/project-env/storage.d.ts +6 -0
  62. package/esm/src/server/project-env/storage.d.ts.map +1 -1
  63. package/esm/src/server/project-env/storage.js +8 -0
  64. package/esm/src/server/runtime-handler/index.d.ts +1 -1
  65. package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
  66. package/esm/src/server/runtime-handler/index.js +3 -0
  67. package/esm/src/server/runtime-handler/project-isolation.d.ts +5 -0
  68. package/esm/src/server/runtime-handler/project-isolation.d.ts.map +1 -1
  69. package/esm/src/server/runtime-handler/project-isolation.js +44 -0
  70. package/esm/src/server/shared/renderer/memory/pressure.d.ts +7 -0
  71. package/esm/src/server/shared/renderer/memory/pressure.d.ts.map +1 -1
  72. package/esm/src/server/shared/renderer/memory/pressure.js +7 -0
  73. package/esm/src/task/control-plane.d.ts +105 -0
  74. package/esm/src/task/control-plane.d.ts.map +1 -0
  75. package/esm/src/task/control-plane.js +52 -0
  76. package/esm/src/task/types.d.ts +6 -0
  77. package/esm/src/task/types.d.ts.map +1 -1
  78. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts +4 -4
  79. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts.map +1 -1
  80. package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.js +15 -15
  81. package/esm/src/utils/index.d.ts +10 -1
  82. package/esm/src/utils/index.d.ts.map +1 -1
  83. package/esm/src/utils/index.js +9 -1
  84. package/esm/src/utils/logger/index.d.ts +1 -1
  85. package/esm/src/utils/logger/index.d.ts.map +1 -1
  86. package/esm/src/utils/logger/index.js +1 -1
  87. package/esm/src/utils/logger/logger.d.ts +14 -0
  88. package/esm/src/utils/logger/logger.d.ts.map +1 -1
  89. package/esm/src/utils/logger/logger.js +17 -0
  90. package/esm/src/workflow/claude-code/tool.d.ts +5 -5
  91. package/package.json +8 -1
  92. package/src/cli/commands/knowledge/command.ts +76 -1
  93. package/src/deno.js +13 -6
  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 +106 -3
  97. package/src/src/jobs/index.ts +85 -0
  98. package/src/src/jobs/jobs-client.ts +503 -0
  99. package/src/src/jobs/schemas.ts +202 -0
  100. package/src/src/platform/adapters/veryfront-api-client/retry-handler.ts +15 -6
  101. package/src/src/proxy/handler.ts +27 -19
  102. package/src/src/rendering/orchestrator/lifecycle.ts +11 -0
  103. package/src/src/rendering/orchestrator/pipeline.ts +7 -2
  104. package/src/src/rendering/orchestrator/ssr-orchestrator.ts +119 -0
  105. package/src/src/routing/api/handler.ts +16 -3
  106. package/src/src/routing/api/route-executor.ts +258 -1
  107. package/src/src/security/deno-permissions.ts +13 -1
  108. package/src/src/security/sandbox/project-worker.ts +416 -0
  109. package/src/src/security/sandbox/worker-permissions.ts +77 -0
  110. package/src/src/security/sandbox/worker-pool.ts +459 -0
  111. package/src/src/security/sandbox/worker-types.ts +212 -0
  112. package/src/src/server/handlers/request/internal-tasks-list.handler.ts +103 -0
  113. package/src/src/server/project-env/storage.ts +9 -0
  114. package/src/src/server/runtime-handler/index.ts +3 -0
  115. package/src/src/server/runtime-handler/project-isolation.ts +53 -0
  116. package/src/src/server/shared/renderer/memory/pressure.ts +8 -0
  117. package/src/src/task/control-plane.ts +76 -0
  118. package/src/src/task/types.ts +6 -0
  119. package/src/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.ts +18 -12
  120. package/src/src/utils/index.ts +11 -0
  121. package/src/src/utils/logger/index.ts +1 -0
  122. package/src/src/utils/logger/logger.ts +34 -0
@@ -1,5 +1,5 @@
1
1
  import * as dntShim from "../../../_dnt.shims.js";
2
- import { createContext, normalizeParams } from "./context-builder.js";
2
+ import { createContext, normalizeParams, parseCookies } from "./context-builder.js";
3
3
  import { createError, toError } from "../../errors/veryfront-error.js";
4
4
  import { createAppRouteMethodNotAllowed, createPagesRouteMethodNotAllowed, } from "./method-validator.js";
5
5
  import { isAbsolute, join } from "../../platform/compat/path/index.js";
@@ -7,6 +7,9 @@ import { withSpan } from "../../observability/tracing/otlp-setup.js";
7
7
  import { errorToRFC9457Response } from "../../errors/middleware/http-error-boundary.js";
8
8
  import { serverLogger as logger } from "../../utils/index.js";
9
9
  import { isDevelopment as isDevelopmentEnv } from "../../build/config/environment.js";
10
+ import { getWorkerPool, isWorkerIsolationEnabled, } from "../../security/sandbox/worker-pool.js";
11
+ import { MAX_WORKER_BODY_BYTES, } from "../../security/sandbox/worker-types.js";
12
+ import { getProjectEnvSnapshot } from "../../server/project-env/storage.js";
10
13
  function isDevelopment(adapter) {
11
14
  const env = adapter.env.get("MODE") ??
12
15
  adapter.env.get("NODE_ENV") ??
@@ -80,7 +83,152 @@ function validateResponse(response) {
80
83
  function toHeadResponse(response) {
81
84
  return new dntShim.Response(null, { status: response.status, headers: response.headers });
82
85
  }
83
- export function executeAppRoute(handler, request, match, pathname, adapter) {
86
+ // ---------------------------------------------------------------------------
87
+ // Worker Isolation Helpers
88
+ // ---------------------------------------------------------------------------
89
+ function checkContentLengthLimit(request) {
90
+ const contentLength = request.headers.get("content-length");
91
+ if (!contentLength)
92
+ return;
93
+ const bytes = parseInt(contentLength, 10);
94
+ if (bytes > MAX_WORKER_BODY_BYTES) {
95
+ throw createError({
96
+ type: "api",
97
+ message: `Request body too large for isolated execution (${(bytes / 1024 / 1024).toFixed(1)} MB, limit ${MAX_WORKER_BODY_BYTES / 1024 / 1024} MB)`,
98
+ });
99
+ }
100
+ }
101
+ async function readBodyWithSizeGuard(request) {
102
+ if (!request.body)
103
+ return null;
104
+ // Fast path: reject before buffering if Content-Length is known
105
+ checkContentLengthLimit(request);
106
+ const body = new Uint8Array(await request.arrayBuffer());
107
+ // Fallback: check actual size for chunked/streaming bodies
108
+ if (body.byteLength > MAX_WORKER_BODY_BYTES) {
109
+ throw createError({
110
+ type: "api",
111
+ message: `Request body too large for isolated execution (${(body.byteLength / 1024 / 1024).toFixed(1)} MB, limit ${MAX_WORKER_BODY_BYTES / 1024 / 1024} MB)`,
112
+ });
113
+ }
114
+ return body;
115
+ }
116
+ async function serializeRequest(request) {
117
+ return {
118
+ url: request.url,
119
+ method: request.method,
120
+ headers: [...request.headers.entries()],
121
+ body: await readBodyWithSizeGuard(request),
122
+ };
123
+ }
124
+ function deserializeResponse(s) {
125
+ return new dntShim.Response(s.body, {
126
+ status: s.status,
127
+ statusText: s.statusText,
128
+ headers: s.headers,
129
+ });
130
+ }
131
+ function workerResponseToResponse(workerResponse, pathname, adapter) {
132
+ if (workerResponse.type === "error") {
133
+ const { error } = workerResponse;
134
+ logger.error(`API route error in ${pathname} (worker):`, error.message);
135
+ // If the worker serialized RFC 9457 fields, return them directly
136
+ // to preserve the original status code, type, and detail.
137
+ if (error.status && error.type) {
138
+ return dntShim.Response.json({
139
+ type: error.type,
140
+ title: error.name,
141
+ status: error.status,
142
+ detail: error.detail ?? error.message,
143
+ instance: pathname,
144
+ }, { status: error.status });
145
+ }
146
+ const ctx = { isLocalProject: isDevelopment(adapter) };
147
+ const req = new dntShim.Request(`http://localhost${pathname}`);
148
+ const err = new Error(error.message);
149
+ err.name = error.name;
150
+ return errorToRFC9457Response(err, ctx, req);
151
+ }
152
+ if (workerResponse.type === "result") {
153
+ return deserializeResponse(workerResponse.response);
154
+ }
155
+ // data-result type is not expected in API route execution
156
+ throw new Error(`Unexpected worker response type: ${workerResponse.type}`);
157
+ }
158
+ // ---------------------------------------------------------------------------
159
+ // Isolated Execution (Worker Path)
160
+ // ---------------------------------------------------------------------------
161
+ function executeAppRouteIsolated(modulePath, request, match, pathname, adapter, projectDir) {
162
+ const method = request.method.toUpperCase();
163
+ return withSpan("api.executeAppRoute.isolated", async () => {
164
+ try {
165
+ const pool = getWorkerPool();
166
+ const serialized = await serializeRequest(request);
167
+ const workerResponse = await pool.execute(projectDir, [projectDir], {
168
+ type: "execute-app-route",
169
+ id: dntShim.crypto.randomUUID(),
170
+ modulePath,
171
+ method,
172
+ request: serialized,
173
+ params: match.params,
174
+ projectDir,
175
+ projectEnv: getProjectEnvSnapshot(),
176
+ });
177
+ const response = workerResponseToResponse(workerResponse, pathname, adapter);
178
+ return method === "HEAD" ? toHeadResponse(response) : response;
179
+ }
180
+ catch (error) {
181
+ return handleAPIError(error, pathname, adapter);
182
+ }
183
+ }, {
184
+ "http.method": method,
185
+ "http.path": pathname,
186
+ "api.route.pattern": match.route.pattern,
187
+ "api.isolated": true,
188
+ });
189
+ }
190
+ function executePagesRouteIsolated(modulePath, request, match, pathname, adapter, projectDir) {
191
+ const method = request.method;
192
+ return withSpan("api.executePagesRoute.isolated", async () => {
193
+ try {
194
+ const pool = getWorkerPool();
195
+ const body = await readBodyWithSizeGuard(request);
196
+ const workerResponse = await pool.execute(projectDir, [projectDir], {
197
+ type: "execute-pages-route",
198
+ id: dntShim.crypto.randomUUID(),
199
+ modulePath,
200
+ method,
201
+ context: {
202
+ url: request.url,
203
+ method: request.method,
204
+ headers: [...request.headers.entries()],
205
+ body,
206
+ params: match.params,
207
+ cookies: parseCookies(request.headers.get("cookie") ?? ""),
208
+ },
209
+ projectDir,
210
+ projectEnv: getProjectEnvSnapshot(),
211
+ });
212
+ return workerResponseToResponse(workerResponse, pathname, adapter);
213
+ }
214
+ catch (error) {
215
+ return handleAPIError(error, pathname, adapter);
216
+ }
217
+ }, {
218
+ "http.method": method,
219
+ "http.path": pathname,
220
+ "api.route.pattern": match.route.pattern,
221
+ "api.isolated": true,
222
+ });
223
+ }
224
+ export function executeAppRoute(handler, request, match, pathname, adapter, options) {
225
+ // Isolated path: execute in per-project Worker, fall back to main process on error
226
+ if (isWorkerIsolationEnabled() &&
227
+ options?.modulePath &&
228
+ options?.projectDir) {
229
+ return executeAppRouteIsolated(options.modulePath, request, match, pathname, adapter, options.projectDir);
230
+ }
231
+ // Default path: execute in main process (existing behavior)
84
232
  const method = request.method.toUpperCase();
85
233
  return withSpan("api.executeAppRoute", async () => {
86
234
  const handlerModule = handler;
@@ -102,7 +250,14 @@ export function executeAppRoute(handler, request, match, pathname, adapter) {
102
250
  }
103
251
  }, { "http.method": method, "http.path": pathname, "api.route.pattern": match.route.pattern });
104
252
  }
105
- export function executePagesRoute(handler, request, match, pathname, adapter, projectDir) {
253
+ export function executePagesRoute(handler, request, match, pathname, adapter, projectDir, options) {
254
+ // Isolated path: execute in per-project Worker, fall back to main process on error
255
+ if (isWorkerIsolationEnabled() &&
256
+ options?.modulePath &&
257
+ (options?.projectDir ?? projectDir)) {
258
+ return executePagesRouteIsolated(options.modulePath, request, match, pathname, adapter, options.projectDir ?? projectDir);
259
+ }
260
+ // Default path: execute in main process (existing behavior)
106
261
  const method = request.method;
107
262
  return withSpan("api.executePagesRoute", async () => {
108
263
  const methodHandler = handler[method] ?? handler.default;
@@ -10,7 +10,7 @@
10
10
  * SERVER — CLI server (dev, production, proxy, MCP, split-mode).
11
11
  * Also used by build and test tasks that need equivalent access.
12
12
  */
13
- export declare const SERVER_PERMISSIONS: readonly ["--allow-read", "--allow-write", "--allow-net", "--allow-env", "--allow-run", "--allow-ffi", "--allow-sys"];
13
+ export declare const SERVER_PERMISSIONS: readonly ["--allow-read", "--allow-write", "--allow-net", "--allow-env", "--allow-run", "--allow-sys", "--unstable-worker-options", "--unstable-net"];
14
14
  /**
15
15
  * WORKFLOW_JOB — `ProcessJobExecutor` (RESTRICTED).
16
16
  * Runs user-authored code — no `--allow-run`, `--allow-ffi`, or `--allow-sys`.
@@ -21,4 +21,10 @@ export declare const WORKFLOW_JOB_PERMISSIONS: readonly ["--allow-read", "--allo
21
21
  * Only needs filesystem + env access.
22
22
  */
23
23
  export declare const BUILD_HELPER_PERMISSIONS: readonly ["--allow-read", "--allow-write", "--allow-env"];
24
+ /**
25
+ * RENDER_WORKER — Per-project Worker for isolated code execution.
26
+ * Read-only filesystem (transformed modules), network (data fetchers),
27
+ * env (API keys and config). No subprocess/ffi/sys.
28
+ */
29
+ export declare const RENDER_WORKER_PERMISSIONS: readonly ["--allow-read", "--allow-net", "--allow-env"];
24
30
  //# sourceMappingURL=deno-permissions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deno-permissions.d.ts","sourceRoot":"","sources":["../../../src/src/security/deno-permissions.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,uHAQrB,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,wBAAwB,0EAK3B,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,wBAAwB,2DAI3B,CAAC"}
1
+ {"version":3,"file":"deno-permissions.d.ts","sourceRoot":"","sources":["../../../src/src/security/deno-permissions.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,uJASrB,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,wBAAwB,0EAK3B,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,wBAAwB,2DAI3B,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,yDAI5B,CAAC"}
@@ -16,8 +16,9 @@ export const SERVER_PERMISSIONS = [
16
16
  "--allow-net",
17
17
  "--allow-env",
18
18
  "--allow-run",
19
- "--allow-ffi",
20
19
  "--allow-sys",
20
+ "--unstable-worker-options",
21
+ "--unstable-net",
21
22
  ];
22
23
  /**
23
24
  * WORKFLOW_JOB — `ProcessJobExecutor` (RESTRICTED).
@@ -38,3 +39,13 @@ export const BUILD_HELPER_PERMISSIONS = [
38
39
  "--allow-write",
39
40
  "--allow-env",
40
41
  ];
42
+ /**
43
+ * RENDER_WORKER — Per-project Worker for isolated code execution.
44
+ * Read-only filesystem (transformed modules), network (data fetchers),
45
+ * env (API keys and config). No subprocess/ffi/sys.
46
+ */
47
+ export const RENDER_WORKER_PERMISSIONS = [
48
+ "--allow-read",
49
+ "--allow-net",
50
+ "--allow-env",
51
+ ];
@@ -0,0 +1,61 @@
1
+ import type { WorkerPermissions } from "./worker-permissions.js";
2
+ import type { WorkerRequest, WorkerResponse } from "./worker-types.js";
3
+ export interface ProjectWorkerOptions {
4
+ projectId: string;
5
+ permissions: WorkerPermissions;
6
+ requestTimeoutMs: number;
7
+ }
8
+ /**
9
+ * Status of a project worker.
10
+ */
11
+ export type WorkerStatus = "idle" | "busy" | "crashed" | "terminated";
12
+ export declare class ProjectWorker {
13
+ readonly projectId: string;
14
+ private worker;
15
+ private pending;
16
+ private streamHandlers;
17
+ private requestTimeoutMs;
18
+ private permissions;
19
+ private _requestCount;
20
+ private _lastActivityAt;
21
+ private _status;
22
+ constructor(options: ProjectWorkerOptions);
23
+ get status(): WorkerStatus;
24
+ get requestCount(): number;
25
+ get lastActivityAt(): number;
26
+ get hasPendingRequests(): boolean;
27
+ /**
28
+ * Start the worker. Idempotent — safe to call if already running.
29
+ */
30
+ start(): void;
31
+ /**
32
+ * Execute a request in this worker. Returns a typed response.
33
+ */
34
+ execute(request: WorkerRequest): Promise<WorkerResponse>;
35
+ /**
36
+ * Execute a streaming request. Returns a ReadableStream that yields
37
+ * chunks as they arrive from the Worker via postMessage.
38
+ *
39
+ * Used for streaming SSR where the Worker sends chunks progressively.
40
+ * Falls back to a single-chunk stream if the Worker returns a non-streaming
41
+ * response (ssr-result with full HTML).
42
+ */
43
+ executeStream(request: WorkerRequest): ReadableStream<Uint8Array>;
44
+ /**
45
+ * Health check — send a ping and wait for pong.
46
+ */
47
+ isHealthy(timeoutMs?: number): Promise<boolean>;
48
+ /**
49
+ * Clear the worker's module cache. Used for dev mode hot reload.
50
+ */
51
+ clearModuleCache(): void;
52
+ /**
53
+ * Terminate the worker. Rejects all pending requests.
54
+ */
55
+ terminate(): void;
56
+ private getWorkerScriptUrl;
57
+ private handleMessage;
58
+ private updateIdleStatus;
59
+ private rejectAllPending;
60
+ }
61
+ //# sourceMappingURL=project-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-worker.d.ts","sourceRoot":"","sources":["../../../../src/src/security/sandbox/project-worker.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EAGf,MAAM,mBAAmB,CAAC;AAW3B,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,iBAAiB,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAcD;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,CAAC;AAEtE,qBAAa,aAAa;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,OAAO,CAAqC;IACpD,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,OAAO,CAAwB;gBAE3B,OAAO,EAAE,oBAAoB;IAMzC,IAAI,MAAM,IAAI,YAAY,CAEzB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,kBAAkB,IAAI,OAAO,CAEhC;IAED;;OAEG;IACH,KAAK,IAAI,IAAI;IA+Bb;;OAEG;IACH,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IAqCxD;;;;;;;OAOG;IACH,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,cAAc,CAAC,UAAU,CAAC;IA+FjE;;OAEG;IACG,SAAS,CAAC,SAAS,SAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IA+BpD;;OAEG;IACH,gBAAgB,IAAI,IAAI;IAKxB;;OAEG;IACH,SAAS,IAAI,IAAI;IAuBjB,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,aAAa;IA+CrB,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,gBAAgB;CAazB"}
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Project Worker
3
+ *
4
+ * Wraps a single Deno Worker for one project. Manages the Worker lifecycle,
5
+ * sends/receives structured messages, enforces per-request timeouts,
6
+ * and serializes errors across the Worker boundary.
7
+ *
8
+ * @module security/sandbox/project-worker
9
+ */
10
+ import * as dntShim from "../../../_dnt.shims.js";
11
+ import { serverLogger } from "../../utils/index.js";
12
+ import { isCompiledBinary } from "../../utils/index.js";
13
+ import { withSpan } from "../../observability/tracing/otlp-setup.js";
14
+ import { TIMEOUT_ERROR, UNKNOWN_ERROR } from "../../errors/index.js";
15
+ const logger = serverLogger.component("project-worker");
16
+ const textEncoder = new TextEncoder();
17
+ export class ProjectWorker {
18
+ projectId;
19
+ worker = null;
20
+ pending = new Map();
21
+ streamHandlers = new Map();
22
+ requestTimeoutMs;
23
+ permissions;
24
+ _requestCount = 0;
25
+ _lastActivityAt = Date.now();
26
+ _status = "idle";
27
+ constructor(options) {
28
+ this.projectId = options.projectId;
29
+ this.permissions = options.permissions;
30
+ this.requestTimeoutMs = options.requestTimeoutMs;
31
+ }
32
+ get status() {
33
+ return this._status;
34
+ }
35
+ get requestCount() {
36
+ return this._requestCount;
37
+ }
38
+ get lastActivityAt() {
39
+ return this._lastActivityAt;
40
+ }
41
+ get hasPendingRequests() {
42
+ return this.pending.size > 0;
43
+ }
44
+ /**
45
+ * Start the worker. Idempotent — safe to call if already running.
46
+ */
47
+ start() {
48
+ if (this.worker)
49
+ return;
50
+ const workerUrl = this.getWorkerScriptUrl();
51
+ const workerOptions = {
52
+ type: "module",
53
+ name: `project-worker-${this.projectId}`,
54
+ deno: { permissions: this.permissions },
55
+ };
56
+ // @ts-ignore - Deno Worker accepts extended options
57
+ this.worker = new Worker(workerUrl, workerOptions);
58
+ this._status = "idle";
59
+ this.worker.onmessage = (event) => {
60
+ this.handleMessage(event.data);
61
+ };
62
+ this.worker.onerror = (event) => {
63
+ logger.error("Worker error", {
64
+ projectId: this.projectId,
65
+ error: event.message ?? String(event),
66
+ });
67
+ this._status = "crashed";
68
+ this.rejectAllPending("Worker crashed");
69
+ };
70
+ logger.debug("Worker started", { projectId: this.projectId });
71
+ }
72
+ /**
73
+ * Execute a request in this worker. Returns a typed response.
74
+ */
75
+ execute(request) {
76
+ return withSpan("worker.execute", () => {
77
+ if (!this.worker || this._status === "crashed" || this._status === "terminated") {
78
+ return Promise.reject(UNKNOWN_ERROR.create({ detail: `Worker not available (status: ${this._status})` }));
79
+ }
80
+ this._requestCount++;
81
+ this._lastActivityAt = Date.now();
82
+ this._status = "busy";
83
+ return new Promise((resolve, reject) => {
84
+ const timer = dntShim.setTimeout(() => {
85
+ this.pending.delete(request.id);
86
+ this.updateIdleStatus();
87
+ reject(TIMEOUT_ERROR.create({
88
+ detail: `Worker request timed out after ${this.requestTimeoutMs}ms`,
89
+ }));
90
+ }, this.requestTimeoutMs);
91
+ this.pending.set(request.id, { resolve, reject, timer });
92
+ this.worker.postMessage(request);
93
+ });
94
+ }, {
95
+ "worker.projectId": this.projectId,
96
+ "worker.requestType": request.type,
97
+ "worker.requestId": request.id,
98
+ });
99
+ }
100
+ /**
101
+ * Execute a streaming request. Returns a ReadableStream that yields
102
+ * chunks as they arrive from the Worker via postMessage.
103
+ *
104
+ * Used for streaming SSR where the Worker sends chunks progressively.
105
+ * Falls back to a single-chunk stream if the Worker returns a non-streaming
106
+ * response (ssr-result with full HTML).
107
+ */
108
+ executeStream(request) {
109
+ if (!this.worker || this._status === "crashed" || this._status === "terminated") {
110
+ throw UNKNOWN_ERROR.create({ detail: `Worker not available (status: ${this._status})` });
111
+ }
112
+ this._requestCount++;
113
+ this._lastActivityAt = Date.now();
114
+ this._status = "busy";
115
+ const requestId = request.id;
116
+ return new ReadableStream({
117
+ start: (controller) => {
118
+ let timer = dntShim.setTimeout(() => {
119
+ this.streamHandlers.delete(requestId);
120
+ this.pending.delete(requestId);
121
+ this.updateIdleStatus();
122
+ controller.error(TIMEOUT_ERROR.create({
123
+ detail: `Worker stream timed out after ${this.requestTimeoutMs}ms`,
124
+ }));
125
+ }, this.requestTimeoutMs);
126
+ const resetTimer = () => {
127
+ clearTimeout(timer);
128
+ timer = dntShim.setTimeout(() => {
129
+ this.streamHandlers.delete(requestId);
130
+ this.pending.delete(requestId);
131
+ this.updateIdleStatus();
132
+ controller.error(TIMEOUT_ERROR.create({
133
+ detail: `Worker stream timed out after ${this.requestTimeoutMs}ms`,
134
+ }));
135
+ }, this.requestTimeoutMs);
136
+ };
137
+ // Register a stream handler for this request
138
+ this.streamHandlers.set(requestId, {
139
+ onChunk: (chunk) => {
140
+ resetTimer();
141
+ controller.enqueue(chunk);
142
+ },
143
+ onEnd: () => {
144
+ clearTimeout(timer);
145
+ this.streamHandlers.delete(requestId);
146
+ this.pending.delete(requestId);
147
+ this.updateIdleStatus();
148
+ controller.close();
149
+ },
150
+ onError: (error) => {
151
+ clearTimeout(timer);
152
+ this.streamHandlers.delete(requestId);
153
+ this.pending.delete(requestId);
154
+ this.updateIdleStatus();
155
+ controller.error(error);
156
+ },
157
+ });
158
+ // Also register in pending for non-streaming responses (fallback)
159
+ this.pending.set(requestId, {
160
+ resolve: (response) => {
161
+ clearTimeout(timer);
162
+ this.streamHandlers.delete(requestId);
163
+ this.pending.delete(requestId);
164
+ this.updateIdleStatus();
165
+ // If we get an ssr-result, emit it as a single chunk
166
+ if (response.type === "ssr-result") {
167
+ controller.enqueue(textEncoder.encode(response.html));
168
+ controller.close();
169
+ }
170
+ else if (response.type === "error") {
171
+ const err = new Error(response.error.message);
172
+ err.name = response.error.name;
173
+ controller.error(err);
174
+ }
175
+ else {
176
+ controller.close();
177
+ }
178
+ },
179
+ reject: (error) => {
180
+ clearTimeout(timer);
181
+ this.streamHandlers.delete(requestId);
182
+ this.pending.delete(requestId);
183
+ this.updateIdleStatus();
184
+ controller.error(error);
185
+ },
186
+ timer,
187
+ });
188
+ this.worker.postMessage(request);
189
+ },
190
+ });
191
+ }
192
+ /**
193
+ * Health check — send a ping and wait for pong.
194
+ */
195
+ async isHealthy(timeoutMs = 5_000) {
196
+ if (!this.worker || this._status === "crashed" || this._status === "terminated") {
197
+ return false;
198
+ }
199
+ const id = dntShim.crypto.randomUUID();
200
+ return new Promise((resolve) => {
201
+ const timer = dntShim.setTimeout(() => {
202
+ this.pending.delete(id);
203
+ resolve(false);
204
+ }, timeoutMs);
205
+ this.pending.set(id, {
206
+ resolve: () => {
207
+ clearTimeout(timer);
208
+ this.pending.delete(id);
209
+ resolve(true);
210
+ },
211
+ reject: () => {
212
+ clearTimeout(timer);
213
+ this.pending.delete(id);
214
+ resolve(false);
215
+ },
216
+ timer,
217
+ });
218
+ this.worker.postMessage({ type: "ping", id });
219
+ });
220
+ }
221
+ /**
222
+ * Clear the worker's module cache. Used for dev mode hot reload.
223
+ */
224
+ clearModuleCache() {
225
+ if (!this.worker || this._status === "crashed" || this._status === "terminated")
226
+ return;
227
+ this.worker.postMessage({ type: "clear-cache" });
228
+ }
229
+ /**
230
+ * Terminate the worker. Rejects all pending requests.
231
+ */
232
+ terminate() {
233
+ if (!this.worker)
234
+ return;
235
+ this._status = "terminated";
236
+ this.rejectAllPending("Worker terminated");
237
+ try {
238
+ this.worker.terminate();
239
+ }
240
+ catch (error) {
241
+ logger.debug("Worker terminate failed", {
242
+ projectId: this.projectId,
243
+ error,
244
+ });
245
+ }
246
+ this.worker = null;
247
+ logger.debug("Worker terminated", { projectId: this.projectId });
248
+ }
249
+ // -----------------------------------------------------------------------
250
+ // Private
251
+ // -----------------------------------------------------------------------
252
+ getWorkerScriptUrl() {
253
+ // In compiled binary mode, use a data URL because blob URLs don't work
254
+ // See: deno-sandbox.ts for the same pattern
255
+ if (isCompiledBinary()) {
256
+ // For compiled binaries, we'd need to inline the worker script.
257
+ // For now, fall through to the import.meta.resolve path which works
258
+ // in development and standard Deno execution.
259
+ }
260
+ // Use import.meta.resolve to get the absolute URL of the worker script.
261
+ // This works in both `deno run` and `deno compile` contexts.
262
+ return globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).resolve("./worker-script.ts");
263
+ }
264
+ handleMessage(data) {
265
+ if (data.type === "pong") {
266
+ const pending = this.pending.get(data.id);
267
+ if (pending) {
268
+ clearTimeout(pending.timer);
269
+ pending.resolve(data);
270
+ this.pending.delete(data.id);
271
+ }
272
+ return;
273
+ }
274
+ // Handle streaming SSR chunks
275
+ if (data.type === "stream-chunk") {
276
+ const handler = this.streamHandlers.get(data.id);
277
+ if (handler)
278
+ handler.onChunk(data.chunk);
279
+ return;
280
+ }
281
+ if (data.type === "stream-end") {
282
+ const handler = this.streamHandlers.get(data.id);
283
+ if (handler)
284
+ handler.onEnd();
285
+ return;
286
+ }
287
+ const response = data;
288
+ const pending = this.pending.get(response.id);
289
+ if (!pending) {
290
+ logger.warn("Received response for unknown request", {
291
+ projectId: this.projectId,
292
+ id: response.id,
293
+ });
294
+ return;
295
+ }
296
+ clearTimeout(pending.timer);
297
+ this.pending.delete(response.id);
298
+ this.updateIdleStatus();
299
+ pending.resolve(response);
300
+ }
301
+ updateIdleStatus() {
302
+ if (this.pending.size === 0 && this._status === "busy") {
303
+ this._status = "idle";
304
+ }
305
+ }
306
+ rejectAllPending(reason) {
307
+ for (const [id, pending] of this.pending) {
308
+ clearTimeout(pending.timer);
309
+ pending.reject(UNKNOWN_ERROR.create({ detail: reason }));
310
+ this.pending.delete(id);
311
+ }
312
+ // Clean up stream handlers
313
+ for (const [id, handler] of this.streamHandlers) {
314
+ handler.onError(UNKNOWN_ERROR.create({ detail: reason }));
315
+ this.streamHandlers.delete(id);
316
+ }
317
+ }
318
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Worker Permission Builder
3
+ *
4
+ * Builds scoped Deno Worker permissions for per-project isolation.
5
+ * Each project worker gets the minimum required permissions.
6
+ *
7
+ * @module security/sandbox/worker-permissions
8
+ */
9
+ export interface WorkerPermissions {
10
+ read: string[] | boolean;
11
+ write: boolean;
12
+ net: boolean;
13
+ env: boolean;
14
+ run: boolean;
15
+ ffi: boolean;
16
+ sys: boolean;
17
+ }
18
+ /**
19
+ * Build scoped permissions for a project worker.
20
+ *
21
+ * - read: restricted to the project temp dir (transformed modules) and cache dirs
22
+ * - write: denied (workers produce output via postMessage, not filesystem)
23
+ * - net: allowed (data fetchers and API routes may call external APIs)
24
+ * - env: allowed (user code reads API keys and config from environment)
25
+ * - run: denied (no subprocess spawning from user code)
26
+ * - ffi: denied (no native code from user code)
27
+ * - sys: denied (no system info access from user code)
28
+ */
29
+ export declare function buildWorkerPermissions(readPaths: string[]): WorkerPermissions;
30
+ //# sourceMappingURL=worker-permissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-permissions.d.ts","sourceRoot":"","sources":["../../../../src/src/security/sandbox/worker-permissions.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IACzB,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;CACd;AAcD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EAAE,GAClB,iBAAiB,CA0BnB"}