@serfab/cadre-host 1.14.0 → 1.16.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/src/bin/host.ts
CHANGED
|
@@ -18,12 +18,13 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
21
|
+
import { pathToFileURL } from 'node:url';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
23
|
|
|
24
24
|
import { Command, InvalidArgumentError } from 'commander';
|
|
25
25
|
|
|
26
26
|
import { Installer } from '../installer/index.js';
|
|
27
|
+
import { packageVersion } from '../package-version.js';
|
|
27
28
|
import { readHostConfig, updateHostConfig } from '../installer/config.js';
|
|
28
29
|
import {
|
|
29
30
|
configPath as resolveConfigPath,
|
|
@@ -42,7 +43,7 @@ import {
|
|
|
42
43
|
HOSTED_NODE_REAP_SWEEP_MS,
|
|
43
44
|
HOSTED_NODE_SPAWNING_TTL_MS,
|
|
44
45
|
} from '../hosted/index.js';
|
|
45
|
-
import { NatService } from '../nat/index.js';
|
|
46
|
+
import { effectiveNetworkMode, NatService, NatStore, publicInterfaceAddress } from '../nat/index.js';
|
|
46
47
|
import type { ManualForwardPatch } from '../nat/types.js';
|
|
47
48
|
import { createSecretsStore } from '../nat/secrets/index.js';
|
|
48
49
|
import {
|
|
@@ -55,6 +56,7 @@ import {
|
|
|
55
56
|
import { createLocalUiServer, HostSettingsStore } from '../server/index.js';
|
|
56
57
|
import { openBrowser } from '../installer/browser.js';
|
|
57
58
|
import { printForwardResult, printNatStatus, type NatStatusLike } from './nat-output.js';
|
|
59
|
+
import { isHeadless, uiAccessLines } from './ui-access.js';
|
|
58
60
|
import {
|
|
59
61
|
printClaimPayload,
|
|
60
62
|
printClaimed,
|
|
@@ -80,7 +82,7 @@ const program = new Command();
|
|
|
80
82
|
program
|
|
81
83
|
.name('cadre-host')
|
|
82
84
|
.description('Sereus cadre node manager for self-hosted basement-PC deployments')
|
|
83
|
-
.version(
|
|
85
|
+
.version(packageVersion);
|
|
84
86
|
|
|
85
87
|
// ============================================================================
|
|
86
88
|
// install — run the first-run wizard + register the system service
|
|
@@ -114,7 +116,8 @@ program
|
|
|
114
116
|
...(opts.dataDir ? { dataDir: opts.dataDir } : {}),
|
|
115
117
|
...(typeof opts.uiPort === 'number' ? { uiPort: opts.uiPort } : {}),
|
|
116
118
|
noUpnp: opts.upnp === false,
|
|
117
|
-
|
|
119
|
+
// Headless, a browser would open nowhere the admin can see; the `ssh -L` lines below say how instead.
|
|
120
|
+
openBrowser: opts.browser !== false && !isHeadless(),
|
|
118
121
|
system: Boolean(opts.system),
|
|
119
122
|
...(opts.nodePath ? { nodePath: opts.nodePath } : {}),
|
|
120
123
|
noService: opts.service === false,
|
|
@@ -128,6 +131,15 @@ program
|
|
|
128
131
|
console.log(' Service: not registered (--no-service)');
|
|
129
132
|
console.log(` Run the host: cadre-host start --data-dir "${result.dataDir}"`);
|
|
130
133
|
}
|
|
134
|
+
// A re-install keeps the `network` setting an earlier one recorded in nat.json.
|
|
135
|
+
const publicIp = publicInterfaceAddress();
|
|
136
|
+
const mode = effectiveNetworkMode(new NatStore(result.dataDir).load().network, publicIp);
|
|
137
|
+
console.log(mode === 'public'
|
|
138
|
+
? ` Network: public IP${publicIp ? ` ${publicIp}` : ''}: nodes are reached directly. Allow their ports through your firewall (the UI lists them).`
|
|
139
|
+
: ' Network: behind a router: nodes need UPnP or a port forward (the UI shows which).');
|
|
140
|
+
console.log('');
|
|
141
|
+
for (const line of uiAccessLines(Number(new URL(result.uiUrl).port), isHeadless())) console.log(line);
|
|
142
|
+
console.log('Everything else (a public name, joining a cadre, ports) is in the UI.');
|
|
131
143
|
process.exit(0);
|
|
132
144
|
} catch (err) {
|
|
133
145
|
console.error(`install failed: ${(err as Error).message}`);
|
|
@@ -233,7 +245,7 @@ program
|
|
|
233
245
|
const serviceHost = createServiceHost(platform);
|
|
234
246
|
const updateService = new UpdateService({
|
|
235
247
|
dataDir: cfg.dataDir,
|
|
236
|
-
currentVersion:
|
|
248
|
+
currentVersion: packageVersion,
|
|
237
249
|
settings: cfg.updates,
|
|
238
250
|
...(cfg.updates.manifestUrl ? { manifestUrl: cfg.updates.manifestUrl } : {}),
|
|
239
251
|
restart: async () => {
|
|
@@ -304,6 +316,8 @@ program
|
|
|
304
316
|
orchestrator,
|
|
305
317
|
store: hostedNodeStore,
|
|
306
318
|
addresses: natService,
|
|
319
|
+
// Re-read per claim code so a Settings edit (or a hand edit) applies to the next code shown.
|
|
320
|
+
claimAddressSettings: () => readHostConfig(cfgPath).claimAddresses,
|
|
307
321
|
});
|
|
308
322
|
|
|
309
323
|
// Respawn supervision — nothing else brings a hosted node back. A crash,
|
|
@@ -389,6 +403,12 @@ program
|
|
|
389
403
|
}
|
|
390
404
|
const cfg = readHostConfig(cfgPath);
|
|
391
405
|
const url = `http://127.0.0.1:${cfg.uiPort}`;
|
|
406
|
+
if (isHeadless()) {
|
|
407
|
+
// A browser here would open nowhere the admin can see: say how to reach it instead.
|
|
408
|
+
for (const line of uiAccessLines(cfg.uiPort, true)) console.log(line);
|
|
409
|
+
process.exit(0);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
392
412
|
console.log(url);
|
|
393
413
|
if (opts.browser !== false) {
|
|
394
414
|
const res = openBrowser(url);
|
|
@@ -413,19 +433,6 @@ function parseIntArg(raw: string): number {
|
|
|
413
433
|
return n;
|
|
414
434
|
}
|
|
415
435
|
|
|
416
|
-
function readPackageVersion(): string {
|
|
417
|
-
try {
|
|
418
|
-
// dist/bin/host.js -> ../../package.json
|
|
419
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
420
|
-
const pkgPath = resolve(here, '..', '..', 'package.json');
|
|
421
|
-
const raw = readFileSync(pkgPath, 'utf8');
|
|
422
|
-
const parsed = JSON.parse(raw) as { version?: string };
|
|
423
|
-
return parsed.version ?? '0.0.0-unknown';
|
|
424
|
-
} catch {
|
|
425
|
-
return '0.0.0-unknown';
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
436
|
function waitForTermination(): Promise<void> {
|
|
430
437
|
return new Promise<void>((resolveTerm) => {
|
|
431
438
|
const done = () => resolveTerm();
|
|
@@ -673,18 +680,21 @@ nat
|
|
|
673
680
|
|
|
674
681
|
nat
|
|
675
682
|
.command('settings')
|
|
676
|
-
.description('Update NAT settings (UPnP toggle)')
|
|
683
|
+
.description('Update NAT settings (network mode, UPnP toggle)')
|
|
684
|
+
.option('--network <mode>', 'auto (default: public when this machine holds a public IPv4), lan (behind a router) or public (reached directly, e.g. a VPS)')
|
|
677
685
|
.option('--upnp', 'Ask the router for port mappings over UPnP')
|
|
678
686
|
.option('--no-upnp', 'Stop asking the router for port mappings (manual forwards stay)')
|
|
679
687
|
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
680
688
|
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
681
689
|
.action(async (opts: {
|
|
690
|
+
network?: string;
|
|
682
691
|
upnp?: boolean;
|
|
683
692
|
port: string;
|
|
684
693
|
host: string;
|
|
685
694
|
}) => {
|
|
686
695
|
const url = `http://${opts.host}:${resolvePort(opts.port)}/nat/settings`;
|
|
687
696
|
const patch: Record<string, unknown> = {};
|
|
697
|
+
if (opts.network !== undefined) patch.network = opts.network;
|
|
688
698
|
// Commander assigns `upnp: false` when `--no-upnp` is passed.
|
|
689
699
|
if (opts.upnp === false) patch.upnpEnabled = false;
|
|
690
700
|
if (opts.upnp === true) patch.upnpEnabled = true;
|
package/src/bin/nat-output.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface NatNodeLike {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface NatStatusLike {
|
|
29
|
+
network?: { setting?: string; mode?: string; publicInterfaceIp?: string | null };
|
|
29
30
|
upnpEnabled?: boolean;
|
|
30
31
|
gateway?: {
|
|
31
32
|
found?: boolean;
|
|
@@ -85,6 +86,15 @@ export function printForwardResult(nodeId: string, s: NatStatusLike): void {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
function printHostLines(s: NatStatusLike): void {
|
|
89
|
+
const network = s.network ?? {};
|
|
90
|
+
const how = network.setting === 'auto' || network.setting === undefined ? 'detected' : 'set';
|
|
91
|
+
if (network.mode === 'public') {
|
|
92
|
+
console.log(`Network: public IP ${network.publicInterfaceIp ?? s.externalIp ?? '?'} on this machine (${how}); no router, so no UPnP`);
|
|
93
|
+
console.log(`External IP: ${s.externalIp ?? network.publicInterfaceIp ?? '(unknown)'}`);
|
|
94
|
+
console.log(`Reachability: ${s.directReachability ?? 'unknown'}${s.lastTestedAt ? ` (tested ${s.lastTestedAt})` : ''}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
console.log(`Network: behind a router (${how})`);
|
|
88
98
|
const gateway = s.gateway ?? {};
|
|
89
99
|
const gatewayLine = gateway.found
|
|
90
100
|
? `found (this machine is ${gateway.lanAddress ?? '?'} on its network)`
|
|
@@ -115,7 +125,7 @@ function printNodeReachability(n: NatNodeLike, s: NatStatusLike): void {
|
|
|
115
125
|
printField('TCP', [formatRoute(n.tcp)]);
|
|
116
126
|
printField('WebSocket', [n.ws ? formatRoute(n.ws) : 'not available']);
|
|
117
127
|
printField('Public', n.publicAddrs?.length ? n.publicAddrs : ['none']);
|
|
118
|
-
if (n.reason) printField('Why', [n.reason]);
|
|
128
|
+
if (n.reason) printField(n.verdict === 'direct' ? 'Firewall' : 'Why', [n.reason]);
|
|
119
129
|
const advice = forwardAdvice(n, s);
|
|
120
130
|
if (advice.length > 0) printField('To fix', advice);
|
|
121
131
|
}
|
|
@@ -129,6 +139,7 @@ function formatRoute(r: NatPortRouteLike | undefined): string {
|
|
|
129
139
|
const internal = `internal ${r.internalPort ?? '?'}`;
|
|
130
140
|
if (r.externalPort == null) return `${internal} → not mapped${r.error ? ` (${r.error})` : ''}`;
|
|
131
141
|
const failed = r.error ? `; last attempt failed: ${r.error}` : '';
|
|
142
|
+
if (r.source === 'direct') return `${internal} → public, as-is${failed}`;
|
|
132
143
|
return `${internal} → external ${r.externalPort} (${r.source === 'manual' ? 'forwarded by hand' : 'UPnP'}${failed})`;
|
|
133
144
|
}
|
|
134
145
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How to reach the local UI from where the admin is sitting. On a machine with a desktop, open
|
|
3
|
+
* the URL; over SSH or on a headless server, forward the port from the admin's own computer
|
|
4
|
+
* first. The UI binds 127.0.0.1 only and checks the Host header, so the local port must equal
|
|
5
|
+
* the remote one.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { hostname, userInfo } from 'node:os';
|
|
9
|
+
|
|
10
|
+
/** Over SSH, or Linux with no display server: a browser here would show nothing to the admin. */
|
|
11
|
+
export function isHeadless(platform: NodeJS.Platform = process.platform, env: NodeJS.ProcessEnv = process.env): boolean {
|
|
12
|
+
if (env.SSH_CONNECTION || env.SSH_TTY) return true;
|
|
13
|
+
return platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Who to SSH in as, and where; `sshPort` only when it is not 22. */
|
|
17
|
+
export interface SshLogin {
|
|
18
|
+
user: string;
|
|
19
|
+
host: string;
|
|
20
|
+
sshPort?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The lines telling the admin how to open the UI at `port`. */
|
|
24
|
+
export function uiAccessLines(port: number, headless: boolean, who: SshLogin = currentLogin()): string[] {
|
|
25
|
+
const url = `http://127.0.0.1:${port}`;
|
|
26
|
+
if (!headless) return [`Open the UI: ${url}`];
|
|
27
|
+
const portFlag = who.sshPort !== undefined && who.sshPort !== 22 ? ` -p ${who.sshPort}` : '';
|
|
28
|
+
return [
|
|
29
|
+
'Open the UI from your own computer (this session has no screen):',
|
|
30
|
+
` ssh${portFlag} -L ${port}:127.0.0.1:${port} ${who.user}@${who.host}`,
|
|
31
|
+
` then browse to ${url}`,
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Over SSH, the server address and port the admin's client connected to (`SSH_CONNECTION` is
|
|
37
|
+
* "client-ip client-port server-ip server-port"): a VPS's hostname rarely resolves from the
|
|
38
|
+
* admin's computer, the address they just reached does. Otherwise this machine's hostname.
|
|
39
|
+
*/
|
|
40
|
+
export function currentLogin(env: NodeJS.ProcessEnv = process.env): SshLogin {
|
|
41
|
+
let user = '<you>';
|
|
42
|
+
try {
|
|
43
|
+
user = userInfo().username;
|
|
44
|
+
} catch {
|
|
45
|
+
// no passwd entry (some containers): keep the placeholder
|
|
46
|
+
}
|
|
47
|
+
const [, , serverIp, serverPort] = (env.SSH_CONNECTION ?? '').trim().split(/\s+/);
|
|
48
|
+
if (serverIp && serverPort) return { user, host: serverIp, sshPort: Number(serverPort) };
|
|
49
|
+
return { user, host: hostname() };
|
|
50
|
+
}
|
|
@@ -23,7 +23,6 @@ import { FakeOrchestrator } from './fake-orchestrator.js';
|
|
|
23
23
|
const request = (containerId: string): HostedSpawnRequest => ({
|
|
24
24
|
containerId,
|
|
25
25
|
partyId: 'party-P',
|
|
26
|
-
bootstrapNodes: ['/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWA9hbnKrRnPRSPTRkzXqTHzGE8YpJ3JHZmQ5tGwLRTMmp'],
|
|
27
26
|
profile: 'storage',
|
|
28
27
|
});
|
|
29
28
|
|