@serfab/cadre-host 0.7.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/README.md +296 -101
- package/dist/auth/trust-circle.d.ts +45 -7
- package/dist/auth/trust-circle.js +116 -59
- package/dist/auth/trust-circle.js.map +1 -1
- package/dist/auth/types.d.ts +4 -1
- package/dist/auth/types.js.map +1 -1
- package/dist/bin/host.js +426 -112
- 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 +12 -6
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/installer/browser.js +16 -17
- package/dist/installer/browser.js.map +1 -1
- package/dist/installer/config.d.ts +51 -0
- package/dist/installer/config.js +14 -1
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/index.d.ts +7 -2
- package/dist/installer/index.js +17 -10
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/service-host/launchd.js +1 -1
- package/dist/installer/service-host/launchd.js.map +1 -1
- package/dist/installer/service-host/systemd.js +1 -1
- package/dist/installer/service-host/systemd.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/index.d.ts +1 -1
- package/dist/nat/nat-service.d.ts +45 -3
- package/dist/nat/nat-service.js +116 -3
- package/dist/nat/nat-service.js.map +1 -1
- package/dist/nat/secrets/index.d.ts +7 -0
- package/dist/nat/secrets/index.js +28 -2
- package/dist/nat/secrets/index.js.map +1 -1
- package/dist/nat/types.d.ts +3 -1
- package/dist/nat/types.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +68 -1
- package/dist/orchestrator/host-process-orchestrator.js +272 -39
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/orchestrator/index.d.ts +3 -2
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/state-store.d.ts +6 -5
- package/dist/orchestrator/state-store.js +10 -2
- package/dist/orchestrator/state-store.js.map +1 -1
- package/dist/orchestrator/types.d.ts +45 -10
- package/dist/orchestrator/types.js.map +1 -1
- 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/owner/owner-node-client.d.ts +95 -0
- package/dist/owner/owner-node-client.js +167 -0
- package/dist/owner/owner-node-client.js.map +1 -0
- package/dist/push/index.d.ts +67 -0
- package/dist/push/index.js +101 -0
- package/dist/push/index.js.map +1 -0
- package/dist/server/error-handler.d.ts +17 -0
- package/dist/server/error-handler.js +45 -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 -3
- package/dist/server/routes/nodes.js +39 -24
- 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/server/static.d.ts +12 -0
- package/dist/server/static.js +24 -7
- package/dist/server/static.js.map +1 -1
- package/dist/ui/assets/index-BlMXQPSw.js +14 -0
- package/dist/ui/assets/{index-C9uChk2P.js.map → index-BlMXQPSw.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/dist/update/apply.d.ts +23 -1
- package/dist/update/apply.js +46 -4
- package/dist/update/apply.js.map +1 -1
- package/dist/update/index.d.ts +14 -3
- package/dist/update/index.js +35 -2
- package/dist/update/index.js.map +1 -1
- package/dist/update/manifest.d.ts +28 -9
- package/dist/update/manifest.js +56 -29
- package/dist/update/manifest.js.map +1 -1
- package/dist/update/release-key.d.ts +18 -7
- package/dist/update/release-key.js +29 -14
- package/dist/update/release-key.js.map +1 -1
- package/dist/update/sign.d.ts +46 -0
- package/dist/update/sign.js +53 -0
- package/dist/update/sign.js.map +1 -0
- package/package.json +9 -4
- package/service/README.md +95 -86
- package/src/__tests__/cli-invite.smoke.test.ts +6 -1
- package/src/__tests__/cli-nat.smoke.test.ts +6 -1
- package/src/__tests__/cli.smoke.test.ts +6 -1
- package/src/__tests__/host-owner.smoke.test.ts +123 -0
- package/src/__tests__/orchestrator-owner.test.ts +198 -0
- package/src/__tests__/orchestrator-pin-keys.test.ts +153 -0
- package/src/__tests__/orchestrator-push.test.ts +145 -0
- package/src/auth/__tests__/trust-circle-integration.test.ts +44 -25
- package/src/auth/__tests__/trust-circle.test.ts +91 -24
- package/src/auth/trust-circle.ts +363 -304
- package/src/auth/types.ts +4 -1
- package/src/bin/host.ts +1258 -905
- 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 +66 -3
- package/src/installer/__tests__/config.test.ts +27 -1
- package/src/installer/__tests__/installer.smoke.test.ts +94 -0
- package/src/installer/__tests__/wizard.test.ts +5 -1
- package/src/installer/browser.ts +40 -42
- package/src/installer/config.ts +61 -1
- package/src/installer/index.ts +23 -12
- package/src/installer/service-host/launchd.ts +1 -1
- package/src/installer/service-host/systemd.ts +1 -1
- package/src/installer/wizard.ts +12 -1
- package/src/nat/__tests__/ddns-updater.test.ts +1 -1
- package/src/nat/__tests__/nat-service.test.ts +192 -0
- package/src/nat/__tests__/port-mapper.test.ts +1 -1
- package/src/nat/__tests__/secrets.test.ts +23 -1
- package/src/nat/index.ts +1 -1
- package/src/nat/nat-service.ts +131 -5
- package/src/nat/secrets/index.ts +32 -2
- package/src/nat/types.ts +3 -1
- package/src/orchestrator/host-process-orchestrator.ts +322 -54
- package/src/orchestrator/index.ts +9 -2
- package/src/orchestrator/state-store.ts +21 -3
- package/src/orchestrator/types.ts +48 -2
- package/src/owner/__tests__/owner-node-client.test.ts +194 -0
- package/src/owner/index.ts +12 -0
- package/src/owner/owner-node-client.ts +233 -0
- package/src/push/__tests__/push.test.ts +90 -0
- package/src/push/index.ts +146 -0
- package/src/server/__tests__/error-handler.test.ts +26 -4
- 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 +90 -10
- package/src/server/__tests__/publishers.test.ts +1 -1
- package/src/server/__tests__/server.smoke.test.ts +75 -0
- package/src/server/__tests__/settings-route.test.ts +17 -0
- package/src/server/__tests__/static.test.ts +145 -0
- package/src/server/__tests__/status-route.test.ts +23 -2
- package/src/server/error-handler.ts +47 -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 +49 -26
- package/src/server/routes/settings.ts +11 -3
- package/src/server/routes/status.ts +18 -8
- package/src/server/static.ts +24 -8
- package/src/update/__tests__/apply.test.ts +58 -1
- package/src/update/__tests__/manifest.test.ts +41 -6
- package/src/update/__tests__/release-key.test.ts +42 -0
- package/src/update/__tests__/sign.test.ts +78 -0
- package/src/update/__tests__/update-service.integration.test.ts +44 -9
- package/src/update/apply.ts +46 -4
- package/src/update/index.ts +41 -3
- package/src/update/manifest.ts +214 -172
- package/src/update/release-key.ts +31 -15
- package/src/update/sign.ts +74 -0
- package/dist/ui/assets/index-C9uChk2P.js +0 -14
package/src/index.ts
CHANGED
|
@@ -27,14 +27,20 @@ export type {
|
|
|
27
27
|
ContainerResources,
|
|
28
28
|
} from '@serfab/cadre-provider';
|
|
29
29
|
|
|
30
|
-
export { HostProcessOrchestrator } from './orchestrator/index.js';
|
|
30
|
+
export { HostProcessOrchestrator, OWNER_CONTAINER_ID } from './orchestrator/index.js';
|
|
31
31
|
export type {
|
|
32
32
|
HostProcessConfig,
|
|
33
33
|
PersistedHandle,
|
|
34
34
|
ManagedNodeInfo,
|
|
35
35
|
NodeStateListener,
|
|
36
|
+
NodePorts,
|
|
37
|
+
OwnerAdminEndpoint,
|
|
38
|
+
OwnerSpawnConfig,
|
|
36
39
|
} from './orchestrator/index.js';
|
|
37
40
|
|
|
41
|
+
export { OwnerNodeClient, OwnerNodeUnavailableError } from './owner/index.js';
|
|
42
|
+
export type { OwnerNodeClientOptions } from './owner/index.js';
|
|
43
|
+
|
|
38
44
|
export {
|
|
39
45
|
TrustCircleService,
|
|
40
46
|
TrustCircleStore,
|
|
@@ -54,13 +60,46 @@ export type {
|
|
|
54
60
|
TrustCircleErrorCode,
|
|
55
61
|
} from './auth/index.js';
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
/* ──────────────── donation grant tokens ──────────────── */
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
GrantStore,
|
|
67
|
+
GrantService,
|
|
68
|
+
createGrantAdminHandlers,
|
|
69
|
+
GrantError,
|
|
70
|
+
DEFAULT_MAX_NODES,
|
|
71
|
+
DonationStore,
|
|
72
|
+
DonationService,
|
|
73
|
+
DonationError,
|
|
74
|
+
} from './donation/index.js';
|
|
75
|
+
export type {
|
|
76
|
+
Grant,
|
|
77
|
+
GrantDenyReason,
|
|
78
|
+
GrantValidation,
|
|
79
|
+
GrantValidator,
|
|
80
|
+
GrantFile,
|
|
81
|
+
GrantAdminHandlers,
|
|
82
|
+
GrantErrorCode,
|
|
83
|
+
GrantServiceOptions,
|
|
84
|
+
Donation,
|
|
85
|
+
DonationView,
|
|
86
|
+
DonationStatus,
|
|
87
|
+
DonationFile,
|
|
88
|
+
DonationErrorCode,
|
|
89
|
+
DonationProvisionRequest,
|
|
90
|
+
DonationSeedResult,
|
|
91
|
+
DonationPeerInfo,
|
|
92
|
+
DonationServiceOptions,
|
|
93
|
+
} from './donation/index.js';
|
|
94
|
+
|
|
95
|
+
export { Installer, readHostConfig, updateHostConfig, writeHostConfig } from './installer/index.js';
|
|
58
96
|
export type {
|
|
59
97
|
InstallOptions,
|
|
60
98
|
InstallResult,
|
|
61
99
|
UninstallOptions,
|
|
62
100
|
InstallerOptions,
|
|
63
101
|
HostConfigFile,
|
|
102
|
+
PushSettings,
|
|
64
103
|
WizardAnswers,
|
|
65
104
|
WizardDefaults,
|
|
66
105
|
ServiceHost,
|
|
@@ -68,6 +107,23 @@ export type {
|
|
|
68
107
|
ServiceHostStatus,
|
|
69
108
|
} from './installer/index.js';
|
|
70
109
|
|
|
110
|
+
/* ──────────────── push credentials (FCM/APNs) ──────────────── */
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
resolvePushCredentials,
|
|
114
|
+
setFcmSecret,
|
|
115
|
+
setApnsSecret,
|
|
116
|
+
clearPushSecret,
|
|
117
|
+
pushAccount,
|
|
118
|
+
pushStatus,
|
|
119
|
+
} from './push/index.js';
|
|
120
|
+
export type {
|
|
121
|
+
FcmSecret,
|
|
122
|
+
ApnsSecret,
|
|
123
|
+
PushSecretPlatform,
|
|
124
|
+
PushStatus,
|
|
125
|
+
} from './push/index.js';
|
|
126
|
+
|
|
71
127
|
export {
|
|
72
128
|
UpdateService,
|
|
73
129
|
createUpdateHandlers,
|
|
@@ -78,21 +134,28 @@ export {
|
|
|
78
134
|
fetchManifest,
|
|
79
135
|
verifyManifest,
|
|
80
136
|
canonicalJson,
|
|
81
|
-
|
|
137
|
+
signManifest,
|
|
138
|
+
validateManifestFields,
|
|
139
|
+
buildManifest,
|
|
140
|
+
derivePublicKeyBase64,
|
|
141
|
+
signAndSelfVerify,
|
|
82
142
|
compareVersions,
|
|
83
143
|
parseVersion,
|
|
84
144
|
getReleasePublicKey,
|
|
85
145
|
getReleasePublicKeyBase64,
|
|
86
146
|
ed25519FromRaw,
|
|
147
|
+
isPlaceholderReleaseKey,
|
|
87
148
|
} from './update/index.js';
|
|
88
149
|
export type {
|
|
89
150
|
UpdateServiceOptions,
|
|
90
151
|
UpdateHandlers,
|
|
91
152
|
UpdateState,
|
|
153
|
+
SignedManifest,
|
|
92
154
|
UpdateApplyResult,
|
|
93
155
|
UpdateSettings,
|
|
94
156
|
UpdateManifest,
|
|
95
157
|
UpdateErrorCode,
|
|
158
|
+
ManifestFields,
|
|
96
159
|
NpmExecutor,
|
|
97
160
|
ServiceRestarter,
|
|
98
161
|
} from './update/index.js';
|
|
@@ -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
|
});
|
|
@@ -108,6 +108,100 @@ describe('Installer smoke', () => {
|
|
|
108
108
|
expect(fake.installCalls).toHaveLength(0);
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
it('passes CLI overrides to the wizard as suggested defaults', async () => {
|
|
112
|
+
// Regression: the interactive wizard previously showed the platform
|
|
113
|
+
// default (`~/.local/share/cadre-host`) in the prompt even when the
|
|
114
|
+
// caller passed `--data-dir <path>`. Whatever the user typed was then
|
|
115
|
+
// silently discarded because the CLI flag won post-hoc. The fix: CLI
|
|
116
|
+
// overrides become the wizard's suggested defaults, and the wizard's
|
|
117
|
+
// returned answers stand on their own.
|
|
118
|
+
const fake = new FakeServiceHost();
|
|
119
|
+
const installer = new Installer({ platform: 'linux' });
|
|
120
|
+
let seenDefaults: { dataDir: string; uiPort: number; libp2pPort: number; upnpEnabled: boolean; ownCadre: boolean } | undefined;
|
|
121
|
+
await installer.install({
|
|
122
|
+
nonInteractive: false,
|
|
123
|
+
dataDir: tmp,
|
|
124
|
+
uiPort: 19997,
|
|
125
|
+
libp2pPort: 14003,
|
|
126
|
+
noUpnp: true,
|
|
127
|
+
openBrowser: false,
|
|
128
|
+
noInvite: true,
|
|
129
|
+
serviceHost: fake,
|
|
130
|
+
wizard: async (defaults) => {
|
|
131
|
+
seenDefaults = { ...defaults };
|
|
132
|
+
// Simulate the user pressing Enter at every prompt.
|
|
133
|
+
return {
|
|
134
|
+
dataDir: defaults.dataDir,
|
|
135
|
+
uiPort: defaults.uiPort,
|
|
136
|
+
libp2pPort: defaults.libp2pPort,
|
|
137
|
+
upnpEnabled: defaults.upnpEnabled,
|
|
138
|
+
configureDdns: false,
|
|
139
|
+
ownCadre: defaults.ownCadre,
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(seenDefaults).toEqual({
|
|
145
|
+
dataDir: tmp,
|
|
146
|
+
uiPort: 19997,
|
|
147
|
+
libp2pPort: 14003,
|
|
148
|
+
upnpEnabled: false,
|
|
149
|
+
ownCadre: false,
|
|
150
|
+
});
|
|
151
|
+
expect(fake.installCalls).toHaveLength(1);
|
|
152
|
+
expect(fake.installCalls[0]!.dataDir).toBe(tmp);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('honors wizard-typed input when no CLI override is passed', async () => {
|
|
156
|
+
// Mirror of the previous test: with no `--data-dir`, the wizard's
|
|
157
|
+
// returned value is what the installer uses.
|
|
158
|
+
const fake = new FakeServiceHost();
|
|
159
|
+
const installer = new Installer({ platform: 'linux' });
|
|
160
|
+
await installer.install({
|
|
161
|
+
nonInteractive: false,
|
|
162
|
+
openBrowser: false,
|
|
163
|
+
noInvite: true,
|
|
164
|
+
serviceHost: fake,
|
|
165
|
+
wizard: async () => ({
|
|
166
|
+
dataDir: tmp,
|
|
167
|
+
uiPort: 19996,
|
|
168
|
+
libp2pPort: 14004,
|
|
169
|
+
upnpEnabled: true,
|
|
170
|
+
configureDdns: false,
|
|
171
|
+
ownCadre: false,
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
expect(fake.installCalls).toHaveLength(1);
|
|
175
|
+
expect(fake.installCalls[0]!.dataDir).toBe(tmp);
|
|
176
|
+
});
|
|
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
|
+
|
|
111
205
|
it('re-running install preserves the existing identity', async () => {
|
|
112
206
|
const fake = new FakeServiceHost();
|
|
113
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/browser.ts
CHANGED
|
@@ -1,42 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Best-effort URL opener. Never throws — if the platform launcher isn't
|
|
3
|
-
* available (headless box, missing xdg-utils, etc.) we just log the URL.
|
|
4
|
-
*
|
|
5
|
-
* Implemented inline rather than pulling in the `open` npm dep so the
|
|
6
|
-
* installer keeps its dependency tree small.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { spawn } from 'node:child_process';
|
|
10
|
-
|
|
11
|
-
export interface OpenBrowserResult {
|
|
12
|
-
attempted: boolean;
|
|
13
|
-
spawned: boolean;
|
|
14
|
-
/** Set when the spawn attempt errored synchronously. */
|
|
15
|
-
error?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function openBrowser(url: string, platform: NodeJS.Platform = process.platform): OpenBrowserResult {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return { attempted: true, spawned:
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort URL opener. Never throws — if the platform launcher isn't
|
|
3
|
+
* available (headless box, missing xdg-utils, etc.) we just log the URL.
|
|
4
|
+
*
|
|
5
|
+
* Implemented inline rather than pulling in the `open` npm dep so the
|
|
6
|
+
* installer keeps its dependency tree small.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
export interface OpenBrowserResult {
|
|
12
|
+
attempted: boolean;
|
|
13
|
+
spawned: boolean;
|
|
14
|
+
/** Set when the spawn attempt errored synchronously. */
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function openBrowser(url: string, platform: NodeJS.Platform = process.platform): OpenBrowserResult {
|
|
19
|
+
if (platform === 'darwin') return spawnOpener('open', [url]);
|
|
20
|
+
if (platform === 'win32') return spawnOpener('rundll32', ['url.dll,FileProtocolHandler', url]);
|
|
21
|
+
// linux + everything else POSIX-ish
|
|
22
|
+
return spawnOpener('xdg-open', [url]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function spawnOpener(cmd: string, args: string[]): OpenBrowserResult {
|
|
26
|
+
try {
|
|
27
|
+
const child = spawn(cmd, args, { detached: true, stdio: 'ignore' });
|
|
28
|
+
// The launcher's absence (ENOENT on a headless box without xdg-utils,
|
|
29
|
+
// etc.) is reported asynchronously via the child's `error` event, not
|
|
30
|
+
// by `spawn()` throwing. Without a listener, Node escalates to an
|
|
31
|
+
// unhandled-error crash and kills the install. Swallow it: this is a
|
|
32
|
+
// best-effort convenience, the install / UI commands have already
|
|
33
|
+
// printed the URL.
|
|
34
|
+
child.on('error', () => { /* best-effort */ });
|
|
35
|
+
child.unref();
|
|
36
|
+
return { attempted: true, spawned: true };
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return { attempted: true, spawned: false, error: (err as Error).message };
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/installer/config.ts
CHANGED
|
@@ -21,6 +21,41 @@ 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
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Non-secret push (FCM/APNs) settings. The actual private keys (and the FCM
|
|
41
|
+
* service-account / APNs key identifiers) live in the secret store — see
|
|
42
|
+
* `src/push/`. Only these app-config bits, which are safe to keep in plaintext
|
|
43
|
+
* `host.config.json`, are stored here. Absent ⇒ push is not configured.
|
|
44
|
+
*/
|
|
45
|
+
export interface PushSettings {
|
|
46
|
+
/** APNs app-config (the `.p8` private key + key/team ids stay in the secret store). */
|
|
47
|
+
apns?: {
|
|
48
|
+
/** App bundle id → APNs `apns-topic` header. */
|
|
49
|
+
bundleId: string;
|
|
50
|
+
/** `true` → production APNs host; false/undefined → sandbox (dev/TestFlight). */
|
|
51
|
+
production?: boolean;
|
|
52
|
+
};
|
|
53
|
+
/** Per-(peer,strand) anti-spam cooldown (ms) forwarded into the node's push block. */
|
|
54
|
+
cooldownMs?: number;
|
|
55
|
+
/** Per-strand burst-coalescing window (ms) forwarded into the node's push block. */
|
|
56
|
+
debounceMs?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
24
59
|
export interface HostConfigFile {
|
|
25
60
|
version: 2;
|
|
26
61
|
/** A stable per-install id; useful for log correlation and the control-network partyId. */
|
|
@@ -41,6 +76,27 @@ export interface HostConfigFile {
|
|
|
41
76
|
installerVersion: string;
|
|
42
77
|
/** Update-flow settings; defaults: { autoApply: false }. */
|
|
43
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;
|
|
85
|
+
/**
|
|
86
|
+
* Non-secret push settings (bundle id, sandbox/prod toggle, cooldown/debounce).
|
|
87
|
+
* Optional — absent when push is not configured. Private keys never live here;
|
|
88
|
+
* they are read from the secret store at node-spawn time (`src/push/`).
|
|
89
|
+
*/
|
|
90
|
+
push?: PushSettings;
|
|
91
|
+
}
|
|
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;
|
|
44
100
|
}
|
|
45
101
|
|
|
46
102
|
const CURRENT_VERSION = 2;
|
|
@@ -75,7 +131,7 @@ export function readHostConfig(filePath: string): HostConfigFile {
|
|
|
75
131
|
try {
|
|
76
132
|
parsed = JSON.parse(raw);
|
|
77
133
|
} catch (err) {
|
|
78
|
-
throw new Error(`Failed to parse host.config.json at ${filePath}: ${(err as Error).message}
|
|
134
|
+
throw new Error(`Failed to parse host.config.json at ${filePath}: ${(err as Error).message}`, { cause: err });
|
|
79
135
|
}
|
|
80
136
|
if (!parsed || typeof parsed !== 'object') {
|
|
81
137
|
throw new Error(`host.config.json at ${filePath} is not a JSON object`);
|
|
@@ -149,5 +205,9 @@ function isHostConfigShape(v: unknown): v is HostConfigFile {
|
|
|
149
205
|
if (!updates || typeof updates !== 'object') return false;
|
|
150
206
|
if (typeof updates.autoApply !== 'boolean') return false;
|
|
151
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
|
+
}
|
|
152
212
|
return true;
|
|
153
213
|
}
|
package/src/installer/index.ts
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
13
13
|
import { randomBytes } from 'node:crypto';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
14
|
import debug from 'debug';
|
|
16
15
|
|
|
17
16
|
import { NatStore } from '../nat/index.js';
|
|
@@ -38,9 +37,11 @@ import {
|
|
|
38
37
|
DEFAULT_UI_PORT,
|
|
39
38
|
DEFAULT_LIBP2P_PORT,
|
|
40
39
|
type WizardAnswers,
|
|
40
|
+
type WizardDefaults,
|
|
41
41
|
} from './wizard.js';
|
|
42
42
|
|
|
43
|
-
export type { HostConfigFile } from './config.js';
|
|
43
|
+
export type { HostConfigFile, PushSettings } from './config.js';
|
|
44
|
+
export { readHostConfig, updateHostConfig, writeHostConfig } from './config.js';
|
|
44
45
|
export type { WizardAnswers, WizardDefaults } from './wizard.js';
|
|
45
46
|
export type { ServiceHost, ServiceHostContext, ServiceHostStatus } from './service-host/index.js';
|
|
46
47
|
|
|
@@ -57,6 +58,8 @@ export interface InstallOptions {
|
|
|
57
58
|
libp2pPort?: number;
|
|
58
59
|
/** Disable UPnP probing on first run. */
|
|
59
60
|
noUpnp?: boolean;
|
|
61
|
+
/** Also run the host's own personal cadre here (founder persona). Default false. */
|
|
62
|
+
ownCadre?: boolean;
|
|
60
63
|
/** Open browser at end (default true; suppressed by --no-browser or non-TTY). */
|
|
61
64
|
openBrowser?: boolean;
|
|
62
65
|
/** Skip the enrollment-invite step (no QR printed). */
|
|
@@ -67,6 +70,8 @@ export interface InstallOptions {
|
|
|
67
70
|
nodePath?: string;
|
|
68
71
|
/** Test-only: stub the service-host registration. */
|
|
69
72
|
serviceHost?: ServiceHost;
|
|
73
|
+
/** Test-only: stub the interactive wizard. Production callers leave unset. */
|
|
74
|
+
wizard?: (defaults: WizardDefaults) => Promise<WizardAnswers>;
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
export interface InstallResult {
|
|
@@ -143,6 +148,7 @@ export class Installer {
|
|
|
143
148
|
installedAt: new Date().toISOString(),
|
|
144
149
|
installerVersion: this.installerVersion,
|
|
145
150
|
updates: { autoApply: false },
|
|
151
|
+
ownCadre: { enabled: answers.ownCadre },
|
|
146
152
|
};
|
|
147
153
|
const cfgPath = configPath(answers.dataDir);
|
|
148
154
|
writeHostConfig(cfgPath, cfg);
|
|
@@ -225,6 +231,7 @@ export class Installer {
|
|
|
225
231
|
libp2pPort: opts.libp2pPort ?? defaults.libp2pPort,
|
|
226
232
|
upnpEnabled: opts.noUpnp ? false : defaults.upnpEnabled,
|
|
227
233
|
configureDdns: false,
|
|
234
|
+
ownCadre: opts.ownCadre ?? defaults.ownCadre,
|
|
228
235
|
};
|
|
229
236
|
}
|
|
230
237
|
|
|
@@ -232,17 +239,21 @@ export class Installer {
|
|
|
232
239
|
if (opts.nonInteractive) {
|
|
233
240
|
return this.resolveAnswersFromOptions(opts);
|
|
234
241
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
// CLI flags overlay the platform defaults so the wizard's `[...]` hint
|
|
243
|
+
// shows the user's chosen value. Pressing Enter accepts it; typing a new
|
|
244
|
+
// value also works (and now actually has an effect — the previous
|
|
245
|
+
// implementation showed the platform default in the prompt and silently
|
|
246
|
+
// discarded user input because the CLI flag won post-hoc).
|
|
247
|
+
const platformDefaults = defaultsForPlatform(this.platform);
|
|
248
|
+
const defaults: WizardDefaults = {
|
|
249
|
+
dataDir: opts.dataDir ?? platformDefaults.dataDir,
|
|
250
|
+
uiPort: opts.uiPort ?? platformDefaults.uiPort,
|
|
251
|
+
libp2pPort: opts.libp2pPort ?? platformDefaults.libp2pPort,
|
|
252
|
+
upnpEnabled: opts.noUpnp ? false : platformDefaults.upnpEnabled,
|
|
253
|
+
ownCadre: opts.ownCadre ?? platformDefaults.ownCadre,
|
|
245
254
|
};
|
|
255
|
+
const wizard = opts.wizard ?? runWizard;
|
|
256
|
+
return await wizard(defaults);
|
|
246
257
|
}
|
|
247
258
|
}
|
|
248
259
|
|
|
@@ -85,6 +85,6 @@ function run(cmd: string, args: string[]): void {
|
|
|
85
85
|
try {
|
|
86
86
|
execFileSync(cmd, args, { stdio: 'inherit' });
|
|
87
87
|
} catch (err) {
|
|
88
|
-
throw new Error(`${cmd} ${args.join(' ')} failed: ${(err as Error).message}
|
|
88
|
+
throw new Error(`${cmd} ${args.join(' ')} failed: ${(err as Error).message}`, { cause: err });
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -83,6 +83,6 @@ function run(cmd: string, args: string[]): void {
|
|
|
83
83
|
try {
|
|
84
84
|
execFileSync(cmd, args, { stdio: 'inherit' });
|
|
85
85
|
} catch (err) {
|
|
86
|
-
throw new Error(`${cmd} ${args.join(' ')} failed: ${(err as Error).message}
|
|
86
|
+
throw new Error(`${cmd} ${args.join(' ')} failed: ${(err as Error).message}`, { cause: err });
|
|
87
87
|
}
|
|
88
88
|
}
|
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. */
|
|
@@ -67,7 +67,7 @@ describe('DdnsUpdater', () => {
|
|
|
67
67
|
it('no-op when externallyManaged', async () => {
|
|
68
68
|
const provider = makeProvider();
|
|
69
69
|
const secrets = makeSecrets({ [ddnsAccount('duckdns', 'token')]: 'T' });
|
|
70
|
-
|
|
70
|
+
const ip: string | null = '1.2.3.4';
|
|
71
71
|
const u = new DdnsUpdater({
|
|
72
72
|
settings: { ...baseSettings, externallyManaged: true },
|
|
73
73
|
secrets,
|