@serfab/cadre-host 1.13.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 +347 -423
- package/dist/bin/host.d.ts +6 -3
- package/dist/bin/host.js +332 -524
- package/dist/bin/host.js.map +1 -1
- package/dist/bin/join-output.d.ts +56 -0
- package/dist/bin/join-output.js +92 -0
- package/dist/bin/join-output.js.map +1 -0
- package/dist/bin/nat-output.d.ts +54 -0
- package/dist/bin/nat-output.js +138 -0
- package/dist/bin/nat-output.js.map +1 -0
- 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 +66 -0
- package/dist/hosted/claim-details.js +60 -0
- package/dist/hosted/claim-details.js.map +1 -0
- package/dist/hosted/hosted-node-service.d.ts +325 -0
- package/dist/hosted/hosted-node-service.js +678 -0
- package/dist/hosted/hosted-node-service.js.map +1 -0
- package/dist/hosted/hosted-node-store.d.ts +40 -0
- package/dist/hosted/hosted-node-store.js +126 -0
- package/dist/hosted/hosted-node-store.js.map +1 -0
- package/dist/{donation/donation-supervisor.d.ts → hosted/hosted-node-supervisor.d.ts} +70 -53
- package/dist/hosted/hosted-node-supervisor.js +396 -0
- package/dist/hosted/hosted-node-supervisor.js.map +1 -0
- package/dist/hosted/hosted-node-watcher.d.ts +95 -0
- package/dist/hosted/hosted-node-watcher.js +260 -0
- package/dist/hosted/hosted-node-watcher.js.map +1 -0
- package/dist/hosted/index.d.ts +16 -0
- package/dist/hosted/index.js +13 -0
- package/dist/hosted/index.js.map +1 -0
- package/dist/hosted/node-status.d.ts +51 -0
- package/dist/hosted/node-status.js +77 -0
- package/dist/hosted/node-status.js.map +1 -0
- package/dist/hosted/types.d.ts +141 -0
- package/dist/hosted/types.js +22 -0
- package/dist/hosted/types.js.map +1 -0
- package/dist/index.d.ts +12 -24
- package/dist/index.js +9 -19
- package/dist/index.js.map +1 -1
- package/dist/installer/config.d.ts +27 -34
- package/dist/installer/config.js +46 -15
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/index.d.ts +9 -19
- package/dist/installer/index.js +21 -85
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/paths.d.ts +0 -2
- package/dist/installer/paths.js +0 -4
- package/dist/installer/paths.js.map +1 -1
- package/dist/installer/wizard.d.ts +12 -9
- package/dist/installer/wizard.js +15 -14
- package/dist/installer/wizard.js.map +1 -1
- package/dist/nat/address-resolver.d.ts +18 -17
- package/dist/nat/address-resolver.js +79 -25
- package/dist/nat/address-resolver.js.map +1 -1
- package/dist/nat/address-watch.d.ts +37 -0
- package/dist/nat/address-watch.js +84 -0
- package/dist/nat/address-watch.js.map +1 -0
- package/dist/nat/ddns/duckdns.d.ts +2 -1
- package/dist/nat/ddns/duckdns.js +5 -1
- package/dist/nat/ddns/duckdns.js.map +1 -1
- package/dist/nat/ddns/index.d.ts +6 -0
- package/dist/nat/ddns/index.js.map +1 -1
- package/dist/nat/index.d.ts +13 -9
- package/dist/nat/index.js +7 -5
- package/dist/nat/index.js.map +1 -1
- package/dist/nat/nat-service.d.ts +220 -77
- package/dist/nat/nat-service.js +653 -239
- package/dist/nat/nat-service.js.map +1 -1
- package/dist/nat/nat-store.d.ts +12 -5
- package/dist/nat/nat-store.js +124 -29
- 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/port-mapper.d.ts +83 -79
- package/dist/nat/port-mapper.js +148 -189
- package/dist/nat/port-mapper.js.map +1 -1
- package/dist/nat/reachability.d.ts +32 -15
- package/dist/nat/reachability.js +110 -22
- package/dist/nat/reachability.js.map +1 -1
- package/dist/nat/types.d.ts +98 -27
- package/dist/nat/types.js +8 -5
- package/dist/nat/types.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +32 -74
- package/dist/orchestrator/host-process-orchestrator.js +84 -233
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/{installer/identity.d.ts → orchestrator/identity-file.d.ts} +5 -5
- package/dist/{installer/identity.js → orchestrator/identity-file.js} +6 -6
- package/dist/orchestrator/identity-file.js.map +1 -0
- package/dist/orchestrator/index.d.ts +2 -3
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/node-identity.d.ts +6 -6
- package/dist/orchestrator/node-identity.js +7 -7
- package/dist/orchestrator/node-identity.js.map +1 -1
- package/dist/orchestrator/port-allocator.d.ts +5 -6
- package/dist/orchestrator/port-allocator.js +9 -11
- package/dist/orchestrator/port-allocator.js.map +1 -1
- package/dist/orchestrator/state-store.d.ts +3 -5
- package/dist/orchestrator/state-store.js +2 -10
- package/dist/orchestrator/state-store.js.map +1 -1
- package/dist/orchestrator/types.d.ts +33 -22
- package/dist/orchestrator/types.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/error-handler.d.ts +5 -25
- package/dist/server/error-handler.js +11 -74
- package/dist/server/error-handler.js.map +1 -1
- package/dist/server/events/types.d.ts +7 -11
- package/dist/server/events/types.js +2 -1
- package/dist/server/events/types.js.map +1 -1
- package/dist/server/index.d.ts +11 -37
- package/dist/server/index.js +30 -48
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/hosted-nodes.d.ts +28 -0
- package/dist/server/routes/hosted-nodes.js +68 -0
- package/dist/server/routes/hosted-nodes.js.map +1 -0
- package/dist/server/routes/nat.d.ts +4 -1
- package/dist/server/routes/nat.js +13 -2
- package/dist/server/routes/nat.js.map +1 -1
- package/dist/server/routes/nodes.d.ts +6 -20
- package/dist/server/routes/nodes.js +7 -86
- package/dist/server/routes/nodes.js.map +1 -1
- package/dist/server/routes/settings.d.ts +3 -7
- package/dist/server/routes/settings.js +18 -8
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/status.d.ts +2 -25
- package/dist/server/routes/status.js +3 -43
- package/dist/server/routes/status.js.map +1 -1
- package/dist/server/server.js +8 -8
- package/dist/server/settings-store.d.ts +2 -2
- package/dist/server/settings-store.js +2 -2
- package/dist/server/static.js +1 -2
- package/dist/server/static.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-Ccr2GjPT.js.map +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/store.d.ts +1 -1
- package/dist/update/store.js +1 -1
- package/dist/update/types.d.ts +1 -1
- package/dist/update/types.js +1 -1
- package/package.json +5 -7
- package/src/__tests__/cli-nat.smoke.test.ts +219 -212
- package/src/__tests__/cli.smoke.test.ts +2 -2
- package/src/__tests__/global-setup.ts +2 -3
- package/src/__tests__/orchestrator-claim-secret.test.ts +111 -0
- package/src/__tests__/orchestrator-env-scrub.test.ts +160 -163
- package/src/__tests__/orchestrator-handle-contract.ts +134 -134
- package/src/__tests__/orchestrator-node-identity.test.ts +177 -203
- package/src/__tests__/orchestrator-ports.test.ts +124 -143
- package/src/__tests__/orchestrator-push.test.ts +138 -146
- package/src/__tests__/orchestrator.test.ts +1017 -1063
- package/src/__tests__/ui-access.test.ts +30 -0
- package/src/bin/host.ts +397 -572
- package/src/bin/join-output.ts +120 -0
- package/src/bin/nat-output.ts +188 -0
- package/src/bin/ui-access.ts +50 -0
- package/src/{donation → hosted}/__tests__/fake-orchestrator.test.ts +172 -173
- package/src/{donation → hosted}/__tests__/fake-orchestrator.ts +43 -23
- package/src/hosted/__tests__/hosted-node-service.test.ts +708 -0
- package/src/hosted/__tests__/hosted-node-supervisor.test.ts +475 -0
- package/src/hosted/__tests__/test-invitation.ts +33 -0
- package/src/hosted/claim-details.ts +103 -0
- package/src/hosted/hosted-node-service.ts +814 -0
- package/src/hosted/hosted-node-store.ts +144 -0
- package/src/hosted/hosted-node-supervisor.ts +439 -0
- package/src/hosted/hosted-node-watcher.ts +288 -0
- package/src/hosted/index.ts +53 -0
- package/src/hosted/node-status.ts +97 -0
- package/src/hosted/types.ts +160 -0
- package/src/index.ts +72 -91
- package/src/installer/__tests__/config.test.ts +15 -40
- package/src/installer/__tests__/installer.smoke.test.ts +184 -263
- package/src/installer/__tests__/wizard.test.ts +25 -13
- package/src/installer/config.ts +201 -175
- package/src/installer/index.ts +252 -327
- package/src/installer/paths.ts +104 -109
- package/src/installer/wizard.ts +24 -26
- package/src/nat/__tests__/address-resolver.test.ts +107 -82
- package/src/nat/__tests__/nat-service.test.ts +608 -515
- package/src/nat/__tests__/nat-store.test.ts +49 -12
- package/src/nat/__tests__/network-mode.test.ts +38 -0
- package/src/nat/__tests__/port-mapper.test.ts +30 -191
- package/src/nat/__tests__/reachability.test.ts +96 -13
- package/src/nat/address-resolver.ts +94 -52
- package/src/nat/address-watch.ts +92 -0
- package/src/nat/ddns/duckdns.ts +5 -1
- package/src/nat/ddns/index.ts +6 -0
- package/src/nat/index.ts +33 -13
- package/src/nat/nat-service.ts +942 -501
- package/src/nat/nat-store.ts +126 -31
- package/src/nat/network-mode.ts +36 -0
- package/src/nat/port-mapper.ts +241 -329
- package/src/nat/reachability.ts +155 -34
- package/src/nat/types.ts +110 -37
- package/src/{installer/__tests__/identity.test.ts → orchestrator/__tests__/identity-file.test.ts} +34 -34
- package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
- package/src/orchestrator/host-process-orchestrator.ts +1089 -1260
- package/src/{installer/identity.ts → orchestrator/identity-file.ts} +56 -56
- package/src/orchestrator/index.ts +9 -10
- package/src/orchestrator/node-identity.ts +41 -41
- package/src/orchestrator/port-allocator.ts +71 -73
- package/src/orchestrator/state-store.ts +76 -90
- package/src/orchestrator/types.ts +34 -23
- package/src/package-version.ts +4 -0
- package/src/server/__tests__/bus.test.ts +38 -38
- package/src/server/__tests__/error-handler.test.ts +81 -118
- package/src/server/__tests__/fakes.ts +15 -18
- package/src/server/__tests__/hosted-nodes-route.test.ts +161 -0
- package/src/server/__tests__/nodes-route.test.ts +114 -283
- package/src/server/__tests__/publishers.test.ts +193 -180
- package/src/server/__tests__/server.smoke.test.ts +134 -223
- package/src/server/__tests__/settings-route.test.ts +210 -195
- package/src/server/__tests__/sse-route.test.ts +119 -119
- package/src/server/__tests__/status-route.test.ts +82 -121
- package/src/server/error-handler.ts +112 -178
- package/src/server/events/types.ts +20 -24
- package/src/server/index.ts +223 -267
- package/src/server/routes/hosted-nodes.ts +83 -0
- package/src/server/routes/nat.ts +75 -63
- package/src/server/routes/nodes.ts +96 -188
- package/src/server/routes/settings.ts +20 -15
- package/src/server/routes/status.ts +5 -67
- package/src/server/server.ts +8 -8
- package/src/server/settings-store.ts +2 -2
- package/src/server/static.ts +1 -2
- package/src/update/index.ts +2 -2
- package/src/update/store.ts +1 -1
- package/src/update/types.ts +1 -1
- package/dist/auth/duration.d.ts +0 -1
- package/dist/auth/duration.js +0 -32
- package/dist/auth/duration.js.map +0 -1
- package/dist/auth/index.d.ts +0 -11
- package/dist/auth/index.js +0 -9
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/self-label.d.ts +0 -36
- package/dist/auth/self-label.js +0 -57
- package/dist/auth/self-label.js.map +0 -1
- package/dist/auth/trust-circle-store.d.ts +0 -26
- package/dist/auth/trust-circle-store.js +0 -129
- package/dist/auth/trust-circle-store.js.map +0 -1
- package/dist/auth/trust-circle.d.ts +0 -156
- package/dist/auth/trust-circle.js +0 -307
- package/dist/auth/trust-circle.js.map +0 -1
- package/dist/auth/types.d.ts +0 -84
- package/dist/auth/types.js +0 -24
- package/dist/auth/types.js.map +0 -1
- package/dist/donation/donation-service.d.ts +0 -389
- package/dist/donation/donation-service.js +0 -828
- package/dist/donation/donation-service.js.map +0 -1
- package/dist/donation/donation-store.d.ts +0 -51
- package/dist/donation/donation-store.js +0 -152
- package/dist/donation/donation-store.js.map +0 -1
- package/dist/donation/donation-supervisor.js +0 -364
- package/dist/donation/donation-supervisor.js.map +0 -1
- package/dist/donation/grant-service.d.ts +0 -80
- package/dist/donation/grant-service.js +0 -210
- package/dist/donation/grant-service.js.map +0 -1
- package/dist/donation/grant-store.d.ts +0 -38
- package/dist/donation/grant-store.js +0 -135
- package/dist/donation/grant-store.js.map +0 -1
- package/dist/donation/index.d.ts +0 -20
- package/dist/donation/index.js +0 -17
- package/dist/donation/index.js.map +0 -1
- package/dist/donation/types.d.ts +0 -224
- package/dist/donation/types.js +0 -40
- package/dist/donation/types.js.map +0 -1
- package/dist/installer/identity.js.map +0 -1
- package/dist/owner/index.d.ts +0 -11
- package/dist/owner/index.js +0 -11
- package/dist/owner/index.js.map +0 -1
- package/dist/owner/owner-node-client.d.ts +0 -110
- package/dist/owner/owner-node-client.js +0 -184
- package/dist/owner/owner-node-client.js.map +0 -1
- package/dist/server/routes/bootstrap-node-validation.d.ts +0 -115
- package/dist/server/routes/bootstrap-node-validation.js +0 -197
- package/dist/server/routes/bootstrap-node-validation.js.map +0 -1
- package/dist/server/routes/grants-admin.d.ts +0 -29
- package/dist/server/routes/grants-admin.js +0 -58
- package/dist/server/routes/grants-admin.js.map +0 -1
- package/dist/server/routes/grants.d.ts +0 -28
- package/dist/server/routes/grants.js +0 -165
- package/dist/server/routes/grants.js.map +0 -1
- package/dist/server/routes/provision-request-validation.d.ts +0 -45
- package/dist/server/routes/provision-request-validation.js +0 -97
- package/dist/server/routes/provision-request-validation.js.map +0 -1
- package/dist/server/routes/strands.d.ts +0 -20
- package/dist/server/routes/strands.js +0 -38
- package/dist/server/routes/strands.js.map +0 -1
- package/dist/server/routes/trust-circle.d.ts +0 -17
- package/dist/server/routes/trust-circle.js +0 -35
- package/dist/server/routes/trust-circle.js.map +0 -1
- package/dist/strands/index.d.ts +0 -7
- package/dist/strands/index.js +0 -6
- package/dist/strands/index.js.map +0 -1
- package/dist/strands/strand-service.d.ts +0 -50
- package/dist/strands/strand-service.js +0 -103
- package/dist/strands/strand-service.js.map +0 -1
- package/dist/strands/types.d.ts +0 -93
- package/dist/strands/types.js +0 -23
- package/dist/strands/types.js.map +0 -1
- package/dist/ui/assets/index-C6nELNfC.js +0 -23
- package/dist/ui/assets/index-C6nELNfC.js.map +0 -1
- package/dist/ui/assets/index-TtIYSeZO.css +0 -1
- package/src/__tests__/cli-invite.smoke.test.ts +0 -192
- package/src/__tests__/host-owner.smoke.test.ts +0 -123
- package/src/__tests__/orchestrator-owner.test.ts +0 -234
- package/src/__tests__/orchestrator-pin-keys.test.ts +0 -152
- package/src/auth/__tests__/duration.test.ts +0 -28
- package/src/auth/__tests__/self-label.test.ts +0 -85
- package/src/auth/__tests__/trust-circle-integration.test.ts +0 -152
- package/src/auth/__tests__/trust-circle-store.test.ts +0 -115
- package/src/auth/__tests__/trust-circle.test.ts +0 -445
- package/src/auth/duration.ts +0 -33
- package/src/auth/index.ts +0 -21
- package/src/auth/self-label.ts +0 -74
- package/src/auth/trust-circle-store.ts +0 -155
- package/src/auth/trust-circle.ts +0 -395
- package/src/auth/types.ts +0 -99
- package/src/donation/__tests__/donation-service.test.ts +0 -1193
- package/src/donation/__tests__/donation-supervisor.test.ts +0 -548
- package/src/donation/__tests__/grant-service.test.ts +0 -213
- package/src/donation/__tests__/grant-store.test.ts +0 -113
- package/src/donation/donation-service.ts +0 -1033
- package/src/donation/donation-store.ts +0 -176
- package/src/donation/donation-supervisor.ts +0 -411
- package/src/donation/grant-service.ts +0 -252
- package/src/donation/grant-store.ts +0 -152
- package/src/donation/index.ts +0 -65
- package/src/donation/types.ts +0 -260
- package/src/owner/__tests__/owner-node-client.test.ts +0 -194
- package/src/owner/index.ts +0 -12
- package/src/owner/owner-node-client.ts +0 -259
- package/src/server/__tests__/bootstrap-node-validation.test.ts +0 -138
- package/src/server/__tests__/grants-admin-route.test.ts +0 -171
- package/src/server/__tests__/grants-route.test.ts +0 -394
- package/src/server/__tests__/strands-route.test.ts +0 -234
- package/src/server/routes/bootstrap-node-validation.ts +0 -206
- package/src/server/routes/grants-admin.ts +0 -74
- package/src/server/routes/grants.ts +0 -193
- package/src/server/routes/provision-request-validation.ts +0 -105
- package/src/server/routes/strands.ts +0 -54
- package/src/server/routes/trust-circle.ts +0 -49
- package/src/strands/__tests__/strand-service.test.ts +0 -165
- package/src/strands/index.ts +0 -14
- package/src/strands/strand-service.ts +0 -135
- package/src/strands/types.ts +0 -102
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable output of the `cadre-host join` and `cadre-host node` commands:
|
|
3
|
+
* the claim payload as a terminal QR code and as text, the warning when the node
|
|
4
|
+
* is not reachable from outside, the claimed line, the outcome of a join by
|
|
5
|
+
* invitation, and the node list.
|
|
6
|
+
*
|
|
7
|
+
* The types are loose mirrors of `ClaimDetails` and `HostedNodeView`
|
|
8
|
+
* (`../hosted/`): the body arrives over HTTP from whichever cadre-host version is
|
|
9
|
+
* running, so every field is optional and a missing one prints as `?`.
|
|
10
|
+
*
|
|
11
|
+
* Streams: the payload text goes to stdout ALONE, so `cadre-host join --no-qr`
|
|
12
|
+
* can be piped or copied; everything else (the QR, the instructions, the
|
|
13
|
+
* warning, the claimed line) goes to stderr. The payload carries the claim
|
|
14
|
+
* secret by design; nothing else printed here contains it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
|
|
19
|
+
export interface ClaimDetailsLike {
|
|
20
|
+
payload?: string;
|
|
21
|
+
peerId?: string;
|
|
22
|
+
multiaddrs?: string[];
|
|
23
|
+
reachability?: { verdict?: string; reason?: string | null } | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface HostedNodeLike {
|
|
27
|
+
id?: string;
|
|
28
|
+
join?: { kind?: string };
|
|
29
|
+
status?: string;
|
|
30
|
+
partyId?: string;
|
|
31
|
+
ownerKey?: string;
|
|
32
|
+
memberPeerId?: string;
|
|
33
|
+
connected?: boolean;
|
|
34
|
+
error?: string;
|
|
35
|
+
retryable?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const requireForQr = createRequire(import.meta.url);
|
|
39
|
+
|
|
40
|
+
/** Print the QR code (unless `qr` is false) and the instructions on stderr, then the payload text on stdout. */
|
|
41
|
+
export function printClaimPayload(details: ClaimDetailsLike, opts: { qr: boolean }): void {
|
|
42
|
+
console.error('Scan this with the Sereus app on the phone that owns the cadre, or paste the text into it.');
|
|
43
|
+
if (opts.qr && details.payload) renderQr(details.payload);
|
|
44
|
+
console.error(`Node ${details.peerId ?? '?'} at:`);
|
|
45
|
+
for (const addr of details.multiaddrs ?? []) console.error(` ${addr}`);
|
|
46
|
+
printReachabilityWarning(details);
|
|
47
|
+
console.log(details.payload ?? '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* One line on stderr when the NAT layer says the node cannot be reached from
|
|
52
|
+
* outside: the claim still works from the home network over the LAN address.
|
|
53
|
+
*/
|
|
54
|
+
export function printReachabilityWarning(details: ClaimDetailsLike): void {
|
|
55
|
+
const reachability = details.reachability;
|
|
56
|
+
if (!reachability || reachability.verdict !== 'unreachable') return;
|
|
57
|
+
console.error(
|
|
58
|
+
'Warning: this node cannot be reached from outside your home network yet'
|
|
59
|
+
+ `${reachability.reason ? ` — ${reachability.reason}` : ''}. `
|
|
60
|
+
+ 'A phone on your home network can still claim it; run `cadre-host nat status` for the ports to forward.',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The line printed once the watcher reports the claim. */
|
|
65
|
+
export function printClaimed(node: HostedNodeLike): void {
|
|
66
|
+
console.error(`✓ Claimed by owner ${ownerFingerprint(node.ownerKey)} into cadre ${node.partyId ?? '?'}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* How a join by invitation ended: the member that admitted the node, or the failure. A
|
|
71
|
+
* retryable failure means no member could be reached, which is the usual case for a cadre's
|
|
72
|
+
* first always-on node: its only members are phones, which nothing can dial.
|
|
73
|
+
*/
|
|
74
|
+
export function printInvitationOutcome(node: HostedNodeLike): void {
|
|
75
|
+
if (node.status === 'joined') {
|
|
76
|
+
console.error(`✓ Joined cadre ${node.partyId ?? '?'} at member ${node.memberPeerId ?? '(unnamed)'}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
console.error(`✗ Could not join: ${node.error ?? 'unknown error'}`);
|
|
80
|
+
if (!node.retryable) return;
|
|
81
|
+
console.error(
|
|
82
|
+
'If this is the cadre\'s first always-on node, no member can be reached yet: use `cadre-host join` without '
|
|
83
|
+
+ '`--invitation` and scan the QR code from the phone instead. Otherwise, once a member is online, run '
|
|
84
|
+
+ `\`cadre-host node retry ${node.id ?? '<id>'}\`.`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** One line per hosted node: id, status, party, owner fingerprint, connected. */
|
|
89
|
+
export function printNodeList(nodes: HostedNodeLike[]): void {
|
|
90
|
+
if (nodes.length === 0) {
|
|
91
|
+
console.log('Hosted nodes: none. Run `cadre-host join` to start one.');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
console.log('Hosted nodes:');
|
|
95
|
+
for (const node of nodes) {
|
|
96
|
+
// An invitation node names its cadre from the start; a claim node only once claimed.
|
|
97
|
+
const party = node.status === 'joined' || node.join?.kind === 'invitation' ? `cadre ${node.partyId ?? '?'}` : (node.status ?? '?');
|
|
98
|
+
const owner = node.ownerKey ? ` owner ${ownerFingerprint(node.ownerKey)}` : '';
|
|
99
|
+
const connected = node.status === 'joined' ? ` ${node.connected ? 'connected' : 'not connected'}` : '';
|
|
100
|
+
const error = node.error ? ` (${node.error})` : '';
|
|
101
|
+
console.log(` ${node.id ?? '?'} ${node.status ?? '?'} ${party}${owner}${connected}${error}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The first 8 characters of an owner key, as the UI shows it. */
|
|
106
|
+
function ownerFingerprint(ownerKey: string | undefined): string {
|
|
107
|
+
return ownerKey ? ownerKey.slice(0, 8) : '?';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Best-effort QR render on stderr — fall back to the bare text if the lib chokes. */
|
|
111
|
+
function renderQr(text: string): void {
|
|
112
|
+
try {
|
|
113
|
+
const qr = requireForQr('qrcode-terminal') as {
|
|
114
|
+
generate: (text: string, opts?: { small?: boolean }, cb?: (s: string) => void) => void;
|
|
115
|
+
};
|
|
116
|
+
qr.generate(text, { small: true }, (rendered) => { console.error(rendered); });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error(`(qrcode-terminal unavailable: ${(err as Error).message})`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable output of the `cadre-host nat` commands: the host-level NAT
|
|
3
|
+
* state, then one block per hosted node, with the forward to make on the
|
|
4
|
+
* router when a node cannot be reached from outside.
|
|
5
|
+
*
|
|
6
|
+
* The types are loose mirrors of `NatStatusSnapshot` (`../nat/types.ts`): the
|
|
7
|
+
* body arrives over HTTP from whichever cadre-host version is running, so every
|
|
8
|
+
* field is optional and a missing one prints as `?`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface NatPortRouteLike {
|
|
12
|
+
internalPort?: number;
|
|
13
|
+
externalPort?: number | null;
|
|
14
|
+
source?: string | null;
|
|
15
|
+
error?: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface NatNodeLike {
|
|
19
|
+
nodeId?: string;
|
|
20
|
+
running?: boolean;
|
|
21
|
+
verdict?: string;
|
|
22
|
+
reason?: string | null;
|
|
23
|
+
tcp?: NatPortRouteLike;
|
|
24
|
+
ws?: NatPortRouteLike | null;
|
|
25
|
+
publicAddrs?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface NatStatusLike {
|
|
29
|
+
network?: { setting?: string; mode?: string; publicInterfaceIp?: string | null };
|
|
30
|
+
upnpEnabled?: boolean;
|
|
31
|
+
gateway?: {
|
|
32
|
+
found?: boolean;
|
|
33
|
+
lanAddress?: string | null;
|
|
34
|
+
routerExternalIp?: string | null;
|
|
35
|
+
lastError?: string | null;
|
|
36
|
+
};
|
|
37
|
+
externalIp?: string | null;
|
|
38
|
+
cgnatDetected?: boolean;
|
|
39
|
+
directReachability?: string;
|
|
40
|
+
lastTestedAt?: string | null;
|
|
41
|
+
nodes?: NatNodeLike[];
|
|
42
|
+
ddns?: {
|
|
43
|
+
providerId?: string | null;
|
|
44
|
+
hostname?: string | null;
|
|
45
|
+
externallyManaged?: boolean;
|
|
46
|
+
lastUpdateAt?: string | null;
|
|
47
|
+
lastUpdateOk?: boolean | null;
|
|
48
|
+
lastError?: string | null;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type PortKind = 'tcp' | 'ws';
|
|
53
|
+
|
|
54
|
+
interface PortEntry {
|
|
55
|
+
kind: PortKind;
|
|
56
|
+
route: NatPortRouteLike;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const KIND_LABEL: Record<PortKind, string> = { tcp: 'TCP', ws: 'WebSocket' };
|
|
60
|
+
|
|
61
|
+
/** Width of the label column inside a node block. */
|
|
62
|
+
const FIELD_WIDTH = 11;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A forward changes the node's public addresses only when it changes the
|
|
66
|
+
* external port or makes a port routable, and the restart that follows is
|
|
67
|
+
* rate-limited per node (`NAT_ADDRESS_RESTART_MIN_INTERVAL_MS`).
|
|
68
|
+
*/
|
|
69
|
+
const FORWARD_RESTART_NOTE =
|
|
70
|
+
'If its public addresses changed, the node restarts to announce them; a node already restarted ' +
|
|
71
|
+
'for an address change in the last 10 minutes waits until those 10 minutes are up.';
|
|
72
|
+
|
|
73
|
+
export function printNatStatus(s: NatStatusLike): void {
|
|
74
|
+
printHostLines(s);
|
|
75
|
+
printDdns(s);
|
|
76
|
+
const nodes = s.nodes ?? [];
|
|
77
|
+
console.log(nodes.length === 0 ? '\nNodes: none' : '\nNodes:');
|
|
78
|
+
for (const n of nodes) printNodeReachability(n, s);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The block for the node a forward was saved for, then what happens next. */
|
|
82
|
+
export function printForwardResult(nodeId: string, s: NatStatusLike): void {
|
|
83
|
+
const node = s.nodes?.find((n) => n.nodeId === nodeId);
|
|
84
|
+
if (node) printNodeReachability(node, s);
|
|
85
|
+
console.log(`\n${FORWARD_RESTART_NOTE}`);
|
|
86
|
+
}
|
|
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})`);
|
|
98
|
+
const gateway = s.gateway ?? {};
|
|
99
|
+
const gatewayLine = gateway.found
|
|
100
|
+
? `found (this machine is ${gateway.lanAddress ?? '?'} on its network)`
|
|
101
|
+
: `not found${gateway.lastError ? ` — ${gateway.lastError}` : ''}`;
|
|
102
|
+
console.log(`UPnP: ${s.upnpEnabled === false ? 'off' : 'on'}; router ${gatewayLine}`);
|
|
103
|
+
if (gateway.routerExternalIp) {
|
|
104
|
+
console.log(`Router IP: ${gateway.routerExternalIp}`);
|
|
105
|
+
}
|
|
106
|
+
console.log(`External IP: ${s.externalIp ?? '(unknown)'}${s.cgnatDetected ? ' [CGNAT detected]' : ''}`);
|
|
107
|
+
console.log(`Reachability: ${s.directReachability ?? 'unknown'}${s.lastTestedAt ? ` (tested ${s.lastTestedAt})` : ''}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function printDdns(s: NatStatusLike): void {
|
|
111
|
+
const ddns = s.ddns ?? {};
|
|
112
|
+
if (!ddns.providerId && !ddns.hostname) {
|
|
113
|
+
console.log('DDNS: not configured');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.log(`DDNS: ${ddns.hostname ?? '(no hostname)'} via ${ddns.providerId ?? '(no provider)'}${ddns.externallyManaged ? ' [externally managed]' : ''}`);
|
|
117
|
+
if (ddns.lastUpdateAt) {
|
|
118
|
+
const ok = ddns.lastUpdateOk ? 'OK' : 'FAIL';
|
|
119
|
+
console.log(`DDNS update: ${ok} at ${ddns.lastUpdateAt}${ddns.lastError ? ` — ${ddns.lastError}` : ''}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function printNodeReachability(n: NatNodeLike, s: NatStatusLike): void {
|
|
124
|
+
console.log(` ${n.nodeId ?? '?'}: ${n.verdict ?? '?'}${n.running === false ? ' [stopped]' : ''}`);
|
|
125
|
+
printField('TCP', [formatRoute(n.tcp)]);
|
|
126
|
+
printField('WebSocket', [n.ws ? formatRoute(n.ws) : 'not available']);
|
|
127
|
+
printField('Public', n.publicAddrs?.length ? n.publicAddrs : ['none']);
|
|
128
|
+
if (n.reason) printField(n.verdict === 'direct' ? 'Firewall' : 'Why', [n.reason]);
|
|
129
|
+
const advice = forwardAdvice(n, s);
|
|
130
|
+
if (advice.length > 0) printField('To fix', advice);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function printField(label: string, values: string[]): void {
|
|
134
|
+
values.forEach((value, i) => console.log(` ${(i === 0 ? label : '').padEnd(FIELD_WIDTH)}${value}`));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatRoute(r: NatPortRouteLike | undefined): string {
|
|
138
|
+
if (!r) return '?';
|
|
139
|
+
const internal = `internal ${r.internalPort ?? '?'}`;
|
|
140
|
+
if (r.externalPort == null) return `${internal} → not mapped${r.error ? ` (${r.error})` : ''}`;
|
|
141
|
+
const failed = r.error ? `; last attempt failed: ${r.error}` : '';
|
|
142
|
+
if (r.source === 'direct') return `${internal} → public, as-is${failed}`;
|
|
143
|
+
return `${internal} → external ${r.externalPort} (${r.source === 'manual' ? 'forwarded by hand' : 'UPnP'}${failed})`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* What to do for an unreachable node, one line per array entry. Behind CGNAT a
|
|
148
|
+
* router forward does not help, but the detection can misfire, so the forward
|
|
149
|
+
* command is still offered for every port. With every port routed there is
|
|
150
|
+
* nothing to forward: what is missing is the host's public address, which the
|
|
151
|
+
* reason already explains.
|
|
152
|
+
*
|
|
153
|
+
* NOTE: the UI builds the same sentence (`forwardInstruction` in
|
|
154
|
+
* `ui/src/lib/reachability.ts`); if the rule for which ports to forward
|
|
155
|
+
* changes, have the server send the port list in `NodeReachability` instead
|
|
156
|
+
* of changing both copies.
|
|
157
|
+
*/
|
|
158
|
+
function forwardAdvice(n: NatNodeLike, s: NatStatusLike): string[] {
|
|
159
|
+
if (n.verdict !== 'unreachable') return [];
|
|
160
|
+
const nodeId = n.nodeId ?? '<node-id>';
|
|
161
|
+
const ports = portsOf(n);
|
|
162
|
+
if (s.cgnatDetected) {
|
|
163
|
+
return [
|
|
164
|
+
'Carrier-grade NAT detected: a forward on your router will not help, and cadre-host cannot reserve a relay for its nodes yet.',
|
|
165
|
+
'If the detection is wrong and you did forward ports, tell cadre-host with:',
|
|
166
|
+
` ${forwardCommand(nodeId, ports)}`,
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
const unrouted = ports.filter(({ route }) => route.externalPort == null);
|
|
170
|
+
if (unrouted.length === 0) return [];
|
|
171
|
+
const lan = s.gateway?.lanAddress ?? null;
|
|
172
|
+
const forwards = unrouted.map(({ kind, route }) => {
|
|
173
|
+
const port = route.internalPort ?? '?';
|
|
174
|
+
return `${KIND_LABEL[kind]} port ${port} to ${lan ? `${lan}:${port}` : `port ${port} on this machine's LAN address`}`;
|
|
175
|
+
});
|
|
176
|
+
return [`On your router, forward ${forwards.join(' and ')}, then run:`, ` ${forwardCommand(nodeId, unrouted)}`];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function portsOf(n: NatNodeLike): PortEntry[] {
|
|
180
|
+
const out: PortEntry[] = [];
|
|
181
|
+
if (n.tcp) out.push({ kind: 'tcp', route: n.tcp });
|
|
182
|
+
if (n.ws) out.push({ kind: 'ws', route: n.ws });
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function forwardCommand(nodeId: string, ports: PortEntry[]): string {
|
|
187
|
+
return ['cadre-host nat forward', nodeId, ...ports.map(({ kind }) => `--${kind} <external port>`)].join(' ');
|
|
188
|
+
}
|
|
@@ -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
|
+
}
|