@serfab/cadre-host 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -4
- package/dist/bin/host.js +32 -20
- package/dist/bin/host.js.map +1 -1
- package/dist/bin/nat-output.d.ts +5 -0
- package/dist/bin/nat-output.js +12 -1
- package/dist/bin/nat-output.js.map +1 -1
- package/dist/bin/ui-access.d.ts +22 -0
- package/dist/bin/ui-access.js +44 -0
- package/dist/bin/ui-access.js.map +1 -0
- package/dist/hosted/claim-details.d.ts +25 -3
- package/dist/hosted/claim-details.js +31 -37
- package/dist/hosted/claim-details.js.map +1 -1
- package/dist/hosted/hosted-node-service.d.ts +7 -0
- package/dist/hosted/hosted-node-service.js +16 -4
- package/dist/hosted/hosted-node-service.js.map +1 -1
- package/dist/hosted/hosted-node-watcher.d.ts +16 -4
- package/dist/hosted/hosted-node-watcher.js +45 -9
- package/dist/hosted/hosted-node-watcher.js.map +1 -1
- package/dist/hosted/node-status.d.ts +19 -3
- package/dist/hosted/node-status.js +21 -5
- package/dist/hosted/node-status.js.map +1 -1
- package/dist/hosted/types.d.ts +12 -5
- package/dist/hosted/types.js.map +1 -1
- package/dist/installer/config.d.ts +23 -0
- package/dist/installer/config.js +46 -0
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/index.d.ts +2 -0
- package/dist/installer/index.js +7 -16
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/wizard.d.ts +12 -2
- package/dist/installer/wizard.js +15 -7
- package/dist/installer/wizard.js.map +1 -1
- package/dist/nat/index.d.ts +2 -1
- package/dist/nat/index.js +1 -0
- package/dist/nat/index.js.map +1 -1
- package/dist/nat/nat-service.d.ts +22 -5
- package/dist/nat/nat-service.js +75 -26
- package/dist/nat/nat-service.js.map +1 -1
- package/dist/nat/nat-store.js +11 -0
- package/dist/nat/nat-store.js.map +1 -1
- package/dist/nat/network-mode.d.ts +19 -0
- package/dist/nat/network-mode.js +32 -0
- package/dist/nat/network-mode.js.map +1 -0
- package/dist/nat/reachability.d.ts +5 -4
- package/dist/nat/reachability.js +19 -6
- package/dist/nat/reachability.js.map +1 -1
- package/dist/nat/types.d.ts +26 -6
- package/dist/nat/types.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +3 -3
- package/dist/orchestrator/host-process-orchestrator.js +6 -15
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/package-version.d.ts +1 -0
- package/dist/package-version.js +4 -0
- package/dist/package-version.js.map +1 -0
- package/dist/server/routes/settings.d.ts +1 -0
- package/dist/server/routes/settings.js +15 -1
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/status.js +2 -32
- package/dist/server/routes/status.js.map +1 -1
- package/dist/ui/assets/index-BHd8CWZA.css +1 -0
- package/dist/ui/assets/index-Ccr2GjPT.js +23 -0
- package/dist/ui/assets/{index-4FWuU8ye.js.map → index-Ccr2GjPT.js.map} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +4 -5
- package/src/__tests__/cli.smoke.test.ts +1 -1
- package/src/__tests__/orchestrator-claim-secret.test.ts +1 -2
- package/src/__tests__/orchestrator-env-scrub.test.ts +160 -163
- package/src/__tests__/orchestrator-handle-contract.ts +1 -1
- package/src/__tests__/orchestrator-node-identity.test.ts +0 -5
- package/src/__tests__/orchestrator-push.test.ts +3 -3
- package/src/__tests__/orchestrator.test.ts +3 -5
- package/src/__tests__/ui-access.test.ts +30 -0
- package/src/bin/host.ts +30 -20
- package/src/bin/nat-output.ts +12 -1
- package/src/bin/ui-access.ts +50 -0
- package/src/hosted/__tests__/fake-orchestrator.test.ts +0 -1
- package/src/hosted/__tests__/fake-orchestrator.ts +263 -265
- package/src/hosted/__tests__/hosted-node-service.test.ts +708 -609
- package/src/hosted/__tests__/hosted-node-supervisor.test.ts +475 -474
- package/src/hosted/claim-details.ts +44 -37
- package/src/hosted/hosted-node-service.ts +23 -4
- package/src/hosted/hosted-node-watcher.ts +288 -250
- package/src/hosted/node-status.ts +31 -5
- package/src/hosted/types.ts +160 -153
- package/src/installer/__tests__/installer.smoke.test.ts +184 -182
- package/src/installer/__tests__/wizard.test.ts +76 -56
- package/src/installer/config.ts +66 -0
- package/src/installer/index.ts +252 -259
- package/src/installer/wizard.ts +101 -87
- package/src/nat/__tests__/nat-service.test.ts +608 -540
- package/src/nat/__tests__/nat-store.test.ts +130 -127
- package/src/nat/__tests__/network-mode.test.ts +38 -0
- package/src/nat/index.ts +91 -87
- package/src/nat/nat-service.ts +79 -26
- package/src/nat/nat-store.ts +249 -237
- package/src/nat/network-mode.ts +36 -0
- package/src/nat/reachability.ts +155 -143
- package/src/nat/types.ts +232 -209
- package/src/orchestrator/host-process-orchestrator.ts +1089 -1098
- package/src/package-version.ts +4 -0
- package/src/server/__tests__/fakes.ts +41 -40
- package/src/server/__tests__/hosted-nodes-route.test.ts +161 -160
- package/src/server/__tests__/settings-route.test.ts +31 -0
- package/src/server/routes/settings.ts +128 -115
- package/src/server/routes/status.ts +72 -102
- package/dist/ui/assets/index-4FWuU8ye.js +0 -19
- package/dist/ui/assets/index-Di9Q4d01.css +0 -1
package/dist/ui/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<meta name="color-scheme" content="light dark" />
|
|
7
7
|
<title>cadre-host</title>
|
|
8
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
8
|
+
<script type="module" crossorigin src="./assets/index-Ccr2GjPT.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BHd8CWZA.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="app"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serfab/cadre-host",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Self-hosted cadre node manager for basement-PC deployments",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -63,10 +63,9 @@
|
|
|
63
63
|
"@libp2p/crypto": "^5.1.23",
|
|
64
64
|
"@libp2p/interface": "^3.3.0",
|
|
65
65
|
"@libp2p/peer-id": "^6.0.15",
|
|
66
|
-
"@
|
|
67
|
-
"@serfab/cadre-
|
|
68
|
-
"@serfab/cadre-
|
|
69
|
-
"@serfab/cadre-provider": "^1.14.0",
|
|
66
|
+
"@serfab/cadre-cli": "^1.15.0",
|
|
67
|
+
"@serfab/cadre-core": "^1.15.0",
|
|
68
|
+
"@serfab/cadre-provider": "^1.15.0",
|
|
70
69
|
"commander": "^14.0.2",
|
|
71
70
|
"debug": "^4.4.3",
|
|
72
71
|
"fastify": "^5.7.1",
|
|
@@ -42,7 +42,7 @@ describe('cadre-host CLI smoke test', () => {
|
|
|
42
42
|
encoding: 'utf8',
|
|
43
43
|
});
|
|
44
44
|
// The built CLI reads package.json at runtime, so this holds even against a
|
|
45
|
-
// stale dist — it catches
|
|
45
|
+
// stale dist — it catches dist/package-version.js resolving the wrong path.
|
|
46
46
|
const pkg = JSON.parse(
|
|
47
47
|
readFileSync(resolve(here, '../../package.json'), 'utf8'),
|
|
48
48
|
) as { version: string };
|
|
@@ -93,7 +93,6 @@ describe('HostProcessOrchestrator claim secret', () => {
|
|
|
93
93
|
await orch.createContainer({
|
|
94
94
|
containerId: 'hn_claim',
|
|
95
95
|
partyId: 'unclaimed',
|
|
96
|
-
bootstrapNodes: [],
|
|
97
96
|
profile: 'storage',
|
|
98
97
|
claimSecret: SECRET,
|
|
99
98
|
});
|
|
@@ -105,7 +104,7 @@ describe('HostProcessOrchestrator claim secret', () => {
|
|
|
105
104
|
|
|
106
105
|
// No secret, no variable: `cadre-cli start` treats a set-but-empty value as unset,
|
|
107
106
|
// but the child must not even see the name.
|
|
108
|
-
await orch.createContainer({ containerId: 'hn_plain', partyId: 'p',
|
|
107
|
+
await orch.createContainer({ containerId: 'hn_plain', partyId: 'p', profile: 'storage' });
|
|
109
108
|
const plain = JSON.parse(await waitForFile(join(rootDir, 'hn_plain', 'spawn-seen.json'))) as { secret: string | null };
|
|
110
109
|
expect(plain.secret).toBeNull();
|
|
111
110
|
});
|
|
@@ -1,163 +1,160 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HostProcessOrchestrator env-scrub test — the manager's own `process.env` must
|
|
3
|
-
* not leak `CADRE_*` config-override vars into spawned children.
|
|
4
|
-
*
|
|
5
|
-
* `launchChild` used to build each child's environment as
|
|
6
|
-
* `{ ...process.env, ...extraEnv, <fixed per-child vars> }`, so ANY `CADRE_*`
|
|
7
|
-
* var set on the manager (not just the ones the orchestrator deliberately
|
|
8
|
-
* sets) reached every child. Because `@serfab/cadre-cli` treats an inherited
|
|
9
|
-
* `CADRE_*` var as a config override that beats the per-child config file, a
|
|
10
|
-
* `CADRE_PARTY_ID` set on the manager could silently pull every child into
|
|
11
|
-
* that party.
|
|
12
|
-
*
|
|
13
|
-
* A fake CLI records the vars under test from its environment to a file so
|
|
14
|
-
* the scrub is observable without a real cadre node.
|
|
15
|
-
*
|
|
16
|
-
* The same record carries `CADRE_LISTEN_ADDRS`, one of the vars the orchestrator
|
|
17
|
-
* deliberately SETS: a child must be handed a WebSocket listener beside its TCP one,
|
|
18
|
-
* on the ports its handle holds, or a phone cannot reach it.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
22
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
23
|
-
import { tmpdir } from 'node:os';
|
|
24
|
-
import { join } from 'node:path';
|
|
25
|
-
|
|
26
|
-
import { HostProcessOrchestrator } from '../orchestrator/host-process-orchestrator.js';
|
|
27
|
-
import { removeAllNodes } from './orchestrator-teardown.js';
|
|
28
|
-
|
|
29
|
-
// Writes the vars under test (as seen in the child env) next to the startup token.
|
|
30
|
-
const FAKE_CLI = `
|
|
31
|
-
import fs from 'node:fs';
|
|
32
|
-
import path from 'node:path';
|
|
33
|
-
const args = process.argv.slice(2);
|
|
34
|
-
const get = (flag) => { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : undefined; };
|
|
35
|
-
const tokenPath = get('--startup-token-file');
|
|
36
|
-
const token = process.env.CADRE_STARTUP_TOKEN ?? '';
|
|
37
|
-
if (tokenPath) {
|
|
38
|
-
const dir = path.dirname(tokenPath);
|
|
39
|
-
const record = {
|
|
40
|
-
CADRE_PARTY_ID: process.env.CADRE_PARTY_ID ?? null,
|
|
41
|
-
CADRE_STORAGE_PATH: process.env.CADRE_STORAGE_PATH ?? null,
|
|
42
|
-
CADRE_ADMIN_PORT: process.env.CADRE_ADMIN_PORT ?? null,
|
|
43
|
-
CADRE_LISTEN_ADDRS: process.env.CADRE_LISTEN_ADDRS ?? null,
|
|
44
|
-
DEBUG: process.env.DEBUG ?? null,
|
|
45
|
-
};
|
|
46
|
-
// Write then rename: the test polls for the file's existence, and must never read it half-written.
|
|
47
|
-
const seenPath = path.join(dir, 'env-seen.json');
|
|
48
|
-
try { fs.writeFileSync(seenPath + '.tmp', JSON.stringify(record), 'utf8'); fs.renameSync(seenPath + '.tmp', seenPath); } catch (e) { console.error(e); }
|
|
49
|
-
if (token) { try { fs.writeFileSync(tokenPath, token, 'utf8'); } catch (e) { console.error(e); } }
|
|
50
|
-
}
|
|
51
|
-
process.on('SIGTERM', () => process.exit(0));
|
|
52
|
-
setInterval(() => {}, 1 << 30);
|
|
53
|
-
`;
|
|
54
|
-
|
|
55
|
-
let tmpRoot: string;
|
|
56
|
-
let scriptPath: string;
|
|
57
|
-
const orchestrators: HostProcessOrchestrator[] = [];
|
|
58
|
-
let savedEnv: Record<string, string | undefined>;
|
|
59
|
-
|
|
60
|
-
beforeEach(() => {
|
|
61
|
-
tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-env-scrub-'));
|
|
62
|
-
scriptPath = join(tmpRoot, 'fake-cli.mjs');
|
|
63
|
-
writeFileSync(scriptPath, FAKE_CLI, 'utf8');
|
|
64
|
-
savedEnv = {
|
|
65
|
-
CADRE_PARTY_ID: process.env.CADRE_PARTY_ID,
|
|
66
|
-
CADRE_STORAGE_PATH: process.env.CADRE_STORAGE_PATH,
|
|
67
|
-
CADRE_ADMIN_PORT: process.env.CADRE_ADMIN_PORT,
|
|
68
|
-
DEBUG: process.env.DEBUG,
|
|
69
|
-
};
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
afterEach(async () => {
|
|
73
|
-
try {
|
|
74
|
-
await removeAllNodes(orchestrators);
|
|
75
|
-
} finally {
|
|
76
|
-
await sleep(50);
|
|
77
|
-
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
78
|
-
for (const [key, value] of Object.entries(savedEnv)) {
|
|
79
|
-
if (value === undefined) delete process.env[key];
|
|
80
|
-
else process.env[key] = value;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
function makeOrchestrator(rootDir: string): HostProcessOrchestrator {
|
|
86
|
-
mkdirSync(rootDir, { recursive: true });
|
|
87
|
-
const orch = new HostProcessOrchestrator({
|
|
88
|
-
rootDir,
|
|
89
|
-
portRange: { start: 18000, end: 18499 },
|
|
90
|
-
stopTimeoutMs: 1500,
|
|
91
|
-
spawn: { entrypoint: scriptPath },
|
|
92
|
-
});
|
|
93
|
-
orchestrators.push(orch);
|
|
94
|
-
return orch;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function waitForFile(path: string, timeoutMs = 5000): Promise<string> {
|
|
98
|
-
const deadline = Date.now() + timeoutMs;
|
|
99
|
-
while (Date.now() < deadline) {
|
|
100
|
-
if (existsSync(path)) return readFileSync(path, 'utf8');
|
|
101
|
-
await sleep(50);
|
|
102
|
-
}
|
|
103
|
-
throw new Error(`file never appeared: ${path}`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function sleep(ms: number): Promise<void> {
|
|
107
|
-
return new Promise((res) => setTimeout(res, ms));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
describe('HostProcessOrchestrator child env scrub', () => {
|
|
111
|
-
it('does not leak manager CADRE_* config-override vars into children, but passes through non-CADRE vars', async () => {
|
|
112
|
-
process.env.CADRE_PARTY_ID = 'conflicting-party';
|
|
113
|
-
process.env.CADRE_STORAGE_PATH = '/tmp/should-not-leak';
|
|
114
|
-
// Not an ENV_MAPPINGS config override — read directly by the cli's `start`
|
|
115
|
-
// command — so it only stays scrubbed if the scrub is prefix-wide.
|
|
116
|
-
process.env.CADRE_ADMIN_PORT = '19999';
|
|
117
|
-
process.env.DEBUG = 'some:value';
|
|
118
|
-
|
|
119
|
-
const rootDir = join(tmpRoot, 'a');
|
|
120
|
-
const orch = makeOrchestrator(rootDir);
|
|
121
|
-
await orch.init();
|
|
122
|
-
|
|
123
|
-
await orch.createContainer({
|
|
124
|
-
containerId: 'child-1',
|
|
125
|
-
partyId: 'party-1',
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
expect(seen.
|
|
141
|
-
expect(seen.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
await orch.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
expect(seen.CADRE_LISTEN_ADDRS).toBe(`/ip4/0.0.0.0/tcp/${ports.p2p},/ip4/0.0.0.0/tcp/${ports.ws}/ws`);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* HostProcessOrchestrator env-scrub test — the manager's own `process.env` must
|
|
3
|
+
* not leak `CADRE_*` config-override vars into spawned children.
|
|
4
|
+
*
|
|
5
|
+
* `launchChild` used to build each child's environment as
|
|
6
|
+
* `{ ...process.env, ...extraEnv, <fixed per-child vars> }`, so ANY `CADRE_*`
|
|
7
|
+
* var set on the manager (not just the ones the orchestrator deliberately
|
|
8
|
+
* sets) reached every child. Because `@serfab/cadre-cli` treats an inherited
|
|
9
|
+
* `CADRE_*` var as a config override that beats the per-child config file, a
|
|
10
|
+
* `CADRE_PARTY_ID` set on the manager could silently pull every child into
|
|
11
|
+
* that party.
|
|
12
|
+
*
|
|
13
|
+
* A fake CLI records the vars under test from its environment to a file so
|
|
14
|
+
* the scrub is observable without a real cadre node.
|
|
15
|
+
*
|
|
16
|
+
* The same record carries `CADRE_LISTEN_ADDRS`, one of the vars the orchestrator
|
|
17
|
+
* deliberately SETS: a child must be handed a WebSocket listener beside its TCP one,
|
|
18
|
+
* on the ports its handle holds, or a phone cannot reach it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
22
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { tmpdir } from 'node:os';
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
|
|
26
|
+
import { HostProcessOrchestrator } from '../orchestrator/host-process-orchestrator.js';
|
|
27
|
+
import { removeAllNodes } from './orchestrator-teardown.js';
|
|
28
|
+
|
|
29
|
+
// Writes the vars under test (as seen in the child env) next to the startup token.
|
|
30
|
+
const FAKE_CLI = `
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
const args = process.argv.slice(2);
|
|
34
|
+
const get = (flag) => { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : undefined; };
|
|
35
|
+
const tokenPath = get('--startup-token-file');
|
|
36
|
+
const token = process.env.CADRE_STARTUP_TOKEN ?? '';
|
|
37
|
+
if (tokenPath) {
|
|
38
|
+
const dir = path.dirname(tokenPath);
|
|
39
|
+
const record = {
|
|
40
|
+
CADRE_PARTY_ID: process.env.CADRE_PARTY_ID ?? null,
|
|
41
|
+
CADRE_STORAGE_PATH: process.env.CADRE_STORAGE_PATH ?? null,
|
|
42
|
+
CADRE_ADMIN_PORT: process.env.CADRE_ADMIN_PORT ?? null,
|
|
43
|
+
CADRE_LISTEN_ADDRS: process.env.CADRE_LISTEN_ADDRS ?? null,
|
|
44
|
+
DEBUG: process.env.DEBUG ?? null,
|
|
45
|
+
};
|
|
46
|
+
// Write then rename: the test polls for the file's existence, and must never read it half-written.
|
|
47
|
+
const seenPath = path.join(dir, 'env-seen.json');
|
|
48
|
+
try { fs.writeFileSync(seenPath + '.tmp', JSON.stringify(record), 'utf8'); fs.renameSync(seenPath + '.tmp', seenPath); } catch (e) { console.error(e); }
|
|
49
|
+
if (token) { try { fs.writeFileSync(tokenPath, token, 'utf8'); } catch (e) { console.error(e); } }
|
|
50
|
+
}
|
|
51
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
52
|
+
setInterval(() => {}, 1 << 30);
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
let tmpRoot: string;
|
|
56
|
+
let scriptPath: string;
|
|
57
|
+
const orchestrators: HostProcessOrchestrator[] = [];
|
|
58
|
+
let savedEnv: Record<string, string | undefined>;
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-env-scrub-'));
|
|
62
|
+
scriptPath = join(tmpRoot, 'fake-cli.mjs');
|
|
63
|
+
writeFileSync(scriptPath, FAKE_CLI, 'utf8');
|
|
64
|
+
savedEnv = {
|
|
65
|
+
CADRE_PARTY_ID: process.env.CADRE_PARTY_ID,
|
|
66
|
+
CADRE_STORAGE_PATH: process.env.CADRE_STORAGE_PATH,
|
|
67
|
+
CADRE_ADMIN_PORT: process.env.CADRE_ADMIN_PORT,
|
|
68
|
+
DEBUG: process.env.DEBUG,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(async () => {
|
|
73
|
+
try {
|
|
74
|
+
await removeAllNodes(orchestrators);
|
|
75
|
+
} finally {
|
|
76
|
+
await sleep(50);
|
|
77
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
78
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
79
|
+
if (value === undefined) delete process.env[key];
|
|
80
|
+
else process.env[key] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function makeOrchestrator(rootDir: string): HostProcessOrchestrator {
|
|
86
|
+
mkdirSync(rootDir, { recursive: true });
|
|
87
|
+
const orch = new HostProcessOrchestrator({
|
|
88
|
+
rootDir,
|
|
89
|
+
portRange: { start: 18000, end: 18499 },
|
|
90
|
+
stopTimeoutMs: 1500,
|
|
91
|
+
spawn: { entrypoint: scriptPath },
|
|
92
|
+
});
|
|
93
|
+
orchestrators.push(orch);
|
|
94
|
+
return orch;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function waitForFile(path: string, timeoutMs = 5000): Promise<string> {
|
|
98
|
+
const deadline = Date.now() + timeoutMs;
|
|
99
|
+
while (Date.now() < deadline) {
|
|
100
|
+
if (existsSync(path)) return readFileSync(path, 'utf8');
|
|
101
|
+
await sleep(50);
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`file never appeared: ${path}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sleep(ms: number): Promise<void> {
|
|
107
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe('HostProcessOrchestrator child env scrub', () => {
|
|
111
|
+
it('does not leak manager CADRE_* config-override vars into children, but passes through non-CADRE vars', async () => {
|
|
112
|
+
process.env.CADRE_PARTY_ID = 'conflicting-party';
|
|
113
|
+
process.env.CADRE_STORAGE_PATH = '/tmp/should-not-leak';
|
|
114
|
+
// Not an ENV_MAPPINGS config override — read directly by the cli's `start`
|
|
115
|
+
// command — so it only stays scrubbed if the scrub is prefix-wide.
|
|
116
|
+
process.env.CADRE_ADMIN_PORT = '19999';
|
|
117
|
+
process.env.DEBUG = 'some:value';
|
|
118
|
+
|
|
119
|
+
const rootDir = join(tmpRoot, 'a');
|
|
120
|
+
const orch = makeOrchestrator(rootDir);
|
|
121
|
+
await orch.init();
|
|
122
|
+
|
|
123
|
+
await orch.createContainer({
|
|
124
|
+
containerId: 'child-1',
|
|
125
|
+
partyId: 'party-1',
|
|
126
|
+
profile: 'storage',
|
|
127
|
+
});
|
|
128
|
+
await orch.createContainer({
|
|
129
|
+
containerId: 'child-2',
|
|
130
|
+
partyId: 'party-2',
|
|
131
|
+
profile: 'storage',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const seen1 = JSON.parse(await waitForFile(join(rootDir, 'child-1', 'env-seen.json')));
|
|
135
|
+
const seen2 = JSON.parse(await waitForFile(join(rootDir, 'child-2', 'env-seen.json')));
|
|
136
|
+
|
|
137
|
+
for (const seen of [seen1, seen2]) {
|
|
138
|
+
expect(seen.CADRE_PARTY_ID).toBeNull();
|
|
139
|
+
expect(seen.CADRE_STORAGE_PATH).toBeNull();
|
|
140
|
+
expect(seen.CADRE_ADMIN_PORT).toBeNull();
|
|
141
|
+
expect(seen.DEBUG).toBe('some:value');
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('hands each child a TCP and a WebSocket listen address on its own ports', async () => {
|
|
146
|
+
const rootDir = join(tmpRoot, 'b');
|
|
147
|
+
const orch = makeOrchestrator(rootDir);
|
|
148
|
+
await orch.init();
|
|
149
|
+
|
|
150
|
+
await orch.createContainer({
|
|
151
|
+
containerId: 'child-1',
|
|
152
|
+
partyId: 'party-1',
|
|
153
|
+
profile: 'storage',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const seen = JSON.parse(await waitForFile(join(rootDir, 'child-1', 'env-seen.json')));
|
|
157
|
+
const ports = orch.getNode('child-1')!.ports;
|
|
158
|
+
expect(seen.CADRE_LISTEN_ADDRS).toBe(`/ip4/0.0.0.0/tcp/${ports.p2p},/ip4/0.0.0.0/tcp/${ports.ws}/ws`);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -87,7 +87,7 @@ export function describeHandleContract<O extends ContractOrchestrator>(
|
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
// Launching over a live child hands its ports to a second child that then
|
|
90
|
-
// dies on them, while the caller records the new dockerId
|
|
90
|
+
// dies on them, while the caller records the new dockerId.
|
|
91
91
|
it('refuses to re-spawn a container whose previous child is still running', async () => {
|
|
92
92
|
const orch = harness.make();
|
|
93
93
|
const first = await spawn(orch, CONTAINER);
|
|
@@ -95,7 +95,6 @@ describe('HostProcessOrchestrator node identity', () => {
|
|
|
95
95
|
await orch.createContainer({
|
|
96
96
|
containerId: 'hosted-1',
|
|
97
97
|
partyId: 'party-P',
|
|
98
|
-
bootstrapNodes: [],
|
|
99
98
|
profile: 'storage',
|
|
100
99
|
});
|
|
101
100
|
|
|
@@ -114,7 +113,6 @@ describe('HostProcessOrchestrator node identity', () => {
|
|
|
114
113
|
const first = await orch.createContainer({
|
|
115
114
|
containerId: 'hosted-2',
|
|
116
115
|
partyId: 'party-P',
|
|
117
|
-
bootstrapNodes: [],
|
|
118
116
|
profile: 'storage',
|
|
119
117
|
});
|
|
120
118
|
const workdir = join(rootDir, 'hosted-2');
|
|
@@ -127,7 +125,6 @@ describe('HostProcessOrchestrator node identity', () => {
|
|
|
127
125
|
await orch.createContainer({
|
|
128
126
|
containerId: 'hosted-2',
|
|
129
127
|
partyId: 'party-P',
|
|
130
|
-
bootstrapNodes: [],
|
|
131
128
|
profile: 'storage',
|
|
132
129
|
});
|
|
133
130
|
|
|
@@ -143,7 +140,6 @@ describe('HostProcessOrchestrator node identity', () => {
|
|
|
143
140
|
const created = await orch.createContainer({
|
|
144
141
|
containerId: 'hosted-3',
|
|
145
142
|
partyId: 'party-P',
|
|
146
|
-
bootstrapNodes: [],
|
|
147
143
|
profile: 'storage',
|
|
148
144
|
});
|
|
149
145
|
const identityPath = join(rootDir, 'hosted-3', 'identity.key');
|
|
@@ -170,7 +166,6 @@ describe('HostProcessOrchestrator node identity', () => {
|
|
|
170
166
|
|
|
171
167
|
const request = {
|
|
172
168
|
partyId: 'party-P',
|
|
173
|
-
bootstrapNodes: [],
|
|
174
169
|
profile: 'storage' as const,
|
|
175
170
|
};
|
|
176
171
|
await expect(orch.createContainer({ ...request, containerId: 'hosted-bad' })).rejects.toThrow();
|
|
@@ -30,7 +30,7 @@ setInterval(() => {}, 1 << 30);
|
|
|
30
30
|
const FCM = { projectId: 'proj', clientEmail: 'svc@proj.iam', privateKey: 'FCM-SECRET-PEM' };
|
|
31
31
|
const APNS = { keyId: 'KID', teamId: 'TEAM', bundleId: 'com.example.app', privateKey: 'APNS-SECRET-P8', production: false };
|
|
32
32
|
/** A storage node — the one profile whose spawn carries push. */
|
|
33
|
-
const NODE = { containerId: 'n1', partyId: 'p',
|
|
33
|
+
const NODE = { containerId: 'n1', partyId: 'p', profile: 'storage' as const };
|
|
34
34
|
|
|
35
35
|
let tmpRoot: string;
|
|
36
36
|
let scriptPath: string;
|
|
@@ -129,10 +129,10 @@ describe('HostProcessOrchestrator push injection', () => {
|
|
|
129
129
|
const orch = makeOrchestrator(rootDir, async () => ({ fcm: FCM, apns: APNS, cooldownMs: 1000 }));
|
|
130
130
|
await orch.init();
|
|
131
131
|
|
|
132
|
-
await orch.createContainer({ containerId: 'storage-1', partyId: 'p',
|
|
132
|
+
await orch.createContainer({ containerId: 'storage-1', partyId: 'p', profile: 'storage' });
|
|
133
133
|
expect(readChildConfig(rootDir, 'storage-1').push).toEqual({ fcm: FCM, apns: APNS, cooldownMs: 1000 });
|
|
134
134
|
|
|
135
|
-
await orch.createContainer({ containerId: 'txn-1', partyId: 'p',
|
|
135
|
+
await orch.createContainer({ containerId: 'txn-1', partyId: 'p', profile: 'transaction' });
|
|
136
136
|
expect(readChildConfig(rootDir, 'txn-1').push).toBeUndefined();
|
|
137
137
|
});
|
|
138
138
|
});
|
|
@@ -119,7 +119,6 @@ function makeRequest(containerId: string, opts: { profile?: 'storage' | 'transac
|
|
|
119
119
|
return {
|
|
120
120
|
containerId,
|
|
121
121
|
partyId: 'party-' + containerId,
|
|
122
|
-
bootstrapNodes: ['/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWA9hbnKrRnPRSPTRkzXqTHzGE8YpJ3JHZmQ5tGwLRTMmp'],
|
|
123
122
|
profile: opts.profile ?? 'transaction' as 'storage' | 'transaction',
|
|
124
123
|
};
|
|
125
124
|
}
|
|
@@ -556,9 +555,9 @@ describe('HostProcessOrchestrator.isRunning', () => {
|
|
|
556
555
|
*
|
|
557
556
|
* The incident this guards against: the respawn supervisor asked `isRunning`
|
|
558
557
|
* during that window, got `false`, and re-spawned the container. The re-spawn
|
|
559
|
-
* released and reused the same ports
|
|
560
|
-
*
|
|
561
|
-
*
|
|
558
|
+
* released and reused the same ports and returned success; the second child
|
|
559
|
+
* then died on the port the first still held, leaving the host's records naming
|
|
560
|
+
* a dead child while the live one ran with no handle naming it.
|
|
562
561
|
*/
|
|
563
562
|
describe('HostProcessOrchestrator re-spawn over a child that is still starting', () => {
|
|
564
563
|
const TOKEN_DELAY_MS = 3000;
|
|
@@ -911,7 +910,6 @@ describe('child survives orchestrator exit', () => {
|
|
|
911
910
|
const request = {
|
|
912
911
|
containerId: 'cross1',
|
|
913
912
|
partyId: 'party-cross1',
|
|
914
|
-
bootstrapNodes: ['/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWA9hbnKrRnPRSPTRkzXqTHzGE8YpJ3JHZmQ5tGwLRTMmp'],
|
|
915
913
|
profile: 'transaction',
|
|
916
914
|
};
|
|
917
915
|
const payload = {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { currentLogin, isHeadless, uiAccessLines } from '../bin/ui-access.js';
|
|
4
|
+
|
|
5
|
+
describe('isHeadless', () => {
|
|
6
|
+
it('is true over SSH on any platform, and on Linux without a display', () => {
|
|
7
|
+
expect(isHeadless('darwin', { SSH_CONNECTION: '1.2.3.4 5 6.7.8.9 22' })).toBe(true);
|
|
8
|
+
expect(isHeadless('linux', {})).toBe(true);
|
|
9
|
+
expect(isHeadless('linux', { DISPLAY: ':0' })).toBe(false);
|
|
10
|
+
expect(isHeadless('linux', { WAYLAND_DISPLAY: 'wayland-0' })).toBe(false);
|
|
11
|
+
expect(isHeadless('darwin', {})).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('uiAccessLines', () => {
|
|
16
|
+
it('gives the URL on a desktop, and the SSH forward (same port both sides) when headless', () => {
|
|
17
|
+
expect(uiAccessLines(8765, false)).toEqual(['Open the UI: http://127.0.0.1:8765']);
|
|
18
|
+
const lines = uiAccessLines(8765, true, { user: 'kyle', host: 'kjeib' });
|
|
19
|
+
expect(lines).toContain(' ssh -L 8765:127.0.0.1:8765 kyle@kjeib');
|
|
20
|
+
expect(lines.join('\n')).toContain('http://127.0.0.1:8765');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('currentLogin', () => {
|
|
25
|
+
it('over SSH, names the server address and port the client connected to', () => {
|
|
26
|
+
const who = currentLogin({ SSH_CONNECTION: '198.51.100.20 51234 203.0.113.7 2222' });
|
|
27
|
+
expect(who).toMatchObject({ host: '203.0.113.7', sshPort: 2222 });
|
|
28
|
+
expect(uiAccessLines(8765, true, who)[1]).toBe(` ssh -p 2222 -L 8765:127.0.0.1:8765 ${who.user}@203.0.113.7`);
|
|
29
|
+
});
|
|
30
|
+
});
|