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.
- package/README.md +2 -0
- package/esm/cli/commands/files/command.d.ts +3 -3
- package/esm/cli/commands/pull/command.js +2 -2
- package/esm/cli/commands/task/command.d.ts.map +1 -1
- package/esm/cli/commands/task/command.js +57 -51
- package/esm/cli/commands/workflow/command-help.d.ts +3 -0
- package/esm/cli/commands/workflow/command-help.d.ts.map +1 -0
- package/esm/cli/commands/workflow/command-help.js +19 -0
- package/esm/cli/commands/workflow/command.d.ts +5 -0
- package/esm/cli/commands/workflow/command.d.ts.map +1 -0
- package/esm/cli/commands/workflow/command.js +119 -0
- package/esm/cli/commands/workflow/handler.d.ts +28 -0
- package/esm/cli/commands/workflow/handler.d.ts.map +1 -0
- package/esm/cli/commands/workflow/handler.js +19 -0
- package/esm/cli/help/command-definitions.d.ts.map +1 -1
- package/esm/cli/help/command-definitions.js +2 -0
- package/esm/cli/router.d.ts.map +1 -1
- package/esm/cli/router.js +2 -0
- package/esm/cli/shared/project-source-context.d.ts +18 -0
- package/esm/cli/shared/project-source-context.d.ts.map +1 -0
- package/esm/cli/shared/project-source-context.js +52 -0
- package/esm/deno.d.ts +6 -0
- package/esm/deno.js +12 -6
- package/esm/src/data/server-data-fetcher.d.ts.map +1 -1
- package/esm/src/data/server-data-fetcher.js +17 -1
- package/esm/src/jobs/index.d.ts +34 -0
- package/esm/src/jobs/index.d.ts.map +1 -0
- package/esm/src/jobs/index.js +33 -0
- package/esm/src/jobs/jobs-client.d.ts +134 -0
- package/esm/src/jobs/jobs-client.d.ts.map +1 -0
- package/esm/src/jobs/jobs-client.js +218 -0
- package/esm/src/jobs/schemas.d.ts +1304 -0
- package/esm/src/jobs/schemas.d.ts.map +1 -0
- package/esm/src/jobs/schemas.js +159 -0
- package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts +3 -3
- package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts.map +1 -1
- package/esm/src/platform/adapters/veryfront-api-client/operations.js +12 -10
- package/esm/src/platform/adapters/veryfront-api-client/retry-handler.d.ts +4 -0
- package/esm/src/platform/adapters/veryfront-api-client/retry-handler.d.ts.map +1 -1
- package/esm/src/platform/adapters/veryfront-api-client/retry-handler.js +12 -6
- package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.d.ts +4 -4
- package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.js +4 -4
- package/esm/src/platform/index.d.ts +1 -0
- package/esm/src/platform/index.d.ts.map +1 -1
- package/esm/src/platform/index.js +1 -0
- package/esm/src/proxy/handler.d.ts.map +1 -1
- package/esm/src/proxy/handler.js +21 -21
- package/esm/src/routing/api/route-executor.d.ts.map +1 -1
- package/esm/src/routing/api/route-executor.js +30 -3
- package/esm/src/security/deno-permissions.d.ts +1 -1
- package/esm/src/security/deno-permissions.d.ts.map +1 -1
- package/esm/src/security/deno-permissions.js +2 -1
- package/esm/src/security/sandbox/project-worker.d.ts.map +1 -1
- package/esm/src/security/sandbox/project-worker.js +2 -2
- package/esm/src/security/sandbox/worker-permissions.d.ts.map +1 -1
- package/esm/src/security/sandbox/worker-permissions.js +23 -20
- package/esm/src/security/sandbox/worker-pool.d.ts.map +1 -1
- package/esm/src/security/sandbox/worker-pool.js +17 -14
- package/esm/src/security/sandbox/worker-types.d.ts +2 -0
- package/esm/src/security/sandbox/worker-types.d.ts.map +1 -1
- package/esm/src/security/sandbox/worker-types.js +2 -0
- package/esm/src/task/types.d.ts +6 -0
- package/esm/src/task/types.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/cli/commands/pull/command.ts +3 -3
- package/src/cli/commands/task/command.ts +66 -57
- package/src/cli/commands/workflow/command-help.ts +21 -0
- package/src/cli/commands/workflow/command.ts +152 -0
- package/src/cli/commands/workflow/handler.ts +25 -0
- package/src/cli/help/command-definitions.ts +2 -0
- package/src/cli/router.ts +2 -0
- package/src/cli/shared/project-source-context.ts +96 -0
- package/src/deno.js +12 -6
- package/src/src/data/server-data-fetcher.ts +30 -2
- package/src/src/jobs/index.ts +85 -0
- package/src/src/jobs/jobs-client.ts +503 -0
- package/src/src/jobs/schemas.ts +202 -0
- package/src/src/platform/adapters/veryfront-api-client/operations.ts +14 -14
- package/src/src/platform/adapters/veryfront-api-client/retry-handler.ts +15 -6
- package/src/src/platform/adapters/veryfront-api-client/schemas/api.schema.ts +4 -4
- package/src/src/platform/index.ts +1 -0
- package/src/src/proxy/handler.ts +27 -19
- package/src/src/routing/api/route-executor.ts +43 -7
- package/src/src/security/deno-permissions.ts +2 -1
- package/src/src/security/sandbox/project-worker.ts +2 -2
- package/src/src/security/sandbox/worker-permissions.ts +22 -19
- package/src/src/security/sandbox/worker-pool.ts +21 -13
- package/src/src/security/sandbox/worker-types.ts +3 -0
- 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
|
-
|
|
157
|
+
branchRef = "main",
|
|
158
158
|
options: ListFilesOptions = {},
|
|
159
159
|
): Promise<FileListResult> {
|
|
160
160
|
const params = buildListParams(options);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
176
|
+
branchRef = "main",
|
|
178
177
|
options: Omit<ListFilesOptions, "cursor"> = {},
|
|
179
178
|
): Promise<ProjectFile[]> {
|
|
180
179
|
const allFiles = await listAllFiles((cursor) =>
|
|
181
|
-
this.listBranchFiles(projectRef,
|
|
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
|
-
|
|
189
|
+
branchRef,
|
|
191
190
|
totalFiles: allFiles.length,
|
|
192
191
|
});
|
|
193
192
|
|
|
194
193
|
return allFiles;
|
|
195
194
|
}
|
|
196
195
|
|
|
197
|
-
getBranchFile(projectRef: string,
|
|
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
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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":
|
|
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
|
-
|
|
53
|
-
|
|
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, {
|
|
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}/
|
|
183
|
-
description: "List files
|
|
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}/
|
|
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";
|
package/src/src/proxy/handler.ts
CHANGED
|
@@ -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(
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 =
|
|
291
|
+
const body = await readBodyWithSizeGuard(request);
|
|
256
292
|
|
|
257
293
|
const workerResponse = await pool.execute(
|
|
258
294
|
projectDir,
|
|
@@ -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(
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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,
|
package/src/src/task/types.ts
CHANGED
|
@@ -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
|
}
|