@orion-agents/orion-code 0.2.2 → 0.3.1
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/CHANGELOG.md +82 -0
- package/README.md +89 -14
- package/README.zh-CN.md +77 -14
- package/bin/orion +15 -4
- package/dist/cli.js +51 -172
- package/dist/cli.js.map +1 -1
- package/dist/commands/model-command-handlers.d.ts +2 -2
- package/dist/commands/model-command-handlers.d.ts.map +1 -1
- package/dist/commands/model-command-handlers.js +113 -32
- package/dist/commands/model-command-handlers.js.map +1 -1
- package/dist/commands/session-command-handlers.d.ts.map +1 -1
- package/dist/commands/session-command-handlers.js +106 -15
- package/dist/commands/session-command-handlers.js.map +1 -1
- package/dist/commands/types.d.ts +13 -3
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/types.js +1 -1
- package/dist/commands/types.js.map +1 -1
- package/dist/core/tool-artifacts.d.ts +2 -0
- package/dist/core/tool-artifacts.d.ts.map +1 -1
- package/dist/core/tool-artifacts.js +27 -1
- package/dist/core/tool-artifacts.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/agent-loop.d.ts.map +1 -1
- package/dist/runtime/agent-loop.js +3 -3
- package/dist/runtime/agent-loop.js.map +1 -1
- package/dist/runtime/agent-runtime-controller.d.ts +32 -1
- package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
- package/dist/runtime/agent-runtime-controller.js +241 -27
- package/dist/runtime/agent-runtime-controller.js.map +1 -1
- package/dist/runtime/agent-runtime-protocol.d.ts +4 -0
- package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
- package/dist/runtime/agent-runtime-protocol.js.map +1 -1
- package/dist/runtime/agent-runtime-runner.d.ts +2 -1
- package/dist/runtime/agent-runtime-runner.d.ts.map +1 -1
- package/dist/runtime/durable-tool-receipt-reader.d.ts +32 -0
- package/dist/runtime/durable-tool-receipt-reader.d.ts.map +1 -0
- package/dist/runtime/durable-tool-receipt-reader.js +130 -0
- package/dist/runtime/durable-tool-receipt-reader.js.map +1 -0
- package/dist/runtime/legacy-thread-materializer.d.ts +31 -0
- package/dist/runtime/legacy-thread-materializer.d.ts.map +1 -1
- package/dist/runtime/legacy-thread-materializer.js +71 -17
- package/dist/runtime/legacy-thread-materializer.js.map +1 -1
- package/dist/runtime/orion-runtime-v1.d.ts +2 -0
- package/dist/runtime/orion-runtime-v1.d.ts.map +1 -1
- package/dist/runtime/orion-runtime-v1.js +7 -1
- package/dist/runtime/orion-runtime-v1.js.map +1 -1
- package/dist/runtime/orion-session-runner.d.ts +7 -2
- package/dist/runtime/orion-session-runner.d.ts.map +1 -1
- package/dist/runtime/orion-session-runner.js +16 -6
- package/dist/runtime/orion-session-runner.js.map +1 -1
- package/dist/runtime/product-bootstrap.d.ts +22 -0
- package/dist/runtime/product-bootstrap.d.ts.map +1 -0
- package/dist/runtime/product-bootstrap.js +374 -0
- package/dist/runtime/product-bootstrap.js.map +1 -0
- package/dist/runtime/product-orion-runtime.d.ts +2 -1
- package/dist/runtime/product-orion-runtime.d.ts.map +1 -1
- package/dist/runtime/product-orion-runtime.js +83 -13
- package/dist/runtime/product-orion-runtime.js.map +1 -1
- package/dist/runtime/protocol/runtime-protocol-v1.d.ts +1 -1
- package/dist/runtime/protocol/runtime-protocol-v1.js +1 -1
- package/dist/runtime/protocol/runtime-protocol-v1.js.map +1 -1
- package/dist/runtime/release-receipts.d.ts +66 -3
- package/dist/runtime/release-receipts.d.ts.map +1 -1
- package/dist/runtime/release-receipts.js +340 -7
- package/dist/runtime/release-receipts.js.map +1 -1
- package/dist/runtime/step-snapshot.d.ts.map +1 -1
- package/dist/runtime/step-snapshot.js +22 -2
- package/dist/runtime/step-snapshot.js.map +1 -1
- package/dist/runtime/subagents/runtime-integration.d.ts +1 -1
- package/dist/runtime/subagents/runtime-integration.d.ts.map +1 -1
- package/dist/runtime/subagents/runtime-integration.js +5 -2
- package/dist/runtime/subagents/runtime-integration.js.map +1 -1
- package/dist/runtime/thread-event-store.d.ts +80 -0
- package/dist/runtime/thread-event-store.d.ts.map +1 -1
- package/dist/runtime/thread-event-store.js +456 -22
- package/dist/runtime/thread-event-store.js.map +1 -1
- package/dist/runtime/thread-projection.d.ts +10 -0
- package/dist/runtime/thread-projection.d.ts.map +1 -1
- package/dist/runtime/thread-projection.js +40 -0
- package/dist/runtime/thread-projection.js.map +1 -1
- package/dist/runtime/thread-runtime.d.ts +3 -1
- package/dist/runtime/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/thread-runtime.js +6 -0
- package/dist/runtime/thread-runtime.js.map +1 -1
- package/dist/runtime/thread-session-index.d.ts +84 -0
- package/dist/runtime/thread-session-index.d.ts.map +1 -0
- package/dist/runtime/thread-session-index.js +503 -0
- package/dist/runtime/thread-session-index.js.map +1 -0
- package/dist/runtime/thread-session-view.d.ts +70 -9
- package/dist/runtime/thread-session-view.d.ts.map +1 -1
- package/dist/runtime/thread-session-view.js +200 -82
- package/dist/runtime/thread-session-view.js.map +1 -1
- package/dist/runtime/thread-ui-adapter.d.ts +2 -0
- package/dist/runtime/thread-ui-adapter.d.ts.map +1 -1
- package/dist/runtime/thread-ui-adapter.js +86 -4
- package/dist/runtime/thread-ui-adapter.js.map +1 -1
- package/dist/runtime/tool-detail-repository.d.ts.map +1 -1
- package/dist/runtime/tool-detail-repository.js +23 -19
- package/dist/runtime/tool-detail-repository.js.map +1 -1
- package/dist/runtime/tool-receipt-validator.d.ts +21 -0
- package/dist/runtime/tool-receipt-validator.d.ts.map +1 -0
- package/dist/runtime/tool-receipt-validator.js +157 -0
- package/dist/runtime/tool-receipt-validator.js.map +1 -0
- package/dist/runtime/ui-events.d.ts +35 -0
- package/dist/runtime/ui-events.d.ts.map +1 -1
- package/dist/runtime/ui-events.js +1 -0
- package/dist/runtime/ui-events.js.map +1 -1
- package/dist/runtime/ui-view-model.d.ts +2 -0
- package/dist/runtime/ui-view-model.d.ts.map +1 -1
- package/dist/runtime/ui-view-model.js +7 -3
- package/dist/runtime/ui-view-model.js.map +1 -1
- package/dist/services/global-config.d.ts +25 -0
- package/dist/services/global-config.d.ts.map +1 -1
- package/dist/services/global-config.js +373 -5
- package/dist/services/global-config.js.map +1 -1
- package/dist/services/redaction.d.ts +4 -0
- package/dist/services/redaction.d.ts.map +1 -1
- package/dist/services/redaction.js +63 -0
- package/dist/services/redaction.js.map +1 -1
- package/dist/services/session-index.d.ts +16 -0
- package/dist/services/session-index.d.ts.map +1 -1
- package/dist/services/session-index.js +45 -36
- package/dist/services/session-index.js.map +1 -1
- package/dist/services/session-storage.d.ts +59 -0
- package/dist/services/session-storage.d.ts.map +1 -1
- package/dist/services/session-storage.js +306 -107
- package/dist/services/session-storage.js.map +1 -1
- package/dist/services/settings-coordinator.d.ts +174 -0
- package/dist/services/settings-coordinator.d.ts.map +1 -0
- package/dist/services/settings-coordinator.js +605 -0
- package/dist/services/settings-coordinator.js.map +1 -0
- package/dist/services/settings-document-repository.d.ts +138 -0
- package/dist/services/settings-document-repository.d.ts.map +1 -0
- package/dist/services/settings-document-repository.js +551 -0
- package/dist/services/settings-document-repository.js.map +1 -0
- package/dist/services/workspace-registry.d.ts +42 -0
- package/dist/services/workspace-registry.d.ts.map +1 -0
- package/dist/services/workspace-registry.js +222 -0
- package/dist/services/workspace-registry.js.map +1 -0
- package/dist/terminal-ui/launch.d.ts.map +1 -1
- package/dist/terminal-ui/launch.js +5 -0
- package/dist/terminal-ui/launch.js.map +1 -1
- package/dist/tui-ui/state.d.ts.map +1 -1
- package/dist/tui-ui/state.js +5 -0
- package/dist/tui-ui/state.js.map +1 -1
- package/dist/web/errors.d.ts +6 -0
- package/dist/web/errors.d.ts.map +1 -0
- package/dist/web/errors.js +13 -0
- package/dist/web/errors.js.map +1 -0
- package/dist/web/event-hub.d.ts +43 -0
- package/dist/web/event-hub.d.ts.map +1 -0
- package/dist/web/event-hub.js +323 -0
- package/dist/web/event-hub.js.map +1 -0
- package/dist/web/file-read-service.d.ts +55 -0
- package/dist/web/file-read-service.d.ts.map +1 -0
- package/dist/web/file-read-service.js +399 -0
- package/dist/web/file-read-service.js.map +1 -0
- package/dist/web/git-read-model-service.d.ts +77 -0
- package/dist/web/git-read-model-service.d.ts.map +1 -0
- package/dist/web/git-read-model-service.js +631 -0
- package/dist/web/git-read-model-service.js.map +1 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +28 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/launch.d.ts +9 -0
- package/dist/web/launch.d.ts.map +1 -0
- package/dist/web/launch.js +64 -0
- package/dist/web/launch.js.map +1 -0
- package/dist/web/protocol.d.ts +408 -0
- package/dist/web/protocol.d.ts.map +1 -0
- package/dist/web/protocol.js +369 -0
- package/dist/web/protocol.js.map +1 -0
- package/dist/web/review-service.d.ts +38 -0
- package/dist/web/review-service.d.ts.map +1 -0
- package/dist/web/review-service.js +74 -0
- package/dist/web/review-service.js.map +1 -0
- package/dist/web/server.d.ts +20 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +744 -0
- package/dist/web/server.js.map +1 -0
- package/dist/web/terminal-manager.d.ts +155 -0
- package/dist/web/terminal-manager.d.ts.map +1 -0
- package/dist/web/terminal-manager.js +728 -0
- package/dist/web/terminal-manager.js.map +1 -0
- package/dist/web/terminal-server.d.ts +8 -0
- package/dist/web/terminal-server.d.ts.map +1 -0
- package/dist/web/terminal-server.js +275 -0
- package/dist/web/terminal-server.js.map +1 -0
- package/dist/web/workbench-controller.d.ts +99 -0
- package/dist/web/workbench-controller.d.ts.map +1 -0
- package/dist/web/workbench-controller.js +1267 -0
- package/dist/web/workbench-controller.js.map +1 -0
- package/dist/web-client/assets/DiffViewer-DpKoD07C.js +5 -0
- package/dist/web-client/assets/FilesPanel-BkoLmgjR.js +2 -0
- package/dist/web-client/assets/GitPanel-D20StcdG.js +1 -0
- package/dist/web-client/assets/ReviewPanel-3MVWbMQV.js +1 -0
- package/dist/web-client/assets/TerminalPanel-BLQ592Fb.js +21 -0
- package/dist/web-client/assets/TerminalPanel-DLuoa74B.css +1 -0
- package/dist/web-client/assets/index-BkIDl_xk.js +16 -0
- package/dist/web-client/assets/index-DXMpnWE8.css +1 -0
- package/dist/web-client/index.html +16 -0
- package/docs/architecture/v0.3.0-web-api.yaml +1340 -0
- package/docs/architecture/v0.3.1-web-api.yaml +2290 -0
- package/docs/migration/v0.2.2-to-v0.3.0-settings.md +114 -0
- package/docs/migration/v0.2.2-to-v0.3.0.md +55 -0
- package/docs/migration/v0.3.0-to-v0.3.1.md +88 -0
- package/docs/orion.example.json +9 -2
- package/docs/plan/v0.3.0-node-runtime-compatibility-plan.md +57 -0
- package/docs/plan/v0.3.0-settings-integration-plan.md +740 -0
- package/docs/plan/v0.3.0-web-workbench-plan.md +376 -0
- package/docs/plan/v0.3.1-web-workbench-professional-shell-plan.md +848 -0
- package/docs/readme.md +22 -1
- package/docs/test/v0.3.1-web-workbench-e2e-plan.md +140 -0
- package/npm-shrinkwrap.json +1229 -45
- package/package.json +47 -11
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startOrionWebServer = startOrionWebServer;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const http_1 = require("http");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const version_1 = require("../product/version");
|
|
9
|
+
const redaction_1 = require("../services/redaction");
|
|
10
|
+
const protocol_1 = require("./protocol");
|
|
11
|
+
const errors_1 = require("./errors");
|
|
12
|
+
const terminal_server_1 = require("./terminal-server");
|
|
13
|
+
const workbench_controller_1 = require("./workbench-controller");
|
|
14
|
+
const HOST = '127.0.0.1';
|
|
15
|
+
const API_PREFIX = '/api/v1';
|
|
16
|
+
async function startOrionWebServer(options) {
|
|
17
|
+
const requestedPort = options.port ?? 3080;
|
|
18
|
+
if (!Number.isSafeInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
|
|
19
|
+
throw new Error('Web port must be an integer from 0 through 65535.');
|
|
20
|
+
}
|
|
21
|
+
const nonce = options.nonce ?? (0, crypto_1.randomBytes)(32).toString('base64url');
|
|
22
|
+
if (!/^[A-Za-z0-9_-]+$/u.test(nonce)) {
|
|
23
|
+
throw new Error('Web nonce must use unpadded base64url characters.');
|
|
24
|
+
}
|
|
25
|
+
const workbench = options.workbench ?? (await workbench_controller_1.WebWorkbenchController.create({ cwd: options.cwd }));
|
|
26
|
+
const staticRoot = options.staticRoot ?? resolveDefaultStaticRoot();
|
|
27
|
+
let origin = '';
|
|
28
|
+
let closing;
|
|
29
|
+
const server = (0, http_1.createServer)((request, response) => {
|
|
30
|
+
void handleRequest({ request, response, nonce, origin, staticRoot, workbench }).catch(error => {
|
|
31
|
+
if (response.headersSent) {
|
|
32
|
+
response.destroy();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
sendProblem(response, error);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
const terminalSockets = (0, terminal_server_1.attachTerminalWebSocketServer)(server, () => origin, workbench.terminalManager);
|
|
39
|
+
server.requestTimeout = 30000;
|
|
40
|
+
server.headersTimeout = 15000;
|
|
41
|
+
server.keepAliveTimeout = 5000;
|
|
42
|
+
server.maxHeadersCount = 64;
|
|
43
|
+
await listen(server, requestedPort);
|
|
44
|
+
const address = server.address();
|
|
45
|
+
if (!address || typeof address === 'string') {
|
|
46
|
+
await workbench.shutdown();
|
|
47
|
+
throw new Error('Web server did not expose a TCP address.');
|
|
48
|
+
}
|
|
49
|
+
const port = address.port;
|
|
50
|
+
origin = `http://${HOST}:${port}`;
|
|
51
|
+
const heartbeat = setInterval(() => workbench.eventHub.heartbeat(), 15000);
|
|
52
|
+
heartbeat.unref();
|
|
53
|
+
const close = () => {
|
|
54
|
+
if (closing)
|
|
55
|
+
return closing;
|
|
56
|
+
closing = (async () => {
|
|
57
|
+
clearInterval(heartbeat);
|
|
58
|
+
await terminalSockets.close();
|
|
59
|
+
await workbench.shutdown();
|
|
60
|
+
await closeServer(server);
|
|
61
|
+
})();
|
|
62
|
+
return closing;
|
|
63
|
+
};
|
|
64
|
+
return Object.freeze({ host: HOST, port, url: origin, nonce, workbench, server, close });
|
|
65
|
+
}
|
|
66
|
+
async function handleRequest(context) {
|
|
67
|
+
const { request, response } = context;
|
|
68
|
+
applySecurityHeaders(response, context.nonce);
|
|
69
|
+
assertHost(request, context.origin);
|
|
70
|
+
const url = new URL(request.url ?? '/', context.origin);
|
|
71
|
+
if (!url.pathname.startsWith(API_PREFIX)) {
|
|
72
|
+
await serveStatic(request, response, url.pathname, context.staticRoot);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const path = url.pathname.slice(API_PREFIX.length) || '/';
|
|
76
|
+
const method = request.method ?? 'GET';
|
|
77
|
+
if (method === 'GET' && path === '/health') {
|
|
78
|
+
sendJson(response, 200, { ok: true, version: version_1.PACKAGE_VERSION });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (method === 'GET' && path === '/bootstrap') {
|
|
82
|
+
sendJson(response, 200, context.workbench.bootstrap(context.nonce));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (method === 'GET' && path === '/events') {
|
|
86
|
+
openEventStream(request, response, url, context.workbench);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (method === 'GET' && path === '/workspaces') {
|
|
90
|
+
sendJson(response, 200, collectionPage(url, 'workspaces', context.workbench.listWorkspaces(), workspace => workspace.id));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const workspaceSessionsMatch = path.match(/^\/workspaces\/([^/]+)\/sessions$/);
|
|
94
|
+
if (method === 'GET' && workspaceSessionsMatch) {
|
|
95
|
+
const workspaceId = safeDecodePathSegment(workspaceSessionsMatch[1]);
|
|
96
|
+
sendJson(response, 200, collectionPage(url, `workspace-sessions-${workspaceId}`, context.workbench.listWorkspaceSessions(workspaceId), session => session.id));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const workspaceSummaryMatch = path.match(/^\/workspaces\/([^/]+)\/summary$/);
|
|
100
|
+
if (method === 'GET' && workspaceSummaryMatch) {
|
|
101
|
+
const workspaceId = requireUuid(safeDecodePathSegment(workspaceSummaryMatch[1]), 'workspaceId');
|
|
102
|
+
sendJson(response, 200, await context.workbench.workspaceProjectSummary(workspaceId));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const workspacePinMatch = path.match(/^\/workspaces\/([^/]+)\/pin$/);
|
|
106
|
+
if (method === 'POST' && workspacePinMatch) {
|
|
107
|
+
assertMutation(request, context.nonce, context.origin);
|
|
108
|
+
const body = requireRecord(await readJson(request), 'Workspace pin request');
|
|
109
|
+
assertOnlyKeys(body, ['requestId', 'pinned', 'expectedContextRevision', 'workspaceId']);
|
|
110
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
111
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
112
|
+
if (typeof body.pinned !== 'boolean') {
|
|
113
|
+
throw new errors_1.WebWorkbenchError(400, 'pinned must be a boolean.', 'invalid_request');
|
|
114
|
+
}
|
|
115
|
+
const workspaceId = requireUuid(safeDecodePathSegment(workspacePinMatch[1]), 'workspaceId');
|
|
116
|
+
const result = await context.workbench.executeMutation(requestId, 'workspace.pin', { targetWorkspaceId: workspaceId, pinned: body.pinned, ...contextGuard }, () => ({
|
|
117
|
+
requestId,
|
|
118
|
+
workspace: context.workbench.setWorkspacePinned(workspaceId, body.pinned, contextGuard),
|
|
119
|
+
}));
|
|
120
|
+
sendJson(response, 200, result);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const workspaceRemoveMatch = path.match(/^\/workspaces\/([^/]+)$/);
|
|
124
|
+
if (method === 'DELETE' && workspaceRemoveMatch) {
|
|
125
|
+
assertMutation(request, context.nonce, context.origin);
|
|
126
|
+
const body = requireRecord(await readJson(request), 'Workspace removal request');
|
|
127
|
+
assertOnlyKeys(body, ['requestId', 'expectedContextRevision', 'workspaceId']);
|
|
128
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
129
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
130
|
+
const workspaceId = requireUuid(safeDecodePathSegment(workspaceRemoveMatch[1]), 'workspaceId');
|
|
131
|
+
const result = await context.workbench.executeMutation(requestId, 'workspace.remove', { targetWorkspaceId: workspaceId, ...contextGuard }, () => {
|
|
132
|
+
context.workbench.removeWorkspace(workspaceId, contextGuard);
|
|
133
|
+
return { requestId, removed: true };
|
|
134
|
+
});
|
|
135
|
+
sendJson(response, 200, result);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (method === 'POST' && path === '/context/activate') {
|
|
139
|
+
assertMutation(request, context.nonce, context.origin);
|
|
140
|
+
const body = (0, protocol_1.parseWebContextActivate)(await readJson(request));
|
|
141
|
+
const result = await context.workbench.executeMutation(body.requestId, 'context.activate', {
|
|
142
|
+
expectedContextRevision: body.expectedContextRevision,
|
|
143
|
+
workspaceId: body.workspaceId,
|
|
144
|
+
sessionId: body.sessionId,
|
|
145
|
+
}, async () => {
|
|
146
|
+
await context.workbench.activateContext(body);
|
|
147
|
+
return {
|
|
148
|
+
requestId: body.requestId,
|
|
149
|
+
contextRevision: context.workbench.contextRevision,
|
|
150
|
+
bootstrap: context.workbench.bootstrap(context.nonce),
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
sendJson(response, 200, result);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (method === 'POST' && path === '/workspaces/activate') {
|
|
157
|
+
assertMutation(request, context.nonce, context.origin);
|
|
158
|
+
const body = requireRecord(await readJson(request), 'Workspace request');
|
|
159
|
+
assertOnlyKeys(body, ['requestId', 'path', 'expectedContextRevision', 'workspaceId']);
|
|
160
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
161
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
162
|
+
const workspace = requireText(body.path, 'path', 4096);
|
|
163
|
+
const result = await context.workbench.executeMutation(requestId, 'workspace.activate', { path: workspace, ...contextGuard }, async () => {
|
|
164
|
+
await context.workbench.switchWorkspace(workspace, contextGuard);
|
|
165
|
+
return {
|
|
166
|
+
requestId,
|
|
167
|
+
active: context.workbench.workspace,
|
|
168
|
+
page: (0, workbench_controller_1.pageCollectionItems)('workspaces', context.workbench.listWorkspaces(), undefined, 50, workspace => workspace.id),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
sendJson(response, 200, result);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (method === 'GET' && path === '/sessions') {
|
|
175
|
+
const contextGuard = requireContextGuardQuery(url);
|
|
176
|
+
sendJson(response, 200, collectionPage(url, 'sessions', context.workbench.listSessions(contextGuard), session => session.id));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (method === 'POST' && path === '/sessions') {
|
|
180
|
+
assertMutation(request, context.nonce, context.origin);
|
|
181
|
+
const body = requireRecord(await readJson(request), 'Session request');
|
|
182
|
+
assertOnlyKeys(body, ['requestId', 'name', 'expectedContextRevision', 'workspaceId']);
|
|
183
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
184
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
185
|
+
const name = body.name === undefined ? undefined : requireText(body.name, 'name', 120);
|
|
186
|
+
const result = await context.workbench.executeMutation(requestId, 'session.create', { name, ...contextGuard }, async () => ({
|
|
187
|
+
requestId,
|
|
188
|
+
session: await context.workbench.createSession(name, contextGuard),
|
|
189
|
+
}));
|
|
190
|
+
sendJson(response, 201, result);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const activateMatch = path.match(/^\/sessions\/([^/]+)\/activate$/);
|
|
194
|
+
if (method === 'POST' && activateMatch) {
|
|
195
|
+
assertMutation(request, context.nonce, context.origin);
|
|
196
|
+
const body = requireRecord(await readJson(request), 'Session activation request');
|
|
197
|
+
assertOnlyKeys(body, ['requestId', 'expectedContextRevision', 'workspaceId']);
|
|
198
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
199
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
200
|
+
const sessionId = safeDecodePathSegment(activateMatch[1]);
|
|
201
|
+
const result = await context.workbench.executeMutation(requestId, 'session.activate', { sessionId, ...contextGuard }, async () => ({
|
|
202
|
+
...(await context.workbench.activateSession(sessionId, contextGuard)),
|
|
203
|
+
requestId,
|
|
204
|
+
}));
|
|
205
|
+
sendJson(response, 200, result);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const snapshotMatch = path.match(/^\/sessions\/([^/]+)\/snapshot$/);
|
|
209
|
+
if (method === 'GET' && snapshotMatch) {
|
|
210
|
+
const contextGuard = requireContextGuardQuery(url);
|
|
211
|
+
sendJson(response, 200, context.workbench.sessionSnapshot(safeDecodePathSegment(snapshotMatch[1]), url.searchParams.get('cursor') ?? undefined, pageSize(url), url.searchParams.get('tail') === '1', contextGuard));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const sessionMatch = path.match(/^\/sessions\/([^/]+)$/);
|
|
215
|
+
if (method === 'PATCH' && sessionMatch) {
|
|
216
|
+
assertMutation(request, context.nonce, context.origin);
|
|
217
|
+
const body = requireRecord(await readJson(request), 'Session update request');
|
|
218
|
+
assertOnlyKeys(body, ['requestId', 'name', 'expectedContextRevision', 'workspaceId']);
|
|
219
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
220
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
221
|
+
const name = typeof body.name === 'string' ? body.name : '';
|
|
222
|
+
const sessionId = safeDecodePathSegment(sessionMatch[1]);
|
|
223
|
+
const result = await context.workbench.executeMutation(requestId, 'session.rename', { sessionId, name, ...contextGuard }, () => ({
|
|
224
|
+
requestId,
|
|
225
|
+
session: context.workbench.renameSession(sessionId, name, contextGuard),
|
|
226
|
+
}));
|
|
227
|
+
sendJson(response, 200, result);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (method === 'POST' && path === '/commands') {
|
|
231
|
+
assertMutation(request, context.nonce, context.origin);
|
|
232
|
+
sendJson(response, 202, await context.workbench.dispatch(await readJson(request)));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (method === 'GET' && path === '/settings') {
|
|
236
|
+
sendJson(response, 200, await context.workbench.settings());
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (method === 'PATCH' && path === '/settings') {
|
|
240
|
+
assertMutation(request, context.nonce, context.origin, 'settings_write_forbidden');
|
|
241
|
+
const body = (0, protocol_1.parseWebSettingsUpdate)(await readJson(request));
|
|
242
|
+
const result = await context.workbench.executeMutation(body.requestId, 'settings.update', { expectedRevision: body.expectedRevision, operations: body.operations }, async () => ({ requestId: body.requestId, ...(await context.workbench.updateSettings(body)) }));
|
|
243
|
+
sendJson(response, 200, result);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (method === 'POST' && path === '/settings/open-document') {
|
|
247
|
+
assertMutation(request, context.nonce, context.origin, 'settings_write_forbidden');
|
|
248
|
+
const { requestId } = (0, protocol_1.parseWebOpenSettingsDocument)(await readJson(request));
|
|
249
|
+
const result = await context.workbench.executeMutation(requestId, 'settings.open-document', {}, async () => ({ requestId, opened: await context.workbench.openSettingsDocument() }));
|
|
250
|
+
sendJson(response, 200, result);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (method === 'GET' && path === '/skills') {
|
|
254
|
+
sendJson(response, 200, collectionPage(url, 'skills', await context.workbench.skills(), skill => skill.id));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (method === 'GET' && path === '/mcp') {
|
|
258
|
+
sendJson(response, 200, collectionPage(url, 'mcp', context.workbench.mcp(), server => server.id));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (method === 'GET' && path === '/files') {
|
|
262
|
+
const contextGuard = requireContextGuardQuery(url);
|
|
263
|
+
sendJson(response, 200, context.workbench.listFiles(contextGuard, {
|
|
264
|
+
...(url.searchParams.get('parentId')
|
|
265
|
+
? { parentId: url.searchParams.get('parentId') }
|
|
266
|
+
: {}),
|
|
267
|
+
...(url.searchParams.get('cursor')
|
|
268
|
+
? { cursor: url.searchParams.get('cursor') }
|
|
269
|
+
: {}),
|
|
270
|
+
pageSize: pageSize(url),
|
|
271
|
+
}));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const fileContentMatch = path.match(/^\/files\/([^/]+)\/content$/);
|
|
275
|
+
if (method === 'GET' && fileContentMatch) {
|
|
276
|
+
const contextGuard = requireContextGuardQuery(url);
|
|
277
|
+
sendJson(response, 200, context.workbench.readFileContent(contextGuard, {
|
|
278
|
+
fileId: safeDecodePathSegment(fileContentMatch[1]),
|
|
279
|
+
...(url.searchParams.get('cursor')
|
|
280
|
+
? { cursor: url.searchParams.get('cursor') }
|
|
281
|
+
: {}),
|
|
282
|
+
limitBytes: boundedInteger(url.searchParams.get('limitBytes'), 64 * 1024, 1, 256 * 1024),
|
|
283
|
+
}));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (method === 'GET' && path === '/git/status') {
|
|
287
|
+
const contextGuard = requireContextGuardQuery(url);
|
|
288
|
+
sendJson(response, 200, await context.workbench.gitStatus(contextGuard, {
|
|
289
|
+
...(url.searchParams.get('cursor')
|
|
290
|
+
? { cursor: url.searchParams.get('cursor') }
|
|
291
|
+
: {}),
|
|
292
|
+
pageSize: boundedInteger(url.searchParams.get('pageSize'), 200, 1, 2000),
|
|
293
|
+
}));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (method === 'GET' && path === '/git/log') {
|
|
297
|
+
const contextGuard = requireContextGuardQuery(url);
|
|
298
|
+
sendJson(response, 200, await context.workbench.gitLog(contextGuard, {
|
|
299
|
+
...(url.searchParams.get('cursor')
|
|
300
|
+
? { cursor: url.searchParams.get('cursor') }
|
|
301
|
+
: {}),
|
|
302
|
+
pageSize: boundedInteger(url.searchParams.get('pageSize'), 30, 1, 100),
|
|
303
|
+
}));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const gitDiffMatch = path.match(/^\/git\/diff\/([^/]+)$/);
|
|
307
|
+
if (method === 'GET' && gitDiffMatch) {
|
|
308
|
+
const contextGuard = requireContextGuardQuery(url);
|
|
309
|
+
sendJson(response, 200, await context.workbench.gitDiff(contextGuard, {
|
|
310
|
+
fileId: safeDecodePathSegment(gitDiffMatch[1]),
|
|
311
|
+
...(url.searchParams.get('cursor')
|
|
312
|
+
? { cursor: url.searchParams.get('cursor') }
|
|
313
|
+
: {}),
|
|
314
|
+
lineLimit: boundedInteger(url.searchParams.get('lineLimit'), 240, 1, 500),
|
|
315
|
+
byteLimit: boundedInteger(url.searchParams.get('byteLimit'), 256 * 1024, 1024, 1024 * 1024),
|
|
316
|
+
}));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (method === 'GET' && path === '/review') {
|
|
320
|
+
sendJson(response, 200, await context.workbench.review(requireContextGuardQuery(url)));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (method === 'GET' && path === '/terminals') {
|
|
324
|
+
const contextGuard = requireContextGuardQuery(url);
|
|
325
|
+
sendJson(response, 200, collectionPage(url, 'terminals', context.workbench.listTerminals(contextGuard), terminal => terminal.id));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (method === 'POST' && path === '/terminals') {
|
|
329
|
+
assertMutation(request, context.nonce, context.origin, 'terminal_create_forbidden');
|
|
330
|
+
assertTerminalUserGesture(request);
|
|
331
|
+
const body = requireRecord(await readJson(request), 'Terminal create request');
|
|
332
|
+
assertOnlyKeys(body, ['requestId', 'expectedContextRevision', 'workspaceId', 'cols', 'rows']);
|
|
333
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
334
|
+
const expectedContextRevision = requireUuid(body.expectedContextRevision, 'expectedContextRevision');
|
|
335
|
+
const workspaceId = requireUuid(body.workspaceId, 'workspaceId');
|
|
336
|
+
const cols = boundedIntegerValue(body.cols, 100, 2, 400, 'cols');
|
|
337
|
+
const rows = boundedIntegerValue(body.rows, 30, 1, 200, 'rows');
|
|
338
|
+
const result = await context.workbench.executeMutation(requestId, 'terminal.create', { expectedContextRevision, workspaceId, cols, rows }, () => ({
|
|
339
|
+
requestId,
|
|
340
|
+
...context.workbench.terminalManager.create({
|
|
341
|
+
expectedContextRevision,
|
|
342
|
+
workspaceId,
|
|
343
|
+
cols,
|
|
344
|
+
rows,
|
|
345
|
+
}),
|
|
346
|
+
}));
|
|
347
|
+
sendJson(response, 201, result);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const terminalTicketMatch = path.match(/^\/terminals\/([^/]+)\/attach-ticket$/);
|
|
351
|
+
if (method === 'POST' && terminalTicketMatch) {
|
|
352
|
+
assertMutation(request, context.nonce, context.origin, 'terminal_attach_forbidden');
|
|
353
|
+
const body = requireRecord(await readJson(request), 'Terminal ticket request');
|
|
354
|
+
assertOnlyKeys(body, ['requestId', 'expectedContextRevision', 'workspaceId']);
|
|
355
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
356
|
+
const expectedContextRevision = requireUuid(body.expectedContextRevision, 'expectedContextRevision');
|
|
357
|
+
const workspaceId = requireUuid(body.workspaceId, 'workspaceId');
|
|
358
|
+
const terminalId = safeDecodePathSegment(terminalTicketMatch[1]);
|
|
359
|
+
const result = await context.workbench.executeMutation(requestId, 'terminal.attach-ticket', { terminalId, expectedContextRevision, workspaceId }, () => ({
|
|
360
|
+
requestId,
|
|
361
|
+
...context.workbench.terminalManager.issueAttachTicket({
|
|
362
|
+
terminalId,
|
|
363
|
+
expectedContextRevision,
|
|
364
|
+
workspaceId,
|
|
365
|
+
}),
|
|
366
|
+
}));
|
|
367
|
+
sendJson(response, 200, result);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const terminalMatch = path.match(/^\/terminals\/([^/]+)$/);
|
|
371
|
+
if (method === 'DELETE' && terminalMatch) {
|
|
372
|
+
assertMutation(request, context.nonce, context.origin, 'terminal_close_forbidden');
|
|
373
|
+
const body = requireRecord(await readJson(request), 'Terminal close request');
|
|
374
|
+
assertOnlyKeys(body, ['requestId', 'expectedContextRevision', 'workspaceId']);
|
|
375
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
376
|
+
const expectedContextRevision = requireUuid(body.expectedContextRevision, 'expectedContextRevision');
|
|
377
|
+
const workspaceId = requireUuid(body.workspaceId, 'workspaceId');
|
|
378
|
+
const terminalId = safeDecodePathSegment(terminalMatch[1]);
|
|
379
|
+
const result = await context.workbench.executeMutation(requestId, 'terminal.close', { terminalId, expectedContextRevision, workspaceId }, () => ({
|
|
380
|
+
requestId,
|
|
381
|
+
terminal: context.workbench.terminalManager.closeTerminal({
|
|
382
|
+
terminalId,
|
|
383
|
+
expectedContextRevision,
|
|
384
|
+
workspaceId,
|
|
385
|
+
}),
|
|
386
|
+
}));
|
|
387
|
+
sendJson(response, 200, result);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (method === 'GET' && path === '/tool-details') {
|
|
391
|
+
sendJson(response, 200, collectionPage(url, 'tool-details', await context.workbench.listToolDetails(), detail => `${detail.sequence}:${detail.callId}`));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const toolDetailMatch = path.match(/^\/tool-details\/([^/]+)$/);
|
|
395
|
+
if (method === 'GET' && toolDetailMatch) {
|
|
396
|
+
const offsetBytes = boundedInteger(url.searchParams.get('offsetBytes'), 0, 0, 2 ** 31 - 1);
|
|
397
|
+
const limitBytes = boundedInteger(url.searchParams.get('limitBytes'), 64 * 1024, 1, 1024 * 1024);
|
|
398
|
+
sendJson(response, 200, await context.workbench.readToolDetail(safeDecodePathSegment(toolDetailMatch[1]), offsetBytes, limitBytes));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (method === 'GET' && path === '/diagnostics') {
|
|
402
|
+
sendJson(response, 200, await context.workbench.diagnostics());
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
throw new HttpProblem(404, 'Route not found.');
|
|
406
|
+
}
|
|
407
|
+
function openEventStream(request, response, url, workbench) {
|
|
408
|
+
const rawCursor = url.searchParams.get('cursor') ?? request.headers['last-event-id'] ?? '0';
|
|
409
|
+
const cursor = Number(rawCursor);
|
|
410
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0) {
|
|
411
|
+
throw new HttpProblem(400, 'Event cursor must be a non-negative integer.');
|
|
412
|
+
}
|
|
413
|
+
response.writeHead(200, {
|
|
414
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
415
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
416
|
+
Connection: 'keep-alive',
|
|
417
|
+
'X-Accel-Buffering': 'no',
|
|
418
|
+
});
|
|
419
|
+
response.flushHeaders();
|
|
420
|
+
response.write('retry: 1000\n\n');
|
|
421
|
+
const detach = workbench.eventHub.attach(response, cursor);
|
|
422
|
+
request.once('aborted', detach);
|
|
423
|
+
response.once('close', detach);
|
|
424
|
+
}
|
|
425
|
+
function assertMutation(request, nonce, origin, forbiddenCode = 'request_forbidden') {
|
|
426
|
+
if (request.headers.origin !== origin) {
|
|
427
|
+
throw new HttpProblem(403, 'Mutation requires the exact loopback Origin.', forbiddenCode);
|
|
428
|
+
}
|
|
429
|
+
const fetchSite = request.headers['sec-fetch-site'];
|
|
430
|
+
if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'none') {
|
|
431
|
+
throw new HttpProblem(403, 'Cross-site mutation is not allowed.', forbiddenCode);
|
|
432
|
+
}
|
|
433
|
+
if (request.headers[protocol_1.WEB_NONCE_HEADER] !== nonce) {
|
|
434
|
+
throw new HttpProblem(403, 'Invalid Web Workbench nonce.', forbiddenCode);
|
|
435
|
+
}
|
|
436
|
+
const contentType = request.headers['content-type']?.split(';', 1)[0]?.trim().toLowerCase();
|
|
437
|
+
if (contentType !== 'application/json') {
|
|
438
|
+
throw new HttpProblem(415, 'Mutations require application/json.');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function assertTerminalUserGesture(request) {
|
|
442
|
+
if (request.headers[protocol_1.WEB_USER_GESTURE_HEADER] !== 'terminal-create-v1') {
|
|
443
|
+
throw new HttpProblem(403, 'Terminal creation requires an explicit browser user gesture.', 'terminal_user_gesture_required');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function assertHost(request, origin) {
|
|
447
|
+
if (!origin)
|
|
448
|
+
return;
|
|
449
|
+
const expected = new URL(origin).host;
|
|
450
|
+
if (request.headers.host !== expected) {
|
|
451
|
+
throw new HttpProblem(421, 'Request Host does not match the loopback listener.');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async function readJson(request) {
|
|
455
|
+
const declared = Number(request.headers['content-length'] ?? 0);
|
|
456
|
+
if (Number.isFinite(declared) && declared > protocol_1.WEB_MAX_BODY_BYTES) {
|
|
457
|
+
throw new HttpProblem(413, 'Request body is too large.');
|
|
458
|
+
}
|
|
459
|
+
const chunks = [];
|
|
460
|
+
let bytes = 0;
|
|
461
|
+
for await (const chunk of request) {
|
|
462
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
463
|
+
bytes += buffer.length;
|
|
464
|
+
if (bytes > protocol_1.WEB_MAX_BODY_BYTES)
|
|
465
|
+
throw new HttpProblem(413, 'Request body is too large.');
|
|
466
|
+
chunks.push(buffer);
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
throw new HttpProblem(400, 'Request body is not valid JSON.');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async function serveStatic(request, response, pathname, staticRoot) {
|
|
476
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
477
|
+
throw new HttpProblem(405, 'Static assets support only GET and HEAD.');
|
|
478
|
+
}
|
|
479
|
+
if (!(0, fs_1.existsSync)(staticRoot) || !(0, fs_1.statSync)(staticRoot).isDirectory()) {
|
|
480
|
+
throw new HttpProblem(503, 'Web client assets are missing; run npm run build:web.');
|
|
481
|
+
}
|
|
482
|
+
const decoded = safeDecode(pathname);
|
|
483
|
+
const relative = (0, path_1.normalize)(decoded.replace(/^[/\\]+/, ''));
|
|
484
|
+
if (relative === '..' || relative.startsWith(`..${path_1.sep}`)) {
|
|
485
|
+
throw new HttpProblem(403, 'Static asset path escapes the Web root.');
|
|
486
|
+
}
|
|
487
|
+
const resolvedRoot = (0, path_1.resolve)(staticRoot);
|
|
488
|
+
const candidate = (0, path_1.resolve)(resolvedRoot, relative || 'index.html');
|
|
489
|
+
const lexicalBoundary = `${resolvedRoot}${path_1.sep}`;
|
|
490
|
+
if (candidate !== resolvedRoot && !candidate.startsWith(lexicalBoundary)) {
|
|
491
|
+
throw new HttpProblem(403, 'Static asset path escapes the Web root.');
|
|
492
|
+
}
|
|
493
|
+
const selected = (0, fs_1.existsSync)(candidate) && (0, fs_1.statSync)(candidate).isFile()
|
|
494
|
+
? candidate
|
|
495
|
+
: (0, path_1.join)(resolvedRoot, 'index.html');
|
|
496
|
+
if (!(0, fs_1.existsSync)(selected) || !(0, fs_1.statSync)(selected).isFile()) {
|
|
497
|
+
throw new HttpProblem(404, 'Asset not found.');
|
|
498
|
+
}
|
|
499
|
+
const canonicalRoot = (0, fs_1.realpathSync)(resolvedRoot);
|
|
500
|
+
const file = (0, fs_1.realpathSync)(selected);
|
|
501
|
+
const canonicalBoundary = `${canonicalRoot}${path_1.sep}`;
|
|
502
|
+
if (file !== canonicalRoot && !file.startsWith(canonicalBoundary)) {
|
|
503
|
+
throw new HttpProblem(403, 'Static asset path escapes the Web root.');
|
|
504
|
+
}
|
|
505
|
+
const headers = {
|
|
506
|
+
'Content-Type': contentType(file),
|
|
507
|
+
'Content-Length': (0, fs_1.statSync)(file).size,
|
|
508
|
+
'Cache-Control': file.endsWith('index.html')
|
|
509
|
+
? 'no-store'
|
|
510
|
+
: 'public, max-age=31536000, immutable',
|
|
511
|
+
};
|
|
512
|
+
response.writeHead(200, headers);
|
|
513
|
+
if (request.method === 'HEAD') {
|
|
514
|
+
response.end();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
await new Promise((resolveStream, reject) => {
|
|
518
|
+
const stream = (0, fs_1.createReadStream)(file);
|
|
519
|
+
stream.once('error', reject);
|
|
520
|
+
stream.once('end', resolveStream);
|
|
521
|
+
stream.pipe(response);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
function applySecurityHeaders(response, styleNonce) {
|
|
525
|
+
response.setHeader('Content-Security-Policy', `default-src 'self'; base-uri 'none'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; style-src 'self'; style-src-elem 'self' 'nonce-${styleNonce}'; style-src-attr 'unsafe-inline'`);
|
|
526
|
+
response.setHeader('X-Content-Type-Options', 'nosniff');
|
|
527
|
+
response.setHeader('X-Frame-Options', 'DENY');
|
|
528
|
+
response.setHeader('Referrer-Policy', 'no-referrer');
|
|
529
|
+
response.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
|
530
|
+
response.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
|
|
531
|
+
response.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
|
532
|
+
}
|
|
533
|
+
function sendJson(response, status, value) {
|
|
534
|
+
const body = JSON.stringify(value);
|
|
535
|
+
response.writeHead(status, {
|
|
536
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
537
|
+
'Content-Length': Buffer.byteLength(body),
|
|
538
|
+
'Cache-Control': 'no-store',
|
|
539
|
+
});
|
|
540
|
+
response.end(body);
|
|
541
|
+
}
|
|
542
|
+
function sendProblem(response, error) {
|
|
543
|
+
const status = error instanceof HttpProblem || error instanceof errors_1.WebWorkbenchError
|
|
544
|
+
? error.status
|
|
545
|
+
: error instanceof protocol_1.WebProtocolError
|
|
546
|
+
? error.status
|
|
547
|
+
: 500;
|
|
548
|
+
const detail = status >= 500
|
|
549
|
+
? 'The local Web Workbench request failed.'
|
|
550
|
+
: (0, redaction_1.redactTraceText)(error instanceof Error ? error.message : String(error));
|
|
551
|
+
const code = error instanceof HttpProblem ||
|
|
552
|
+
error instanceof errors_1.WebWorkbenchError ||
|
|
553
|
+
error instanceof protocol_1.WebProtocolError
|
|
554
|
+
? error.code
|
|
555
|
+
: 'web_internal_error';
|
|
556
|
+
const body = JSON.stringify({
|
|
557
|
+
type: `https://orioncode.dev/problems/${code}`,
|
|
558
|
+
title: statusTitle(status),
|
|
559
|
+
status,
|
|
560
|
+
code,
|
|
561
|
+
detail,
|
|
562
|
+
});
|
|
563
|
+
response.writeHead(status, {
|
|
564
|
+
'Content-Type': 'application/problem+json; charset=utf-8',
|
|
565
|
+
'Content-Length': Buffer.byteLength(body),
|
|
566
|
+
'Cache-Control': 'no-store',
|
|
567
|
+
});
|
|
568
|
+
response.end(body);
|
|
569
|
+
}
|
|
570
|
+
class HttpProblem extends Error {
|
|
571
|
+
constructor(status, message, code = defaultProblemCode(status)) {
|
|
572
|
+
super(message);
|
|
573
|
+
this.status = status;
|
|
574
|
+
this.code = code;
|
|
575
|
+
this.name = 'HttpProblem';
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function defaultProblemCode(status) {
|
|
579
|
+
switch (status) {
|
|
580
|
+
case 400:
|
|
581
|
+
return 'invalid_request';
|
|
582
|
+
case 403:
|
|
583
|
+
return 'request_forbidden';
|
|
584
|
+
case 409:
|
|
585
|
+
return 'request_conflict';
|
|
586
|
+
case 413:
|
|
587
|
+
return 'request_too_large';
|
|
588
|
+
case 415:
|
|
589
|
+
return 'unsupported_media_type';
|
|
590
|
+
case 421:
|
|
591
|
+
return 'misdirected_request';
|
|
592
|
+
case 503:
|
|
593
|
+
return 'service_unavailable';
|
|
594
|
+
default:
|
|
595
|
+
return `web_${status}`;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function requireRecord(value, name) {
|
|
599
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
600
|
+
throw new HttpProblem(400, `${name} must be an object.`);
|
|
601
|
+
}
|
|
602
|
+
return value;
|
|
603
|
+
}
|
|
604
|
+
function requireText(value, name, maxLength) {
|
|
605
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
606
|
+
throw new HttpProblem(400, `${name} must be a non-empty string.`);
|
|
607
|
+
}
|
|
608
|
+
if (value.length > maxLength)
|
|
609
|
+
throw new HttpProblem(400, `${name} is too long.`);
|
|
610
|
+
return value.trim();
|
|
611
|
+
}
|
|
612
|
+
function requireUuid(value, name) {
|
|
613
|
+
const text = requireText(value, name, 128);
|
|
614
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(text)) {
|
|
615
|
+
throw new HttpProblem(400, `${name} must be a UUID.`);
|
|
616
|
+
}
|
|
617
|
+
return text;
|
|
618
|
+
}
|
|
619
|
+
function requireContextGuardRecord(value) {
|
|
620
|
+
return Object.freeze({
|
|
621
|
+
expectedContextRevision: requireUuid(value.expectedContextRevision, 'expectedContextRevision'),
|
|
622
|
+
workspaceId: requireUuid(value.workspaceId, 'workspaceId'),
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
function requireContextGuardQuery(url) {
|
|
626
|
+
return Object.freeze({
|
|
627
|
+
expectedContextRevision: requireUuid(url.searchParams.get('expectedContextRevision'), 'expectedContextRevision'),
|
|
628
|
+
workspaceId: requireUuid(url.searchParams.get('workspaceId'), 'workspaceId'),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
function boundedIntegerValue(value, fallback, minimum, maximum, name) {
|
|
632
|
+
if (value === undefined)
|
|
633
|
+
return fallback;
|
|
634
|
+
if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum) {
|
|
635
|
+
throw new HttpProblem(400, `${name} must be between ${minimum} and ${maximum}.`);
|
|
636
|
+
}
|
|
637
|
+
return Number(value);
|
|
638
|
+
}
|
|
639
|
+
function assertOnlyKeys(value, allowedKeys) {
|
|
640
|
+
const allowed = new Set(allowedKeys);
|
|
641
|
+
const unknown = Object.keys(value).find(key => !allowed.has(key));
|
|
642
|
+
if (unknown)
|
|
643
|
+
throw new HttpProblem(400, `Unknown request field: ${unknown}.`);
|
|
644
|
+
}
|
|
645
|
+
function statusTitle(status) {
|
|
646
|
+
if (status === 400)
|
|
647
|
+
return 'Invalid request';
|
|
648
|
+
if (status === 403)
|
|
649
|
+
return 'Request rejected';
|
|
650
|
+
if (status === 404)
|
|
651
|
+
return 'Not found';
|
|
652
|
+
if (status === 409)
|
|
653
|
+
return 'State conflict';
|
|
654
|
+
if (status === 413)
|
|
655
|
+
return 'Payload too large';
|
|
656
|
+
if (status === 415)
|
|
657
|
+
return 'Unsupported media type';
|
|
658
|
+
if (status === 421)
|
|
659
|
+
return 'Misdirected request';
|
|
660
|
+
if (status === 503)
|
|
661
|
+
return 'Service unavailable';
|
|
662
|
+
return 'Internal server error';
|
|
663
|
+
}
|
|
664
|
+
function safeDecode(pathname) {
|
|
665
|
+
try {
|
|
666
|
+
return decodeURIComponent(pathname);
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
throw new HttpProblem(400, 'Malformed asset path.');
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function safeDecodePathSegment(value) {
|
|
673
|
+
const decoded = safeDecode(value);
|
|
674
|
+
if (!decoded || decoded.includes('/') || decoded.includes('\\')) {
|
|
675
|
+
throw new HttpProblem(400, 'Malformed path identifier.');
|
|
676
|
+
}
|
|
677
|
+
return decoded;
|
|
678
|
+
}
|
|
679
|
+
function boundedInteger(raw, fallback, minimum, maximum) {
|
|
680
|
+
if (raw === null)
|
|
681
|
+
return fallback;
|
|
682
|
+
const value = Number(raw);
|
|
683
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
684
|
+
throw new HttpProblem(400, `Integer query value must be between ${minimum} and ${maximum}.`);
|
|
685
|
+
}
|
|
686
|
+
return value;
|
|
687
|
+
}
|
|
688
|
+
function pageSize(url) {
|
|
689
|
+
return boundedInteger(url.searchParams.get('pageSize'), 50, 1, 100);
|
|
690
|
+
}
|
|
691
|
+
function collectionPage(url, scope, items, keyOf) {
|
|
692
|
+
return (0, workbench_controller_1.pageCollectionItems)(scope, items, url.searchParams.get('cursor') ?? undefined, pageSize(url), keyOf);
|
|
693
|
+
}
|
|
694
|
+
function contentType(path) {
|
|
695
|
+
switch ((0, path_1.extname)(path).toLowerCase()) {
|
|
696
|
+
case '.html':
|
|
697
|
+
return 'text/html; charset=utf-8';
|
|
698
|
+
case '.js':
|
|
699
|
+
case '.mjs':
|
|
700
|
+
return 'text/javascript; charset=utf-8';
|
|
701
|
+
case '.css':
|
|
702
|
+
return 'text/css; charset=utf-8';
|
|
703
|
+
case '.json':
|
|
704
|
+
return 'application/json; charset=utf-8';
|
|
705
|
+
case '.svg':
|
|
706
|
+
return 'image/svg+xml';
|
|
707
|
+
case '.png':
|
|
708
|
+
return 'image/png';
|
|
709
|
+
case '.woff2':
|
|
710
|
+
return 'font/woff2';
|
|
711
|
+
default:
|
|
712
|
+
return 'application/octet-stream';
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function resolveDefaultStaticRoot() {
|
|
716
|
+
const candidates = [
|
|
717
|
+
(0, path_1.resolve)(__dirname, '../../dist/web-client'),
|
|
718
|
+
(0, path_1.resolve)(__dirname, '../../web/dist'),
|
|
719
|
+
(0, path_1.resolve)(process.cwd(), 'dist/web-client'),
|
|
720
|
+
];
|
|
721
|
+
return candidates.find(candidate => (0, fs_1.existsSync)(candidate)) ?? candidates[0];
|
|
722
|
+
}
|
|
723
|
+
function listen(server, port) {
|
|
724
|
+
return new Promise((resolveListen, reject) => {
|
|
725
|
+
const onError = (error) => {
|
|
726
|
+
server.off('listening', onListening);
|
|
727
|
+
reject(error);
|
|
728
|
+
};
|
|
729
|
+
const onListening = () => {
|
|
730
|
+
server.off('error', onError);
|
|
731
|
+
resolveListen();
|
|
732
|
+
};
|
|
733
|
+
server.once('error', onError);
|
|
734
|
+
server.once('listening', onListening);
|
|
735
|
+
server.listen(port, HOST);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
function closeServer(server) {
|
|
739
|
+
if (!server.listening)
|
|
740
|
+
return Promise.resolve();
|
|
741
|
+
server.closeAllConnections?.();
|
|
742
|
+
return new Promise(resolveClose => server.close(() => resolveClose()));
|
|
743
|
+
}
|
|
744
|
+
//# sourceMappingURL=server.js.map
|