@serfab/cadre-host 0.8.1 → 0.9.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/dist/auth/trust-circle.d.ts +5 -5
- package/dist/auth/trust-circle.js +10 -10
- package/dist/auth/trust-circle.js.map +1 -1
- package/dist/auth/types.d.ts +2 -2
- package/dist/bin/host.js +255 -54
- package/dist/bin/host.js.map +1 -1
- package/dist/donation/donation-service.d.ts +136 -0
- package/dist/donation/donation-service.js +306 -0
- package/dist/donation/donation-service.js.map +1 -0
- package/dist/donation/donation-store.d.ts +42 -0
- package/dist/donation/donation-store.js +141 -0
- package/dist/donation/donation-store.js.map +1 -0
- package/dist/donation/grant-service.d.ts +71 -0
- package/dist/donation/grant-service.js +163 -0
- package/dist/donation/grant-service.js.map +1 -0
- package/dist/donation/grant-store.d.ts +38 -0
- package/dist/donation/grant-store.js +135 -0
- package/dist/donation/grant-store.js.map +1 -0
- package/dist/donation/index.d.ts +18 -0
- package/dist/donation/index.js +16 -0
- package/dist/donation/index.js.map +1 -0
- package/dist/donation/types.d.ts +175 -0
- package/dist/donation/types.js +40 -0
- package/dist/donation/types.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/installer/config.d.ts +26 -0
- package/dist/installer/config.js +13 -0
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/index.d.ts +2 -0
- package/dist/installer/index.js +3 -0
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/wizard.d.ts +4 -0
- package/dist/installer/wizard.js +5 -1
- package/dist/installer/wizard.js.map +1 -1
- package/dist/nat/nat-service.d.ts +5 -5
- package/dist/nat/nat-service.js +9 -9
- package/dist/nat/nat-service.js.map +1 -1
- package/dist/nat/types.d.ts +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +35 -27
- package/dist/orchestrator/host-process-orchestrator.js +79 -53
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/orchestrator/index.d.ts +3 -3
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/state-store.d.ts +5 -5
- package/dist/orchestrator/state-store.js +2 -2
- package/dist/orchestrator/state-store.js.map +1 -1
- package/dist/orchestrator/types.d.ts +8 -8
- package/dist/owner/index.d.ts +11 -0
- package/dist/owner/index.js +11 -0
- package/dist/owner/index.js.map +1 -0
- package/dist/{authority/authority-node-client.d.ts → owner/owner-node-client.d.ts} +32 -14
- package/dist/{authority/authority-node-client.js → owner/owner-node-client.js} +39 -15
- package/dist/owner/owner-node-client.js.map +1 -0
- package/dist/server/error-handler.d.ts +15 -0
- package/dist/server/error-handler.js +41 -0
- package/dist/server/error-handler.js.map +1 -1
- package/dist/server/index.d.ts +26 -2
- package/dist/server/index.js +37 -16
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/grants-admin.d.ts +17 -0
- package/dist/server/routes/grants-admin.js +32 -0
- package/dist/server/routes/grants-admin.js.map +1 -0
- package/dist/server/routes/grants.d.ts +28 -0
- package/dist/server/routes/grants.js +163 -0
- package/dist/server/routes/grants.js.map +1 -0
- package/dist/server/routes/nodes.d.ts +5 -4
- package/dist/server/routes/nodes.js +19 -18
- package/dist/server/routes/nodes.js.map +1 -1
- package/dist/server/routes/settings.d.ts +6 -1
- package/dist/server/routes/settings.js +5 -2
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/status.d.ts +8 -4
- package/dist/server/routes/status.js +9 -4
- package/dist/server/routes/status.js.map +1 -1
- package/dist/update/release-key.js +7 -5
- package/dist/update/release-key.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/{host-authority.smoke.test.ts → host-owner.smoke.test.ts} +7 -7
- package/src/__tests__/{orchestrator-authority.test.ts → orchestrator-owner.test.ts} +33 -33
- package/src/__tests__/orchestrator-pin-keys.test.ts +153 -0
- package/src/__tests__/orchestrator-push.test.ts +10 -10
- package/src/auth/__tests__/trust-circle-integration.test.ts +11 -11
- package/src/auth/__tests__/trust-circle.test.ts +10 -10
- package/src/auth/trust-circle.ts +10 -10
- package/src/auth/types.ts +2 -2
- package/src/bin/host.ts +273 -53
- package/src/donation/__tests__/donation-service.test.ts +290 -0
- package/src/donation/__tests__/grant-service.test.ts +213 -0
- package/src/donation/__tests__/grant-store.test.ts +113 -0
- package/src/donation/donation-service.ts +392 -0
- package/src/donation/donation-store.ts +164 -0
- package/src/donation/grant-service.ts +200 -0
- package/src/donation/grant-store.ts +152 -0
- package/src/donation/index.ts +49 -0
- package/src/donation/types.ts +220 -0
- package/src/index.ts +37 -5
- package/src/installer/__tests__/config.test.ts +27 -1
- package/src/installer/__tests__/installer.smoke.test.ts +31 -1
- package/src/installer/__tests__/wizard.test.ts +5 -1
- package/src/installer/config.ts +34 -0
- package/src/installer/index.ts +5 -0
- package/src/installer/wizard.ts +12 -1
- package/src/nat/__tests__/nat-service.test.ts +5 -5
- package/src/nat/nat-service.ts +10 -10
- package/src/nat/types.ts +1 -1
- package/src/orchestrator/host-process-orchestrator.ts +88 -60
- package/src/orchestrator/index.ts +3 -3
- package/src/orchestrator/state-store.ts +7 -7
- package/src/orchestrator/types.ts +8 -8
- package/src/{authority/__tests__/authority-node-client.test.ts → owner/__tests__/owner-node-client.test.ts} +45 -17
- package/src/owner/index.ts +12 -0
- package/src/{authority/authority-node-client.ts → owner/owner-node-client.ts} +55 -21
- package/src/server/__tests__/error-handler.test.ts +22 -0
- package/src/server/__tests__/grants-admin-route.test.ts +73 -0
- package/src/server/__tests__/grants-route.test.ts +189 -0
- package/src/server/__tests__/nodes-route.test.ts +25 -25
- package/src/server/__tests__/server.smoke.test.ts +75 -0
- package/src/server/__tests__/settings-route.test.ts +17 -0
- package/src/server/__tests__/status-route.test.ts +21 -0
- package/src/server/error-handler.ts +43 -0
- package/src/server/index.ts +62 -17
- package/src/server/routes/grants-admin.ts +41 -0
- package/src/server/routes/grants.ts +186 -0
- package/src/server/routes/nodes.ts +19 -18
- package/src/server/routes/settings.ts +11 -3
- package/src/server/routes/status.ts +18 -8
- package/src/update/__tests__/release-key.test.ts +16 -3
- package/src/update/release-key.ts +7 -5
- package/dist/authority/authority-node-client.js.map +0 -1
- package/dist/authority/index.d.ts +0 -11
- package/dist/authority/index.js +0 -11
- package/dist/authority/index.js.map +0 -1
- package/src/authority/index.ts +0 -12
|
@@ -3,7 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
|
-
import { readHostConfig, writeHostConfig, type HostConfigFile } from '../config.js';
|
|
6
|
+
import { readHostConfig, writeHostConfig, hostOwnsCadre, type HostConfigFile } from '../config.js';
|
|
7
7
|
|
|
8
8
|
describe('host.config.json round-trip', () => {
|
|
9
9
|
let tmp: string;
|
|
@@ -83,4 +83,30 @@ describe('host.config.json round-trip', () => {
|
|
|
83
83
|
const path = join(tmp, 'host.config.json');
|
|
84
84
|
expect(() => writeHostConfig(path, { ...makeCfg(), version: 3 as never })).toThrow(/version=3/);
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
it('round-trips ownCadre and hostOwnsCadre reflects it', () => {
|
|
88
|
+
const path = join(tmp, 'host.config.json');
|
|
89
|
+
const cfg = makeCfg({ ownCadre: { enabled: true } });
|
|
90
|
+
writeHostConfig(path, cfg);
|
|
91
|
+
const read = readHostConfig(path);
|
|
92
|
+
expect(read.ownCadre).toEqual({ enabled: true });
|
|
93
|
+
expect(hostOwnsCadre(read)).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('treats an absent ownCadre as donor-only (hostOwnsCadre false)', () => {
|
|
97
|
+
// A pre-ownCadre v2 config (field absent) must read back cleanly and be
|
|
98
|
+
// donor-only — no migration, field stays absent.
|
|
99
|
+
const path = join(tmp, 'host.config.json');
|
|
100
|
+
const cfg = makeCfg();
|
|
101
|
+
writeHostConfig(path, cfg);
|
|
102
|
+
const read = readHostConfig(path);
|
|
103
|
+
expect(read.ownCadre).toBeUndefined();
|
|
104
|
+
expect(hostOwnsCadre(read)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('rejects a malformed ownCadre shape', () => {
|
|
108
|
+
const path = join(tmp, 'host.config.json');
|
|
109
|
+
writeFileSync(path, JSON.stringify({ ...makeCfg(), ownCadre: { enabled: 'yes' } }));
|
|
110
|
+
expect(() => readHostConfig(path)).toThrow(/missing required fields/);
|
|
111
|
+
});
|
|
86
112
|
});
|
|
@@ -117,7 +117,7 @@ describe('Installer smoke', () => {
|
|
|
117
117
|
// returned answers stand on their own.
|
|
118
118
|
const fake = new FakeServiceHost();
|
|
119
119
|
const installer = new Installer({ platform: 'linux' });
|
|
120
|
-
let seenDefaults: { dataDir: string; uiPort: number; libp2pPort: number; upnpEnabled: boolean } | undefined;
|
|
120
|
+
let seenDefaults: { dataDir: string; uiPort: number; libp2pPort: number; upnpEnabled: boolean; ownCadre: boolean } | undefined;
|
|
121
121
|
await installer.install({
|
|
122
122
|
nonInteractive: false,
|
|
123
123
|
dataDir: tmp,
|
|
@@ -136,6 +136,7 @@ describe('Installer smoke', () => {
|
|
|
136
136
|
libp2pPort: defaults.libp2pPort,
|
|
137
137
|
upnpEnabled: defaults.upnpEnabled,
|
|
138
138
|
configureDdns: false,
|
|
139
|
+
ownCadre: defaults.ownCadre,
|
|
139
140
|
};
|
|
140
141
|
},
|
|
141
142
|
});
|
|
@@ -145,6 +146,7 @@ describe('Installer smoke', () => {
|
|
|
145
146
|
uiPort: 19997,
|
|
146
147
|
libp2pPort: 14003,
|
|
147
148
|
upnpEnabled: false,
|
|
149
|
+
ownCadre: false,
|
|
148
150
|
});
|
|
149
151
|
expect(fake.installCalls).toHaveLength(1);
|
|
150
152
|
expect(fake.installCalls[0]!.dataDir).toBe(tmp);
|
|
@@ -166,12 +168,40 @@ describe('Installer smoke', () => {
|
|
|
166
168
|
libp2pPort: 14004,
|
|
167
169
|
upnpEnabled: true,
|
|
168
170
|
configureDdns: false,
|
|
171
|
+
ownCadre: false,
|
|
169
172
|
}),
|
|
170
173
|
});
|
|
171
174
|
expect(fake.installCalls).toHaveLength(1);
|
|
172
175
|
expect(fake.installCalls[0]!.dataDir).toBe(tmp);
|
|
173
176
|
});
|
|
174
177
|
|
|
178
|
+
it('writes ownCadre.enabled=false by default and true when opted in', async () => {
|
|
179
|
+
const { readHostConfig } = await import('../config.js');
|
|
180
|
+
|
|
181
|
+
// Default (donor-only): --own-cadre absent → ownCadre.enabled false.
|
|
182
|
+
const fakeA = new FakeServiceHost();
|
|
183
|
+
const installerA = new Installer({ platform: 'linux' });
|
|
184
|
+
await installerA.install({
|
|
185
|
+
nonInteractive: true, dataDir: tmp, openBrowser: false, noInvite: true, serviceHost: fakeA,
|
|
186
|
+
});
|
|
187
|
+
const donorCfg = readHostConfig(join(tmp, 'host.config.json'));
|
|
188
|
+
expect(donorCfg.ownCadre).toEqual({ enabled: false });
|
|
189
|
+
|
|
190
|
+
// Opt-in: ownCadre:true (the --own-cadre flag) → ownCadre.enabled true.
|
|
191
|
+
const tmp2 = mkdtempSync(join(tmpdir(), 'cadre-host-installer-owncadre-'));
|
|
192
|
+
try {
|
|
193
|
+
const fakeB = new FakeServiceHost();
|
|
194
|
+
const installerB = new Installer({ platform: 'linux' });
|
|
195
|
+
await installerB.install({
|
|
196
|
+
nonInteractive: true, dataDir: tmp2, ownCadre: true, openBrowser: false, noInvite: true, serviceHost: fakeB,
|
|
197
|
+
});
|
|
198
|
+
const founderCfg = readHostConfig(join(tmp2, 'host.config.json'));
|
|
199
|
+
expect(founderCfg.ownCadre).toEqual({ enabled: true });
|
|
200
|
+
} finally {
|
|
201
|
+
rmSync(tmp2, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
175
205
|
it('re-running install preserves the existing identity', async () => {
|
|
176
206
|
const fake = new FakeServiceHost();
|
|
177
207
|
const installer = new Installer({ platform: 'linux' });
|
|
@@ -7,6 +7,7 @@ const defaults: WizardDefaults = {
|
|
|
7
7
|
uiPort: 8765,
|
|
8
8
|
libp2pPort: 4001,
|
|
9
9
|
upnpEnabled: true,
|
|
10
|
+
ownCadre: false,
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
function scripted(answers: ReadonlyArray<string>): (label: string, fallback: string) => Promise<string> {
|
|
@@ -19,13 +20,14 @@ function scripted(answers: ReadonlyArray<string>): (label: string, fallback: str
|
|
|
19
20
|
|
|
20
21
|
describe('runWizardWith', () => {
|
|
21
22
|
it('returns the defaults when every answer is blank', async () => {
|
|
22
|
-
const out = await runWizardWith(defaults, scripted(['', '', '', '', '']));
|
|
23
|
+
const out = await runWizardWith(defaults, scripted(['', '', '', '', '', '']));
|
|
23
24
|
expect(out).toEqual({
|
|
24
25
|
dataDir: '/data/cadre-host',
|
|
25
26
|
uiPort: 8765,
|
|
26
27
|
libp2pPort: 4001,
|
|
27
28
|
upnpEnabled: true,
|
|
28
29
|
configureDdns: false,
|
|
30
|
+
ownCadre: false,
|
|
29
31
|
});
|
|
30
32
|
});
|
|
31
33
|
|
|
@@ -36,6 +38,7 @@ describe('runWizardWith', () => {
|
|
|
36
38
|
'5000',
|
|
37
39
|
'n',
|
|
38
40
|
'y',
|
|
41
|
+
'y',
|
|
39
42
|
]));
|
|
40
43
|
expect(out).toEqual({
|
|
41
44
|
dataDir: '/srv/host',
|
|
@@ -43,6 +46,7 @@ describe('runWizardWith', () => {
|
|
|
43
46
|
libp2pPort: 5000,
|
|
44
47
|
upnpEnabled: false,
|
|
45
48
|
configureDdns: true,
|
|
49
|
+
ownCadre: true,
|
|
46
50
|
});
|
|
47
51
|
});
|
|
48
52
|
|
package/src/installer/config.ts
CHANGED
|
@@ -21,6 +21,21 @@ export interface UpdatesConfig {
|
|
|
21
21
|
manifestUrl?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Whether this host also runs its own personal cadre (the opt-in "founder"
|
|
26
|
+
* persona), on top of the always-on node-donor role.
|
|
27
|
+
*
|
|
28
|
+
* Default (absent or `enabled: false`) is donor-only: `cadre-host start` brings
|
|
29
|
+
* up the donation surface but spawns **no** owner node, so the trust-circle and
|
|
30
|
+
* NAT surfaces stay inactive. Set `enabled: true` (install-time choice) to also
|
|
31
|
+
* spawn the host's own owner node and enable `/auth` + `/nat`. See
|
|
32
|
+
* docs/cadre-host.md § Two roles: donor and founder.
|
|
33
|
+
*/
|
|
34
|
+
export interface OwnCadreConfig {
|
|
35
|
+
/** When true, `start` spawns the host-owned owner node + trust-circle + NAT. */
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
/**
|
|
25
40
|
* Non-secret push (FCM/APNs) settings. The actual private keys (and the FCM
|
|
26
41
|
* service-account / APNs key identifiers) live in the secret store — see
|
|
@@ -61,6 +76,12 @@ export interface HostConfigFile {
|
|
|
61
76
|
installerVersion: string;
|
|
62
77
|
/** Update-flow settings; defaults: { autoApply: false }. */
|
|
63
78
|
updates: UpdatesConfig;
|
|
79
|
+
/**
|
|
80
|
+
* Host-own-cadre (founder persona) opt-in. Optional — absent is treated as
|
|
81
|
+
* `{ enabled: false }` (donor-only). Written by the installer; read via
|
|
82
|
+
* `hostOwnsCadre()`. Not editable through /api/settings (install-time only).
|
|
83
|
+
*/
|
|
84
|
+
ownCadre?: OwnCadreConfig;
|
|
64
85
|
/**
|
|
65
86
|
* Non-secret push settings (bundle id, sandbox/prod toggle, cooldown/debounce).
|
|
66
87
|
* Optional — absent when push is not configured. Private keys never live here;
|
|
@@ -69,6 +90,15 @@ export interface HostConfigFile {
|
|
|
69
90
|
push?: PushSettings;
|
|
70
91
|
}
|
|
71
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Whether this host runs its own personal cadre (founder persona). Absent
|
|
95
|
+
* `ownCadre` ⇒ donor-only (false). The single source of truth for the
|
|
96
|
+
* founder/donor gate in `bin/host.ts start`.
|
|
97
|
+
*/
|
|
98
|
+
export function hostOwnsCadre(cfg: HostConfigFile): boolean {
|
|
99
|
+
return cfg.ownCadre?.enabled === true;
|
|
100
|
+
}
|
|
101
|
+
|
|
72
102
|
const CURRENT_VERSION = 2;
|
|
73
103
|
|
|
74
104
|
export function writeHostConfig(filePath: string, cfg: HostConfigFile): void {
|
|
@@ -175,5 +205,9 @@ function isHostConfigShape(v: unknown): v is HostConfigFile {
|
|
|
175
205
|
if (!updates || typeof updates !== 'object') return false;
|
|
176
206
|
if (typeof updates.autoApply !== 'boolean') return false;
|
|
177
207
|
if (updates.manifestUrl !== undefined && typeof updates.manifestUrl !== 'string') return false;
|
|
208
|
+
if (o.ownCadre !== undefined) {
|
|
209
|
+
const own = o.ownCadre as Record<string, unknown>;
|
|
210
|
+
if (!own || typeof own !== 'object' || typeof own.enabled !== 'boolean') return false;
|
|
211
|
+
}
|
|
178
212
|
return true;
|
|
179
213
|
}
|
package/src/installer/index.ts
CHANGED
|
@@ -58,6 +58,8 @@ export interface InstallOptions {
|
|
|
58
58
|
libp2pPort?: number;
|
|
59
59
|
/** Disable UPnP probing on first run. */
|
|
60
60
|
noUpnp?: boolean;
|
|
61
|
+
/** Also run the host's own personal cadre here (founder persona). Default false. */
|
|
62
|
+
ownCadre?: boolean;
|
|
61
63
|
/** Open browser at end (default true; suppressed by --no-browser or non-TTY). */
|
|
62
64
|
openBrowser?: boolean;
|
|
63
65
|
/** Skip the enrollment-invite step (no QR printed). */
|
|
@@ -146,6 +148,7 @@ export class Installer {
|
|
|
146
148
|
installedAt: new Date().toISOString(),
|
|
147
149
|
installerVersion: this.installerVersion,
|
|
148
150
|
updates: { autoApply: false },
|
|
151
|
+
ownCadre: { enabled: answers.ownCadre },
|
|
149
152
|
};
|
|
150
153
|
const cfgPath = configPath(answers.dataDir);
|
|
151
154
|
writeHostConfig(cfgPath, cfg);
|
|
@@ -228,6 +231,7 @@ export class Installer {
|
|
|
228
231
|
libp2pPort: opts.libp2pPort ?? defaults.libp2pPort,
|
|
229
232
|
upnpEnabled: opts.noUpnp ? false : defaults.upnpEnabled,
|
|
230
233
|
configureDdns: false,
|
|
234
|
+
ownCadre: opts.ownCadre ?? defaults.ownCadre,
|
|
231
235
|
};
|
|
232
236
|
}
|
|
233
237
|
|
|
@@ -246,6 +250,7 @@ export class Installer {
|
|
|
246
250
|
uiPort: opts.uiPort ?? platformDefaults.uiPort,
|
|
247
251
|
libp2pPort: opts.libp2pPort ?? platformDefaults.libp2pPort,
|
|
248
252
|
upnpEnabled: opts.noUpnp ? false : platformDefaults.upnpEnabled,
|
|
253
|
+
ownCadre: opts.ownCadre ?? platformDefaults.ownCadre,
|
|
249
254
|
};
|
|
250
255
|
const wizard = opts.wizard ?? runWizard;
|
|
251
256
|
return await wizard(defaults);
|
package/src/installer/wizard.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface WizardDefaults {
|
|
|
16
16
|
uiPort: number;
|
|
17
17
|
libp2pPort: number;
|
|
18
18
|
upnpEnabled: boolean;
|
|
19
|
+
/** Also run the host's own personal cadre here (founder persona). Default false. */
|
|
20
|
+
ownCadre: boolean;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export interface WizardAnswers {
|
|
@@ -24,6 +26,8 @@ export interface WizardAnswers {
|
|
|
24
26
|
libp2pPort: number;
|
|
25
27
|
upnpEnabled: boolean;
|
|
26
28
|
configureDdns: boolean;
|
|
29
|
+
/** Also run the host's own personal cadre here (founder persona). */
|
|
30
|
+
ownCadre: boolean;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export const DEFAULT_UI_PORT = 8765;
|
|
@@ -35,6 +39,7 @@ export function defaultsForPlatform(platform: SupportedPlatform): WizardDefaults
|
|
|
35
39
|
uiPort: DEFAULT_UI_PORT,
|
|
36
40
|
libp2pPort: DEFAULT_LIBP2P_PORT,
|
|
37
41
|
upnpEnabled: true,
|
|
42
|
+
ownCadre: false,
|
|
38
43
|
};
|
|
39
44
|
}
|
|
40
45
|
|
|
@@ -54,7 +59,13 @@ export async function runWizardWith(
|
|
|
54
59
|
defaults.upnpEnabled,
|
|
55
60
|
);
|
|
56
61
|
const configureDdns = parseBool(await ask('Configure DDNS now? [y/N]', 'N'), false);
|
|
57
|
-
|
|
62
|
+
// Founder persona (opt-in). Most installs just donate nodes to friends and
|
|
63
|
+
// answer no — a no leaves cadre-host donor-only (no owner node, no /auth+/nat).
|
|
64
|
+
const ownCadre = parseBool(
|
|
65
|
+
await ask('Also run your own personal cadre on this machine? (most people just donating nodes to friends say no) [y/N]', defaults.ownCadre ? 'Y' : 'N'),
|
|
66
|
+
defaults.ownCadre,
|
|
67
|
+
);
|
|
68
|
+
return { dataDir, uiPort, libp2pPort, upnpEnabled, configureDdns, ownCadre };
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
/** Production entrypoint — drives the wizard over stdin/stdout. */
|
|
@@ -9,7 +9,7 @@ import type { PortMapper, PortMappingResult } from '../port-mapper.js';
|
|
|
9
9
|
import type { SecretsStore } from '../secrets/index.js';
|
|
10
10
|
import { ddnsAccount } from '../secrets/index.js';
|
|
11
11
|
import type { CadreNodeLike } from '../nat-service.js';
|
|
12
|
-
import {
|
|
12
|
+
import { OwnerNodeUnavailableError } from '../../owner/owner-node-client.js';
|
|
13
13
|
|
|
14
14
|
class StubMapper implements PortMapper {
|
|
15
15
|
mapCalls = 0;
|
|
@@ -53,8 +53,8 @@ function makeNode(peerId = '12D3KooWHost', addrs: string[] = ['/ip4/192.168.1.10
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* A node that mimics a freshly spawned
|
|
57
|
-
* `
|
|
56
|
+
* A node that mimics a freshly spawned owner child: `getPeerId` throws
|
|
57
|
+
* `OwnerNodeUnavailableError` for the first `failTimes` calls (admin channel
|
|
58
58
|
* not bound yet), then returns a real peer ID. Exposes the call count so tests
|
|
59
59
|
* can assert how many attempts the retry loop made.
|
|
60
60
|
*/
|
|
@@ -67,7 +67,7 @@ function makeFlakyNode(
|
|
|
67
67
|
const node: CadreNodeLike = {
|
|
68
68
|
getPeerId: async () => {
|
|
69
69
|
calls += 1;
|
|
70
|
-
if (calls <= failTimes) throw new
|
|
70
|
+
if (calls <= failTimes) throw new OwnerNodeUnavailableError('admin channel not bound yet');
|
|
71
71
|
return peerId;
|
|
72
72
|
},
|
|
73
73
|
getMultiaddrs: async () => addrs,
|
|
@@ -421,7 +421,7 @@ describe('NatService — initial invite-address push retry', () => {
|
|
|
421
421
|
it('gives up after the bounded timeout but still resolves start() (best-effort)', async () => {
|
|
422
422
|
// Always unavailable — the retry budget elapses without a successful push.
|
|
423
423
|
const node: CadreNodeLike = {
|
|
424
|
-
getPeerId: async () => { throw new
|
|
424
|
+
getPeerId: async () => { throw new OwnerNodeUnavailableError('never ready'); },
|
|
425
425
|
getMultiaddrs: async () => [],
|
|
426
426
|
};
|
|
427
427
|
const svc = new NatService({
|
package/src/nat/nat-service.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
type BuildInviteAddressesInput,
|
|
25
25
|
} from './address-resolver.js';
|
|
26
26
|
import { createSecretsStore, ddnsAccount, type SecretsStore } from './secrets/index.js';
|
|
27
|
-
import {
|
|
27
|
+
import { OwnerNodeUnavailableError } from '../owner/owner-node-client.js';
|
|
28
28
|
|
|
29
29
|
const log = debug('cadre:host:nat-service');
|
|
30
30
|
|
|
@@ -41,7 +41,7 @@ function sleep(ms: number): Promise<void> {
|
|
|
41
41
|
* so tests can inject a mock without a real libp2p stack. Mirrors
|
|
42
42
|
* `CadreNodeLike` in `auth/trust-circle.ts`.
|
|
43
43
|
*
|
|
44
|
-
* Both methods are async: in production they round-trip over the
|
|
44
|
+
* Both methods are async: in production they round-trip over the owner
|
|
45
45
|
* node's loopback admin channel (`GET /admin/identity`, `GET /admin/multiaddrs`).
|
|
46
46
|
* Synchronous test mocks can return a value directly (it's awaited).
|
|
47
47
|
*/
|
|
@@ -165,7 +165,7 @@ export class NatService {
|
|
|
165
165
|
await this.ddnsUpdater.start();
|
|
166
166
|
|
|
167
167
|
// Initial invite-address push. Retries on `node_unavailable` until the
|
|
168
|
-
// freshly spawned
|
|
168
|
+
// freshly spawned owner node accepts it (bounded; best-effort after).
|
|
169
169
|
await this.pushInitialAddresses();
|
|
170
170
|
}
|
|
171
171
|
|
|
@@ -237,7 +237,7 @@ export class NatService {
|
|
|
237
237
|
|
|
238
238
|
/**
|
|
239
239
|
* Build the multiaddrs to embed in invites. The peer ID and libp2p
|
|
240
|
-
* fallback addresses are fetched over the
|
|
240
|
+
* fallback addresses are fetched over the owner node's admin channel; a
|
|
241
241
|
* node-unavailable failure surfaces as `NatError('node_unavailable')`.
|
|
242
242
|
*/
|
|
243
243
|
async getInviteAddresses(): Promise<string[]> {
|
|
@@ -247,8 +247,8 @@ export class NatService {
|
|
|
247
247
|
peerId = await this.cadreNode.getPeerId();
|
|
248
248
|
libp2pAddrs = await this.cadreNode.getMultiaddrs();
|
|
249
249
|
} catch (err) {
|
|
250
|
-
if (err instanceof
|
|
251
|
-
throw new NatError('node_unavailable', `
|
|
250
|
+
if (err instanceof OwnerNodeUnavailableError) {
|
|
251
|
+
throw new NatError('node_unavailable', `Owner node unavailable: ${err.message}`);
|
|
252
252
|
}
|
|
253
253
|
throw err;
|
|
254
254
|
}
|
|
@@ -256,7 +256,7 @@ export class NatService {
|
|
|
256
256
|
// an empty peerId. Building addresses now would mint malformed `…/p2p/`
|
|
257
257
|
// suffixes (and push them to the node), so treat it as not-ready.
|
|
258
258
|
if (!peerId) {
|
|
259
|
-
throw new NatError('node_unavailable', '
|
|
259
|
+
throw new NatError('node_unavailable', 'Owner node has no peer ID yet (not ready)');
|
|
260
260
|
}
|
|
261
261
|
const status = this.getStatus();
|
|
262
262
|
const input: BuildInviteAddressesInput = {
|
|
@@ -273,7 +273,7 @@ export class NatService {
|
|
|
273
273
|
/**
|
|
274
274
|
* Register a listener fired when the invite addresses may have changed
|
|
275
275
|
* (after `start`, `putSettings`, and `testReachability`). `cadre-host start`
|
|
276
|
-
* wires this to `
|
|
276
|
+
* wires this to `OwnerNodeClient.pushInviteAddresses`. Returns an
|
|
277
277
|
* unsubscribe fn.
|
|
278
278
|
*/
|
|
279
279
|
onAddressesChanged(listener: AddressesChangedListener): () => void {
|
|
@@ -283,7 +283,7 @@ export class NatService {
|
|
|
283
283
|
|
|
284
284
|
/**
|
|
285
285
|
* Recompute invite addresses and notify listeners. Best-effort: when the
|
|
286
|
-
*
|
|
286
|
+
* owner node is unreachable (or the build fails), the notification is
|
|
287
287
|
* skipped — the push is retried on the next NAT event.
|
|
288
288
|
*/
|
|
289
289
|
private async fireAddressesChanged(): Promise<void> {
|
|
@@ -310,7 +310,7 @@ export class NatService {
|
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
/**
|
|
313
|
-
* Initial invite-address push. The
|
|
313
|
+
* Initial invite-address push. The owner node's admin channel is a freshly
|
|
314
314
|
* spawned detached child that may not be bound yet (and may not have a libp2p
|
|
315
315
|
* peer ID yet), so a single attempt races readiness and silently drops the first
|
|
316
316
|
* NAT-resolved address set. Retry on `node_unavailable` until the node accepts it
|
package/src/nat/types.ts
CHANGED
|
@@ -107,7 +107,7 @@ export type NatErrorCode =
|
|
|
107
107
|
| 'secrets_unavailable'
|
|
108
108
|
| 'storage_error'
|
|
109
109
|
| 'invalid_config'
|
|
110
|
-
/** The
|
|
110
|
+
/** The owner node's admin channel is unreachable / not ready. */
|
|
111
111
|
| 'node_unavailable';
|
|
112
112
|
|
|
113
113
|
/** Typed error carrying a stable `code` for HTTP mapping in the local-ui. */
|