veryfront 0.1.75 → 0.1.77

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 (89) hide show
  1. package/README.md +2 -0
  2. package/esm/cli/commands/files/command.d.ts +3 -3
  3. package/esm/cli/commands/pull/command.js +2 -2
  4. package/esm/cli/commands/task/command.d.ts.map +1 -1
  5. package/esm/cli/commands/task/command.js +57 -51
  6. package/esm/cli/commands/workflow/command-help.d.ts +3 -0
  7. package/esm/cli/commands/workflow/command-help.d.ts.map +1 -0
  8. package/esm/cli/commands/workflow/command-help.js +19 -0
  9. package/esm/cli/commands/workflow/command.d.ts +5 -0
  10. package/esm/cli/commands/workflow/command.d.ts.map +1 -0
  11. package/esm/cli/commands/workflow/command.js +119 -0
  12. package/esm/cli/commands/workflow/handler.d.ts +28 -0
  13. package/esm/cli/commands/workflow/handler.d.ts.map +1 -0
  14. package/esm/cli/commands/workflow/handler.js +19 -0
  15. package/esm/cli/help/command-definitions.d.ts.map +1 -1
  16. package/esm/cli/help/command-definitions.js +2 -0
  17. package/esm/cli/router.d.ts.map +1 -1
  18. package/esm/cli/router.js +2 -0
  19. package/esm/cli/shared/project-source-context.d.ts +18 -0
  20. package/esm/cli/shared/project-source-context.d.ts.map +1 -0
  21. package/esm/cli/shared/project-source-context.js +52 -0
  22. package/esm/deno.d.ts +6 -0
  23. package/esm/deno.js +12 -6
  24. package/esm/src/data/server-data-fetcher.d.ts.map +1 -1
  25. package/esm/src/data/server-data-fetcher.js +17 -1
  26. package/esm/src/jobs/index.d.ts +34 -0
  27. package/esm/src/jobs/index.d.ts.map +1 -0
  28. package/esm/src/jobs/index.js +33 -0
  29. package/esm/src/jobs/jobs-client.d.ts +134 -0
  30. package/esm/src/jobs/jobs-client.d.ts.map +1 -0
  31. package/esm/src/jobs/jobs-client.js +218 -0
  32. package/esm/src/jobs/schemas.d.ts +1304 -0
  33. package/esm/src/jobs/schemas.d.ts.map +1 -0
  34. package/esm/src/jobs/schemas.js +159 -0
  35. package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts +3 -3
  36. package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts.map +1 -1
  37. package/esm/src/platform/adapters/veryfront-api-client/operations.js +12 -10
  38. package/esm/src/platform/adapters/veryfront-api-client/retry-handler.d.ts +4 -0
  39. package/esm/src/platform/adapters/veryfront-api-client/retry-handler.d.ts.map +1 -1
  40. package/esm/src/platform/adapters/veryfront-api-client/retry-handler.js +12 -6
  41. package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.d.ts +4 -4
  42. package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.js +4 -4
  43. package/esm/src/platform/index.d.ts +1 -0
  44. package/esm/src/platform/index.d.ts.map +1 -1
  45. package/esm/src/platform/index.js +1 -0
  46. package/esm/src/proxy/handler.d.ts.map +1 -1
  47. package/esm/src/proxy/handler.js +21 -21
  48. package/esm/src/routing/api/route-executor.d.ts.map +1 -1
  49. package/esm/src/routing/api/route-executor.js +30 -3
  50. package/esm/src/security/deno-permissions.d.ts +1 -1
  51. package/esm/src/security/deno-permissions.d.ts.map +1 -1
  52. package/esm/src/security/deno-permissions.js +2 -1
  53. package/esm/src/security/sandbox/project-worker.d.ts.map +1 -1
  54. package/esm/src/security/sandbox/project-worker.js +2 -2
  55. package/esm/src/security/sandbox/worker-permissions.d.ts.map +1 -1
  56. package/esm/src/security/sandbox/worker-permissions.js +23 -20
  57. package/esm/src/security/sandbox/worker-pool.d.ts.map +1 -1
  58. package/esm/src/security/sandbox/worker-pool.js +17 -14
  59. package/esm/src/security/sandbox/worker-types.d.ts +2 -0
  60. package/esm/src/security/sandbox/worker-types.d.ts.map +1 -1
  61. package/esm/src/security/sandbox/worker-types.js +2 -0
  62. package/esm/src/task/types.d.ts +6 -0
  63. package/esm/src/task/types.d.ts.map +1 -1
  64. package/package.json +5 -1
  65. package/src/cli/commands/pull/command.ts +3 -3
  66. package/src/cli/commands/task/command.ts +66 -57
  67. package/src/cli/commands/workflow/command-help.ts +21 -0
  68. package/src/cli/commands/workflow/command.ts +152 -0
  69. package/src/cli/commands/workflow/handler.ts +25 -0
  70. package/src/cli/help/command-definitions.ts +2 -0
  71. package/src/cli/router.ts +2 -0
  72. package/src/cli/shared/project-source-context.ts +96 -0
  73. package/src/deno.js +12 -6
  74. package/src/src/data/server-data-fetcher.ts +30 -2
  75. package/src/src/jobs/index.ts +85 -0
  76. package/src/src/jobs/jobs-client.ts +503 -0
  77. package/src/src/jobs/schemas.ts +202 -0
  78. package/src/src/platform/adapters/veryfront-api-client/operations.ts +14 -14
  79. package/src/src/platform/adapters/veryfront-api-client/retry-handler.ts +15 -6
  80. package/src/src/platform/adapters/veryfront-api-client/schemas/api.schema.ts +4 -4
  81. package/src/src/platform/index.ts +1 -0
  82. package/src/src/proxy/handler.ts +27 -19
  83. package/src/src/routing/api/route-executor.ts +43 -7
  84. package/src/src/security/deno-permissions.ts +2 -1
  85. package/src/src/security/sandbox/project-worker.ts +2 -2
  86. package/src/src/security/sandbox/worker-permissions.ts +22 -19
  87. package/src/src/security/sandbox/worker-pool.ts +21 -13
  88. package/src/src/security/sandbox/worker-types.ts +3 -0
  89. package/src/src/task/types.ts +6 -0
@@ -154,14 +154,13 @@ export class VeryfrontAPIOperations {
154
154
 
155
155
  async listBranchFiles(
156
156
  projectRef: string,
157
- branchName = "main",
157
+ branchRef = "main",
158
158
  options: ListFilesOptions = {},
159
159
  ): Promise<FileListResult> {
160
160
  const params = buildListParams(options);
161
- const url = `/projects/${encodeURIComponent(projectRef)}/branches/${
162
- encodeURIComponent(branchName)
163
- }/files?${params}`;
164
- logger.debug("listBranchFiles", { projectRef, branchName, pattern: options.pattern });
161
+ params.set("branch", branchRef);
162
+ const url = `/projects/${encodeURIComponent(projectRef)}/files?${params}`;
163
+ logger.debug("listBranchFiles", { projectRef, branchRef, pattern: options.pattern });
165
164
 
166
165
  const raw = await this.request(url);
167
166
  const response = ListBranchFilesResponseSchema.parse(raw);
@@ -174,11 +173,11 @@ export class VeryfrontAPIOperations {
174
173
 
175
174
  async listAllBranchFiles(
176
175
  projectRef: string,
177
- branchName = "main",
176
+ branchRef = "main",
178
177
  options: Omit<ListFilesOptions, "cursor"> = {},
179
178
  ): Promise<ProjectFile[]> {
180
179
  const allFiles = await listAllFiles((cursor) =>
181
- this.listBranchFiles(projectRef, branchName, {
180
+ this.listBranchFiles(projectRef, branchRef, {
182
181
  ...options,
183
182
  cursor,
184
183
  limit: DEFAULT_PAGE_LIMIT,
@@ -187,21 +186,22 @@ export class VeryfrontAPIOperations {
187
186
 
188
187
  logger.debug("listAllBranchFiles DONE", {
189
188
  projectRef,
190
- branchName,
189
+ branchRef,
191
190
  totalFiles: allFiles.length,
192
191
  });
193
192
 
194
193
  return allFiles;
195
194
  }
196
195
 
197
- getBranchFile(projectRef: string, branchName: string, pathOrId: string): Promise<FileDetail> {
196
+ getBranchFile(projectRef: string, branchRef: string, pathOrId: string): Promise<FileDetail> {
198
197
  return withSpan(
199
198
  SpanNames.API_GET_FILE,
200
199
  async () => {
201
- const url = `/projects/${encodeURIComponent(projectRef)}/branches/${
202
- encodeURIComponent(branchName)
203
- }/files/${encodeURIComponent(pathOrId)}`;
204
- logger.debug("getBranchFile", { projectRef, branchName, pathOrId });
200
+ const params = new URLSearchParams({ branch: branchRef });
201
+ const url = `/projects/${encodeURIComponent(projectRef)}/files/${
202
+ encodeURIComponent(pathOrId)
203
+ }?${params}`;
204
+ logger.debug("getBranchFile", { projectRef, branchRef, pathOrId });
205
205
 
206
206
  const raw = await this.request(url);
207
207
  const response = BranchFileDetailSchema.parse(raw);
@@ -217,7 +217,7 @@ export class VeryfrontAPIOperations {
217
217
  {
218
218
  "api.operation": "getBranchFile",
219
219
  "api.project": projectRef,
220
- "api.branch": branchName,
220
+ "api.branch": branchRef,
221
221
  "api.path": pathOrId,
222
222
  },
223
223
  );
@@ -18,6 +18,9 @@ export interface RequestOptions {
18
18
  returnText?: boolean;
19
19
  /** Request timeout in milliseconds. Defaults to 30000ms (30 seconds). */
20
20
  timeoutMs?: number;
21
+ method?: string;
22
+ body?: dntShim.BodyInit | null;
23
+ headers?: dntShim.HeadersInit;
21
24
  }
22
25
 
23
26
  /** Default timeout for API requests (30 seconds) */
@@ -48,13 +51,19 @@ export async function requestWithRetry(
48
51
  async () => {
49
52
  const startTime = performance.now();
50
53
 
51
- const headers = new dntShim.Headers({
52
- Authorization: `Bearer ${apiToken}`,
53
- "Content-Type": "application/json",
54
- });
54
+ const headers = new dntShim.Headers(options.headers);
55
+ headers.set("Authorization", `Bearer ${apiToken}`);
56
+ if (!headers.has("Content-Type")) {
57
+ headers.set("Content-Type", "application/json");
58
+ }
55
59
  injectContext(headers);
56
60
 
57
- const response = await dntShim.fetch(url, { headers, signal: controller.signal });
61
+ const response = await dntShim.fetch(url, {
62
+ method: options.method ?? "GET",
63
+ headers,
64
+ body: options.body,
65
+ signal: controller.signal,
66
+ });
58
67
  const duration = performance.now() - startTime;
59
68
 
60
69
  recordApiRequest(response.status);
@@ -89,7 +98,7 @@ export async function requestWithRetry(
89
98
  return { data, status: response.status, duration };
90
99
  },
91
100
  {
92
- "http.method": "GET",
101
+ "http.method": options.method ?? "GET",
93
102
  "http.url": url,
94
103
  "http.target": urlPath,
95
104
  "http.host": urlObj.host,
@@ -179,13 +179,13 @@ export const API_ENDPOINTS = {
179
179
  },
180
180
  listBranchFiles: {
181
181
  method: "GET" as const,
182
- path: "/projects/{projectRef}/branches/{branchName}/files",
183
- description: "List files in a branch (draft/working copy)",
182
+ path: "/projects/{projectRef}/files?branch={branchRef}",
183
+ description: "List files for a branch ref or name (omit branch for main branch)",
184
184
  },
185
185
  getBranchFile: {
186
186
  method: "GET" as const,
187
- path: "/projects/{projectRef}/branches/{branchName}/files/{pathOrId}",
188
- description: "Get file from a branch by path or UUID",
187
+ path: "/projects/{projectRef}/files/{pathOrId}?branch={branchRef}",
188
+ description: "Get file from a branch ref or name by path or UUID",
189
189
  },
190
190
  listEnvironmentFiles: {
191
191
  method: "GET" as const,
@@ -66,6 +66,7 @@ export { isDeno } from "./compat/runtime.js";
66
66
 
67
67
  // Adapters: filesystem
68
68
  export { createFSAdapter, VeryfrontFSAdapter } from "./adapters/fs/index.js";
69
+ export { enhanceAdapterWithFS, isExtendedFSAdapter } from "./adapters/fs/index.js";
69
70
 
70
71
  // Adapters: API client
71
72
  export { VeryfrontApiClient } from "./adapters/veryfront-api-client/index.js";
@@ -3,10 +3,11 @@ import { TokenManager, type TokenScope } from "./token-manager.js";
3
3
  import { type ParsedDomain, parseProjectDomain } from "../server/utils/domain-parser.js";
4
4
  import type { TokenCache } from "./cache/types.js";
5
5
  import { createFileSystem } from "../platform/compat/fs.js";
6
- import { cwd } from "../platform/compat/process.js";
6
+ import { cwd, getEnv } from "../platform/compat/process.js";
7
7
  import { join } from "../platform/compat/path/index.js";
8
8
  import { injectContext, ProxySpanNames, withSpan } from "./tracing.js";
9
9
  import { computeContentSourceId } from "../cache/keys.js";
10
+ import { jwtVerify } from "jose";
10
11
 
11
12
  export const INTERNAL_PROXY_HEADERS = [
12
13
  "x-token",
@@ -151,19 +152,27 @@ function extractUserToken(cookieHeader: string): string | undefined {
151
152
  return match?.[1] ? decodeURIComponent(match[1]) : undefined;
152
153
  }
153
154
 
154
- function extractUserIdFromToken(token: string): string | undefined {
155
+ async function extractUserIdFromToken(
156
+ token: string,
157
+ log?: ProxyLogger,
158
+ ): Promise<string | undefined> {
159
+ const jwtSecret = getEnv("JWT_SECRET");
160
+
161
+ if (!jwtSecret) {
162
+ log?.warn("JWT_SECRET not configured — cannot verify user token");
163
+ return undefined;
164
+ }
165
+
155
166
  try {
156
- const payload = token.split(".")[1];
157
- if (!payload) return undefined;
158
- // JWT payloads are base64url-encoded: normalize to standard base64 before decoding
159
- let base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
160
- const remainder = base64.length % 4;
161
- if (remainder === 2) base64 += "==";
162
- else if (remainder === 3) base64 += "=";
163
- const decoded = JSON.parse(atob(base64));
164
- return decoded?.userId;
165
- } catch (_) {
166
- /* expected: malformed JWT token */
167
+ const secret = new TextEncoder().encode(jwtSecret);
168
+ const { payload } = await jwtVerify(token, secret, {
169
+ algorithms: ["HS256"],
170
+ });
171
+ return (payload as { userId?: string }).userId;
172
+ } catch (error) {
173
+ log?.debug("JWT verification failed", {
174
+ error: error instanceof Error ? error.message : String(error),
175
+ });
167
176
  return undefined;
168
177
  }
169
178
  }
@@ -276,13 +285,13 @@ export function createProxyHandler(options: ProxyHandlerOptions) {
276
285
  return `https://veryfront.com/sign-in?from=${encodeURIComponent(returnPath)}`;
277
286
  }
278
287
 
279
- function checkProtectedAccess(
288
+ async function checkProtectedAccess(
280
289
  req: dntShim.Request,
281
290
  matchingEnv: NonNullable<DomainLookupResult["environments"]>[number] | undefined,
282
291
  userToken: string | undefined,
283
292
  users: DomainLookupResult["users"],
284
293
  logContext: Record<string, unknown>,
285
- ): { status: number; message: string; redirectUrl?: string } | null {
294
+ ): Promise<{ status: number; message: string; redirectUrl?: string } | null> {
286
295
  if (!matchingEnv?.protected) return null;
287
296
 
288
297
  if (!userToken) {
@@ -295,9 +304,8 @@ export function createProxyHandler(options: ProxyHandlerOptions) {
295
304
  return { status: 302, message: "Authentication required", redirectUrl };
296
305
  }
297
306
 
298
- const userId = extractUserIdFromToken(userToken);
307
+ const userId = await extractUserIdFromToken(userToken, logger);
299
308
  if (!userId) {
300
- // Malformed token — treat as unauthenticated so user can re-sign-in
301
309
  const redirectUrl = makeAuthRedirectUrl(req);
302
310
  logger?.info("Could not extract userId from token", {
303
311
  ...logContext,
@@ -334,7 +342,7 @@ export function createProxyHandler(options: ProxyHandlerOptions) {
334
342
 
335
343
  const matchingEnv = lookupResult.environments?.find(envMatcher);
336
344
 
337
- const protectionError = checkProtectedAccess(
345
+ const protectionError = await checkProtectedAccess(
338
346
  req,
339
347
  matchingEnv,
340
348
  userToken,
@@ -442,7 +450,7 @@ export function createProxyHandler(options: ProxyHandlerOptions) {
442
450
  releaseId = matchingEnv?.active_release_id ?? undefined;
443
451
  environmentId = matchingEnv?.id;
444
452
 
445
- const protectionError = checkProtectedAccess(
453
+ const protectionError = await checkProtectedAccess(
446
454
  req,
447
455
  matchingEnv,
448
456
  userToken,
@@ -24,10 +24,11 @@ import {
24
24
  getWorkerPool,
25
25
  isWorkerIsolationEnabled,
26
26
  } from "../../security/sandbox/worker-pool.js";
27
- import type {
28
- SerializedRequest,
29
- SerializedResponse,
30
- WorkerResponse,
27
+ import {
28
+ MAX_WORKER_BODY_BYTES,
29
+ type SerializedRequest,
30
+ type SerializedResponse,
31
+ type WorkerResponse,
31
32
  } from "../../security/sandbox/worker-types.js";
32
33
  import { getProjectEnvSnapshot } from "../../server/project-env/storage.js";
33
34
 
@@ -129,13 +130,48 @@ function toHeadResponse(response: dntShim.Response): dntShim.Response {
129
130
  // Worker Isolation Helpers
130
131
  // ---------------------------------------------------------------------------
131
132
 
133
+ function checkContentLengthLimit(request: dntShim.Request): void {
134
+ const contentLength = request.headers.get("content-length");
135
+ if (!contentLength) return;
136
+
137
+ const bytes = parseInt(contentLength, 10);
138
+ if (bytes > MAX_WORKER_BODY_BYTES) {
139
+ throw createError({
140
+ type: "api",
141
+ message: `Request body too large for isolated execution (${
142
+ (bytes / 1024 / 1024).toFixed(1)
143
+ } MB, limit ${MAX_WORKER_BODY_BYTES / 1024 / 1024} MB)`,
144
+ });
145
+ }
146
+ }
147
+
148
+ async function readBodyWithSizeGuard(request: dntShim.Request): Promise<Uint8Array | null> {
149
+ if (!request.body) return null;
150
+
151
+ // Fast path: reject before buffering if Content-Length is known
152
+ checkContentLengthLimit(request);
153
+
154
+ const body = new Uint8Array(await request.arrayBuffer());
155
+
156
+ // Fallback: check actual size for chunked/streaming bodies
157
+ if (body.byteLength > MAX_WORKER_BODY_BYTES) {
158
+ throw createError({
159
+ type: "api",
160
+ message: `Request body too large for isolated execution (${
161
+ (body.byteLength / 1024 / 1024).toFixed(1)
162
+ } MB, limit ${MAX_WORKER_BODY_BYTES / 1024 / 1024} MB)`,
163
+ });
164
+ }
165
+
166
+ return body;
167
+ }
168
+
132
169
  async function serializeRequest(request: dntShim.Request): Promise<SerializedRequest> {
133
- const body = request.body ? new Uint8Array(await request.arrayBuffer()) : null;
134
170
  return {
135
171
  url: request.url,
136
172
  method: request.method,
137
173
  headers: [...request.headers.entries()],
138
- body,
174
+ body: await readBodyWithSizeGuard(request),
139
175
  };
140
176
  }
141
177
 
@@ -252,7 +288,7 @@ function executePagesRouteIsolated(
252
288
  async () => {
253
289
  try {
254
290
  const pool = getWorkerPool();
255
- const body = request.body ? new Uint8Array(await request.arrayBuffer()) : null;
291
+ const body = await readBodyWithSizeGuard(request);
256
292
 
257
293
  const workerResponse = await pool.execute(
258
294
  projectDir,
@@ -17,8 +17,9 @@ export const SERVER_PERMISSIONS = [
17
17
  "--allow-net",
18
18
  "--allow-env",
19
19
  "--allow-run",
20
- "--allow-ffi",
21
20
  "--allow-sys",
21
+ "--unstable-worker-options",
22
+ "--unstable-net",
22
23
  ] as const;
23
24
 
24
25
  /**
@@ -23,6 +23,7 @@ import type {
23
23
  } from "./worker-types.js";
24
24
 
25
25
  const logger = serverLogger.component("project-worker");
26
+ const textEncoder = new TextEncoder();
26
27
 
27
28
  type ExtendedWorkerOptions = {
28
29
  type: "module";
@@ -179,7 +180,6 @@ export class ProjectWorker {
179
180
  this._status = "busy";
180
181
 
181
182
  const requestId = request.id;
182
- const encoder = new TextEncoder();
183
183
 
184
184
  return new ReadableStream<Uint8Array>({
185
185
  start: (controller) => {
@@ -240,7 +240,7 @@ export class ProjectWorker {
240
240
 
241
241
  // If we get an ssr-result, emit it as a single chunk
242
242
  if (response.type === "ssr-result") {
243
- controller.enqueue(encoder.encode(response.html));
243
+ controller.enqueue(textEncoder.encode(response.html));
244
244
  controller.close();
245
245
  } else if (response.type === "error") {
246
246
  const err = new Error(response.error.message);
@@ -23,6 +23,18 @@ export interface WorkerPermissions {
23
23
  sys: boolean;
24
24
  }
25
25
 
26
+ // Cache compiled binary check — Deno.execPath() is a syscall that never changes at runtime
27
+ const _isCompiledBinary = (() => {
28
+ try {
29
+ const exec = typeof dntShim.Deno !== "undefined" ? dntShim.Deno.execPath?.() : undefined;
30
+ if (!exec) return false;
31
+ const name = exec.split(/[/\\]/).pop()?.toLowerCase() ?? "";
32
+ return name !== "deno" && name !== "deno.exe";
33
+ } catch {
34
+ return false;
35
+ }
36
+ })();
37
+
26
38
  /**
27
39
  * Build scoped permissions for a project worker.
28
40
  *
@@ -41,25 +53,16 @@ export function buildWorkerPermissions(
41
53
  // is outside the project directory. Rather than trying to enumerate all
42
54
  // read paths, grant full read access — the security boundary is enforced
43
55
  // by denying write/run/ffi/sys, not by restricting reads.
44
- // Check for compiled binary by testing if execPath is NOT "deno"/"deno.exe"
45
- try {
46
- const exec = typeof dntShim.Deno !== "undefined" ? dntShim.Deno.execPath?.() : undefined;
47
- if (exec) {
48
- const name = exec.split(/[/\\]/).pop()?.toLowerCase() ?? "";
49
- if (name !== "deno" && name !== "deno.exe") {
50
- return {
51
- read: true,
52
- write: false,
53
- net: true,
54
- env: true,
55
- run: false,
56
- ffi: false,
57
- sys: false,
58
- };
59
- }
60
- }
61
- } catch {
62
- // execPath may not be available
56
+ if (_isCompiledBinary) {
57
+ return {
58
+ read: true,
59
+ write: false,
60
+ net: true,
61
+ env: true,
62
+ run: false,
63
+ ffi: false,
64
+ sys: false,
65
+ };
63
66
  }
64
67
 
65
68
  return {
@@ -121,21 +121,29 @@ export class WorkerPool {
121
121
 
122
122
  if (shouldRecycle && !this.recycling.has(projectId)) {
123
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
- });
124
+
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
+
134
+ // Warm replacement: let the old worker handle this last request,
135
+ // then evict it and create a replacement after the request settles.
136
+ // This avoids cold-start latency for the caller AND prevents the
137
+ // old worker from being terminated while it still has pending work.
138
+ const result = worker.execute(request);
139
+
140
+ void result.finally(() => {
133
141
  this.evictWorker(projectId);
134
- const fresh = this.getOrCreateWorker(projectId, readPaths);
135
- return fresh.execute(request);
136
- } finally {
142
+ this.getOrCreateWorker(projectId, readPaths);
137
143
  this.recycling.delete(projectId);
138
- }
144
+ });
145
+
146
+ return result;
139
147
  }
140
148
 
141
149
  return worker.execute(request);
@@ -198,6 +198,9 @@ export interface WorkerPoolConfig {
198
198
  memoryBudgetMb: number;
199
199
  }
200
200
 
201
+ /** Maximum request body size for worker isolation (10 MB) */
202
+ export const MAX_WORKER_BODY_BYTES = 10 * 1024 * 1024;
203
+
201
204
  export const DEFAULT_WORKER_POOL_CONFIG: WorkerPoolConfig = {
202
205
  maxPoolSize: 20,
203
206
  idleTimeoutMs: 300_000,
@@ -26,6 +26,12 @@ export interface TaskDefinition {
26
26
  name?: string;
27
27
  /** Task description */
28
28
  description?: string;
29
+ /** Optional JSON-schema-like input contract surfaced in APIs/UIs */
30
+ inputSchema?: Record<string, unknown>;
31
+ /** Optional JSON-schema-like output contract surfaced in APIs/UIs */
32
+ outputSchema?: Record<string, unknown>;
33
+ /** Whether this task can be scheduled via cron jobs */
34
+ schedulable?: boolean;
29
35
  /** The function to execute */
30
36
  run: (ctx: TaskContext) => Promise<unknown> | unknown;
31
37
  }