@serfab/cadre-host 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -4
- package/dist/bin/host.js +32 -20
- package/dist/bin/host.js.map +1 -1
- package/dist/bin/nat-output.d.ts +5 -0
- package/dist/bin/nat-output.js +12 -1
- package/dist/bin/nat-output.js.map +1 -1
- package/dist/bin/ui-access.d.ts +22 -0
- package/dist/bin/ui-access.js +44 -0
- package/dist/bin/ui-access.js.map +1 -0
- package/dist/hosted/claim-details.d.ts +25 -3
- package/dist/hosted/claim-details.js +31 -37
- package/dist/hosted/claim-details.js.map +1 -1
- package/dist/hosted/hosted-node-service.d.ts +7 -0
- package/dist/hosted/hosted-node-service.js +16 -4
- package/dist/hosted/hosted-node-service.js.map +1 -1
- package/dist/hosted/hosted-node-watcher.d.ts +16 -4
- package/dist/hosted/hosted-node-watcher.js +45 -9
- package/dist/hosted/hosted-node-watcher.js.map +1 -1
- package/dist/hosted/node-status.d.ts +19 -3
- package/dist/hosted/node-status.js +21 -5
- package/dist/hosted/node-status.js.map +1 -1
- package/dist/hosted/types.d.ts +12 -5
- package/dist/hosted/types.js.map +1 -1
- package/dist/installer/config.d.ts +23 -0
- package/dist/installer/config.js +46 -0
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/index.d.ts +2 -0
- package/dist/installer/index.js +7 -16
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/wizard.d.ts +12 -2
- package/dist/installer/wizard.js +15 -7
- package/dist/installer/wizard.js.map +1 -1
- package/dist/nat/index.d.ts +2 -1
- package/dist/nat/index.js +1 -0
- package/dist/nat/index.js.map +1 -1
- package/dist/nat/nat-service.d.ts +22 -5
- package/dist/nat/nat-service.js +75 -26
- package/dist/nat/nat-service.js.map +1 -1
- package/dist/nat/nat-store.js +11 -0
- package/dist/nat/nat-store.js.map +1 -1
- package/dist/nat/network-mode.d.ts +19 -0
- package/dist/nat/network-mode.js +32 -0
- package/dist/nat/network-mode.js.map +1 -0
- package/dist/nat/reachability.d.ts +5 -4
- package/dist/nat/reachability.js +19 -6
- package/dist/nat/reachability.js.map +1 -1
- package/dist/nat/types.d.ts +26 -6
- package/dist/nat/types.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +3 -3
- package/dist/orchestrator/host-process-orchestrator.js +6 -15
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/package-version.d.ts +1 -0
- package/dist/package-version.js +4 -0
- package/dist/package-version.js.map +1 -0
- package/dist/server/routes/settings.d.ts +1 -0
- package/dist/server/routes/settings.js +15 -1
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/status.js +2 -32
- package/dist/server/routes/status.js.map +1 -1
- package/dist/ui/assets/index-BHd8CWZA.css +1 -0
- package/dist/ui/assets/index-Ccr2GjPT.js +23 -0
- package/dist/ui/assets/{index-4FWuU8ye.js.map → index-Ccr2GjPT.js.map} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +4 -5
- package/src/__tests__/cli.smoke.test.ts +1 -1
- package/src/__tests__/orchestrator-claim-secret.test.ts +1 -2
- package/src/__tests__/orchestrator-env-scrub.test.ts +160 -163
- package/src/__tests__/orchestrator-handle-contract.ts +1 -1
- package/src/__tests__/orchestrator-node-identity.test.ts +0 -5
- package/src/__tests__/orchestrator-push.test.ts +3 -3
- package/src/__tests__/orchestrator.test.ts +3 -5
- package/src/__tests__/ui-access.test.ts +30 -0
- package/src/bin/host.ts +30 -20
- package/src/bin/nat-output.ts +12 -1
- package/src/bin/ui-access.ts +50 -0
- package/src/hosted/__tests__/fake-orchestrator.test.ts +0 -1
- package/src/hosted/__tests__/fake-orchestrator.ts +263 -265
- package/src/hosted/__tests__/hosted-node-service.test.ts +708 -609
- package/src/hosted/__tests__/hosted-node-supervisor.test.ts +475 -474
- package/src/hosted/claim-details.ts +44 -37
- package/src/hosted/hosted-node-service.ts +23 -4
- package/src/hosted/hosted-node-watcher.ts +288 -250
- package/src/hosted/node-status.ts +31 -5
- package/src/hosted/types.ts +160 -153
- package/src/installer/__tests__/installer.smoke.test.ts +184 -182
- package/src/installer/__tests__/wizard.test.ts +76 -56
- package/src/installer/config.ts +66 -0
- package/src/installer/index.ts +252 -259
- package/src/installer/wizard.ts +101 -87
- package/src/nat/__tests__/nat-service.test.ts +608 -540
- package/src/nat/__tests__/nat-store.test.ts +130 -127
- package/src/nat/__tests__/network-mode.test.ts +38 -0
- package/src/nat/index.ts +91 -87
- package/src/nat/nat-service.ts +79 -26
- package/src/nat/nat-store.ts +249 -237
- package/src/nat/network-mode.ts +36 -0
- package/src/nat/reachability.ts +155 -143
- package/src/nat/types.ts +232 -209
- package/src/orchestrator/host-process-orchestrator.ts +1089 -1098
- package/src/package-version.ts +4 -0
- package/src/server/__tests__/fakes.ts +41 -40
- package/src/server/__tests__/hosted-nodes-route.test.ts +161 -160
- package/src/server/__tests__/settings-route.test.ts +31 -0
- package/src/server/routes/settings.ts +128 -115
- package/src/server/routes/status.ts +72 -102
- package/dist/ui/assets/index-4FWuU8ye.js +0 -19
- package/dist/ui/assets/index-Di9Q4d01.css +0 -1
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
* `HostedNodeService.claimDetails` reads the child's `/status` and hands it here.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { encodeNodeClaimPayload, selectNodeClaimAddresses, withClaimDestination } from '@serfab/cadre-core';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import type { NodeReachability } from '../nat/types.js';
|
|
8
|
+
import type { ClaimAddressSettings } from '../installer/config.js';
|
|
9
|
+
import type { NetworkMode, NodeReachability } from '../nat/types.js';
|
|
11
10
|
import type { NodeStatus } from './node-status.js';
|
|
12
11
|
import { HostedNodeError } from './types.js';
|
|
13
12
|
|
|
@@ -16,17 +15,21 @@ export interface ClaimDetails {
|
|
|
16
15
|
/** `encodeNodeClaimPayload` of the three fields below — the text the QR code carries. */
|
|
17
16
|
payload: string;
|
|
18
17
|
peerId: string;
|
|
19
|
-
/** Public
|
|
18
|
+
/** Public names (or public IPs when there is no name), then the one LAN address; see {@link dialAddressesFor}. */
|
|
20
19
|
multiaddrs: string[];
|
|
21
20
|
/** The NAT layer's verdict for the node, or null when it has no entry yet. A phone at home reaches the LAN address either way. */
|
|
22
21
|
reachability: NodeReachability | null;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
/** The NAT layer's
|
|
24
|
+
/** The NAT layer's answers the claim details need. `NatService` satisfies this. */
|
|
26
25
|
export interface HostedNodeAddressSource {
|
|
27
26
|
/** The node's public multiaddrs (no `/p2p/` suffix), TCP then WebSocket. */
|
|
28
27
|
publicAddressesFor(nodeId: string, ports: { p2p: number; ws?: number }): string[];
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* `gateway.lanAddress` is this machine's address on the router's subnet, when a UPnP gateway
|
|
30
|
+
* answered; `network.mode` is whether it sits behind a router (`lan`) or holds a public IPv4.
|
|
31
|
+
*/
|
|
32
|
+
getStatus(): { nodes: NodeReachability[]; gateway: { lanAddress: string | null }; network: { mode: NetworkMode } };
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/** One node's claim details, from its record's secret, its `/status`, and its ports. */
|
|
@@ -37,6 +40,10 @@ export interface ClaimDetailsInput {
|
|
|
37
40
|
/** The orchestrator handle's ports; absent, the code carries LAN addresses only. */
|
|
38
41
|
ports: { p2p: number; ws?: number } | undefined;
|
|
39
42
|
addresses: HostedNodeAddressSource;
|
|
43
|
+
/** `host.config.json`'s `claimAddresses.addrs`: exact addresses replacing the automatic choice. */
|
|
44
|
+
exactAddrs?: readonly string[];
|
|
45
|
+
/** The LAN address to keep, as {@link claimLanAddress} resolved it. */
|
|
46
|
+
lan: string | null | undefined;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
/**
|
|
@@ -58,39 +65,39 @@ export function buildClaimDetails(input: ClaimDetailsInput): ClaimDetails {
|
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/**
|
|
61
|
-
* The
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* (
|
|
65
|
-
* entries are dropped — nothing off this machine can use the former, and no
|
|
66
|
-
* hosted node holds a relay reservation today.
|
|
68
|
+
* The LAN address a node code keeps (`selectNodeClaimAddresses`' `lan`), from `claimAddresses.lan`:
|
|
69
|
+
* `'none'` is null; an IP is itself; `'auto'` (default) is null in public-IP mode (no phone shares
|
|
70
|
+
* a LAN with a VPS), else the gateway's `lanAddress`, else the machine's primary address
|
|
71
|
+
* (`primaryLan`, probed only then), else undefined (keep every private address).
|
|
67
72
|
*/
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
export async function claimLanAddress(
|
|
74
|
+
settings: ClaimAddressSettings | undefined,
|
|
75
|
+
addresses: HostedNodeAddressSource,
|
|
76
|
+
primaryLan: () => Promise<string | undefined>,
|
|
77
|
+
): Promise<string | null | undefined> {
|
|
78
|
+
const lan = settings?.lan ?? 'auto';
|
|
79
|
+
if (lan === 'none') return null;
|
|
80
|
+
if (lan !== 'auto') return lan;
|
|
81
|
+
const { network, gateway } = addresses.getStatus();
|
|
82
|
+
if (network.mode === 'public') return null;
|
|
83
|
+
return gateway.lanAddress ?? await primaryLan();
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
87
|
+
* The addresses the QR code names, in dial order. A phone dials each in turn on its own
|
|
88
|
+
* timeout, so every address it cannot reach delays a failing claim and enlarges the code.
|
|
89
|
+
*
|
|
90
|
+
* - With `exactAddrs`, exactly those, each given the node's peer id.
|
|
91
|
+
* - Otherwise cadre-core's `selectNodeClaimAddresses` over the NAT layer's public addresses
|
|
92
|
+
* and the node's own (`/status`): WebSocket and relay only (a phone has no TCP), public
|
|
93
|
+
* names before raw public IPs, and of the private addresses only `lan`. Docker bridges,
|
|
94
|
+
* VPNs and other interfaces a phone cannot reach are left out.
|
|
82
95
|
*/
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
if (components.some((c) => c.code === CODE_P2P_CIRCUIT)) return false;
|
|
91
|
-
const host = components[0];
|
|
92
|
-
if (!host?.value) return false;
|
|
93
|
-
if (host.code === CODE_IP4) return !host.value.startsWith('127.') && host.value !== '0.0.0.0';
|
|
94
|
-
if (host.code === CODE_IP6) return host.value !== '::1' && host.value !== '::';
|
|
95
|
-
return true;
|
|
96
|
+
function dialAddressesFor({ id, status, ports, addresses, exactAddrs, lan }: ClaimDetailsInput): string[] {
|
|
97
|
+
// NOTE: `claimAddresses.addrs` is host-wide, so every hosted node's code carries the same
|
|
98
|
+
// addresses with its own peer id; that reaches each node only if they route per node (one
|
|
99
|
+
// node, or a port per node). If several nodes behind one proxy is reported, make it per node.
|
|
100
|
+
if (exactAddrs && exactAddrs.length > 0) return [...new Set(exactAddrs.map((a) => withClaimDestination(a, status.peerId)))];
|
|
101
|
+
const publicAddrs = ports ? addresses.publicAddressesFor(id, ports) : [];
|
|
102
|
+
return selectNodeClaimAddresses([...publicAddrs, ...status.multiaddrs], status.peerId, { lan });
|
|
96
103
|
}
|
|
@@ -2,10 +2,12 @@ import { randomBytes } from 'node:crypto';
|
|
|
2
2
|
import debug from 'debug';
|
|
3
3
|
|
|
4
4
|
import { CLAIM_SECRET_BYTES, decodeCadreInvitation, type CadreInvitation } from '@serfab/cadre-core';
|
|
5
|
+
import { primaryLanAddress } from '@serfab/cadre-core/primary-lan-address';
|
|
5
6
|
import type { Orchestrator, OrchestratorCreateResult } from '@serfab/cadre-provider';
|
|
6
7
|
|
|
7
8
|
import type { HostedSpawnRequest, ManagedNodeInfo, NodeStateListener } from '../orchestrator/types.js';
|
|
8
|
-
import
|
|
9
|
+
import type { ClaimAddressSettings } from '../installer/config.js';
|
|
10
|
+
import { buildClaimDetails, claimLanAddress, type ClaimDetails, type HostedNodeAddressSource } from './claim-details.js';
|
|
9
11
|
import type { HostedNodeStore } from './hosted-node-store.js';
|
|
10
12
|
import { HostedNodeWatcher } from './hosted-node-watcher.js';
|
|
11
13
|
import { PLACEHOLDER_PARTY, errorMessage, readNodeStatus, statusUrlFor } from './node-status.js';
|
|
@@ -130,6 +132,10 @@ export interface HostedNodeServiceOptions {
|
|
|
130
132
|
store: HostedNodeStore;
|
|
131
133
|
/** The NAT layer, for the node's public addresses and reachability verdict. */
|
|
132
134
|
addresses: HostedNodeAddressSource;
|
|
135
|
+
/** `host.config.json`'s `claimAddresses`, read on every claim-code request so an edit applies at once. */
|
|
136
|
+
claimAddressSettings?: () => ClaimAddressSettings | undefined;
|
|
137
|
+
/** The machine's primary address, the `lan: 'auto'` fallback when no UPnP gateway answered. */
|
|
138
|
+
primaryLan?: () => Promise<string | undefined>;
|
|
133
139
|
/** Clock override for tests. */
|
|
134
140
|
now?: () => Date;
|
|
135
141
|
}
|
|
@@ -159,6 +165,8 @@ export class HostedNodeService {
|
|
|
159
165
|
private readonly orchestrator: HostedNodeOrchestrator;
|
|
160
166
|
private readonly store: HostedNodeStore;
|
|
161
167
|
private readonly addresses: HostedNodeAddressSource;
|
|
168
|
+
private readonly claimAddressSettings: () => ClaimAddressSettings | undefined;
|
|
169
|
+
private readonly primaryLan: () => Promise<string | undefined>;
|
|
162
170
|
private readonly now: () => Date;
|
|
163
171
|
private readonly listeners = new Set<HostedNodeChangeListener>();
|
|
164
172
|
private readonly watcher: HostedNodeWatcher;
|
|
@@ -171,6 +179,8 @@ export class HostedNodeService {
|
|
|
171
179
|
this.orchestrator = opts.orchestrator;
|
|
172
180
|
this.store = opts.store;
|
|
173
181
|
this.addresses = opts.addresses;
|
|
182
|
+
this.claimAddressSettings = opts.claimAddressSettings ?? (() => undefined);
|
|
183
|
+
this.primaryLan = opts.primaryLan ?? primaryLanAddress;
|
|
174
184
|
this.now = opts.now ?? (() => new Date());
|
|
175
185
|
this.watcher = new HostedNodeWatcher({
|
|
176
186
|
store: this.store,
|
|
@@ -335,7 +345,17 @@ export class HostedNodeService {
|
|
|
335
345
|
const { secret } = node.join;
|
|
336
346
|
const status = await readNodeStatus(node.statusEndpoint);
|
|
337
347
|
this.recordPeerId(id, status.peerId);
|
|
338
|
-
|
|
348
|
+
const settings = this.claimAddressSettings();
|
|
349
|
+
const exactAddrs = settings?.addrs ?? [];
|
|
350
|
+
return buildClaimDetails({
|
|
351
|
+
id,
|
|
352
|
+
secret,
|
|
353
|
+
status,
|
|
354
|
+
ports: this.orchestrator.getNode?.(id)?.ports,
|
|
355
|
+
addresses: this.addresses,
|
|
356
|
+
exactAddrs,
|
|
357
|
+
lan: exactAddrs.length > 0 ? undefined : await claimLanAddress(settings, this.addresses, this.primaryLan),
|
|
358
|
+
});
|
|
339
359
|
}
|
|
340
360
|
|
|
341
361
|
/** Cache the peer id on the record, once. Best-effort: the claim details do not depend on it. */
|
|
@@ -767,14 +787,13 @@ function spawnRequestFor(node: HostedNode): HostedSpawnRequest {
|
|
|
767
787
|
const base: HostedSpawnRequest = {
|
|
768
788
|
containerId: node.id,
|
|
769
789
|
partyId: node.partyId,
|
|
770
|
-
bootstrapNodes: [],
|
|
771
790
|
profile: node.profile,
|
|
772
791
|
};
|
|
773
792
|
if (node.join.kind === 'claim') return { ...base, claimSecret: node.join.secret };
|
|
774
793
|
return node.status === 'joined' ? base : { ...base, invitation: node.join.encoded };
|
|
775
794
|
}
|
|
776
795
|
|
|
777
|
-
/** The record fields a spawn result lands.
|
|
796
|
+
/** The record fields a spawn result lands. */
|
|
778
797
|
function handlesOf(result: OrchestratorCreateResult): SpawnedHandles {
|
|
779
798
|
return { dockerId: result.dockerId, statusEndpoint: statusUrlFor(result.healthEndpoint) };
|
|
780
799
|
}
|