feat(worker): /box status, lifecycle, tree, and file HTTP routes
Wire the box helpers into the worker HTTP server as /box/* routes,
authed by the internal bearer token like every other worker route.
Adds a boxRoute matcher (/^\/box\/(status|lifecycle|tree|file)$/) and
dispatches before jobRoute; missing user -> 400, unknown lifecycle
action -> 400, containment throws -> 400 (server keeps serving).
Manual verification (harness on :3922, internal token, user gabriel):
GET /box/status?user=gabriel -> 200 {"status":{...running:false...}}
GET /box/status (no user) -> 400 {"error":"Missing user"}
(no Authorization header) -> 401
POST /box/lifecycle {"action":"bogus"} -> 400 {"error":"Unknown action"}
GET /box/tree?user=gabriel -> 200 {"tree":{"name":"~",...}}
PUT /box/file {"path":"spoon-test.txt","content":"hi from task4"} -> 200 {"success":true}
GET /box/file?path=spoon-test.txt -> 200 {"path":...,"content":"hi from task4"}
GET /box/file?path=../../../../../etc/passwd -> 400 {"error":"Refusing to access path outside home: ..."}
GET /box/status after escape -> 200 (no crash)
GET /box/other?user=gabriel -> 404 {"error":"Not found"}
This commit is contained in:
@@ -3,6 +3,15 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
|
||||
import type { Id } from '@spoon/backend/convex/_generated/dataModel.js';
|
||||
|
||||
import {
|
||||
getBoxStatus,
|
||||
listBoxTree,
|
||||
readBoxFile,
|
||||
restartBox,
|
||||
startBox,
|
||||
stopBox,
|
||||
writeBoxFile,
|
||||
} from './box';
|
||||
import { env } from './env';
|
||||
import { attachTerminalServer } from './terminal';
|
||||
import {
|
||||
@@ -56,6 +65,11 @@ const jobRoute = (pathname: string) => {
|
||||
return { jobId: decodeURIComponent(match[1]), action: match[2] };
|
||||
};
|
||||
|
||||
export const boxRoute = (pathname: string) => {
|
||||
const match = /^\/box\/(status|lifecycle|tree|file)$/.exec(pathname);
|
||||
return match?.[1] ? { action: match[1] } : null;
|
||||
};
|
||||
|
||||
export const startWorkerServer = () => {
|
||||
const server = createServer((request, response) => {
|
||||
void (async () => {
|
||||
@@ -73,6 +87,59 @@ export const startWorkerServer = () => {
|
||||
sendJson(response, 200, await cleanupOrphanedWorkspaces());
|
||||
return;
|
||||
}
|
||||
const box = boxRoute(url.pathname);
|
||||
if (box) {
|
||||
const user = url.searchParams.get('user') ?? '';
|
||||
if (!user) {
|
||||
sendJson(response, 400, { error: 'Missing user' });
|
||||
return;
|
||||
}
|
||||
if (request.method === 'GET' && box.action === 'status') {
|
||||
sendJson(response, 200, { status: await getBoxStatus(user) });
|
||||
return;
|
||||
}
|
||||
if (request.method === 'POST' && box.action === 'lifecycle') {
|
||||
const body = await parseJson<{ action?: string }>(request);
|
||||
if (body.action === 'start') {
|
||||
await startBox(user);
|
||||
} else if (body.action === 'stop') {
|
||||
await stopBox(user);
|
||||
} else if (body.action === 'restart') {
|
||||
await restartBox(user);
|
||||
} else {
|
||||
sendJson(response, 400, { error: 'Unknown action' });
|
||||
return;
|
||||
}
|
||||
sendJson(response, 200, { status: await getBoxStatus(user) });
|
||||
return;
|
||||
}
|
||||
if (request.method === 'GET' && box.action === 'tree') {
|
||||
sendJson(response, 200, { tree: await listBoxTree(user) });
|
||||
return;
|
||||
}
|
||||
if (request.method === 'GET' && box.action === 'file') {
|
||||
const filePath = url.searchParams.get('path') ?? '';
|
||||
sendJson(response, 200, {
|
||||
path: filePath,
|
||||
content: await readBoxFile(user, filePath),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (request.method === 'PUT' && box.action === 'file') {
|
||||
const body = await parseJson<{ path?: string; content?: string }>(
|
||||
request,
|
||||
);
|
||||
sendJson(
|
||||
response,
|
||||
200,
|
||||
await writeBoxFile(user, body.path ?? '', body.content ?? ''),
|
||||
);
|
||||
return;
|
||||
}
|
||||
sendJson(response, 404, { error: 'Not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const route = jobRoute(url.pathname);
|
||||
if (!route) {
|
||||
sendJson(response, 404, { error: 'Not found' });
|
||||
@@ -175,9 +242,11 @@ export const startWorkerServer = () => {
|
||||
const status =
|
||||
message === 'Unauthorized'
|
||||
? 401
|
||||
: message.includes('not supported')
|
||||
? 409
|
||||
: 500;
|
||||
: message.startsWith('Refusing to')
|
||||
? 400
|
||||
: message.includes('not supported')
|
||||
? 409
|
||||
: 500;
|
||||
sendJson(response, status, {
|
||||
error: message,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user