@serfab/cadre-host 0.7.1
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 +101 -0
- package/dist/auth/duration.d.ts +1 -0
- package/dist/auth/duration.js +32 -0
- package/dist/auth/duration.js.map +1 -0
- package/dist/auth/index.d.ts +9 -0
- package/dist/auth/index.js +8 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/trust-circle-store.d.ts +26 -0
- package/dist/auth/trust-circle-store.js +129 -0
- package/dist/auth/trust-circle-store.js.map +1 -0
- package/dist/auth/trust-circle.d.ts +91 -0
- package/dist/auth/trust-circle.js +230 -0
- package/dist/auth/trust-circle.js.map +1 -0
- package/dist/auth/types.d.ts +81 -0
- package/dist/auth/types.js +24 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/bin/host.d.ts +13 -0
- package/dist/bin/host.js +826 -0
- package/dist/bin/host.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/installer/browser.d.ts +14 -0
- package/dist/installer/browser.js +35 -0
- package/dist/installer/browser.js.map +1 -0
- package/dist/installer/config.d.ts +43 -0
- package/dist/installer/config.js +113 -0
- package/dist/installer/config.js.map +1 -0
- package/dist/installer/identity.d.ts +23 -0
- package/dist/installer/identity.js +36 -0
- package/dist/installer/identity.js.map +1 -0
- package/dist/installer/index.d.ts +83 -0
- package/dist/installer/index.js +204 -0
- package/dist/installer/index.js.map +1 -0
- package/dist/installer/paths.d.ts +33 -0
- package/dist/installer/paths.js +84 -0
- package/dist/installer/paths.js.map +1 -0
- package/dist/installer/platform.d.ts +9 -0
- package/dist/installer/platform.js +21 -0
- package/dist/installer/platform.js.map +1 -0
- package/dist/installer/service-host/index.d.ts +11 -0
- package/dist/installer/service-host/index.js +16 -0
- package/dist/installer/service-host/index.js.map +1 -0
- package/dist/installer/service-host/launchd.d.ts +16 -0
- package/dist/installer/service-host/launchd.js +79 -0
- package/dist/installer/service-host/launchd.js.map +1 -0
- package/dist/installer/service-host/nssm.d.ts +20 -0
- package/dist/installer/service-host/nssm.js +88 -0
- package/dist/installer/service-host/nssm.js.map +1 -0
- package/dist/installer/service-host/systemd.d.ts +16 -0
- package/dist/installer/service-host/systemd.js +78 -0
- package/dist/installer/service-host/systemd.js.map +1 -0
- package/dist/installer/service-host/template.d.ts +6 -0
- package/dist/installer/service-host/template.js +20 -0
- package/dist/installer/service-host/template.js.map +1 -0
- package/dist/installer/service-host/types.d.ts +40 -0
- package/dist/installer/service-host/types.js +6 -0
- package/dist/installer/service-host/types.js.map +1 -0
- package/dist/installer/wizard.d.ts +31 -0
- package/dist/installer/wizard.js +67 -0
- package/dist/installer/wizard.js.map +1 -0
- package/dist/nat/address-resolver.d.ts +22 -0
- package/dist/nat/address-resolver.js +31 -0
- package/dist/nat/address-resolver.js.map +1 -0
- package/dist/nat/ddns/duckdns.d.ts +12 -0
- package/dist/nat/ddns/duckdns.js +57 -0
- package/dist/nat/ddns/duckdns.js.map +1 -0
- package/dist/nat/ddns/index.d.ts +30 -0
- package/dist/nat/ddns/index.js +21 -0
- package/dist/nat/ddns/index.js.map +1 -0
- package/dist/nat/ddns/updater.d.ts +68 -0
- package/dist/nat/ddns/updater.js +194 -0
- package/dist/nat/ddns/updater.js.map +1 -0
- package/dist/nat/external-ip.d.ts +62 -0
- package/dist/nat/external-ip.js +114 -0
- package/dist/nat/external-ip.js.map +1 -0
- package/dist/nat/index.d.ts +22 -0
- package/dist/nat/index.js +16 -0
- package/dist/nat/index.js.map +1 -0
- package/dist/nat/nat-service.d.ts +101 -0
- package/dist/nat/nat-service.js +284 -0
- package/dist/nat/nat-service.js.map +1 -0
- package/dist/nat/nat-store.d.ts +19 -0
- package/dist/nat/nat-store.js +136 -0
- package/dist/nat/nat-store.js.map +1 -0
- package/dist/nat/port-mapper.d.ts +98 -0
- package/dist/nat/port-mapper.js +226 -0
- package/dist/nat/port-mapper.js.map +1 -0
- package/dist/nat/reachability.d.ts +19 -0
- package/dist/nat/reachability.js +29 -0
- package/dist/nat/reachability.js.map +1 -0
- package/dist/nat/secrets/file-store.d.ts +23 -0
- package/dist/nat/secrets/file-store.js +102 -0
- package/dist/nat/secrets/file-store.js.map +1 -0
- package/dist/nat/secrets/index.d.ts +36 -0
- package/dist/nat/secrets/index.js +56 -0
- package/dist/nat/secrets/index.js.map +1 -0
- package/dist/nat/secrets/keytar-store.d.ts +18 -0
- package/dist/nat/secrets/keytar-store.js +52 -0
- package/dist/nat/secrets/keytar-store.js.map +1 -0
- package/dist/nat/types.d.ts +120 -0
- package/dist/nat/types.js +24 -0
- package/dist/nat/types.js.map +1 -0
- package/dist/orchestrator/host-process-orchestrator.d.ts +87 -0
- package/dist/orchestrator/host-process-orchestrator.js +557 -0
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -0
- package/dist/orchestrator/index.d.ts +3 -0
- package/dist/orchestrator/index.js +2 -0
- package/dist/orchestrator/index.js.map +1 -0
- package/dist/orchestrator/log-rotator.d.ts +25 -0
- package/dist/orchestrator/log-rotator.js +56 -0
- package/dist/orchestrator/log-rotator.js.map +1 -0
- package/dist/orchestrator/pid-liveness.d.ts +11 -0
- package/dist/orchestrator/pid-liveness.js +26 -0
- package/dist/orchestrator/pid-liveness.js.map +1 -0
- package/dist/orchestrator/port-allocator.d.ts +16 -0
- package/dist/orchestrator/port-allocator.js +38 -0
- package/dist/orchestrator/port-allocator.js.map +1 -0
- package/dist/orchestrator/state-store.d.ts +32 -0
- package/dist/orchestrator/state-store.js +44 -0
- package/dist/orchestrator/state-store.js.map +1 -0
- package/dist/orchestrator/types.d.ts +85 -0
- package/dist/orchestrator/types.js +31 -0
- package/dist/orchestrator/types.js.map +1 -0
- package/dist/server/error-handler.d.ts +26 -0
- package/dist/server/error-handler.js +95 -0
- package/dist/server/error-handler.js.map +1 -0
- package/dist/server/events/bus.d.ts +17 -0
- package/dist/server/events/bus.js +36 -0
- package/dist/server/events/bus.js.map +1 -0
- package/dist/server/events/sse-route.d.ts +22 -0
- package/dist/server/events/sse-route.js +65 -0
- package/dist/server/events/sse-route.js.map +1 -0
- package/dist/server/events/types.d.ts +25 -0
- package/dist/server/events/types.js +8 -0
- package/dist/server/events/types.js.map +1 -0
- package/dist/server/index.d.ts +67 -0
- package/dist/server/index.js +149 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/origin-guard.d.ts +24 -0
- package/dist/server/origin-guard.js +72 -0
- package/dist/server/origin-guard.js.map +1 -0
- package/dist/server/routes/nat.d.ts +16 -0
- package/dist/server/routes/nat.js +42 -0
- package/dist/server/routes/nat.js.map +1 -0
- package/dist/server/routes/nodes.d.ts +23 -0
- package/dist/server/routes/nodes.js +128 -0
- package/dist/server/routes/nodes.js.map +1 -0
- package/dist/server/routes/settings.d.ts +26 -0
- package/dist/server/routes/settings.js +97 -0
- package/dist/server/routes/settings.js.map +1 -0
- package/dist/server/routes/status.d.ts +42 -0
- package/dist/server/routes/status.js +76 -0
- package/dist/server/routes/status.js.map +1 -0
- package/dist/server/routes/trust-circle.d.ts +17 -0
- package/dist/server/routes/trust-circle.js +35 -0
- package/dist/server/routes/trust-circle.js.map +1 -0
- package/dist/server/routes/update.d.ts +16 -0
- package/dist/server/routes/update.js +29 -0
- package/dist/server/routes/update.js.map +1 -0
- package/dist/server/server.d.ts +40 -0
- package/dist/server/server.js +90 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/settings-store.d.ts +24 -0
- package/dist/server/settings-store.js +27 -0
- package/dist/server/settings-store.js.map +1 -0
- package/dist/server/static.d.ts +25 -0
- package/dist/server/static.js +177 -0
- package/dist/server/static.js.map +1 -0
- package/dist/ui/assets/index-BwU5mJGF.css +1 -0
- package/dist/ui/assets/index-C9uChk2P.js +14 -0
- package/dist/ui/assets/index-C9uChk2P.js.map +1 -0
- package/dist/ui/index.html +14 -0
- package/dist/update/apply.d.ts +37 -0
- package/dist/update/apply.js +102 -0
- package/dist/update/apply.js.map +1 -0
- package/dist/update/index.d.ts +101 -0
- package/dist/update/index.js +228 -0
- package/dist/update/index.js.map +1 -0
- package/dist/update/manifest.d.ts +48 -0
- package/dist/update/manifest.js +152 -0
- package/dist/update/manifest.js.map +1 -0
- package/dist/update/release-key.d.ts +24 -0
- package/dist/update/release-key.js +52 -0
- package/dist/update/release-key.js.map +1 -0
- package/dist/update/store.d.ts +22 -0
- package/dist/update/store.js +81 -0
- package/dist/update/store.js.map +1 -0
- package/dist/update/types.d.ts +90 -0
- package/dist/update/types.js +22 -0
- package/dist/update/types.js.map +1 -0
- package/dist/update/version.d.ts +18 -0
- package/dist/update/version.js +76 -0
- package/dist/update/version.js.map +1 -0
- package/package.json +80 -0
- package/service/README.md +86 -0
- package/service/cadre-host.service.tmpl +16 -0
- package/service/com.serfab.cadre-host.plist.tmpl +35 -0
- package/service/install-service.ps1 +98 -0
- package/service/uninstall-service.ps1 +19 -0
- package/src/__tests__/cli-invite.smoke.test.ts +187 -0
- package/src/__tests__/cli-nat.smoke.test.ts +207 -0
- package/src/__tests__/cli.smoke.test.ts +41 -0
- package/src/__tests__/orchestrator.test.ts +570 -0
- package/src/auth/__tests__/duration.test.ts +28 -0
- package/src/auth/__tests__/trust-circle-integration.test.ts +128 -0
- package/src/auth/__tests__/trust-circle-store.test.ts +115 -0
- package/src/auth/__tests__/trust-circle.test.ts +334 -0
- package/src/auth/duration.ts +33 -0
- package/src/auth/index.ts +19 -0
- package/src/auth/trust-circle-store.ts +155 -0
- package/src/auth/trust-circle.ts +304 -0
- package/src/auth/types.ts +96 -0
- package/src/bin/host.ts +905 -0
- package/src/index.ts +167 -0
- package/src/installer/__tests__/config.test.ts +86 -0
- package/src/installer/__tests__/identity.test.ts +34 -0
- package/src/installer/__tests__/installer.smoke.test.ts +124 -0
- package/src/installer/__tests__/paths.test.ts +47 -0
- package/src/installer/__tests__/service-host.test.ts +62 -0
- package/src/installer/__tests__/wizard.test.ts +60 -0
- package/src/installer/browser.ts +42 -0
- package/src/installer/config.ts +153 -0
- package/src/installer/identity.ts +49 -0
- package/src/installer/index.ts +303 -0
- package/src/installer/paths.ts +109 -0
- package/src/installer/platform.ts +27 -0
- package/src/installer/service-host/index.ts +21 -0
- package/src/installer/service-host/launchd.ts +90 -0
- package/src/installer/service-host/nssm.ts +100 -0
- package/src/installer/service-host/systemd.ts +88 -0
- package/src/installer/service-host/template.ts +21 -0
- package/src/installer/service-host/types.ts +48 -0
- package/src/installer/wizard.ts +92 -0
- package/src/nat/__tests__/address-resolver.test.ts +82 -0
- package/src/nat/__tests__/ddns-duckdns.test.ts +83 -0
- package/src/nat/__tests__/ddns-updater.test.ts +253 -0
- package/src/nat/__tests__/external-ip.test.ts +124 -0
- package/src/nat/__tests__/nat-service.test.ts +305 -0
- package/src/nat/__tests__/nat-store.test.ts +93 -0
- package/src/nat/__tests__/port-mapper.test.ts +191 -0
- package/src/nat/__tests__/reachability.test.ts +24 -0
- package/src/nat/__tests__/secrets.test.ts +156 -0
- package/src/nat/address-resolver.ts +51 -0
- package/src/nat/ddns/duckdns.ts +76 -0
- package/src/nat/ddns/index.ts +55 -0
- package/src/nat/ddns/updater.ts +243 -0
- package/src/nat/external-ip.ts +154 -0
- package/src/nat/index.ts +71 -0
- package/src/nat/nat-service.ts +375 -0
- package/src/nat/nat-store.ts +154 -0
- package/src/nat/port-mapper.ts +329 -0
- package/src/nat/reachability.ts +34 -0
- package/src/nat/secrets/file-store.ts +130 -0
- package/src/nat/secrets/index.ts +82 -0
- package/src/nat/secrets/keytar-store.ts +67 -0
- package/src/nat/types.ts +157 -0
- package/src/orchestrator/host-process-orchestrator.ts +626 -0
- package/src/orchestrator/index.ts +3 -0
- package/src/orchestrator/log-rotator.ts +53 -0
- package/src/orchestrator/pid-liveness.ts +24 -0
- package/src/orchestrator/port-allocator.ts +43 -0
- package/src/orchestrator/state-store.ts +72 -0
- package/src/orchestrator/types.ts +95 -0
- package/src/server/__tests__/bus.test.ts +38 -0
- package/src/server/__tests__/error-handler.test.ts +96 -0
- package/src/server/__tests__/nodes-route.test.ts +179 -0
- package/src/server/__tests__/origin-guard.test.ts +72 -0
- package/src/server/__tests__/publishers.test.ts +216 -0
- package/src/server/__tests__/server.smoke.test.ts +178 -0
- package/src/server/__tests__/settings-route.test.ts +177 -0
- package/src/server/__tests__/sse-route.test.ts +119 -0
- package/src/server/__tests__/status-route.test.ts +113 -0
- package/src/server/error-handler.ts +108 -0
- package/src/server/events/bus.ts +42 -0
- package/src/server/events/sse-route.ts +81 -0
- package/src/server/events/types.ts +22 -0
- package/src/server/index.ts +209 -0
- package/src/server/origin-guard.ts +84 -0
- package/src/server/routes/nat.ts +63 -0
- package/src/server/routes/nodes.ts +147 -0
- package/src/server/routes/settings.ts +115 -0
- package/src/server/routes/status.ts +108 -0
- package/src/server/routes/trust-circle.ts +49 -0
- package/src/server/routes/update.ts +43 -0
- package/src/server/server.ts +122 -0
- package/src/server/settings-store.ts +37 -0
- package/src/server/static.ts +191 -0
- package/src/update/__tests__/apply.test.ts +101 -0
- package/src/update/__tests__/manifest.test.ts +93 -0
- package/src/update/__tests__/store.test.ts +81 -0
- package/src/update/__tests__/update-service.integration.test.ts +221 -0
- package/src/update/__tests__/version.test.ts +31 -0
- package/src/update/apply.ts +148 -0
- package/src/update/index.ts +303 -0
- package/src/update/manifest.ts +172 -0
- package/src/update/release-key.ts +55 -0
- package/src/update/store.ts +101 -0
- package/src/update/types.ts +109 -0
- package/src/update/version.ts +80 -0
package/dist/bin/host.js
ADDED
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entrypoint for cadre-host — the self-hosted cadre node manager.
|
|
4
|
+
*
|
|
5
|
+
* Most subcommands are still stubs at this stage (real implementations land
|
|
6
|
+
* in the sibling tickets cadre-host-nat, cadre-host-installer,
|
|
7
|
+
* cadre-host-local-ui).
|
|
8
|
+
*
|
|
9
|
+
* The `invite` and `trust` commands talk to the running cadre-host
|
|
10
|
+
* management API over loopback. They are thin HTTP clients — they don't
|
|
11
|
+
* spin up an inline service, so cadre-host must be running.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { Command } from 'commander';
|
|
18
|
+
import { parseDuration } from '../auth/duration.js';
|
|
19
|
+
import { Installer } from '../installer/index.js';
|
|
20
|
+
import { readHostConfig, updateHostConfig } from '../installer/config.js';
|
|
21
|
+
import { configPath as resolveConfigPath, defaultDataDir, defaultHostJs, defaultServiceDir, identityPath as resolveIdentityPath, } from '../installer/paths.js';
|
|
22
|
+
import { detectPlatform } from '../installer/platform.js';
|
|
23
|
+
import { createServiceHost } from '../installer/service-host/index.js';
|
|
24
|
+
import { UpdateService } from '../update/index.js';
|
|
25
|
+
import { HostProcessOrchestrator } from '../orchestrator/index.js';
|
|
26
|
+
import { TrustCircleService, TrustCircleStore } from '../auth/index.js';
|
|
27
|
+
import { NatService } from '../nat/index.js';
|
|
28
|
+
import { createLocalUiServer, HostSettingsStore } from '../server/index.js';
|
|
29
|
+
import { openBrowser } from '../installer/browser.js';
|
|
30
|
+
const DEFAULT_PORT = Number(process.env.CADRE_HOST_PORT ?? '8765');
|
|
31
|
+
/** libp2p peer-ID prefixes by key type: Ed25519, secp256k1, legacy RSA. */
|
|
32
|
+
const PEER_ID_PREFIXES = ['12D3Koo', '16Uiu2HAm', 'Qm'];
|
|
33
|
+
function looksLikePeerId(id) {
|
|
34
|
+
return PEER_ID_PREFIXES.some((p) => id.startsWith(p));
|
|
35
|
+
}
|
|
36
|
+
function resolvePort(raw) {
|
|
37
|
+
const n = Number(raw);
|
|
38
|
+
if (!Number.isFinite(n) || n <= 0 || n > 65535) {
|
|
39
|
+
// eslint-disable-next-line no-console
|
|
40
|
+
console.error(`Invalid --port: ${raw}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
return n;
|
|
44
|
+
}
|
|
45
|
+
const program = new Command();
|
|
46
|
+
program
|
|
47
|
+
.name('cadre-host')
|
|
48
|
+
.description('Sereus cadre node manager for self-hosted basement-PC deployments')
|
|
49
|
+
.version('0.6.0');
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// install — run the first-run wizard + register the system service
|
|
52
|
+
// ============================================================================
|
|
53
|
+
program
|
|
54
|
+
.command('install')
|
|
55
|
+
.description('Install cadre-host as a system service and run first-run setup')
|
|
56
|
+
.option('--non-interactive', 'Use defaults / CLI flags only; never prompt')
|
|
57
|
+
.option('--data-dir <path>', 'Override the data directory')
|
|
58
|
+
.option('--ui-port <port>', 'Override the management UI port', parseIntArg)
|
|
59
|
+
.option('--libp2p-port <port>', 'Override the cadre libp2p port', parseIntArg)
|
|
60
|
+
.option('--no-upnp', 'Disable UPnP/NAT-PMP probing on first run')
|
|
61
|
+
.option('--no-browser', 'Do not open a browser after install')
|
|
62
|
+
.option('--no-invite', 'Skip generating the first enrollment invite')
|
|
63
|
+
.option('--system', 'System-wide install (not yet supported in v1)')
|
|
64
|
+
.option('--node-path <path>', 'Override the node binary embedded in the service unit')
|
|
65
|
+
.action(async (opts) => {
|
|
66
|
+
const installer = new Installer();
|
|
67
|
+
try {
|
|
68
|
+
const result = await installer.install({
|
|
69
|
+
nonInteractive: Boolean(opts.nonInteractive),
|
|
70
|
+
...(opts.dataDir ? { dataDir: opts.dataDir } : {}),
|
|
71
|
+
...(typeof opts.uiPort === 'number' ? { uiPort: opts.uiPort } : {}),
|
|
72
|
+
...(typeof opts.libp2pPort === 'number' ? { libp2pPort: opts.libp2pPort } : {}),
|
|
73
|
+
noUpnp: opts.upnp === false,
|
|
74
|
+
openBrowser: opts.browser !== false,
|
|
75
|
+
noInvite: opts.invite === false,
|
|
76
|
+
system: Boolean(opts.system),
|
|
77
|
+
...(opts.nodePath ? { nodePath: opts.nodePath } : {}),
|
|
78
|
+
});
|
|
79
|
+
// eslint-disable-next-line no-console
|
|
80
|
+
console.log(`cadre-host installed.`);
|
|
81
|
+
// eslint-disable-next-line no-console
|
|
82
|
+
console.log(` Data dir: ${result.dataDir}`);
|
|
83
|
+
// eslint-disable-next-line no-console
|
|
84
|
+
console.log(` UI: ${result.uiUrl}`);
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.log(` Service: ${result.serviceName}`);
|
|
87
|
+
if (result.enrollmentInvite) {
|
|
88
|
+
printEnrollmentInvite(result.enrollmentInvite);
|
|
89
|
+
}
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
// eslint-disable-next-line no-console
|
|
94
|
+
console.error(`install failed: ${err.message}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// uninstall — stop + deregister the system service
|
|
100
|
+
// ============================================================================
|
|
101
|
+
program
|
|
102
|
+
.command('uninstall')
|
|
103
|
+
.description('Stop and uninstall the cadre-host service')
|
|
104
|
+
.option('--yes', 'Skip the confirmation prompt (for scripts)')
|
|
105
|
+
.option('--remove-data', 'Also remove the data directory (default: preserve)')
|
|
106
|
+
.option('--data-dir <path>', 'Override the data directory to clean up')
|
|
107
|
+
.action(async (opts) => {
|
|
108
|
+
const installer = new Installer();
|
|
109
|
+
try {
|
|
110
|
+
// --remove-data is destructive and irreversible (identity + trust circle
|
|
111
|
+
// are wiped). Require explicit --yes when stdin isn't a TTY, and prompt
|
|
112
|
+
// confirmation when it is.
|
|
113
|
+
if (opts.removeData && !opts.yes) {
|
|
114
|
+
if (!process.stdin.isTTY) {
|
|
115
|
+
console.error('uninstall: --remove-data requires --yes when stdin is not a TTY.');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const dataDir = opts.dataDir ?? '(default data directory)';
|
|
120
|
+
const confirmed = await confirmDestructive(`This will permanently delete ${dataDir} (identity, trust circle, NAT state). Continue? [y/N] `);
|
|
121
|
+
if (!confirmed) {
|
|
122
|
+
console.error('uninstall aborted.');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
await installer.uninstall({
|
|
128
|
+
yes: Boolean(opts.yes),
|
|
129
|
+
removeData: Boolean(opts.removeData),
|
|
130
|
+
...(opts.dataDir ? { dataDir: opts.dataDir } : {}),
|
|
131
|
+
});
|
|
132
|
+
// eslint-disable-next-line no-console
|
|
133
|
+
console.log('cadre-host uninstalled.');
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
// eslint-disable-next-line no-console
|
|
138
|
+
console.error(`uninstall failed: ${err.message}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// status — show service-host registration / running state
|
|
144
|
+
// ============================================================================
|
|
145
|
+
program
|
|
146
|
+
.command('status')
|
|
147
|
+
.description('Show running status of cadre-host and the cadre nodes it manages')
|
|
148
|
+
.action(async () => {
|
|
149
|
+
const installer = new Installer();
|
|
150
|
+
try {
|
|
151
|
+
const status = await installer.status();
|
|
152
|
+
// eslint-disable-next-line no-console
|
|
153
|
+
console.log(`Service installed: ${status.installed ? 'yes' : 'no'}`);
|
|
154
|
+
// eslint-disable-next-line no-console
|
|
155
|
+
console.log(`Service running: ${status.running ? 'yes' : 'no'}`);
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
// eslint-disable-next-line no-console
|
|
160
|
+
console.error(`status failed: ${err.message}`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// start — load config + identity, bind management API listener, wait
|
|
166
|
+
// ============================================================================
|
|
167
|
+
program
|
|
168
|
+
.command('start')
|
|
169
|
+
.description('Start cadre-host in the foreground')
|
|
170
|
+
.option('--data-dir <path>', 'Override the data directory (env: CADRE_HOST_DATA_DIR)')
|
|
171
|
+
.option('--no-tui', 'Reserved — currently a no-op (no TUI implemented yet)')
|
|
172
|
+
.action(async (opts) => {
|
|
173
|
+
try {
|
|
174
|
+
void opts.tui;
|
|
175
|
+
const platform = detectPlatform();
|
|
176
|
+
const dataDir = opts.dataDir ?? process.env.CADRE_HOST_DATA_DIR ?? defaultDataDir(platform);
|
|
177
|
+
const cfgPath = resolveConfigPath(dataDir);
|
|
178
|
+
if (!existsSync(cfgPath)) {
|
|
179
|
+
// eslint-disable-next-line no-console
|
|
180
|
+
console.error(`cadre-host start: ${cfgPath} not found. ` +
|
|
181
|
+
`Run \`cadre-host install\` first (or pass --data-dir to an existing install).`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const cfg = readHostConfig(cfgPath);
|
|
186
|
+
const idPath = resolveIdentityPath(cfg.dataDir);
|
|
187
|
+
if (!existsSync(idPath)) {
|
|
188
|
+
// eslint-disable-next-line no-console
|
|
189
|
+
console.error(`cadre-host start: identity file not found at ${idPath}. Re-run \`cadre-host install\`.`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// eslint-disable-next-line no-console
|
|
194
|
+
console.log(`cadre-host starting (dataDir=${cfg.dataDir}, uiPort=${cfg.uiPort})`);
|
|
195
|
+
// Update flow: notify-by-default; auto-apply opt-in via host.config.json.
|
|
196
|
+
// The local-UI ticket (6.5) wires the handlers into HTTP routes; here we
|
|
197
|
+
// just construct the service so the in-process timer + state file are
|
|
198
|
+
// populated.
|
|
199
|
+
const serviceHost = createServiceHost(platform);
|
|
200
|
+
const updateService = new UpdateService({
|
|
201
|
+
dataDir: cfg.dataDir,
|
|
202
|
+
currentVersion: readPackageVersionForStart(),
|
|
203
|
+
settings: cfg.updates,
|
|
204
|
+
...(cfg.updates.manifestUrl ? { manifestUrl: cfg.updates.manifestUrl } : {}),
|
|
205
|
+
restart: async () => {
|
|
206
|
+
try {
|
|
207
|
+
await serviceHost.restart({
|
|
208
|
+
nodePath: process.execPath,
|
|
209
|
+
hostJs: defaultHostJs(),
|
|
210
|
+
dataDir: cfg.dataDir,
|
|
211
|
+
serviceDir: defaultServiceDir(),
|
|
212
|
+
});
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
return err.message;
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
// Persist settings changes back to host.config.json on PUT /update/settings.
|
|
221
|
+
updateService.onSettingsChanged((next) => {
|
|
222
|
+
try {
|
|
223
|
+
updateHostConfig(cfgPath, { updates: next });
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
// eslint-disable-next-line no-console
|
|
227
|
+
console.error(`failed to persist updated settings to ${cfgPath}: ${err.message}`);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
// Don't block startup on the network round-trip — call and forget.
|
|
231
|
+
void updateService.check();
|
|
232
|
+
updateService.start();
|
|
233
|
+
// Wire the long-lived HTTP management server. The trust-circle and
|
|
234
|
+
// NAT services depend on a libp2p `CadreNodeLike`; v1 has no node-
|
|
235
|
+
// spawning path yet (that's a follow-up ticket), so we inject minimal
|
|
236
|
+
// stubs that fail loudly if a real call is attempted. The /auth/* and
|
|
237
|
+
// /nat/* routes are still mounted so the CLI's existing HTTP contract
|
|
238
|
+
// is intact — operations that genuinely need the libp2p node will
|
|
239
|
+
// surface a NatError / TrustCircleError, mapped to 500 by the server.
|
|
240
|
+
const orchestrator = new HostProcessOrchestrator({ rootDir: join(cfg.dataDir, 'orchestrator') });
|
|
241
|
+
await orchestrator.init();
|
|
242
|
+
const trustCircle = new TrustCircleService({
|
|
243
|
+
cadreNode: missingCadreNodeStub('trust-circle'),
|
|
244
|
+
store: new TrustCircleStore(cfg.dataDir),
|
|
245
|
+
});
|
|
246
|
+
const natService = new NatService({
|
|
247
|
+
rootDir: cfg.dataDir,
|
|
248
|
+
cadreNode: missingNatNodeStub(),
|
|
249
|
+
});
|
|
250
|
+
// Best-effort NAT start. Failures here aren't fatal — the local UI
|
|
251
|
+
// can still serve the trust circle, settings, etc.
|
|
252
|
+
try {
|
|
253
|
+
await natService.start();
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
// eslint-disable-next-line no-console
|
|
257
|
+
console.error(`NAT start failed: ${err.message}`);
|
|
258
|
+
}
|
|
259
|
+
const settingsStore = new HostSettingsStore({ dataDir: cfg.dataDir });
|
|
260
|
+
const server = createLocalUiServer({
|
|
261
|
+
uiPort: cfg.uiPort,
|
|
262
|
+
dataDir: cfg.dataDir,
|
|
263
|
+
orchestrator,
|
|
264
|
+
trustCircle,
|
|
265
|
+
nat: natService,
|
|
266
|
+
update: updateService,
|
|
267
|
+
settingsStore,
|
|
268
|
+
});
|
|
269
|
+
const { url, port } = await server.start();
|
|
270
|
+
if (port !== cfg.uiPort) {
|
|
271
|
+
// eslint-disable-next-line no-console
|
|
272
|
+
console.log(`(configured port ${cfg.uiPort} was in use — bound on ${port} instead)`);
|
|
273
|
+
}
|
|
274
|
+
// eslint-disable-next-line no-console
|
|
275
|
+
console.log(`cadre-host local UI: ${url}`);
|
|
276
|
+
await waitForTermination();
|
|
277
|
+
try {
|
|
278
|
+
await server.stop();
|
|
279
|
+
}
|
|
280
|
+
catch { /* ignore */ }
|
|
281
|
+
try {
|
|
282
|
+
await natService.stop();
|
|
283
|
+
}
|
|
284
|
+
catch { /* ignore */ }
|
|
285
|
+
updateService.stop();
|
|
286
|
+
// eslint-disable-next-line no-console
|
|
287
|
+
console.log('cadre-host stopped.');
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
// eslint-disable-next-line no-console
|
|
292
|
+
console.error(`start failed: ${err.message}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// ui — print the local-UI URL and open it in a browser
|
|
298
|
+
// ============================================================================
|
|
299
|
+
program
|
|
300
|
+
.command('ui')
|
|
301
|
+
.description('Print the local-UI URL and open it in the default browser')
|
|
302
|
+
.option('--data-dir <path>', 'Override the data directory (env: CADRE_HOST_DATA_DIR)')
|
|
303
|
+
.option('--no-browser', 'Print the URL but do not open a browser')
|
|
304
|
+
.action((opts) => {
|
|
305
|
+
try {
|
|
306
|
+
const platform = detectPlatform();
|
|
307
|
+
const dataDir = opts.dataDir ?? process.env.CADRE_HOST_DATA_DIR ?? defaultDataDir(platform);
|
|
308
|
+
const cfgPath = resolveConfigPath(dataDir);
|
|
309
|
+
if (!existsSync(cfgPath)) {
|
|
310
|
+
// eslint-disable-next-line no-console
|
|
311
|
+
console.error(`cadre-host ui: ${cfgPath} not found. Run \`cadre-host install\` first.`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const cfg = readHostConfig(cfgPath);
|
|
316
|
+
const url = `http://127.0.0.1:${cfg.uiPort}`;
|
|
317
|
+
// eslint-disable-next-line no-console
|
|
318
|
+
console.log(url);
|
|
319
|
+
if (opts.browser !== false) {
|
|
320
|
+
const res = openBrowser(url);
|
|
321
|
+
if (res.spawned) {
|
|
322
|
+
// eslint-disable-next-line no-console
|
|
323
|
+
console.log('(Opening in your default browser...)');
|
|
324
|
+
}
|
|
325
|
+
else if (res.error) {
|
|
326
|
+
// eslint-disable-next-line no-console
|
|
327
|
+
console.error(`(unable to launch browser: ${res.error})`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
process.exit(0);
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
// eslint-disable-next-line no-console
|
|
334
|
+
console.error(`ui failed: ${err.message}`);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
function parseIntArg(raw) {
|
|
339
|
+
const n = Number(raw);
|
|
340
|
+
if (!Number.isFinite(n) || !Number.isInteger(n)) {
|
|
341
|
+
throw new Error(`Invalid integer: ${raw}`);
|
|
342
|
+
}
|
|
343
|
+
return n;
|
|
344
|
+
}
|
|
345
|
+
function readPackageVersionForStart() {
|
|
346
|
+
try {
|
|
347
|
+
// dist/bin/host.js -> ../../package.json
|
|
348
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
349
|
+
const pkgPath = resolve(here, '..', '..', 'package.json');
|
|
350
|
+
const raw = readFileSync(pkgPath, 'utf8');
|
|
351
|
+
const parsed = JSON.parse(raw);
|
|
352
|
+
return parsed.version ?? '0.0.0-unknown';
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return '0.0.0-unknown';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Returns a `CadreNodeLike` for TrustCircleService that fails loudly on any
|
|
360
|
+
* libp2p call. cadre-host v1 doesn't yet spawn the host's own cadre node —
|
|
361
|
+
* that ticket is separate. The trust-circle HTTP endpoints still load the
|
|
362
|
+
* local labels/pending file (listing works), but issuing/redeeming an invite
|
|
363
|
+
* requires a real node and surfaces a 500.
|
|
364
|
+
*/
|
|
365
|
+
function missingCadreNodeStub(name) {
|
|
366
|
+
return {
|
|
367
|
+
async createInvite() { throw new Error(`${name}: cadre-host v1 has no inline cadre node — cannot issue invite`); },
|
|
368
|
+
async acceptPhone() { throw new Error(`${name}: cadre-host v1 has no inline cadre node — cannot redeem`); },
|
|
369
|
+
async removePeer() { throw new Error(`${name}: cadre-host v1 has no inline cadre node — cannot remove peer`); },
|
|
370
|
+
encodeInvite() { throw new Error(`${name}: cadre-host v1 has no inline cadre node — cannot encode invite`); },
|
|
371
|
+
getControlDatabase() { return null; },
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function missingNatNodeStub() {
|
|
375
|
+
return {
|
|
376
|
+
getPeerId() { return ''; },
|
|
377
|
+
getMultiaddrs() { return []; },
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function waitForTermination() {
|
|
381
|
+
return new Promise((resolveTerm) => {
|
|
382
|
+
const done = () => resolveTerm();
|
|
383
|
+
process.once('SIGINT', done);
|
|
384
|
+
process.once('SIGTERM', done);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
async function confirmDestructive(message) {
|
|
388
|
+
const { createInterface } = await import('node:readline');
|
|
389
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
390
|
+
try {
|
|
391
|
+
const answer = await new Promise((res) => rl.question(message, res));
|
|
392
|
+
return /^(y|yes)$/i.test(answer.trim());
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
rl.close();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const requireForQr = createRequire(import.meta.url);
|
|
399
|
+
function printEnrollmentInvite(invite) {
|
|
400
|
+
// Best-effort QR render — fall back to the bare token if the lib chokes.
|
|
401
|
+
// eslint-disable-next-line no-console
|
|
402
|
+
console.log('\nEnroll your first device with this invite:');
|
|
403
|
+
try {
|
|
404
|
+
const qr = requireForQr('qrcode-terminal');
|
|
405
|
+
qr.generate(invite.encodedInvite, { small: true }, (rendered) => {
|
|
406
|
+
// eslint-disable-next-line no-console
|
|
407
|
+
console.log(rendered);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
// eslint-disable-next-line no-console
|
|
412
|
+
console.error(`(qrcode-terminal unavailable: ${err.message})`);
|
|
413
|
+
}
|
|
414
|
+
// eslint-disable-next-line no-console
|
|
415
|
+
console.log(` ${invite.encodedInvite}`);
|
|
416
|
+
if (invite.expiresAt) {
|
|
417
|
+
// eslint-disable-next-line no-console
|
|
418
|
+
console.log(` (expires ${invite.expiresAt})`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
program
|
|
422
|
+
.command('invite')
|
|
423
|
+
.description('Generate an invite to add a member to the trust circle')
|
|
424
|
+
.argument('<label>', 'Display label for the new member (e.g. "Mom\'s phone")')
|
|
425
|
+
.option('--ttl <duration>', 'Invite lifetime (e.g. 24h, 7d, 30m)', '24h')
|
|
426
|
+
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
427
|
+
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
428
|
+
.action(async (label, opts) => {
|
|
429
|
+
let ttlMs;
|
|
430
|
+
try {
|
|
431
|
+
ttlMs = parseDuration(opts.ttl);
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
// eslint-disable-next-line no-console
|
|
435
|
+
console.error(`Invalid --ttl: ${err.message}`);
|
|
436
|
+
process.exit(1);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const port = resolvePort(opts.port);
|
|
440
|
+
const url = `http://${opts.host}:${port}/auth/invites`;
|
|
441
|
+
let response;
|
|
442
|
+
try {
|
|
443
|
+
response = await fetch(url, {
|
|
444
|
+
method: 'POST',
|
|
445
|
+
headers: { 'content-type': 'application/json' },
|
|
446
|
+
body: JSON.stringify({ label, ttlMs }),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
// eslint-disable-next-line no-console
|
|
451
|
+
console.error(`Failed to reach cadre-host at ${url}: ${err.message}\n` +
|
|
452
|
+
`Hint: is cadre-host running? Try \`cadre-host start\`.`);
|
|
453
|
+
process.exit(2);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (!response.ok) {
|
|
457
|
+
const text = await response.text().catch(() => '');
|
|
458
|
+
// eslint-disable-next-line no-console
|
|
459
|
+
console.error(`cadre-host returned ${response.status}: ${text || response.statusText}`);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const body = await response.json();
|
|
464
|
+
if (!body.encodedInvite) {
|
|
465
|
+
// eslint-disable-next-line no-console
|
|
466
|
+
console.error('cadre-host returned malformed response (missing encodedInvite)');
|
|
467
|
+
process.exit(1);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
// eslint-disable-next-line no-console
|
|
471
|
+
console.log(body.encodedInvite);
|
|
472
|
+
if (body.expiresAt) {
|
|
473
|
+
// eslint-disable-next-line no-console
|
|
474
|
+
console.error(`(expires at ${body.expiresAt})`);
|
|
475
|
+
}
|
|
476
|
+
process.exit(0);
|
|
477
|
+
});
|
|
478
|
+
const trust = program
|
|
479
|
+
.command('trust')
|
|
480
|
+
.description('Manage the trust circle (list members, revoke invites/members)');
|
|
481
|
+
trust
|
|
482
|
+
.command('list')
|
|
483
|
+
.description('List trust-circle members and pending invites')
|
|
484
|
+
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
485
|
+
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
486
|
+
.action(async (opts) => {
|
|
487
|
+
const url = `http://${opts.host}:${resolvePort(opts.port)}/auth/trust-circle`;
|
|
488
|
+
let response;
|
|
489
|
+
try {
|
|
490
|
+
response = await fetch(url);
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
// eslint-disable-next-line no-console
|
|
494
|
+
console.error(`Failed to reach cadre-host at ${url}: ${err.message}`);
|
|
495
|
+
process.exit(2);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (!response.ok) {
|
|
499
|
+
// eslint-disable-next-line no-console
|
|
500
|
+
console.error(`cadre-host returned ${response.status}: ${response.statusText}`);
|
|
501
|
+
process.exit(1);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const body = await response.json();
|
|
505
|
+
// eslint-disable-next-line no-console
|
|
506
|
+
console.log('Members:');
|
|
507
|
+
if (body.members.length === 0) {
|
|
508
|
+
// eslint-disable-next-line no-console
|
|
509
|
+
console.log(' (none)');
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
for (const m of body.members) {
|
|
513
|
+
const self = m.self ? ' [self]' : '';
|
|
514
|
+
// eslint-disable-next-line no-console
|
|
515
|
+
console.log(` ${m.peerId} ${m.label}${self}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// eslint-disable-next-line no-console
|
|
519
|
+
console.log('\nPending invites:');
|
|
520
|
+
if (body.pending.length === 0) {
|
|
521
|
+
// eslint-disable-next-line no-console
|
|
522
|
+
console.log(' (none)');
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
for (const p of body.pending) {
|
|
526
|
+
const expires = p.expiresAt ? ` (expires ${p.expiresAt})` : '';
|
|
527
|
+
// eslint-disable-next-line no-console
|
|
528
|
+
console.log(` ${p.token} ${p.label}${expires}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
process.exit(0);
|
|
532
|
+
});
|
|
533
|
+
trust
|
|
534
|
+
.command('revoke')
|
|
535
|
+
.description('Revoke a pending invite (by token) or remove a member (by peerId)')
|
|
536
|
+
.argument('<id>', 'Token (pending invite) or peerId (existing member) to remove')
|
|
537
|
+
.option('--kind <kind>', 'Force interpretation: "invite" | "member" (default: auto)', 'auto')
|
|
538
|
+
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
539
|
+
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
540
|
+
.action(async (id, opts) => {
|
|
541
|
+
const base = `http://${opts.host}:${resolvePort(opts.port)}`;
|
|
542
|
+
const kind = opts.kind === 'auto'
|
|
543
|
+
? (looksLikePeerId(id) ? 'member' : 'invite')
|
|
544
|
+
: opts.kind;
|
|
545
|
+
const path = kind === 'member'
|
|
546
|
+
? `/auth/members/${encodeURIComponent(id)}`
|
|
547
|
+
: `/auth/invites/${encodeURIComponent(id)}`;
|
|
548
|
+
let response;
|
|
549
|
+
try {
|
|
550
|
+
response = await fetch(`${base}${path}`, { method: 'DELETE' });
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
// eslint-disable-next-line no-console
|
|
554
|
+
console.error(`Failed to reach cadre-host at ${base}: ${err.message}`);
|
|
555
|
+
process.exit(2);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (!response.ok) {
|
|
559
|
+
const text = await response.text().catch(() => '');
|
|
560
|
+
// eslint-disable-next-line no-console
|
|
561
|
+
console.error(`cadre-host returned ${response.status}: ${text || response.statusText}`);
|
|
562
|
+
process.exit(1);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
// eslint-disable-next-line no-console
|
|
566
|
+
console.log(`revoked ${kind}: ${id}`);
|
|
567
|
+
process.exit(0);
|
|
568
|
+
});
|
|
569
|
+
// ============================================================================
|
|
570
|
+
// nat subcommands
|
|
571
|
+
// ============================================================================
|
|
572
|
+
const nat = program
|
|
573
|
+
.command('nat')
|
|
574
|
+
.description('Manage NAT traversal, port forwarding, and dynamic DNS');
|
|
575
|
+
nat
|
|
576
|
+
.command('status')
|
|
577
|
+
.description('Show NAT, external-IP, and DDNS status')
|
|
578
|
+
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
579
|
+
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
580
|
+
.option('--json', 'Output raw JSON instead of a human-readable summary')
|
|
581
|
+
.action(async (opts) => {
|
|
582
|
+
const url = `http://${opts.host}:${resolvePort(opts.port)}/nat/status`;
|
|
583
|
+
const body = await getJson(url);
|
|
584
|
+
if (opts.json) {
|
|
585
|
+
// eslint-disable-next-line no-console
|
|
586
|
+
console.log(JSON.stringify(body, null, 2));
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
printNatStatus(body);
|
|
590
|
+
}
|
|
591
|
+
process.exit(0);
|
|
592
|
+
});
|
|
593
|
+
nat
|
|
594
|
+
.command('test')
|
|
595
|
+
.description('Re-run reachability probes and print the result')
|
|
596
|
+
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
597
|
+
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
598
|
+
.option('--json', 'Output raw JSON instead of a human-readable summary')
|
|
599
|
+
.action(async (opts) => {
|
|
600
|
+
const url = `http://${opts.host}:${resolvePort(opts.port)}/nat/test`;
|
|
601
|
+
const body = await postJson(url, {});
|
|
602
|
+
if (opts.json) {
|
|
603
|
+
// eslint-disable-next-line no-console
|
|
604
|
+
console.log(JSON.stringify(body, null, 2));
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
printNatStatus(body);
|
|
608
|
+
}
|
|
609
|
+
process.exit(0);
|
|
610
|
+
});
|
|
611
|
+
nat
|
|
612
|
+
.command('settings')
|
|
613
|
+
.description('Update NAT settings (external port, UPnP toggle)')
|
|
614
|
+
.option('--external-port <port>', 'External port to map')
|
|
615
|
+
.option('--internal-port <port>', 'Internal libp2p port (defaults to external)')
|
|
616
|
+
.option('--no-upnp', 'Disable UPnP/NAT-PMP port mapping')
|
|
617
|
+
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
618
|
+
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
619
|
+
.action(async (opts) => {
|
|
620
|
+
const url = `http://${opts.host}:${resolvePort(opts.port)}/nat/settings`;
|
|
621
|
+
const patch = {};
|
|
622
|
+
if (opts.externalPort)
|
|
623
|
+
patch.externalPort = resolvePort(opts.externalPort);
|
|
624
|
+
if (opts.internalPort)
|
|
625
|
+
patch.internalPort = resolvePort(opts.internalPort);
|
|
626
|
+
// Commander assigns `upnp: false` when `--no-upnp` is passed.
|
|
627
|
+
if (opts.upnp === false)
|
|
628
|
+
patch.upnpEnabled = false;
|
|
629
|
+
if (opts.upnp === true)
|
|
630
|
+
patch.upnpEnabled = true;
|
|
631
|
+
const body = await putJson(url, patch);
|
|
632
|
+
printNatStatus(body);
|
|
633
|
+
process.exit(0);
|
|
634
|
+
});
|
|
635
|
+
const ddns = nat
|
|
636
|
+
.command('ddns')
|
|
637
|
+
.description('Configure dynamic DNS');
|
|
638
|
+
ddns
|
|
639
|
+
.command('set')
|
|
640
|
+
.description('Configure a DDNS provider (and store credentials in the keychain)')
|
|
641
|
+
.argument('<provider>', 'Provider ID (e.g. "duckdns")')
|
|
642
|
+
.option('--hostname <hostname>', 'Hostname to publish (e.g. foo.duckdns.org)')
|
|
643
|
+
.option('--token <token>', 'Provider token (a secret value; prompts if omitted)')
|
|
644
|
+
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
645
|
+
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
646
|
+
.action(async (provider, opts) => {
|
|
647
|
+
if (!opts.hostname) {
|
|
648
|
+
// eslint-disable-next-line no-console
|
|
649
|
+
console.error('--hostname is required');
|
|
650
|
+
process.exit(1);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const config = {};
|
|
654
|
+
if (opts.token)
|
|
655
|
+
config.token = opts.token;
|
|
656
|
+
if (!config.token) {
|
|
657
|
+
if (!process.stdin.isTTY) {
|
|
658
|
+
// eslint-disable-next-line no-console
|
|
659
|
+
console.error('Missing --token for provider "' + provider + '" and stdin is not a TTY. ' +
|
|
660
|
+
'Re-run with --token <secret>.');
|
|
661
|
+
process.exit(1);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
config.token = await readSecretLine('DDNS token: ');
|
|
666
|
+
}
|
|
667
|
+
catch (err) {
|
|
668
|
+
// eslint-disable-next-line no-console
|
|
669
|
+
console.error('Failed to read token: ' + err.message);
|
|
670
|
+
process.exit(1);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const url = `http://${opts.host}:${resolvePort(opts.port)}/nat/ddns`;
|
|
675
|
+
const body = await putJson(url, {
|
|
676
|
+
providerId: provider,
|
|
677
|
+
hostname: opts.hostname,
|
|
678
|
+
config,
|
|
679
|
+
externallyManaged: false,
|
|
680
|
+
});
|
|
681
|
+
printNatStatus(body);
|
|
682
|
+
process.exit(0);
|
|
683
|
+
});
|
|
684
|
+
ddns
|
|
685
|
+
.command('external')
|
|
686
|
+
.description('Declare an externally-managed DDNS hostname (cadre-host does not update it)')
|
|
687
|
+
.requiredOption('--hostname <hostname>', 'Hostname (no provider; cadre-host will not push updates)')
|
|
688
|
+
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
689
|
+
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
690
|
+
.action(async (opts) => {
|
|
691
|
+
const url = `http://${opts.host}:${resolvePort(opts.port)}/nat/settings`;
|
|
692
|
+
const body = await putJson(url, {
|
|
693
|
+
ddns: {
|
|
694
|
+
providerId: 'external',
|
|
695
|
+
hostname: opts.hostname,
|
|
696
|
+
externallyManaged: true,
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
printNatStatus(body);
|
|
700
|
+
process.exit(0);
|
|
701
|
+
});
|
|
702
|
+
async function getJson(url) {
|
|
703
|
+
return await callJson(url, 'GET');
|
|
704
|
+
}
|
|
705
|
+
async function postJson(url, body) {
|
|
706
|
+
return await callJson(url, 'POST', body);
|
|
707
|
+
}
|
|
708
|
+
async function putJson(url, body) {
|
|
709
|
+
return await callJson(url, 'PUT', body);
|
|
710
|
+
}
|
|
711
|
+
async function callJson(url, method, body) {
|
|
712
|
+
let response;
|
|
713
|
+
try {
|
|
714
|
+
const init = { method };
|
|
715
|
+
if (body !== undefined) {
|
|
716
|
+
init.headers = { 'content-type': 'application/json' };
|
|
717
|
+
init.body = JSON.stringify(body);
|
|
718
|
+
}
|
|
719
|
+
response = await fetch(url, init);
|
|
720
|
+
}
|
|
721
|
+
catch (err) {
|
|
722
|
+
// eslint-disable-next-line no-console
|
|
723
|
+
console.error(`Failed to reach cadre-host at ${url}: ${err.message}\n` +
|
|
724
|
+
`Hint: is cadre-host running? Try \`cadre-host start\`.`);
|
|
725
|
+
process.exit(2);
|
|
726
|
+
throw err;
|
|
727
|
+
}
|
|
728
|
+
if (!response.ok) {
|
|
729
|
+
const text = await response.text().catch(() => '');
|
|
730
|
+
// eslint-disable-next-line no-console
|
|
731
|
+
console.error(`cadre-host returned ${response.status}: ${text || response.statusText}`);
|
|
732
|
+
process.exit(1);
|
|
733
|
+
throw new Error('non-ok');
|
|
734
|
+
}
|
|
735
|
+
if (response.status === 204)
|
|
736
|
+
return {};
|
|
737
|
+
return await response.json();
|
|
738
|
+
}
|
|
739
|
+
function printNatStatus(s) {
|
|
740
|
+
// eslint-disable-next-line no-console
|
|
741
|
+
console.log(`Port mapping: ${s.portMode ?? '?'} (external ${s.externalPort ?? '?'} → internal ${s.internalPort ?? '?'})`);
|
|
742
|
+
if (s.routerExternalIp) {
|
|
743
|
+
// eslint-disable-next-line no-console
|
|
744
|
+
console.log(`Router IP: ${s.routerExternalIp}`);
|
|
745
|
+
}
|
|
746
|
+
// eslint-disable-next-line no-console
|
|
747
|
+
console.log(`External IP: ${s.externalIp ?? '(unknown)'}${s.cgnatDetected ? ' [CGNAT detected]' : ''}`);
|
|
748
|
+
// eslint-disable-next-line no-console
|
|
749
|
+
console.log(`Reachability: ${s.directReachability ?? 'unknown'}${s.lastTestedAt ? ` (tested ${s.lastTestedAt})` : ''}`);
|
|
750
|
+
if (s.mappingLeaseExpiresAt) {
|
|
751
|
+
// eslint-disable-next-line no-console
|
|
752
|
+
console.log(`Lease expires: ${s.mappingLeaseExpiresAt}`);
|
|
753
|
+
}
|
|
754
|
+
const ddns = s.ddns ?? {};
|
|
755
|
+
if (ddns.providerId || ddns.hostname) {
|
|
756
|
+
// eslint-disable-next-line no-console
|
|
757
|
+
console.log(`\nDDNS:`);
|
|
758
|
+
// eslint-disable-next-line no-console
|
|
759
|
+
console.log(` Provider: ${ddns.providerId ?? '(none)'}${ddns.externallyManaged ? ' [externally managed]' : ''}`);
|
|
760
|
+
// eslint-disable-next-line no-console
|
|
761
|
+
console.log(` Hostname: ${ddns.hostname ?? '(unset)'}`);
|
|
762
|
+
if (ddns.lastUpdateAt) {
|
|
763
|
+
const ok = ddns.lastUpdateOk ? 'OK' : 'FAIL';
|
|
764
|
+
// eslint-disable-next-line no-console
|
|
765
|
+
console.log(` Last update: ${ok} at ${ddns.lastUpdateAt}${ddns.lastError ? ` — ${ddns.lastError}` : ''}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
// eslint-disable-next-line no-console
|
|
770
|
+
console.log('\nDDNS: not configured');
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/** Read a single line from stdin with echo suppressed (TTY only). */
|
|
774
|
+
function readSecretLine(prompt) {
|
|
775
|
+
return new Promise((resolveLine, rejectLine) => {
|
|
776
|
+
const stdin = process.stdin;
|
|
777
|
+
const stdout = process.stdout;
|
|
778
|
+
const wasRaw = stdin.isRaw;
|
|
779
|
+
try {
|
|
780
|
+
stdout.write(prompt);
|
|
781
|
+
stdin.setRawMode(true);
|
|
782
|
+
}
|
|
783
|
+
catch (err) {
|
|
784
|
+
rejectLine(err);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
stdin.resume();
|
|
788
|
+
stdin.setEncoding('utf8');
|
|
789
|
+
let buf = '';
|
|
790
|
+
const onData = (chunk) => {
|
|
791
|
+
for (const ch of chunk) {
|
|
792
|
+
if (ch === '\n' || ch === '\r' || ch === '') {
|
|
793
|
+
stdin.removeListener('data', onData);
|
|
794
|
+
try {
|
|
795
|
+
stdin.setRawMode(wasRaw);
|
|
796
|
+
}
|
|
797
|
+
catch { /* ignore */ }
|
|
798
|
+
stdin.pause();
|
|
799
|
+
stdout.write('\n');
|
|
800
|
+
resolveLine(buf);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (ch === '') {
|
|
804
|
+
// Ctrl-C
|
|
805
|
+
stdin.removeListener('data', onData);
|
|
806
|
+
try {
|
|
807
|
+
stdin.setRawMode(wasRaw);
|
|
808
|
+
}
|
|
809
|
+
catch { /* ignore */ }
|
|
810
|
+
stdin.pause();
|
|
811
|
+
stdout.write('\n');
|
|
812
|
+
rejectLine(new Error('cancelled'));
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (ch === '' || ch === '\b') {
|
|
816
|
+
buf = buf.slice(0, -1);
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
buf += ch;
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
stdin.on('data', onData);
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
program.parseAsync();
|
|
826
|
+
//# sourceMappingURL=host.js.map
|