veryfront 0.1.75 → 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.
- package/README.md +2 -0
- package/esm/cli/commands/files/command.d.ts +3 -3
- package/esm/deno.d.ts +5 -0
- package/esm/deno.js +11 -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/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/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/server/handlers/request/internal-tasks-list.handler.d.ts +11 -0
- package/esm/src/server/handlers/request/internal-tasks-list.handler.d.ts.map +1 -0
- package/esm/src/server/handlers/request/internal-tasks-list.handler.js +72 -0
- package/esm/src/server/runtime-handler/index.d.ts +1 -1
- package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/index.js +3 -0
- package/esm/src/task/control-plane.d.ts +105 -0
- package/esm/src/task/control-plane.d.ts.map +1 -0
- package/esm/src/task/control-plane.js +52 -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/deno.js +11 -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/retry-handler.ts +15 -6
- 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/server/handlers/request/internal-tasks-list.handler.ts +103 -0
- package/src/src/server/runtime-handler/index.ts +3 -0
- package/src/src/task/control-plane.ts +76 -0
- package/src/src/task/types.ts +6 -0
|
@@ -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,
|
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,
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as dntShim from "../../../../_dnt.shims.js";
|
|
2
|
+
import { PRIORITY_MEDIUM_API } from "../../../utils/constants/index.js";
|
|
3
|
+
import { ZodError } from "zod";
|
|
4
|
+
import {
|
|
5
|
+
type ControlPlaneTasksListRequest,
|
|
6
|
+
ControlPlaneTasksListRequestSchema,
|
|
7
|
+
defaultRuntimeTaskDiscoveryDeps,
|
|
8
|
+
listRuntimeTasks,
|
|
9
|
+
type RuntimeTaskDiscoveryDeps,
|
|
10
|
+
} from "../../../task/control-plane.js";
|
|
11
|
+
import {
|
|
12
|
+
ControlPlaneRequestError,
|
|
13
|
+
verifyControlPlaneRequest,
|
|
14
|
+
} from "../../../internal-agents/control-plane-auth.js";
|
|
15
|
+
import {
|
|
16
|
+
INTERNAL_AGENT_CONTROL_PLANE_MAX_BODY_BYTES,
|
|
17
|
+
InternalAgentRequestBodyTooLargeError,
|
|
18
|
+
readInternalAgentRequestBody,
|
|
19
|
+
} from "../../../internal-agents/request-body.js";
|
|
20
|
+
import { BaseHandler } from "../response/base.js";
|
|
21
|
+
import type { HandlerContext, HandlerMetadata, HandlerPriority, HandlerResult } from "../types.js";
|
|
22
|
+
|
|
23
|
+
export class InternalTasksListHandler extends BaseHandler {
|
|
24
|
+
metadata: HandlerMetadata = {
|
|
25
|
+
name: "InternalTasksListHandler",
|
|
26
|
+
priority: PRIORITY_MEDIUM_API as HandlerPriority,
|
|
27
|
+
patterns: [{ pattern: "/internal/tasks/list", exact: true, method: "POST" }],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly deps: RuntimeTaskDiscoveryDeps = defaultRuntimeTaskDiscoveryDeps,
|
|
32
|
+
) {
|
|
33
|
+
super();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async handle(req: dntShim.Request, ctx: HandlerContext): Promise<HandlerResult> {
|
|
37
|
+
if (!this.shouldHandle(req, ctx)) {
|
|
38
|
+
return this.continue();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return this.withProxyContext(ctx, async () => {
|
|
42
|
+
const builder = this.createResponseBuilder(ctx)
|
|
43
|
+
.withCORS(req, ctx.securityConfig?.cors)
|
|
44
|
+
.withSecurity(ctx.securityConfig ?? undefined, req);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const rawBody = await readInternalAgentRequestBody(
|
|
48
|
+
req,
|
|
49
|
+
INTERNAL_AGENT_CONTROL_PLANE_MAX_BODY_BYTES,
|
|
50
|
+
);
|
|
51
|
+
const payload: ControlPlaneTasksListRequest = ControlPlaneTasksListRequestSchema.parse(
|
|
52
|
+
JSON.parse(rawBody),
|
|
53
|
+
);
|
|
54
|
+
const claims = await verifyControlPlaneRequest(req, ctx, rawBody, {
|
|
55
|
+
expectedSubject: payload.requestId,
|
|
56
|
+
expectedSurface: payload.surface,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
payload.projectId !== claims.project_id ||
|
|
61
|
+
(ctx.projectId !== undefined && payload.projectId !== ctx.projectId)
|
|
62
|
+
) {
|
|
63
|
+
this.logWarn("Internal tasks list request body did not match signed claims", {
|
|
64
|
+
projectSlug: ctx.projectSlug,
|
|
65
|
+
projectId: ctx.projectId,
|
|
66
|
+
requestId: payload.requestId,
|
|
67
|
+
signedRequestId: claims.sub,
|
|
68
|
+
surface: payload.surface,
|
|
69
|
+
signedSurface: claims.surface,
|
|
70
|
+
});
|
|
71
|
+
return this.respond(builder.json({ error: "Invalid control-plane signature" }, 401));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const response = await listRuntimeTasks(ctx, this.deps);
|
|
75
|
+
return this.respond(builder.json(response, 200));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof InternalAgentRequestBodyTooLargeError) {
|
|
78
|
+
return this.respond(builder.json({ error: error.message }, error.status));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (error instanceof ControlPlaneRequestError) {
|
|
82
|
+
this.logWarn("Internal tasks list signature verification failed", {
|
|
83
|
+
error: error.message,
|
|
84
|
+
projectSlug: ctx.projectSlug,
|
|
85
|
+
projectId: ctx.projectId,
|
|
86
|
+
});
|
|
87
|
+
return this.respond(builder.json({ error: error.message }, error.status));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (error instanceof SyntaxError || error instanceof ZodError) {
|
|
91
|
+
this.logWarn("Internal tasks list request validation failed", {
|
|
92
|
+
error: error instanceof Error ? error.message : String(error),
|
|
93
|
+
projectSlug: ctx.projectSlug,
|
|
94
|
+
projectId: ctx.projectId,
|
|
95
|
+
});
|
|
96
|
+
return this.respond(builder.json({ error: "Invalid internal tasks request" }, 400));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -55,6 +55,7 @@ import { MarkdownPreviewHandler } from "../handlers/preview/markdown-preview.han
|
|
|
55
55
|
import { OpenAPIHandler } from "../handlers/request/openapi.handler.js";
|
|
56
56
|
import { OpenAPIDocsHandler } from "../handlers/request/openapi-docs.handler.js";
|
|
57
57
|
import { InternalAgentsListHandler } from "../handlers/request/internal-agents-list.handler.js";
|
|
58
|
+
import { InternalTasksListHandler } from "../handlers/request/internal-tasks-list.handler.js";
|
|
58
59
|
import { AgentStreamHandler } from "../handlers/request/agent-stream.handler.js";
|
|
59
60
|
import { AgentRunResumeHandler } from "../handlers/request/agent-run-resume.handler.js";
|
|
60
61
|
import { AgentRunCancelHandler } from "../handlers/request/agent-run-cancel.handler.js";
|
|
@@ -131,6 +132,7 @@ export const HANDLER_NAMES = [
|
|
|
131
132
|
"OpenAPIHandler",
|
|
132
133
|
"OpenAPIDocsHandler",
|
|
133
134
|
"InternalAgentsListHandler",
|
|
135
|
+
"InternalTasksListHandler",
|
|
134
136
|
"AgentStreamHandler",
|
|
135
137
|
"AgentRunResumeHandler",
|
|
136
138
|
"AgentRunCancelHandler",
|
|
@@ -185,6 +187,7 @@ const handlerFactories: Record<
|
|
|
185
187
|
OpenAPIHandler: () => new OpenAPIHandler(),
|
|
186
188
|
OpenAPIDocsHandler: () => new OpenAPIDocsHandler(),
|
|
187
189
|
InternalAgentsListHandler: () => new InternalAgentsListHandler(),
|
|
190
|
+
InternalTasksListHandler: () => new InternalTasksListHandler(),
|
|
188
191
|
AgentStreamHandler: () => new AgentStreamHandler(),
|
|
189
192
|
AgentRunResumeHandler: () => new AgentRunResumeHandler(),
|
|
190
193
|
AgentRunCancelHandler: () => new AgentRunCancelHandler(),
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ControlPlaneSurfaceSchema } from "../channels/control-plane.js";
|
|
2
|
+
import type { HandlerContext } from "../types/index.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { discoverTasks, type TaskDiscoveryOptions } from "./discovery.js";
|
|
5
|
+
|
|
6
|
+
export const ControlPlaneTasksListRequestSchema = z.object({
|
|
7
|
+
requestId: z.string().min(1),
|
|
8
|
+
projectId: z.string().min(1),
|
|
9
|
+
surface: ControlPlaneSurfaceSchema,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const JsonSchemaRecordSchema = z.record(z.unknown());
|
|
13
|
+
|
|
14
|
+
export const RuntimeTaskSchema = z.object({
|
|
15
|
+
id: z.string().min(1),
|
|
16
|
+
name: z.string().min(1),
|
|
17
|
+
description: z.string().nullable(),
|
|
18
|
+
target: z.string().min(1),
|
|
19
|
+
sourcePath: z.string().min(1),
|
|
20
|
+
inputSchema: JsonSchemaRecordSchema.nullable(),
|
|
21
|
+
outputSchema: JsonSchemaRecordSchema.nullable(),
|
|
22
|
+
schedulable: z.boolean(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const RuntimeTaskListResponseSchema = z.object({
|
|
26
|
+
tasks: z.array(RuntimeTaskSchema),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type ControlPlaneTasksListRequest = z.infer<typeof ControlPlaneTasksListRequestSchema>;
|
|
30
|
+
export type RuntimeTask = z.infer<typeof RuntimeTaskSchema>;
|
|
31
|
+
export type RuntimeTaskListResponse = z.infer<typeof RuntimeTaskListResponseSchema>;
|
|
32
|
+
|
|
33
|
+
export interface RuntimeTaskDiscoveryDeps {
|
|
34
|
+
discoverTasks: (options: TaskDiscoveryOptions) => ReturnType<typeof discoverTasks>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const defaultRuntimeTaskDiscoveryDeps: RuntimeTaskDiscoveryDeps = {
|
|
38
|
+
discoverTasks,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function normalizeJsonSchema(value: unknown): Record<string, unknown> | null {
|
|
42
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return value as Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function listRuntimeTasks(
|
|
50
|
+
ctx: HandlerContext,
|
|
51
|
+
deps: RuntimeTaskDiscoveryDeps = defaultRuntimeTaskDiscoveryDeps,
|
|
52
|
+
): Promise<RuntimeTaskListResponse> {
|
|
53
|
+
const discovery = await deps.discoverTasks({
|
|
54
|
+
projectDir: ctx.projectDir,
|
|
55
|
+
adapter: ctx.adapter,
|
|
56
|
+
config: ctx.config,
|
|
57
|
+
debug: ctx.debug ?? false,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const tasks = discovery.tasks
|
|
61
|
+
.map((task) =>
|
|
62
|
+
RuntimeTaskSchema.parse({
|
|
63
|
+
id: task.id,
|
|
64
|
+
name: task.name,
|
|
65
|
+
description: task.definition.description ?? null,
|
|
66
|
+
target: `task:${task.id}`,
|
|
67
|
+
sourcePath: task.filePath,
|
|
68
|
+
inputSchema: normalizeJsonSchema(task.definition.inputSchema),
|
|
69
|
+
outputSchema: normalizeJsonSchema(task.definition.outputSchema),
|
|
70
|
+
schedulable: task.definition.schedulable ?? true,
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
74
|
+
|
|
75
|
+
return RuntimeTaskListResponseSchema.parse({ tasks });
|
|
76
|
+
}
|
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
|
}
|