@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
package/src/nat/reachability.ts
CHANGED
|
@@ -1,34 +1,155 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Reachability verdicts: one per hosted node, and the host-level roll-up the
|
|
3
|
+
* dashboard badge shows. Heuristics over the mapping table, not a dial-back —
|
|
4
|
+
* libp2p AutoNAT is a later ticket.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
DirectReachability,
|
|
9
|
+
NodeReachability,
|
|
10
|
+
NodeVerdict,
|
|
11
|
+
PortKind,
|
|
12
|
+
PortRoute,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
|
|
15
|
+
export interface NodeVerdictInput {
|
|
16
|
+
tcp: PortRoute;
|
|
17
|
+
/** Null for a handle persisted by an older build without a WebSocket port. */
|
|
18
|
+
ws: PortRoute | null;
|
|
19
|
+
/** Whether a public host part exists: a DDNS hostname, or a public IPv4. */
|
|
20
|
+
hostKnown: boolean;
|
|
21
|
+
cgnatDetected: boolean;
|
|
22
|
+
upnpEnabled: boolean;
|
|
23
|
+
gatewayFound: boolean;
|
|
24
|
+
/** The address a user forwards to; null when no gateway was found. */
|
|
25
|
+
lanAddress: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface NodeVerdictResult {
|
|
29
|
+
verdict: NodeVerdict;
|
|
30
|
+
reason: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `unreachable` when either port has no usable route or the host part is
|
|
35
|
+
* unknown; otherwise `manual` when either port is forwarded by hand, `direct`
|
|
36
|
+
* when the ports are reached as-is (public mode, with the firewall reminder as
|
|
37
|
+
* the reason), else `mapped`. A UPnP route under CGNAT is not usable: the
|
|
38
|
+
* router's mapping is on a carrier-private address.
|
|
39
|
+
*/
|
|
40
|
+
export function evaluateNodeReachability(input: NodeVerdictInput): NodeVerdictResult {
|
|
41
|
+
const ports = portRoutes(input);
|
|
42
|
+
const failing = ports.filter(({ route }) => !usable(route, input.cgnatDetected));
|
|
43
|
+
if (!input.hostKnown) {
|
|
44
|
+
return { verdict: 'unreachable', reason: HOST_UNKNOWN_REASON };
|
|
45
|
+
}
|
|
46
|
+
if (failing.length > 0) {
|
|
47
|
+
return { verdict: 'unreachable', reason: describeFailures(failing, input) };
|
|
48
|
+
}
|
|
49
|
+
if (ports.some(({ route }) => route.source === 'manual')) return { verdict: 'manual', reason: null };
|
|
50
|
+
if (ports.some(({ route }) => route.source === 'direct')) return { verdict: 'direct', reason: firewallReminder(ports) };
|
|
51
|
+
return { verdict: 'mapped', reason: null };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Host roll-up: `cgnat` when CGNAT is detected and no node is reachable
|
|
56
|
+
* through a manual route; `unknown` with no running nodes; `reachable` when
|
|
57
|
+
* every running node is `mapped`/`manual`/`direct`; else `unreachable`.
|
|
58
|
+
*/
|
|
59
|
+
export function evaluateHostReachability(
|
|
60
|
+
nodes: ReadonlyArray<Pick<NodeReachability, 'running' | 'verdict'>>,
|
|
61
|
+
cgnatDetected: boolean,
|
|
62
|
+
): DirectReachability {
|
|
63
|
+
const running = nodes.filter((n) => n.running);
|
|
64
|
+
if (cgnatDetected && !running.some((n) => n.verdict === 'manual')) return 'cgnat';
|
|
65
|
+
if (running.length === 0) return 'unknown';
|
|
66
|
+
return running.every((n) => n.verdict !== 'unreachable') ? 'reachable' : 'unreachable';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const HOST_UNKNOWN_REASON =
|
|
70
|
+
"This host's public address is unknown: no dynamic DNS hostname is set and no public IPv4 address was detected. " +
|
|
71
|
+
'Set a DDNS hostname, or check that this machine can reach the internet.';
|
|
72
|
+
|
|
73
|
+
interface PortEntry {
|
|
74
|
+
kind: PortKind;
|
|
75
|
+
route: PortRoute;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function portRoutes(input: NodeVerdictInput): PortEntry[] {
|
|
79
|
+
const out: PortEntry[] = [{ kind: 'tcp', route: input.tcp }];
|
|
80
|
+
if (input.ws) out.push({ kind: 'ws', route: input.ws });
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function usable(route: PortRoute, cgnatDetected: boolean): boolean {
|
|
85
|
+
if (route.externalPort === null || route.source === null) return false;
|
|
86
|
+
return !(cgnatDetected && route.source === 'upnp');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type FailureCause = 'cgnat' | 'refused' | 'upnp_off' | 'no_gateway' | 'pending';
|
|
90
|
+
|
|
91
|
+
/** Behind CGNAT no router-side remedy helps, so CGNAT outranks whatever else failed the port. */
|
|
92
|
+
function causeOf(route: PortRoute, input: NodeVerdictInput): FailureCause {
|
|
93
|
+
if (input.cgnatDetected) return 'cgnat';
|
|
94
|
+
if (route.error) return 'refused';
|
|
95
|
+
if (!input.upnpEnabled) return 'upnp_off';
|
|
96
|
+
if (!input.gatewayFound) return 'no_gateway';
|
|
97
|
+
return 'pending';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** One sentence per distinct cause, each naming every port that failed for it. */
|
|
101
|
+
function describeFailures(failing: PortEntry[], input: NodeVerdictInput): string {
|
|
102
|
+
const byCause = new Map<FailureCause, PortEntry[]>();
|
|
103
|
+
for (const entry of failing) {
|
|
104
|
+
const cause = causeOf(entry.route, input);
|
|
105
|
+
byCause.set(cause, [...(byCause.get(cause) ?? []), entry]);
|
|
106
|
+
}
|
|
107
|
+
return [...byCause.entries()].map(([cause, entries]) => describeCause(cause, entries, input)).join(' ');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function describeCause(cause: FailureCause, entries: PortEntry[], input: NodeVerdictInput): string {
|
|
111
|
+
const names = entries.map(({ kind, route }) => `${kindLabel(kind)} port ${route.internalPort}`);
|
|
112
|
+
const lan = input.lanAddress ?? "this machine's LAN address";
|
|
113
|
+
const forwardHint = `Forward ${entries.length === 1 ? 'port' : 'ports'} ${listPorts(entries)} to ${lan} on your router, then tell cadre-host the external ${entries.length === 1 ? 'port' : 'ports'}.`;
|
|
114
|
+
switch (cause) {
|
|
115
|
+
case 'cgnat':
|
|
116
|
+
return `This host is behind carrier-grade NAT, so the ${joinNames(names)} cannot be reached from outside through your router, and neither UPnP nor a port forward on your router will help; a relay is needed.`;
|
|
117
|
+
case 'refused': {
|
|
118
|
+
const errors = entries.map(({ route }) => route.error).filter((e): e is string => !!e);
|
|
119
|
+
return `Router refused the ${joinNames(names)} mapping (${errors.join('; ')}). ${forwardHint}`;
|
|
120
|
+
}
|
|
121
|
+
case 'upnp_off':
|
|
122
|
+
return `UPnP is off, so the ${joinNames(names)} ${entries.length === 1 ? 'is' : 'are'} not mapped. Turn UPnP on, or: ${lowerFirst(forwardHint)}`;
|
|
123
|
+
case 'no_gateway':
|
|
124
|
+
return `No UPnP router answered, so the ${joinNames(names)} ${entries.length === 1 ? 'is' : 'are'} not mapped. Enable UPnP on your router, or: ${lowerFirst(forwardHint)}`;
|
|
125
|
+
case 'pending':
|
|
126
|
+
return `The ${joinNames(names)} ${entries.length === 1 ? 'is' : 'are'} not mapped yet; the router is being asked.`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Public mode: nothing to map, but a host or cloud firewall can still block the ports. */
|
|
131
|
+
function firewallReminder(ports: PortEntry[]): string {
|
|
132
|
+
const numbers = ports.map(({ route }) => route.internalPort);
|
|
133
|
+
const ufw = numbers.length === 2 && Math.abs(numbers[0]! - numbers[1]!) === 1
|
|
134
|
+
? `${Math.min(...numbers)}:${Math.max(...numbers)}/tcp`
|
|
135
|
+
: `${numbers.join(',')}/tcp`;
|
|
136
|
+
return `Reached directly at this machine's public address. Allow TCP ${ports.length === 1 ? 'port' : 'ports'} ${listPorts(ports)} through any firewall `
|
|
137
|
+
+ `(for example: sudo ufw allow ${ufw}; and any cloud-provider firewall).`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function kindLabel(kind: PortKind): string {
|
|
141
|
+
return kind === 'tcp' ? 'TCP' : 'WebSocket';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function listPorts(entries: PortEntry[]): string {
|
|
145
|
+
return joinNames(entries.map(({ route }) => String(route.internalPort)));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function joinNames(names: string[]): string {
|
|
149
|
+
if (names.length <= 1) return names[0] ?? '';
|
|
150
|
+
return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function lowerFirst(s: string): string {
|
|
154
|
+
return s.charAt(0).toLowerCase() + s.slice(1);
|
|
155
|
+
}
|
package/src/nat/types.ts
CHANGED
|
@@ -1,52 +1,113 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* NAT/DDNS types for cadre-host.
|
|
3
3
|
*
|
|
4
|
-
* cadre-host runs on home/SMB machines that are typically behind NAT.
|
|
5
|
-
* dialable from the open internet the host must:
|
|
6
|
-
* 1.
|
|
4
|
+
* cadre-host runs on home/SMB machines that are typically behind NAT. For the
|
|
5
|
+
* nodes it runs to be dialable from the open internet the host must:
|
|
6
|
+
* 1. Map every hosted node's two libp2p ports (TCP, and the WebSocket port a
|
|
7
|
+
* phone dials) through the upstream router, or let the user forward them
|
|
8
|
+
* by hand.
|
|
7
9
|
* 2. Detect its external IP (and whether it's behind CGNAT).
|
|
8
10
|
* 3. Optionally publish a stable hostname via dynamic DNS.
|
|
9
11
|
*
|
|
10
|
-
* State for these
|
|
11
|
-
*
|
|
12
|
+
* State for these concerns lives in `<rootDir>/nat.json` (user settings) and is
|
|
13
|
+
* exposed as a single `NatStatusSnapshot` to the management UI, with one
|
|
14
|
+
* `NodeReachability` entry per hosted node.
|
|
12
15
|
*
|
|
13
16
|
* Credentials for DDNS providers are NOT stored in `nat.json`. They go through
|
|
14
17
|
* the SecretsStore (keytar, with a 0600 file fallback).
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
|
-
/**
|
|
18
|
-
export type PortForwardMode =
|
|
19
|
-
| 'auto-upnp'
|
|
20
|
-
| 'auto-natpmp'
|
|
21
|
-
| 'manual'
|
|
22
|
-
| 'failed'
|
|
23
|
-
| 'disabled';
|
|
24
|
-
|
|
25
|
-
/** Reachability verdict — best-effort heuristic until AutoNAT lands. */
|
|
20
|
+
/** Reachability roll-up across the host's running nodes — see `reachability.ts`. */
|
|
26
21
|
export type DirectReachability = 'reachable' | 'unreachable' | 'unknown' | 'cgnat';
|
|
27
22
|
|
|
28
|
-
/**
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
/** The two ports of a hosted node that are mapped: libp2p TCP and libp2p WebSocket. */
|
|
24
|
+
export type PortKind = 'tcp' | 'ws';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How this machine meets the internet. `lan`: behind a router (home or office), so node ports need
|
|
28
|
+
* UPnP or a forward on the router. `public`: an interface holds a public IPv4 (a VPS, a server with a
|
|
29
|
+
* routed address), so node ports are reachable at their own numbers unless a firewall blocks them.
|
|
30
|
+
*/
|
|
31
|
+
export type NetworkMode = 'lan' | 'public';
|
|
32
|
+
|
|
33
|
+
/** The `network` setting: `auto` picks the mode from this machine's interfaces. */
|
|
34
|
+
export type NetworkSetting = 'auto' | NetworkMode;
|
|
35
|
+
|
|
36
|
+
/** The network section of the status snapshot. */
|
|
37
|
+
export interface NatNetworkStatus {
|
|
38
|
+
setting: NetworkSetting;
|
|
39
|
+
/** The mode in effect: the setting, or what `auto` found. */
|
|
40
|
+
mode: NetworkMode;
|
|
41
|
+
/** The public IPv4 an interface holds, when one does; what `auto` decides on. */
|
|
42
|
+
publicInterfaceIp: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Where a port's external route came from. `direct`: public mode, the port as-is. */
|
|
46
|
+
export type PortRouteSource = 'upnp' | 'manual' | 'direct';
|
|
47
|
+
|
|
48
|
+
/** How one of a node's ports is reached from outside the home network. */
|
|
49
|
+
export interface PortRoute {
|
|
33
50
|
internalPort: number;
|
|
34
|
-
/**
|
|
51
|
+
/** Port reachable from outside; null when there is no route. */
|
|
52
|
+
externalPort: number | null;
|
|
53
|
+
source: PortRouteSource | null;
|
|
54
|
+
/** ISO timestamp when the router lease expires; UPnP routes only. */
|
|
55
|
+
leaseExpiresAt: string | null;
|
|
56
|
+
/** Last mapping failure, in plain language; null when the last attempt succeeded. */
|
|
57
|
+
error: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Per-node verdict. `direct`: reachable at this machine's public address, firewall permitting. */
|
|
61
|
+
export type NodeVerdict = 'mapped' | 'manual' | 'direct' | 'unreachable';
|
|
62
|
+
|
|
63
|
+
/** Reachability of one hosted node, as reported in the status snapshot. */
|
|
64
|
+
export interface NodeReachability {
|
|
65
|
+
nodeId: string;
|
|
66
|
+
/** Whether the node's process is running; a stopped node keeps its last routes until the unmap grace elapses. */
|
|
67
|
+
running: boolean;
|
|
68
|
+
verdict: NodeVerdict;
|
|
69
|
+
/**
|
|
70
|
+
* Plain-language reason and remedy when unreachable, e.g. "Router refused
|
|
71
|
+
* the WebSocket port mapping. Forward port 10004 to 192.168.1.20 on your
|
|
72
|
+
* router, then tell cadre-host the external port." For `direct`, the
|
|
73
|
+
* firewall reminder: which ports to allow.
|
|
74
|
+
*/
|
|
75
|
+
reason: string | null;
|
|
76
|
+
tcp: PortRoute;
|
|
77
|
+
/** Null for a handle persisted by an older build without a WebSocket port. */
|
|
78
|
+
ws: PortRoute | null;
|
|
79
|
+
/** Multiaddrs (no `/p2p/` suffix) through which the node is reachable from outside. */
|
|
80
|
+
publicAddrs: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The UPnP gateway as the host last saw it. */
|
|
84
|
+
export interface NatGatewayStatus {
|
|
85
|
+
found: boolean;
|
|
86
|
+
/** This machine's address on the router's subnet — the address a user forwards to. */
|
|
87
|
+
lanAddress: string | null;
|
|
88
|
+
/** The WAN address the router reports for itself. */
|
|
35
89
|
routerExternalIp: string | null;
|
|
36
|
-
/**
|
|
37
|
-
|
|
90
|
+
/** Why discovery or the last gateway operation failed; null when it did not. */
|
|
91
|
+
lastError: string | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Snapshot returned to the management UI / CLI. */
|
|
95
|
+
export interface NatStatusSnapshot {
|
|
96
|
+
network: NatNetworkStatus;
|
|
97
|
+
upnpEnabled: boolean;
|
|
98
|
+
gateway: NatGatewayStatus;
|
|
38
99
|
|
|
39
|
-
// External IP (from public probe
|
|
100
|
+
// External IP (from the public probe, falling back to the router's report).
|
|
40
101
|
externalIp: string | null;
|
|
41
102
|
externalIpDetectedAt: string | null;
|
|
42
103
|
cgnatDetected: boolean;
|
|
43
104
|
|
|
44
|
-
|
|
105
|
+
/** Roll-up across running nodes; see `reachability.ts`. */
|
|
45
106
|
directReachability: DirectReachability;
|
|
46
107
|
lastTestedAt: string | null;
|
|
47
108
|
|
|
48
|
-
// DDNS
|
|
49
109
|
ddns: NatDdnsStatus;
|
|
110
|
+
nodes: NodeReachability[];
|
|
50
111
|
}
|
|
51
112
|
|
|
52
113
|
/** DDNS sub-section of the status snapshot. */
|
|
@@ -65,18 +126,30 @@ export interface NatDdnsStatus {
|
|
|
65
126
|
lastError: string | null;
|
|
66
127
|
}
|
|
67
128
|
|
|
129
|
+
/** The external ports a user forwarded on their router for one node, per port kind. */
|
|
130
|
+
export interface ManualForward {
|
|
131
|
+
tcp?: number;
|
|
132
|
+
ws?: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Patch applied by `PUT /nat/nodes/:nodeId/forward`: `null` clears that port. */
|
|
136
|
+
export interface ManualForwardPatch {
|
|
137
|
+
tcp?: number | null;
|
|
138
|
+
ws?: number | null;
|
|
139
|
+
}
|
|
140
|
+
|
|
68
141
|
/**
|
|
69
142
|
* On-disk shape of `nat.json`. NEVER contains DDNS credentials — those live
|
|
70
143
|
* in the SecretsStore.
|
|
71
144
|
*/
|
|
72
145
|
export interface NatSettingsFile {
|
|
73
146
|
version: 1;
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
internalPort: number;
|
|
78
|
-
/** Whether to attempt UPnP/NAT-PMP at all. */
|
|
147
|
+
/** LAN or public-IP mode; `auto` (default) decides from the interfaces. Absent in older files. */
|
|
148
|
+
network: NetworkSetting;
|
|
149
|
+
/** Whether to ask the router for port mappings at all. Ignored in public mode. */
|
|
79
150
|
upnpEnabled: boolean;
|
|
151
|
+
/** Per node id: the external port the user forwarded on their router, per port kind. */
|
|
152
|
+
forwards: Record<string, ManualForward>;
|
|
80
153
|
ddns: NatDdnsSettings;
|
|
81
154
|
}
|
|
82
155
|
|
|
@@ -107,8 +180,8 @@ export type NatErrorCode =
|
|
|
107
180
|
| 'secrets_unavailable'
|
|
108
181
|
| 'storage_error'
|
|
109
182
|
| 'invalid_config'
|
|
110
|
-
/**
|
|
111
|
-
| '
|
|
183
|
+
/** A manual forward named a node id the host does not run. */
|
|
184
|
+
| 'unknown_node';
|
|
112
185
|
|
|
113
186
|
/** Typed error carrying a stable `code` for HTTP mapping in the local-ui. */
|
|
114
187
|
export class NatError extends Error {
|
|
@@ -139,11 +212,10 @@ export interface DdnsProviderInfo {
|
|
|
139
212
|
}
|
|
140
213
|
|
|
141
214
|
/**
|
|
142
|
-
* Typed HTTP handlers exposed to
|
|
215
|
+
* Typed HTTP handlers exposed to the local UI server for Fastify wiring.
|
|
143
216
|
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* status code.
|
|
217
|
+
* These take typed objects and either return typed results or throw a
|
|
218
|
+
* `NatError` whose `.code` the error handler maps to an HTTP status code.
|
|
147
219
|
*/
|
|
148
220
|
export interface NatHandlers {
|
|
149
221
|
getStatus(): Promise<NatStatusSnapshot>;
|
|
@@ -155,5 +227,6 @@ export interface NatHandlers {
|
|
|
155
227
|
config: Record<string, string>;
|
|
156
228
|
externallyManaged?: boolean;
|
|
157
229
|
}): Promise<NatStatusSnapshot>;
|
|
158
|
-
putSettings(body: Partial<Omit<NatSettingsFile, 'version'>>): Promise<NatStatusSnapshot>;
|
|
230
|
+
putSettings(body: Partial<Omit<NatSettingsFile, 'version' | 'forwards'>>): Promise<NatStatusSnapshot>;
|
|
231
|
+
putForward(nodeId: string, patch: ManualForwardPatch): Promise<NatStatusSnapshot>;
|
|
159
232
|
}
|
package/src/{installer/__tests__/identity.test.ts → orchestrator/__tests__/identity-file.test.ts}
RENAMED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync, statSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
|
|
6
|
-
import { generateIdentity, loadIdentity } from '../identity.js';
|
|
7
|
-
|
|
8
|
-
describe('
|
|
9
|
-
let tmp: string;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
tmp = mkdtempSync(join(tmpdir(), 'cadre-host-id-'));
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('round-trips an Ed25519 keypair through the file', async () => {
|
|
20
|
-
const path = join(tmp, 'identity.key');
|
|
21
|
-
const generated = await generateIdentity(path);
|
|
22
|
-
expect(generated.peerId).toMatch(/^12D3Koo/);
|
|
23
|
-
|
|
24
|
-
const loaded = loadIdentity(path);
|
|
25
|
-
expect(loaded.peerId).toBe(generated.peerId);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it.runIf(process.platform !== 'win32')('persists with mode 0600 on POSIX', async () => {
|
|
29
|
-
const path = join(tmp, 'identity.key');
|
|
30
|
-
await generateIdentity(path);
|
|
31
|
-
const mode = statSync(path).mode & 0o777;
|
|
32
|
-
expect(mode).toBe(0o600);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync, statSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { generateIdentity, loadIdentity } from '../identity-file.js';
|
|
7
|
+
|
|
8
|
+
describe('identity file', () => {
|
|
9
|
+
let tmp: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tmp = mkdtempSync(join(tmpdir(), 'cadre-host-id-'));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('round-trips an Ed25519 keypair through the file', async () => {
|
|
20
|
+
const path = join(tmp, 'identity.key');
|
|
21
|
+
const generated = await generateIdentity(path);
|
|
22
|
+
expect(generated.peerId).toMatch(/^12D3Koo/);
|
|
23
|
+
|
|
24
|
+
const loaded = loadIdentity(path);
|
|
25
|
+
expect(loaded.peerId).toBe(generated.peerId);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it.runIf(process.platform !== 'win32')('persists with mode 0600 on POSIX', async () => {
|
|
29
|
+
const path = join(tmp, 'identity.key');
|
|
30
|
+
await generateIdentity(path);
|
|
31
|
+
const mode = statSync(path).mode & 0o777;
|
|
32
|
+
expect(mode).toBe(0o600);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
|
|
6
|
-
import { ensureNodeIdentity, nodeIdentityPath } from '../node-identity.js';
|
|
7
|
-
|
|
8
|
-
describe('ensureNodeIdentity', () => {
|
|
9
|
-
let tmp: string;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
tmp = mkdtempSync(join(tmpdir(), 'cadre-node-id-'));
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('generates an identity inside the workdir on first call', async () => {
|
|
20
|
-
const { path, peerId } = await ensureNodeIdentity(tmp);
|
|
21
|
-
expect(path).toBe(nodeIdentityPath(tmp));
|
|
22
|
-
expect(path).toBe(join(tmp, 'identity.key'));
|
|
23
|
-
expect(peerId).toMatch(/^12D3Koo/);
|
|
24
|
-
expect(statSync(path).size).toBeGreaterThan(0);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// Reuse — not rotation — is the load-bearing property: a re-spawn of the same
|
|
28
|
-
// container must land on the same peer id or the cadre that admitted it sees a
|
|
29
|
-
// stranger.
|
|
30
|
-
it('reuses the existing key on a second call', async () => {
|
|
31
|
-
const first = await ensureNodeIdentity(tmp);
|
|
32
|
-
const bytesBefore = readFileSync(first.path);
|
|
33
|
-
|
|
34
|
-
const second = await ensureNodeIdentity(tmp);
|
|
35
|
-
expect(second.peerId).toBe(first.peerId);
|
|
36
|
-
expect(readFileSync(second.path).equals(bytesBefore)).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('creates the workdir when it does not exist yet', async () => {
|
|
40
|
-
const workdir = join(tmp, 'nested', '
|
|
41
|
-
const { path, peerId } = await ensureNodeIdentity(workdir);
|
|
42
|
-
expect(path).toBe(join(workdir, 'identity.key'));
|
|
43
|
-
expect(peerId).toMatch(/^12D3Koo/);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// A key that is present but unreadable must FAIL the spawn, not fall through
|
|
47
|
-
// to `generateIdentity`. Regenerating would hand the cadre a stranger under
|
|
48
|
-
// the same containerId — silently reproducing the bug this module exists to
|
|
49
|
-
// fix — where a throw surfaces the damaged file to the operator.
|
|
50
|
-
it('throws rather than re-keying when the existing file does not decode', async () => {
|
|
51
|
-
const path = nodeIdentityPath(tmp);
|
|
52
|
-
writeFileSync(path, 'not a protobuf private key', 'utf8');
|
|
53
|
-
|
|
54
|
-
await expect(ensureNodeIdentity(tmp)).rejects.toThrow();
|
|
55
|
-
expect(readFileSync(path, 'utf8')).toBe('not a protobuf private key');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it.runIf(process.platform !== 'win32')('persists with mode 0600 on POSIX', async () => {
|
|
59
|
-
const { path } = await ensureNodeIdentity(tmp);
|
|
60
|
-
expect(statSync(path).mode & 0o777).toBe(0o600);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { ensureNodeIdentity, nodeIdentityPath } from '../node-identity.js';
|
|
7
|
+
|
|
8
|
+
describe('ensureNodeIdentity', () => {
|
|
9
|
+
let tmp: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tmp = mkdtempSync(join(tmpdir(), 'cadre-node-id-'));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('generates an identity inside the workdir on first call', async () => {
|
|
20
|
+
const { path, peerId } = await ensureNodeIdentity(tmp);
|
|
21
|
+
expect(path).toBe(nodeIdentityPath(tmp));
|
|
22
|
+
expect(path).toBe(join(tmp, 'identity.key'));
|
|
23
|
+
expect(peerId).toMatch(/^12D3Koo/);
|
|
24
|
+
expect(statSync(path).size).toBeGreaterThan(0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Reuse — not rotation — is the load-bearing property: a re-spawn of the same
|
|
28
|
+
// container must land on the same peer id or the cadre that admitted it sees a
|
|
29
|
+
// stranger.
|
|
30
|
+
it('reuses the existing key on a second call', async () => {
|
|
31
|
+
const first = await ensureNodeIdentity(tmp);
|
|
32
|
+
const bytesBefore = readFileSync(first.path);
|
|
33
|
+
|
|
34
|
+
const second = await ensureNodeIdentity(tmp);
|
|
35
|
+
expect(second.peerId).toBe(first.peerId);
|
|
36
|
+
expect(readFileSync(second.path).equals(bytesBefore)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('creates the workdir when it does not exist yet', async () => {
|
|
40
|
+
const workdir = join(tmp, 'nested', 'hn_1');
|
|
41
|
+
const { path, peerId } = await ensureNodeIdentity(workdir);
|
|
42
|
+
expect(path).toBe(join(workdir, 'identity.key'));
|
|
43
|
+
expect(peerId).toMatch(/^12D3Koo/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// A key that is present but unreadable must FAIL the spawn, not fall through
|
|
47
|
+
// to `generateIdentity`. Regenerating would hand the cadre a stranger under
|
|
48
|
+
// the same containerId — silently reproducing the bug this module exists to
|
|
49
|
+
// fix — where a throw surfaces the damaged file to the operator.
|
|
50
|
+
it('throws rather than re-keying when the existing file does not decode', async () => {
|
|
51
|
+
const path = nodeIdentityPath(tmp);
|
|
52
|
+
writeFileSync(path, 'not a protobuf private key', 'utf8');
|
|
53
|
+
|
|
54
|
+
await expect(ensureNodeIdentity(tmp)).rejects.toThrow();
|
|
55
|
+
expect(readFileSync(path, 'utf8')).toBe('not a protobuf private key');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it.runIf(process.platform !== 'win32')('persists with mode 0600 on POSIX', async () => {
|
|
59
|
+
const { path } = await ensureNodeIdentity(tmp);
|
|
60
|
+
expect(statSync(path).mode & 0o777).toBe(0o600);
|
|
61
|
+
});
|
|
62
|
+
});
|