@crouton-kit/crouter 0.3.34 → 0.3.35
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/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +10 -5
- package/dist/clients/attach/attach-cmd.js +732 -704
- package/dist/core/__tests__/fixtures/fake-engine.js +18 -8
- package/dist/core/__tests__/flagship-lifecycle.test.js +4 -1
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +1 -1
- package/dist/core/__tests__/full/detach-focus.test.js +15 -1
- package/dist/core/__tests__/live-mutation-verbs.test.js +8 -2
- package/dist/core/__tests__/live-mutation.test.js +7 -2
- package/dist/core/hearth/config.js +3 -6
- package/dist/core/hearth/index.d.ts +0 -1
- package/dist/core/hearth/index.js +0 -1
- package/dist/core/hearth/model-auth-guest.js +9 -14
- package/dist/core/hearth/providers/blaxel.js +3 -3
- package/dist/core/hearth/types.d.ts +0 -1
- package/dist/web-client/assets/index-DUThOUzU.css +2 -0
- package/dist/web-client/assets/index-IAJVtuVe.js +80 -0
- package/dist/web-client/index.html +2 -2
- package/package.json +2 -3
- package/dist/core/hearth/registry.d.ts +0 -15
- package/dist/core/hearth/registry.js +0 -180
- package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.js +0 -289
- package/dist/hearth/wake-proxy/__tests__/config-timeout.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/config-timeout.test.js +0 -34
- package/dist/hearth/wake-proxy/__tests__/guest-source.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/guest-source.test.js +0 -203
- package/dist/hearth/wake-proxy/__tests__/hardening.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/hardening.test.js +0 -59
- package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.js +0 -372
- package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.js +0 -258
- package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.js +0 -437
- package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.js +0 -15
- package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.js +0 -141
- package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.js +0 -143
- package/dist/hearth/wake-proxy/anthropic-oauth.d.ts +0 -58
- package/dist/hearth/wake-proxy/anthropic-oauth.js +0 -189
- package/dist/hearth/wake-proxy/auth.d.ts +0 -22
- package/dist/hearth/wake-proxy/auth.js +0 -128
- package/dist/hearth/wake-proxy/config.d.ts +0 -2
- package/dist/hearth/wake-proxy/config.js +0 -151
- package/dist/hearth/wake-proxy/guest-source.d.ts +0 -14
- package/dist/hearth/wake-proxy/guest-source.js +0 -130
- package/dist/hearth/wake-proxy/hearth-status.d.ts +0 -44
- package/dist/hearth/wake-proxy/hearth-status.js +0 -267
- package/dist/hearth/wake-proxy/home.d.ts +0 -61
- package/dist/hearth/wake-proxy/home.js +0 -333
- package/dist/hearth/wake-proxy/main.d.ts +0 -1
- package/dist/hearth/wake-proxy/main.js +0 -134
- package/dist/hearth/wake-proxy/model-auth.d.ts +0 -13
- package/dist/hearth/wake-proxy/model-auth.js +0 -345
- package/dist/hearth/wake-proxy/proxy.d.ts +0 -35
- package/dist/hearth/wake-proxy/proxy.js +0 -716
- package/dist/hearth/wake-proxy/public-source-gate.d.ts +0 -9
- package/dist/hearth/wake-proxy/public-source-gate.js +0 -409
- package/dist/hearth/wake-proxy/redact.d.ts +0 -1
- package/dist/hearth/wake-proxy/redact.js +0 -17
- package/dist/hearth/wake-proxy/server.d.ts +0 -11
- package/dist/hearth/wake-proxy/server.js +0 -142
- package/dist/hearth/wake-proxy/state.d.ts +0 -18
- package/dist/hearth/wake-proxy/state.js +0 -342
- package/dist/hearth/wake-proxy/types.d.ts +0 -76
- package/dist/hearth/wake-proxy/types.js +0 -1
- package/dist/web-client/assets/index-BRKxe-hy.js +0 -80
- package/dist/web-client/assets/index-BZUxTkv5.css +0 -2
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
import { runGuestSource } from './guest-source.js';
|
|
3
|
-
import type { HearthWakeProxyRuntime } from './types.js';
|
|
4
|
-
export declare const PUBLIC_SOURCE_FILE_READ_SCRIPT = "import { constants } from 'node:fs';\nimport { open, realpath } from 'node:fs/promises';\nimport { posix as path } from 'node:path';\n\nconst blocked = () => {\n console.log(JSON.stringify({ ok: false, status: 403, error: 'canvas artifact path is not allowed' }));\n process.exit(0);\n};\n\nconst raw = await new Promise((resolve, reject) => {\n let value = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk) => { value += chunk; });\n process.stdin.on('end', () => resolve(value));\n process.stdin.on('error', reject);\n});\n\nlet input;\ntry {\n input = JSON.parse(raw);\n} catch {\n blocked();\n}\nif (input === null || typeof input !== 'object' || Array.isArray(input)) {\n blocked();\n}\nconst canvasHome = typeof input.canvasHome === 'string' ? input.canvasHome.trim() : '';\nconst piAgentDir = typeof input.piAgentDir === 'string' ? input.piAgentDir.trim() : '';\nconst requestedPath = typeof input.path === 'string' ? input.path.trim() : '';\nif (canvasHome === '' || piAgentDir === '' || requestedPath === '') {\n blocked();\n}\nconst normalizedPath = path.normalize(requestedPath);\nif (normalizedPath !== requestedPath || !path.isAbsolute(normalizedPath) || normalizedPath.includes('\\\\')) {\n blocked();\n}\nconst realCanvasHome = await realpath(canvasHome);\nconst allowedNodesPrefix = realCanvasHome + '/nodes/';\nif (!normalizedPath.startsWith(allowedNodesPrefix)) {\n blocked();\n}\nconst relative = normalizedPath.slice(allowedNodesPrefix.length);\nconst segments = relative.split('/');\nconst nodeId = segments.shift() ?? '';\nconst rootKind = segments.shift() ?? '';\nconst fileRemainder = segments.join('/');\nif (nodeId === '' || (rootKind !== 'context' && rootKind !== 'reports') || fileRemainder === '' || fileRemainder.includes('..')) {\n blocked();\n}\nconst targetReal = await realpath(normalizedPath);\nif (targetReal !== normalizedPath) {\n blocked();\n}\nlet privateAgentReal = null;\ntry {\n privateAgentReal = await realpath(piAgentDir);\n} catch {\n privateAgentReal = null;\n}\nif (privateAgentReal !== null && (targetReal === privateAgentReal || targetReal.startsWith(privateAgentReal + '/'))) {\n blocked();\n}\nconst fd = await open(targetReal, constants.O_RDONLY | constants.O_NOFOLLOW);\ntry {\n const stats = await fd.stat();\n if (!stats.isFile() || stats.nlink !== 1) {\n blocked();\n }\n const text = await fd.readFile('utf8');\n console.log(JSON.stringify({ ok: true, stdout: text }));\n} finally {\n await fd.close();\n}\n";
|
|
5
|
-
export interface PublicSourceGateDeps {
|
|
6
|
-
guestSource?: typeof runGuestSource;
|
|
7
|
-
now?: () => number;
|
|
8
|
-
}
|
|
9
|
-
export declare function handlePublicSourceRequest(req: IncomingMessage, res: ServerResponse, runtime: HearthWakeProxyRuntime, deps?: PublicSourceGateDeps): Promise<boolean>;
|
|
@@ -1,409 +0,0 @@
|
|
|
1
|
-
import { posix as pathPosix } from 'node:path';
|
|
2
|
-
import { general } from '../../core/errors.js';
|
|
3
|
-
import { guestEnv } from '../../core/hearth/guest-env.js';
|
|
4
|
-
import { ensureProxyState, loadProxyState } from './state.js';
|
|
5
|
-
import { resolveOwnerWriteAuth } from './auth.js';
|
|
6
|
-
import { redactWakeProxyMessage } from './redact.js';
|
|
7
|
-
import { requireSuccessfulExec, runGuestSource } from './guest-source.js';
|
|
8
|
-
const SOURCE_PATH = '/__crtr/source';
|
|
9
|
-
const BODY_LIMIT_BYTES = 64 * 1024;
|
|
10
|
-
const EXEC_STDIN_LIMIT_BYTES = 16 * 1024;
|
|
11
|
-
export const PUBLIC_SOURCE_FILE_READ_SCRIPT = `import { constants } from 'node:fs';
|
|
12
|
-
import { open, realpath } from 'node:fs/promises';
|
|
13
|
-
import { posix as path } from 'node:path';
|
|
14
|
-
|
|
15
|
-
const blocked = () => {
|
|
16
|
-
console.log(JSON.stringify({ ok: false, status: 403, error: 'canvas artifact path is not allowed' }));
|
|
17
|
-
process.exit(0);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const raw = await new Promise((resolve, reject) => {
|
|
21
|
-
let value = '';
|
|
22
|
-
process.stdin.setEncoding('utf8');
|
|
23
|
-
process.stdin.on('data', (chunk) => { value += chunk; });
|
|
24
|
-
process.stdin.on('end', () => resolve(value));
|
|
25
|
-
process.stdin.on('error', reject);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
let input;
|
|
29
|
-
try {
|
|
30
|
-
input = JSON.parse(raw);
|
|
31
|
-
} catch {
|
|
32
|
-
blocked();
|
|
33
|
-
}
|
|
34
|
-
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
35
|
-
blocked();
|
|
36
|
-
}
|
|
37
|
-
const canvasHome = typeof input.canvasHome === 'string' ? input.canvasHome.trim() : '';
|
|
38
|
-
const piAgentDir = typeof input.piAgentDir === 'string' ? input.piAgentDir.trim() : '';
|
|
39
|
-
const requestedPath = typeof input.path === 'string' ? input.path.trim() : '';
|
|
40
|
-
if (canvasHome === '' || piAgentDir === '' || requestedPath === '') {
|
|
41
|
-
blocked();
|
|
42
|
-
}
|
|
43
|
-
const normalizedPath = path.normalize(requestedPath);
|
|
44
|
-
if (normalizedPath !== requestedPath || !path.isAbsolute(normalizedPath) || normalizedPath.includes('\\\\')) {
|
|
45
|
-
blocked();
|
|
46
|
-
}
|
|
47
|
-
const realCanvasHome = await realpath(canvasHome);
|
|
48
|
-
const allowedNodesPrefix = realCanvasHome + '/nodes/';
|
|
49
|
-
if (!normalizedPath.startsWith(allowedNodesPrefix)) {
|
|
50
|
-
blocked();
|
|
51
|
-
}
|
|
52
|
-
const relative = normalizedPath.slice(allowedNodesPrefix.length);
|
|
53
|
-
const segments = relative.split('/');
|
|
54
|
-
const nodeId = segments.shift() ?? '';
|
|
55
|
-
const rootKind = segments.shift() ?? '';
|
|
56
|
-
const fileRemainder = segments.join('/');
|
|
57
|
-
if (nodeId === '' || (rootKind !== 'context' && rootKind !== 'reports') || fileRemainder === '' || fileRemainder.includes('..')) {
|
|
58
|
-
blocked();
|
|
59
|
-
}
|
|
60
|
-
const targetReal = await realpath(normalizedPath);
|
|
61
|
-
if (targetReal !== normalizedPath) {
|
|
62
|
-
blocked();
|
|
63
|
-
}
|
|
64
|
-
let privateAgentReal = null;
|
|
65
|
-
try {
|
|
66
|
-
privateAgentReal = await realpath(piAgentDir);
|
|
67
|
-
} catch {
|
|
68
|
-
privateAgentReal = null;
|
|
69
|
-
}
|
|
70
|
-
if (privateAgentReal !== null && (targetReal === privateAgentReal || targetReal.startsWith(privateAgentReal + '/'))) {
|
|
71
|
-
blocked();
|
|
72
|
-
}
|
|
73
|
-
const fd = await open(targetReal, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
74
|
-
try {
|
|
75
|
-
const stats = await fd.stat();
|
|
76
|
-
if (!stats.isFile() || stats.nlink !== 1) {
|
|
77
|
-
blocked();
|
|
78
|
-
}
|
|
79
|
-
const text = await fd.readFile('utf8');
|
|
80
|
-
console.log(JSON.stringify({ ok: true, stdout: text }));
|
|
81
|
-
} finally {
|
|
82
|
-
await fd.close();
|
|
83
|
-
}
|
|
84
|
-
`;
|
|
85
|
-
function currentState(runtime) {
|
|
86
|
-
return loadProxyState(runtime.proxy.statePath) ?? ensureProxyState(runtime.proxy);
|
|
87
|
-
}
|
|
88
|
-
function requestPathname(req) {
|
|
89
|
-
return new URL(req.url ?? '/', 'http://127.0.0.1').pathname;
|
|
90
|
-
}
|
|
91
|
-
function respondJson(res, statusCode, value) {
|
|
92
|
-
const body = `${JSON.stringify(value, null, 2)}\n`;
|
|
93
|
-
res.writeHead(statusCode, {
|
|
94
|
-
'content-type': 'application/json; charset=utf-8',
|
|
95
|
-
'content-length': Buffer.byteLength(body),
|
|
96
|
-
'cache-control': 'no-store',
|
|
97
|
-
});
|
|
98
|
-
res.end(body);
|
|
99
|
-
}
|
|
100
|
-
function respondText(res, statusCode, message) {
|
|
101
|
-
const body = `${redactWakeProxyMessage(message)}\n`;
|
|
102
|
-
res.writeHead(statusCode, {
|
|
103
|
-
'content-type': 'text/plain; charset=utf-8',
|
|
104
|
-
'content-length': Buffer.byteLength(body),
|
|
105
|
-
'cache-control': 'no-store',
|
|
106
|
-
});
|
|
107
|
-
res.end(body);
|
|
108
|
-
}
|
|
109
|
-
async function readJsonBody(req, limitBytes) {
|
|
110
|
-
return await new Promise((resolve, reject) => {
|
|
111
|
-
const chunks = [];
|
|
112
|
-
let bytes = 0;
|
|
113
|
-
const cleanup = () => {
|
|
114
|
-
req.off('data', onData);
|
|
115
|
-
req.off('end', onEnd);
|
|
116
|
-
req.off('error', onError);
|
|
117
|
-
req.off('aborted', onAbort);
|
|
118
|
-
};
|
|
119
|
-
const onData = (chunk) => {
|
|
120
|
-
bytes += chunk.byteLength;
|
|
121
|
-
if (bytes > limitBytes) {
|
|
122
|
-
cleanup();
|
|
123
|
-
req.destroy();
|
|
124
|
-
reject(general('request body too large', { limitBytes }));
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
chunks.push(chunk);
|
|
128
|
-
};
|
|
129
|
-
const onEnd = () => {
|
|
130
|
-
cleanup();
|
|
131
|
-
try {
|
|
132
|
-
const text = Buffer.concat(chunks).toString('utf8');
|
|
133
|
-
resolve(text === '' ? {} : JSON.parse(text));
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
reject(error);
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
const onError = (error) => {
|
|
140
|
-
cleanup();
|
|
141
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
142
|
-
};
|
|
143
|
-
const onAbort = () => {
|
|
144
|
-
cleanup();
|
|
145
|
-
reject(general('request aborted while reading body'));
|
|
146
|
-
};
|
|
147
|
-
req.on('data', onData);
|
|
148
|
-
req.once('end', onEnd);
|
|
149
|
-
req.once('error', onError);
|
|
150
|
-
req.once('aborted', onAbort);
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
function isPlainObject(value) {
|
|
154
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
155
|
-
}
|
|
156
|
-
function parseExecRequest(body) {
|
|
157
|
-
if (body.kind !== 'exec')
|
|
158
|
-
return null;
|
|
159
|
-
const keys = Object.keys(body);
|
|
160
|
-
if (!keys.every((key) => key === 'kind' || key === 'bin' || key === 'args' || key === 'cwd' || key === 'stdin'))
|
|
161
|
-
return null;
|
|
162
|
-
if (typeof body.bin !== 'string' || !Array.isArray(body.args) || body.args.some((arg) => typeof arg !== 'string'))
|
|
163
|
-
return null;
|
|
164
|
-
if ('cwd' in body)
|
|
165
|
-
return null;
|
|
166
|
-
const request = {
|
|
167
|
-
kind: 'exec',
|
|
168
|
-
bin: body.bin,
|
|
169
|
-
args: body.args,
|
|
170
|
-
};
|
|
171
|
-
if ('stdin' in body) {
|
|
172
|
-
if (typeof body.stdin !== 'string' || Buffer.byteLength(body.stdin, 'utf8') > EXEC_STDIN_LIMIT_BYTES || !isAllowedCrtrWriteExec(request))
|
|
173
|
-
return null;
|
|
174
|
-
request.stdin = body.stdin;
|
|
175
|
-
}
|
|
176
|
-
return request;
|
|
177
|
-
}
|
|
178
|
-
function parseFileRequest(body) {
|
|
179
|
-
if (body.kind !== 'file')
|
|
180
|
-
return null;
|
|
181
|
-
if (Object.keys(body).some((key) => key !== 'kind' && key !== 'path'))
|
|
182
|
-
return null;
|
|
183
|
-
if (typeof body.path !== 'string' || body.path.trim() === '')
|
|
184
|
-
return null;
|
|
185
|
-
return { kind: 'file', path: body.path };
|
|
186
|
-
}
|
|
187
|
-
function isValidBrowserCanvasArtifactPath(canvasHome, piAgentDir, requestedPath) {
|
|
188
|
-
const normalizedCanvasHome = pathPosix.resolve(canvasHome);
|
|
189
|
-
const normalizedPiAgentDir = pathPosix.resolve(piAgentDir);
|
|
190
|
-
const normalizedPath = pathPosix.normalize(requestedPath);
|
|
191
|
-
if (normalizedPath !== requestedPath || !pathPosix.isAbsolute(normalizedPath) || normalizedPath.includes('\\'))
|
|
192
|
-
return false;
|
|
193
|
-
const allowedNodesPrefix = `${normalizedCanvasHome}/nodes/`;
|
|
194
|
-
const privateAgentPrefix = `${normalizedPiAgentDir}/`;
|
|
195
|
-
if (!normalizedPath.startsWith(allowedNodesPrefix) || normalizedPath.startsWith(privateAgentPrefix))
|
|
196
|
-
return false;
|
|
197
|
-
const relative = normalizedPath.slice(allowedNodesPrefix.length);
|
|
198
|
-
const segments = relative.split('/');
|
|
199
|
-
const nodeId = segments.shift() ?? '';
|
|
200
|
-
const rootKind = segments.shift() ?? '';
|
|
201
|
-
const fileRemainder = segments.join('/');
|
|
202
|
-
return nodeId !== '' && (rootKind === 'context' || rootKind === 'reports') && fileRemainder !== '' && !fileRemainder.includes('..');
|
|
203
|
-
}
|
|
204
|
-
function isSafeNodeId(nodeId) {
|
|
205
|
-
return nodeId.trim() !== '' && !nodeId.includes('/') && !nodeId.includes('\\') && !nodeId.includes('..');
|
|
206
|
-
}
|
|
207
|
-
function isAllowedCrtrWriteExec(req) {
|
|
208
|
-
if (req.bin !== 'crtr')
|
|
209
|
-
return false;
|
|
210
|
-
if (req.args.length < 3 || req.args[req.args.length - 1] !== '--json')
|
|
211
|
-
return false;
|
|
212
|
-
const args = req.args;
|
|
213
|
-
switch (args[0]) {
|
|
214
|
-
case 'node':
|
|
215
|
-
switch (args[1]) {
|
|
216
|
-
case 'new':
|
|
217
|
-
return true;
|
|
218
|
-
case 'msg':
|
|
219
|
-
return args.length >= 4 && isSafeNodeId(args[2] ?? '');
|
|
220
|
-
default:
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
default:
|
|
224
|
-
return false;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
function isAllowedCrtrExec(req) {
|
|
228
|
-
if (req.bin !== 'crtr')
|
|
229
|
-
return false;
|
|
230
|
-
if (req.args.length < 3 || req.args[req.args.length - 1] !== '--json')
|
|
231
|
-
return false;
|
|
232
|
-
const args = req.args;
|
|
233
|
-
switch (args[0]) {
|
|
234
|
-
case 'canvas':
|
|
235
|
-
switch (args[1]) {
|
|
236
|
-
case 'snapshot':
|
|
237
|
-
return args.length === 3;
|
|
238
|
-
case 'revive':
|
|
239
|
-
return (args.length === 4 || args.length === 5) && isSafeNodeId(args[2] ?? '') && (args.length === 4 || args[3] === '--fresh');
|
|
240
|
-
default:
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
case 'node':
|
|
244
|
-
switch (args[1]) {
|
|
245
|
-
case 'inspect':
|
|
246
|
-
return args.length === 5 && args[2] === 'show' && isSafeNodeId(args[3] ?? '');
|
|
247
|
-
case 'snapshot':
|
|
248
|
-
return args.length === 4 && isSafeNodeId(args[2] ?? '');
|
|
249
|
-
case 'new':
|
|
250
|
-
return args.length >= 3;
|
|
251
|
-
case 'msg':
|
|
252
|
-
return args.length >= 4 && isSafeNodeId(args[2] ?? '');
|
|
253
|
-
case 'close':
|
|
254
|
-
return args.length === 5 && args[2] === '--node' && isSafeNodeId(args[3] ?? '');
|
|
255
|
-
default:
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
case 'view':
|
|
259
|
-
return args.length === 3 && args[1] === 'list';
|
|
260
|
-
default:
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
function parseRawHelperResult(stdout) {
|
|
265
|
-
try {
|
|
266
|
-
const parsed = JSON.parse(stdout);
|
|
267
|
-
if (!isPlainObject(parsed))
|
|
268
|
-
return null;
|
|
269
|
-
if (typeof parsed.ok !== 'boolean')
|
|
270
|
-
return null;
|
|
271
|
-
if (parsed.ok === true) {
|
|
272
|
-
if (typeof parsed.stdout !== 'string')
|
|
273
|
-
return null;
|
|
274
|
-
return { ok: true, stdout: parsed.stdout };
|
|
275
|
-
}
|
|
276
|
-
if (typeof parsed.status !== 'number' || typeof parsed.error !== 'string')
|
|
277
|
-
return null;
|
|
278
|
-
return { ok: false, status: parsed.status, error: parsed.error };
|
|
279
|
-
}
|
|
280
|
-
catch {
|
|
281
|
-
return null;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
async function handleAllowedExec(req, tenantId, runtime, deps) {
|
|
285
|
-
const raw = await (deps.guestSource ?? runGuestSource)(tenantId, req, {
|
|
286
|
-
now: deps.now,
|
|
287
|
-
wakeTimeoutMs: runtime.proxy.wakeTimeoutMs,
|
|
288
|
-
bridgeTimeoutMs: runtime.proxy.wakeTimeoutMs,
|
|
289
|
-
});
|
|
290
|
-
return raw;
|
|
291
|
-
}
|
|
292
|
-
async function handleCanvasArtifactRead(req, tenantId, runtime, deps) {
|
|
293
|
-
const env = guestEnv(runtime.m0);
|
|
294
|
-
const canvasHome = env.CRTR_HOME;
|
|
295
|
-
const piAgentDir = env.PI_CODING_AGENT_DIR;
|
|
296
|
-
if (!isValidBrowserCanvasArtifactPath(canvasHome, piAgentDir, req.path)) {
|
|
297
|
-
return { status: 403, body: null };
|
|
298
|
-
}
|
|
299
|
-
const helperRequest = {
|
|
300
|
-
kind: 'exec',
|
|
301
|
-
bin: 'node',
|
|
302
|
-
args: ['--input-type=module', '-e', PUBLIC_SOURCE_FILE_READ_SCRIPT],
|
|
303
|
-
stdin: JSON.stringify({ canvasHome, piAgentDir, path: req.path }),
|
|
304
|
-
};
|
|
305
|
-
try {
|
|
306
|
-
const raw = await (deps.guestSource ?? runGuestSource)(tenantId, helperRequest, {
|
|
307
|
-
now: deps.now,
|
|
308
|
-
wakeTimeoutMs: runtime.proxy.wakeTimeoutMs,
|
|
309
|
-
bridgeTimeoutMs: runtime.proxy.wakeTimeoutMs,
|
|
310
|
-
});
|
|
311
|
-
const successful = requireSuccessfulExec(raw, 'read canvas artifact');
|
|
312
|
-
const parsed = parseRawHelperResult(successful.stdout);
|
|
313
|
-
if (parsed === null) {
|
|
314
|
-
throw general('canvas artifact read helper returned invalid data');
|
|
315
|
-
}
|
|
316
|
-
if (!parsed.ok) {
|
|
317
|
-
return { status: parsed.status ?? 403, body: null };
|
|
318
|
-
}
|
|
319
|
-
return { status: 200, body: { ok: true, stdout: parsed.stdout ?? '', stderr: '', exitCode: 0 } };
|
|
320
|
-
}
|
|
321
|
-
catch (error) {
|
|
322
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
323
|
-
throw general(redactWakeProxyMessage(message));
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
function authResultFromRequest(req, runtime) {
|
|
327
|
-
try {
|
|
328
|
-
const state = currentState(runtime);
|
|
329
|
-
const auth = resolveOwnerWriteAuth(req, runtime.proxy, state);
|
|
330
|
-
if (auth === null)
|
|
331
|
-
return null;
|
|
332
|
-
return { tenantId: auth.tenantId };
|
|
333
|
-
}
|
|
334
|
-
catch (error) {
|
|
335
|
-
throw error instanceof Error ? error : new Error(String(error));
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
export async function handlePublicSourceRequest(req, res, runtime, deps = {}) {
|
|
339
|
-
if (requestPathname(req) !== SOURCE_PATH)
|
|
340
|
-
return false;
|
|
341
|
-
if (req.method !== 'POST') {
|
|
342
|
-
respondText(res, 405, 'method not allowed');
|
|
343
|
-
return true;
|
|
344
|
-
}
|
|
345
|
-
let auth;
|
|
346
|
-
try {
|
|
347
|
-
auth = authResultFromRequest(req, runtime);
|
|
348
|
-
}
|
|
349
|
-
catch {
|
|
350
|
-
respondText(res, 403, 'forbidden');
|
|
351
|
-
return true;
|
|
352
|
-
}
|
|
353
|
-
if (auth === null) {
|
|
354
|
-
respondText(res, 401, 'owner token required');
|
|
355
|
-
return true;
|
|
356
|
-
}
|
|
357
|
-
let body;
|
|
358
|
-
try {
|
|
359
|
-
body = await readJsonBody(req, BODY_LIMIT_BYTES);
|
|
360
|
-
}
|
|
361
|
-
catch (error) {
|
|
362
|
-
respondText(res, 400, error instanceof Error ? error.message : String(error));
|
|
363
|
-
return true;
|
|
364
|
-
}
|
|
365
|
-
if (!isPlainObject(body)) {
|
|
366
|
-
respondText(res, 400, 'source request must be a JSON object');
|
|
367
|
-
return true;
|
|
368
|
-
}
|
|
369
|
-
if (body.kind === 'http') {
|
|
370
|
-
respondText(res, 403, 'forbidden');
|
|
371
|
-
return true;
|
|
372
|
-
}
|
|
373
|
-
if (body.kind === 'exec') {
|
|
374
|
-
const request = parseExecRequest(body);
|
|
375
|
-
if (request === null || !isAllowedCrtrExec(request)) {
|
|
376
|
-
respondText(res, 403, 'forbidden');
|
|
377
|
-
return true;
|
|
378
|
-
}
|
|
379
|
-
try {
|
|
380
|
-
const raw = await handleAllowedExec(request, auth.tenantId, runtime, deps);
|
|
381
|
-
respondJson(res, 200, raw);
|
|
382
|
-
}
|
|
383
|
-
catch (error) {
|
|
384
|
-
respondJson(res, 502, { ok: false, stdout: '', stderr: redactWakeProxyMessage(error instanceof Error ? error.message : String(error)) });
|
|
385
|
-
}
|
|
386
|
-
return true;
|
|
387
|
-
}
|
|
388
|
-
if (body.kind === 'file') {
|
|
389
|
-
const request = parseFileRequest(body);
|
|
390
|
-
if (request === null) {
|
|
391
|
-
respondText(res, 403, 'forbidden');
|
|
392
|
-
return true;
|
|
393
|
-
}
|
|
394
|
-
try {
|
|
395
|
-
const result = await handleCanvasArtifactRead(request, auth.tenantId, runtime, deps);
|
|
396
|
-
if (result.body === null) {
|
|
397
|
-
respondText(res, result.status, 'forbidden');
|
|
398
|
-
return true;
|
|
399
|
-
}
|
|
400
|
-
respondJson(res, result.status, result.body);
|
|
401
|
-
}
|
|
402
|
-
catch (error) {
|
|
403
|
-
respondJson(res, 502, { ok: false, stdout: '', stderr: redactWakeProxyMessage(error instanceof Error ? error.message : String(error)) });
|
|
404
|
-
}
|
|
405
|
-
return true;
|
|
406
|
-
}
|
|
407
|
-
respondText(res, 403, 'forbidden');
|
|
408
|
-
return true;
|
|
409
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function redactWakeProxyMessage(message: string): string;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { ownerAuthRedactedMessage } from './auth.js';
|
|
2
|
-
const REDIRECT_URL_RE = /https?:\/\/localhost:53692\/callback[^\s"'`<>)]*/gi;
|
|
3
|
-
const BEARER_RE = /\bBearer\s+[A-Za-z0-9._~+\/-]+=?=?/gi;
|
|
4
|
-
const CODE_PARAM_RE = /([?&](?:code|state|code_verifier)=)[^&#\s]+/gi;
|
|
5
|
-
const JSON_TOKEN_RE = /("(?:access_token|refresh_token|access|refresh|code|state|code_verifier)"\s*:\s*")[^"]+(")/gi;
|
|
6
|
-
const ASSIGN_TOKEN_RE = /\b(?:access_token|refresh_token|access|refresh|code|state|code_verifier)\s*[:=]\s*[^\s,&}]+/gi;
|
|
7
|
-
const AUTH_JSON_RE = /\bauth\.json\b/gi;
|
|
8
|
-
export function redactWakeProxyMessage(message) {
|
|
9
|
-
return ownerAuthRedactedMessage(message)
|
|
10
|
-
.replace(REDIRECT_URL_RE, '[redacted redirect url]')
|
|
11
|
-
.replace(BEARER_RE, 'Bearer [redacted]')
|
|
12
|
-
.replace(CODE_PARAM_RE, '$1[redacted]')
|
|
13
|
-
.replace(JSON_TOKEN_RE, '$1[redacted]$2')
|
|
14
|
-
.replace(ASSIGN_TOKEN_RE, (value) => value.replace(/[:=]\s*[^\s,&}]+$/, ': [redacted]'))
|
|
15
|
-
.replace(AUTH_JSON_RE, '[redacted auth.json]')
|
|
16
|
-
.replace(/\bsha256:[0-9a-f]{64}\b/gi, 'sha256:[redacted]');
|
|
17
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { type IncomingMessage, type Server as HttpServer, type ServerResponse } from 'node:http';
|
|
2
|
-
import { type ModelAuthRouteDeps } from './model-auth.js';
|
|
3
|
-
import { type PublicSourceGateDeps } from './public-source-gate.js';
|
|
4
|
-
import { type HearthStatusDeps } from './hearth-status.js';
|
|
5
|
-
import type { HearthWakeProxyRuntime } from './types.js';
|
|
6
|
-
export declare function routeRequest(req: IncomingMessage, res: ServerResponse, runtime: HearthWakeProxyRuntime, deps?: {
|
|
7
|
-
modelAuth?: ModelAuthRouteDeps;
|
|
8
|
-
sourceGate?: PublicSourceGateDeps;
|
|
9
|
-
status?: HearthStatusDeps;
|
|
10
|
-
}): Promise<void>;
|
|
11
|
-
export declare function createWakeProxyServer(runtime: HearthWakeProxyRuntime): HttpServer;
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'node:http';
|
|
2
|
-
import { general } from '../../core/errors.js';
|
|
3
|
-
import { ensureProxyState, loadProxyState, readTenantHome } from './state.js';
|
|
4
|
-
import { resolveOwnerAuth, setOwnerCookie } from './auth.js';
|
|
5
|
-
import { handleModelAuthRequest } from './model-auth.js';
|
|
6
|
-
import { handlePublicSourceRequest } from './public-source-gate.js';
|
|
7
|
-
import { loadHearthStatus } from './hearth-status.js';
|
|
8
|
-
import { proxyHttpRequest, proxyUpgrade } from './proxy.js';
|
|
9
|
-
function currentState(runtime) {
|
|
10
|
-
return loadProxyState(runtime.proxy.statePath) ?? ensureProxyState(runtime.proxy);
|
|
11
|
-
}
|
|
12
|
-
function requestUrl(req) {
|
|
13
|
-
return new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
14
|
-
}
|
|
15
|
-
function sendJson(res, statusCode, value) {
|
|
16
|
-
const body = `${JSON.stringify(value, null, 2)}\n`;
|
|
17
|
-
res.writeHead(statusCode, {
|
|
18
|
-
'content-type': 'application/json; charset=utf-8',
|
|
19
|
-
'content-length': Buffer.byteLength(body),
|
|
20
|
-
});
|
|
21
|
-
res.end(body);
|
|
22
|
-
}
|
|
23
|
-
function sendText(res, statusCode, body) {
|
|
24
|
-
res.writeHead(statusCode, {
|
|
25
|
-
'content-type': 'text/plain; charset=utf-8',
|
|
26
|
-
'content-length': Buffer.byteLength(body),
|
|
27
|
-
});
|
|
28
|
-
res.end(body);
|
|
29
|
-
}
|
|
30
|
-
function redirectChat(res, nodeId) {
|
|
31
|
-
res.writeHead(302, {
|
|
32
|
-
location: `/views/chat?target=${encodeURIComponent(nodeId)}`,
|
|
33
|
-
'cache-control': 'no-store',
|
|
34
|
-
'referrer-policy': 'no-referrer',
|
|
35
|
-
});
|
|
36
|
-
res.end();
|
|
37
|
-
}
|
|
38
|
-
function renderOwnerPrompt() {
|
|
39
|
-
return `<!doctype html><meta charset="utf-8"><title>Hearth</title><form method="GET"><label>Owner token <input name="token" type="password" autofocus></label> <button type="submit">Open chat</button></form>`;
|
|
40
|
-
}
|
|
41
|
-
function respondRoot(req, res, runtime) {
|
|
42
|
-
let auth;
|
|
43
|
-
try {
|
|
44
|
-
const state = currentState(runtime);
|
|
45
|
-
auth = resolveOwnerAuth(req, runtime.proxy, state);
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
res.writeHead(403, {
|
|
49
|
-
'content-type': 'text/plain; charset=utf-8',
|
|
50
|
-
'cache-control': 'no-store',
|
|
51
|
-
'referrer-policy': 'no-referrer',
|
|
52
|
-
});
|
|
53
|
-
res.end('forbidden\n');
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (auth !== null) {
|
|
57
|
-
const home = readTenantHome(runtime.proxy, auth.tenantId);
|
|
58
|
-
if (home === null || typeof home.nodeId !== 'string' || home.nodeId.trim() === '') {
|
|
59
|
-
throw general('wake proxy home is missing nodeId — run the existing M0 provision/bootstrap flow', { tenantId: auth.tenantId });
|
|
60
|
-
}
|
|
61
|
-
if (auth.source !== 'cookie') {
|
|
62
|
-
setOwnerCookie(res, auth.token, runtime.proxy);
|
|
63
|
-
}
|
|
64
|
-
redirectChat(res, home.nodeId);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
res.writeHead(401, {
|
|
68
|
-
'content-type': 'text/html; charset=utf-8',
|
|
69
|
-
'cache-control': 'no-store',
|
|
70
|
-
'referrer-policy': 'no-referrer',
|
|
71
|
-
});
|
|
72
|
-
res.end(renderOwnerPrompt());
|
|
73
|
-
}
|
|
74
|
-
async function respondStatus(req, res, runtime, deps = {}) {
|
|
75
|
-
const state = currentState(runtime);
|
|
76
|
-
let auth;
|
|
77
|
-
try {
|
|
78
|
-
auth = resolveOwnerAuth(req, runtime.proxy, state);
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
sendText(res, 403, 'forbidden\n');
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (auth === null) {
|
|
85
|
-
sendText(res, 401, 'owner token required\n');
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
const statusData = await loadHearthStatus(auth.tenantId, runtime, deps);
|
|
89
|
-
sendJson(res, 200, statusData);
|
|
90
|
-
}
|
|
91
|
-
function respondHealthz(res) {
|
|
92
|
-
sendText(res, 200, 'ok\n');
|
|
93
|
-
}
|
|
94
|
-
export async function routeRequest(req, res, runtime, deps = {}) {
|
|
95
|
-
const url = requestUrl(req);
|
|
96
|
-
if (req.method === 'GET' && url.pathname === '/__hearth/healthz') {
|
|
97
|
-
respondHealthz(res);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (req.method === 'GET' && url.pathname === '/__hearth/status') {
|
|
101
|
-
await respondStatus(req, res, runtime, deps.status);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (req.method === 'GET' && url.pathname === '/') {
|
|
105
|
-
respondRoot(req, res, runtime);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
if (url.pathname.startsWith('/__hearth/model-auth/anthropic/')) {
|
|
109
|
-
await handleModelAuthRequest(req, res, runtime, deps.modelAuth);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
if (url.pathname === '/__crtr/source') {
|
|
113
|
-
await handlePublicSourceRequest(req, res, runtime, deps.sourceGate);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
await proxyHttpRequest(req, res, runtime);
|
|
117
|
-
}
|
|
118
|
-
export function createWakeProxyServer(runtime) {
|
|
119
|
-
const server = createServer((req, res) => {
|
|
120
|
-
void routeRequest(req, res, runtime).catch((error) => {
|
|
121
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
-
if (!res.headersSent) {
|
|
123
|
-
sendText(res, 500, `${message}\n`);
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
res.destroy();
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
server.on('connect', (_req, socket) => {
|
|
131
|
-
socket.end('HTTP/1.1 405 Method Not Allowed\r\nConnection: close\r\n\r\n');
|
|
132
|
-
});
|
|
133
|
-
server.on('upgrade', (req, socket, head) => {
|
|
134
|
-
try {
|
|
135
|
-
proxyUpgrade(req, socket, head, runtime);
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
socket.destroy();
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
return server;
|
|
142
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { HomeRecord, M0Config, TenantId } from '../../core/hearth/types.js';
|
|
2
|
-
import type { ActivityHandle, ActivityKind, HearthProxyConfig, HearthProxyState, HearthProxyTenantState, TenantActivitySnapshot } from './types.js';
|
|
3
|
-
export declare function loadProxyState(statePath: string): HearthProxyState | null;
|
|
4
|
-
export declare function saveProxyState(statePath: string, state: HearthProxyState): HearthProxyState;
|
|
5
|
-
export declare function ensureProxyState(config: HearthProxyConfig): HearthProxyState;
|
|
6
|
-
export declare function getTenantState(state: HearthProxyState, tenantId: TenantId): HearthProxyTenantState | null;
|
|
7
|
-
export declare function updateTenantState(config: HearthProxyConfig, tenantId: TenantId, mutate: (tenant: HearthProxyTenantState) => HearthProxyTenantState | void): HearthProxyTenantState;
|
|
8
|
-
export declare function touchTenantActivity(config: HearthProxyConfig, tenantId: TenantId): HearthProxyTenantState;
|
|
9
|
-
export declare function touchTenantWake(config: HearthProxyConfig, tenantId: TenantId): HearthProxyTenantState;
|
|
10
|
-
export declare function touchTenantPause(config: HearthProxyConfig, tenantId: TenantId): HearthProxyTenantState;
|
|
11
|
-
export declare function withTenantLock<T>(tenantId: TenantId, fn: () => Promise<T> | T): Promise<T>;
|
|
12
|
-
export declare function beginActivity(config: HearthProxyConfig, tenantId: TenantId, kind: ActivityKind): ActivityHandle;
|
|
13
|
-
export declare function endActivity(handle: ActivityHandle): TenantActivitySnapshot;
|
|
14
|
-
export declare function getTenantActivitySnapshot(tenantId: TenantId): TenantActivitySnapshot;
|
|
15
|
-
export declare function setTenantIdleTimer(tenantId: TenantId, deadlineAt: string, timer: NodeJS.Timeout): void;
|
|
16
|
-
export declare function clearTenantIdleTimer(tenantId: TenantId): void;
|
|
17
|
-
export declare function readTenantHome(config: HearthProxyConfig, tenantId: TenantId): HomeRecord | null;
|
|
18
|
-
export declare function ensureSeededM0Registry(config: HearthProxyConfig, m0Config: M0Config): HomeRecord;
|