@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,258 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import { EventEmitter } from 'node:events';
|
|
4
|
-
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { test } from 'node:test';
|
|
8
|
-
import { configureWakeProxyRuntime, wakeProxyInternals } from '../home.js';
|
|
9
|
-
import { proxyHttpRequest } from '../proxy.js';
|
|
10
|
-
// Regression for the HTTP wake-path 502/asset-timeout (hearth-soak-4a0dc43-report.md §4): every HTTP
|
|
11
|
-
// forward routed through ensureAwake, so a single momentary slow `probeRelayServing` under guest
|
|
12
|
-
// load false-negatived and escalated the request into the 150s wakeHome/waitForRelayReady
|
|
13
|
-
// path (serial guest execs) — while the direct route was returning 200 in ~0.1s the whole
|
|
14
|
-
// time. The fix: a home crtr already records `running` with a route+token is forwarded DIRECTLY (the
|
|
15
|
-
// forward is the liveness check); ensureAwake/restore is reserved for missing/not-running state or a
|
|
16
|
-
// clear upstream failure. These tests fail on the pre-fix code that gated all HTTP through ensureAwake.
|
|
17
|
-
const tenantId = 'silas';
|
|
18
|
-
const machineId = 'igd2fkrn1y1ol3vw5i6mt';
|
|
19
|
-
const nodeId = 'mqt64sus-e71caa94';
|
|
20
|
-
const routeUrl = 'https://7878-igd2fkrn1y1ol3vw5i6mt.example.dev';
|
|
21
|
-
const ownerToken = 'owner-secret-token';
|
|
22
|
-
const now = '2026-06-25T00:00:00.000Z';
|
|
23
|
-
function makeHome(overrides = {}) {
|
|
24
|
-
return {
|
|
25
|
-
tenantId,
|
|
26
|
-
providerName: 'none',
|
|
27
|
-
machineId,
|
|
28
|
-
homeStorage: {
|
|
29
|
-
kind: 'volume',
|
|
30
|
-
durability: 'durable-volume',
|
|
31
|
-
longTermContract: true,
|
|
32
|
-
ownerMachineId: machineId,
|
|
33
|
-
guestHome: '/home/agent',
|
|
34
|
-
volumeId: '48b59d07-8cc9-46f9-bdfd-ac99a55dff40',
|
|
35
|
-
mountPath: '/home/agent',
|
|
36
|
-
},
|
|
37
|
-
state: 'running',
|
|
38
|
-
nodeId,
|
|
39
|
-
relayToken: 'relay-token',
|
|
40
|
-
webRoute: {
|
|
41
|
-
routeId: '7878-igd2fkrn1y1ol3vw5i6mt.example.dev',
|
|
42
|
-
machineId,
|
|
43
|
-
targetPort: 7878,
|
|
44
|
-
url: routeUrl,
|
|
45
|
-
concreteUrl: routeUrl,
|
|
46
|
-
concreteUrlSource: 'provider',
|
|
47
|
-
createdAt: now,
|
|
48
|
-
raw: null,
|
|
49
|
-
},
|
|
50
|
-
createdAt: now,
|
|
51
|
-
updatedAt: now,
|
|
52
|
-
...overrides,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
function makeRuntime(registryPath, statePath) {
|
|
56
|
-
return {
|
|
57
|
-
proxy: {
|
|
58
|
-
host: '127.0.0.1',
|
|
59
|
-
port: 7878,
|
|
60
|
-
publicOrigin: 'https://hearth-wake-proxy.fly.dev',
|
|
61
|
-
statePath,
|
|
62
|
-
m0RegistryPath: registryPath,
|
|
63
|
-
ownerUserId: 'owner-user',
|
|
64
|
-
defaultTenantId: tenantId,
|
|
65
|
-
ownerTokenHash: createHash('sha256').update(ownerToken, 'utf8').digest('hex'),
|
|
66
|
-
idlePauseMs: 0,
|
|
67
|
-
wakeTimeoutMs: 30_000,
|
|
68
|
-
readyTimeoutMs: 2_000,
|
|
69
|
-
cookieSecure: true,
|
|
70
|
-
seedHome: null,
|
|
71
|
-
},
|
|
72
|
-
m0: {
|
|
73
|
-
providerName: 'none',
|
|
74
|
-
registryPath,
|
|
75
|
-
guestUser: 'agent',
|
|
76
|
-
guestHome: '/home/agent',
|
|
77
|
-
guestCodePath: '/opt/crouter',
|
|
78
|
-
webPort: 7878,
|
|
79
|
-
nodeKind: 'general',
|
|
80
|
-
piPackages: [],
|
|
81
|
-
piAuthJson: null,
|
|
82
|
-
relayToken: null,
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
function setup(home = makeHome()) {
|
|
87
|
-
const tempDir = mkdtempSync(join(tmpdir(), 'crouter-hearth-http-fast-'));
|
|
88
|
-
const registryPath = join(tempDir, 'homes.json');
|
|
89
|
-
const statePath = join(tempDir, 'state.json');
|
|
90
|
-
writeFileSync(registryPath, `${JSON.stringify({ [tenantId]: home }, null, 2)}\n`, 'utf8');
|
|
91
|
-
const runtime = makeRuntime(registryPath, statePath);
|
|
92
|
-
configureWakeProxyRuntime(runtime);
|
|
93
|
-
return runtime;
|
|
94
|
-
}
|
|
95
|
-
class FakeReq extends EventEmitter {
|
|
96
|
-
url;
|
|
97
|
-
method;
|
|
98
|
-
headers;
|
|
99
|
-
constructor(path, method = 'GET') {
|
|
100
|
-
super();
|
|
101
|
-
this.url = path;
|
|
102
|
-
this.method = method;
|
|
103
|
-
this.headers = { authorization: `Bearer ${ownerToken}`, host: 'hearth-wake-proxy.fly.dev' };
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
class FakeRes extends EventEmitter {
|
|
107
|
-
statusCode = 200;
|
|
108
|
-
statusMessage = '';
|
|
109
|
-
headersSent = false;
|
|
110
|
-
writableEnded = false;
|
|
111
|
-
headers = new Map();
|
|
112
|
-
chunks = [];
|
|
113
|
-
socket = { setNoDelay() { } };
|
|
114
|
-
setHeader(name, value) {
|
|
115
|
-
this.headers.set(name.toLowerCase(), value);
|
|
116
|
-
}
|
|
117
|
-
getHeader(name) {
|
|
118
|
-
return this.headers.get(name.toLowerCase());
|
|
119
|
-
}
|
|
120
|
-
hasHeader(name) {
|
|
121
|
-
return this.headers.has(name.toLowerCase());
|
|
122
|
-
}
|
|
123
|
-
writeHead(code, headers) {
|
|
124
|
-
this.statusCode = code;
|
|
125
|
-
this.headersSent = true;
|
|
126
|
-
if (headers)
|
|
127
|
-
for (const [k, v] of Object.entries(headers))
|
|
128
|
-
this.setHeader(k, v);
|
|
129
|
-
}
|
|
130
|
-
flushHeaders() {
|
|
131
|
-
this.headersSent = true;
|
|
132
|
-
}
|
|
133
|
-
write(chunk) {
|
|
134
|
-
this.headersSent = true;
|
|
135
|
-
this.chunks.push(Buffer.from(chunk));
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
end(chunk) {
|
|
139
|
-
this.headersSent = true;
|
|
140
|
-
if (chunk !== undefined)
|
|
141
|
-
this.chunks.push(Buffer.from(chunk));
|
|
142
|
-
this.writableEnded = true;
|
|
143
|
-
this.emit('finish');
|
|
144
|
-
}
|
|
145
|
-
destroy() {
|
|
146
|
-
this.writableEnded = true;
|
|
147
|
-
}
|
|
148
|
-
body() {
|
|
149
|
-
return Buffer.concat(this.chunks).toString('utf8');
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
function asReq(req) {
|
|
153
|
-
return req;
|
|
154
|
-
}
|
|
155
|
-
function asRes(res) {
|
|
156
|
-
return res;
|
|
157
|
-
}
|
|
158
|
-
function htmlResponse(text) {
|
|
159
|
-
return new Response(text, { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
160
|
-
}
|
|
161
|
-
function guardHeavyPath(t) {
|
|
162
|
-
const probe = t.mock.method(wakeProxyInternals, 'probeRelayServing', async () => {
|
|
163
|
-
throw new Error('probeRelayServing must not run on the running-route fast path');
|
|
164
|
-
});
|
|
165
|
-
const wake = t.mock.method(wakeProxyInternals, 'wakeHome', async () => {
|
|
166
|
-
throw new Error('wakeHome must not run on the running-route fast path');
|
|
167
|
-
});
|
|
168
|
-
return { probe, wake };
|
|
169
|
-
}
|
|
170
|
-
test('HTTP request against a known-running route forwards directly without the heavy wake/restore path', async (t) => {
|
|
171
|
-
const runtime = setup();
|
|
172
|
-
const { probe, wake } = guardHeavyPath(t);
|
|
173
|
-
const fetchMock = t.mock.method(globalThis, 'fetch', async () => htmlResponse('<html>chat</html>'));
|
|
174
|
-
const res = new FakeRes();
|
|
175
|
-
await proxyHttpRequest(asReq(new FakeReq('/views/chat')), asRes(res), runtime);
|
|
176
|
-
assert.equal(res.statusCode, 200);
|
|
177
|
-
assert.equal(res.body(), '<html>chat</html>');
|
|
178
|
-
assert.equal(fetchMock.mock.callCount(), 1);
|
|
179
|
-
// The forward went straight to the persisted route — never through ensureAwake's probe/wake.
|
|
180
|
-
assert.equal(probe.mock.callCount(), 0);
|
|
181
|
-
assert.equal(wake.mock.callCount(), 0);
|
|
182
|
-
});
|
|
183
|
-
test('concurrent asset/page requests forward in parallel without serializing behind a slow guest probe', async (t) => {
|
|
184
|
-
const runtime = setup();
|
|
185
|
-
const { probe, wake } = guardHeavyPath(t);
|
|
186
|
-
let inFlight = 0;
|
|
187
|
-
let maxInFlight = 0;
|
|
188
|
-
let release;
|
|
189
|
-
const gate = new Promise((resolve) => {
|
|
190
|
-
release = resolve;
|
|
191
|
-
});
|
|
192
|
-
const fetchMock = t.mock.method(globalThis, 'fetch', async () => {
|
|
193
|
-
inFlight += 1;
|
|
194
|
-
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
195
|
-
await gate;
|
|
196
|
-
inFlight -= 1;
|
|
197
|
-
return htmlResponse('<html>asset</html>');
|
|
198
|
-
});
|
|
199
|
-
const paths = ['/views/chat', '/assets/index-a.css', '/assets/index-b.js', '/assets/index-c.js', '/favicon.svg'];
|
|
200
|
-
const responses = paths.map(() => new FakeRes());
|
|
201
|
-
const calls = paths.map((p, i) => proxyHttpRequest(asReq(new FakeReq(p)), asRes(responses[i]), runtime));
|
|
202
|
-
// Let all five enter forwardUpstream before any resolves; if they serialized behind a per-tenant
|
|
203
|
-
// wake lock/probe, only one would be in flight here.
|
|
204
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
205
|
-
const concurrentBeforeRelease = maxInFlight;
|
|
206
|
-
release();
|
|
207
|
-
await Promise.all(calls);
|
|
208
|
-
assert.equal(concurrentBeforeRelease, paths.length);
|
|
209
|
-
assert.equal(fetchMock.mock.callCount(), paths.length);
|
|
210
|
-
for (const res of responses) {
|
|
211
|
-
assert.equal(res.statusCode, 200);
|
|
212
|
-
assert.equal(res.body(), '<html>asset</html>');
|
|
213
|
-
}
|
|
214
|
-
assert.equal(probe.mock.callCount(), 0);
|
|
215
|
-
assert.equal(wake.mock.callCount(), 0);
|
|
216
|
-
});
|
|
217
|
-
test('a clear upstream failure against the running route invalidates the verdict and wakes deliberately', async (t) => {
|
|
218
|
-
const runtime = setup();
|
|
219
|
-
// ensureAwake's deliberate wake path: the route IS serving (probe true), so it confirms via the
|
|
220
|
-
// cheap probe and never restores — the point is that the failed direct forward ESCALATED into
|
|
221
|
-
// ensureAwake rather than silently 502ing.
|
|
222
|
-
const probe = t.mock.method(wakeProxyInternals, 'probeRelayServing', async () => true);
|
|
223
|
-
const wake = t.mock.method(wakeProxyInternals, 'wakeHome', async () => {
|
|
224
|
-
throw new Error('wakeHome must not run when the relay is serving');
|
|
225
|
-
});
|
|
226
|
-
t.mock.method(wakeProxyInternals, 'touchTenantWake', async () => undefined);
|
|
227
|
-
let fetchCalls = 0;
|
|
228
|
-
const fetchMock = t.mock.method(globalThis, 'fetch', async () => {
|
|
229
|
-
fetchCalls += 1;
|
|
230
|
-
if (fetchCalls === 1)
|
|
231
|
-
throw new TypeError('fetch failed'); // connection refused against stale route
|
|
232
|
-
return htmlResponse('<html>recovered</html>');
|
|
233
|
-
});
|
|
234
|
-
const res = new FakeRes();
|
|
235
|
-
await proxyHttpRequest(asReq(new FakeReq('/views/chat')), asRes(res), runtime);
|
|
236
|
-
assert.equal(res.statusCode, 200);
|
|
237
|
-
assert.equal(res.body(), '<html>recovered</html>');
|
|
238
|
-
assert.equal(fetchMock.mock.callCount(), 2); // direct forward failed, deliberate wake forward succeeded
|
|
239
|
-
assert.equal(probe.mock.callCount(), 1); // entered ensureAwake exactly once
|
|
240
|
-
assert.equal(wake.mock.callCount(), 0);
|
|
241
|
-
});
|
|
242
|
-
test('not-running home skips the fast path and goes straight to the deliberate wake', async (t) => {
|
|
243
|
-
const runtime = setup(makeHome({ state: 'paused' }));
|
|
244
|
-
const probe = t.mock.method(wakeProxyInternals, 'probeRelayServing', async () => true);
|
|
245
|
-
const wake = t.mock.method(wakeProxyInternals, 'wakeHome', async () => {
|
|
246
|
-
throw new Error('wakeHome must not run when the relay is serving');
|
|
247
|
-
});
|
|
248
|
-
t.mock.method(wakeProxyInternals, 'touchTenantWake', async () => undefined);
|
|
249
|
-
const fetchMock = t.mock.method(globalThis, 'fetch', async () => htmlResponse('<html>woke</html>'));
|
|
250
|
-
const res = new FakeRes();
|
|
251
|
-
await proxyHttpRequest(asReq(new FakeReq('/views/chat')), asRes(res), runtime);
|
|
252
|
-
assert.equal(res.statusCode, 200);
|
|
253
|
-
assert.equal(res.body(), '<html>woke</html>');
|
|
254
|
-
// Paused home is not trusted directly — it must enter ensureAwake (probe runs) before forwarding.
|
|
255
|
-
assert.equal(probe.mock.callCount(), 1);
|
|
256
|
-
assert.equal(wake.mock.callCount(), 0);
|
|
257
|
-
assert.equal(fetchMock.mock.callCount(), 1);
|
|
258
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|