@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
package/src/bin/host.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { Command } from 'commander';
|
|
|
21
21
|
|
|
22
22
|
import { parseDuration } from '../auth/duration.js';
|
|
23
23
|
import { Installer } from '../installer/index.js';
|
|
24
|
-
import { readHostConfig, updateHostConfig } from '../installer/config.js';
|
|
24
|
+
import { readHostConfig, updateHostConfig, hostOwnsCadre } from '../installer/config.js';
|
|
25
25
|
import {
|
|
26
26
|
configPath as resolveConfigPath,
|
|
27
27
|
defaultDataDir,
|
|
@@ -34,6 +34,14 @@ import { createServiceHost } from '../installer/service-host/index.js';
|
|
|
34
34
|
import { UpdateService } from '../update/index.js';
|
|
35
35
|
import { HostProcessOrchestrator } from '../orchestrator/index.js';
|
|
36
36
|
import { TrustCircleService, TrustCircleStore } from '../auth/index.js';
|
|
37
|
+
import {
|
|
38
|
+
GrantService,
|
|
39
|
+
GrantStore,
|
|
40
|
+
DonationService,
|
|
41
|
+
DonationStore,
|
|
42
|
+
DONATION_AWAITING_SEED_TTL_MS,
|
|
43
|
+
DONATION_REAP_SWEEP_MS,
|
|
44
|
+
} from '../donation/index.js';
|
|
37
45
|
import { NatService } from '../nat/index.js';
|
|
38
46
|
import { createSecretsStore } from '../nat/secrets/index.js';
|
|
39
47
|
import {
|
|
@@ -43,7 +51,7 @@ import {
|
|
|
43
51
|
clearPushSecret,
|
|
44
52
|
pushStatus,
|
|
45
53
|
} from '../push/index.js';
|
|
46
|
-
import {
|
|
54
|
+
import { OwnerNodeClient } from '../owner/index.js';
|
|
47
55
|
import { createLocalUiServer, HostSettingsStore } from '../server/index.js';
|
|
48
56
|
import { openBrowser } from '../installer/browser.js';
|
|
49
57
|
|
|
@@ -86,6 +94,7 @@ program
|
|
|
86
94
|
.option('--no-upnp', 'Disable UPnP/NAT-PMP probing on first run')
|
|
87
95
|
.option('--no-browser', 'Do not open a browser after install')
|
|
88
96
|
.option('--no-invite', 'Skip generating the first enrollment invite')
|
|
97
|
+
.option('--own-cadre', 'Also run the host\'s own personal cadre here (founder persona; default: donor-only)')
|
|
89
98
|
.option('--system', 'System-wide install (not yet supported in v1)')
|
|
90
99
|
.option('--node-path <path>', 'Override the node binary embedded in the service unit')
|
|
91
100
|
.action(async (opts: {
|
|
@@ -96,6 +105,7 @@ program
|
|
|
96
105
|
upnp?: boolean;
|
|
97
106
|
browser?: boolean;
|
|
98
107
|
invite?: boolean;
|
|
108
|
+
ownCadre?: boolean;
|
|
99
109
|
system?: boolean;
|
|
100
110
|
nodePath?: string;
|
|
101
111
|
}) => {
|
|
@@ -109,6 +119,7 @@ program
|
|
|
109
119
|
noUpnp: opts.upnp === false,
|
|
110
120
|
openBrowser: opts.browser !== false,
|
|
111
121
|
noInvite: opts.invite === false,
|
|
122
|
+
ownCadre: opts.ownCadre === true,
|
|
112
123
|
system: Boolean(opts.system),
|
|
113
124
|
...(opts.nodePath ? { nodePath: opts.nodePath } : {}),
|
|
114
125
|
});
|
|
@@ -259,11 +270,14 @@ program
|
|
|
259
270
|
void updateService.check();
|
|
260
271
|
updateService.start();
|
|
261
272
|
|
|
262
|
-
// Wire the long-lived HTTP management server.
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
273
|
+
// Wire the long-lived HTTP management server. cadre-host's primary role
|
|
274
|
+
// is **node donor**: it spawns generic cadre nodes for *other people's*
|
|
275
|
+
// cadres on request (the donation grant layer below) and never needs an
|
|
276
|
+
// owner node of its own for that. Running the host's **own** personal
|
|
277
|
+
// cadre here — the "founder" persona — is opt-in via `ownCadre.enabled`
|
|
278
|
+
// (see docs/cadre-host.md § Two roles: donor and founder). Only when it
|
|
279
|
+
// is enabled do we spawn the owner node and bring up trust-circle + NAT.
|
|
280
|
+
// The manager never joins the control network (docs/cadre-host.md
|
|
267
281
|
// § Control-plane separation).
|
|
268
282
|
// Push (FCM/APNs) credentials are resolved fresh on every node spawn so a
|
|
269
283
|
// restart re-reads the secret store (rotated keys) and nothing raw is
|
|
@@ -276,52 +290,102 @@ program
|
|
|
276
290
|
});
|
|
277
291
|
await orchestrator.init();
|
|
278
292
|
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
identityPath: idPath,
|
|
285
|
-
partyId: cfg.installId,
|
|
286
|
-
libp2pPort: cfg.libp2pPort,
|
|
287
|
-
});
|
|
288
|
-
console.log('cadre-host: authority node spawned');
|
|
289
|
-
} catch (err) {
|
|
290
|
-
console.error(`authority node spawn failed: ${(err as Error).message}`);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// The client reads the admin endpoint lazily so a node restart's fresh
|
|
294
|
-
// bearer token is picked up automatically.
|
|
295
|
-
const authority = new AuthorityNodeClient(() => orchestrator.getAuthorityAdminEndpoint());
|
|
293
|
+
// Donation grant layer — the always-on donor surface. Local-only:
|
|
294
|
+
// issue/validate/revoke are pure store ops (no node round-trip), so it
|
|
295
|
+
// needs no owner-node handle. The loopback `/grants-admin` surface is
|
|
296
|
+
// mounted by createLocalUiServer regardless of the founder persona.
|
|
297
|
+
const grantService = new GrantService({ store: new GrantStore(cfg.dataDir) });
|
|
296
298
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
299
|
+
// Donation lifecycle service — consumes a validated grant to actually
|
|
300
|
+
// spawn a donated node into the requester's cadre. Drives the
|
|
301
|
+
// grantee-facing `/grants` surface (mounted by createLocalUiServer below).
|
|
302
|
+
const donationService = new DonationService({
|
|
303
|
+
orchestrator,
|
|
304
|
+
grants: grantService,
|
|
305
|
+
store: new DonationStore(cfg.dataDir),
|
|
304
306
|
});
|
|
305
307
|
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
308
|
+
// Reap orphaned donations: a requester that provisioned a node but never
|
|
309
|
+
// presented a seed leaves an `awaiting_seed` child holding host ports.
|
|
310
|
+
// Sweep once at startup (for records recovered from disk by
|
|
311
|
+
// orchestrator.init()), then periodically.
|
|
312
|
+
const reapStale = (): void => {
|
|
313
|
+
void donationService
|
|
314
|
+
.reapStaleAwaitingSeed(DONATION_AWAITING_SEED_TTL_MS)
|
|
315
|
+
.catch((err) => console.error(`donation reap failed: ${(err as Error).message}`));
|
|
316
|
+
};
|
|
317
|
+
reapStale();
|
|
318
|
+
const reapTimer = setInterval(reapStale, DONATION_REAP_SWEEP_MS);
|
|
319
|
+
reapTimer.unref();
|
|
320
|
+
|
|
321
|
+
// Founder stack (opt-in). Absent `ownCadre.enabled`, cadre-host is a pure
|
|
322
|
+
// donor: no owner node, and the /auth + /nat surfaces stay unmounted
|
|
323
|
+
// (they 404). Per-donated-node WAN reachability is deferred to
|
|
324
|
+
// backlog/feat-cadre-host-wan-grant-reachability, so v1 donor mode is
|
|
325
|
+
// loopback-only — nothing for NatService to map without an owner node.
|
|
326
|
+
let trustCircle: TrustCircleService | undefined;
|
|
327
|
+
let natService: NatService | undefined;
|
|
328
|
+
if (hostOwnsCadre(cfg)) {
|
|
329
|
+
// Spawn the owner node. Best-effort: a spawn failure leaves the
|
|
330
|
+
// management API up (trust-circle listing degrades to local labels,
|
|
331
|
+
// owner ops return 503) rather than taking down the whole process.
|
|
312
332
|
try {
|
|
313
|
-
await
|
|
333
|
+
await orchestrator.ensureOwnerNode({
|
|
334
|
+
identityPath: idPath,
|
|
335
|
+
partyId: cfg.installId,
|
|
336
|
+
libp2pPort: cfg.libp2pPort,
|
|
337
|
+
});
|
|
338
|
+
console.log('cadre-host: owner node spawned (host-own-cadre enabled)');
|
|
314
339
|
} catch (err) {
|
|
315
|
-
console.error(`
|
|
340
|
+
console.error(`owner node spawn failed: ${(err as Error).message}`);
|
|
316
341
|
}
|
|
317
|
-
});
|
|
318
342
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
343
|
+
// The client reads the admin endpoint lazily so a node restart's fresh
|
|
344
|
+
// bearer token is picked up automatically.
|
|
345
|
+
const owner = new OwnerNodeClient(() => orchestrator.getOwnerAdminEndpoint());
|
|
346
|
+
|
|
347
|
+
trustCircle = new TrustCircleService({
|
|
348
|
+
cadreNode: owner,
|
|
349
|
+
store: new TrustCircleStore(cfg.dataDir),
|
|
350
|
+
});
|
|
351
|
+
natService = new NatService({
|
|
352
|
+
rootDir: cfg.dataDir,
|
|
353
|
+
cadreNode: owner,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Push NAT-resolved invite addresses to the node on every NAT change.
|
|
357
|
+
// NatService.start() also fires this once as an initial push, retried
|
|
358
|
+
// until the freshly spawned owner node accepts it (bounded; the
|
|
359
|
+
// management API that mints invites comes up only after start() resolves).
|
|
360
|
+
natService.onAddressesChanged(async (addresses) => {
|
|
361
|
+
if (addresses.length === 0) return;
|
|
362
|
+
try {
|
|
363
|
+
await owner.pushInviteAddresses(addresses);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
console.error(`invite-address push failed: ${(err as Error).message}`);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Best-effort NAT start. Failures here aren't fatal — the local UI
|
|
370
|
+
// can still serve the trust circle, settings, etc.
|
|
371
|
+
try {
|
|
372
|
+
await natService.start();
|
|
373
|
+
} catch (err) {
|
|
374
|
+
console.error(`NAT start failed: ${(err as Error).message}`);
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
// Donor-only. If ownCadre was toggled off after a prior founder run,
|
|
378
|
+
// orchestrator.init() re-attaches the still-running owner child (it would
|
|
379
|
+
// otherwise linger in listNodes with no trustCircle/nat wired, serving
|
|
380
|
+
// the host's own cadre despite being disabled). Reap it now so a disabled
|
|
381
|
+
// own-cadre is actually stopped — its workdir + control-DB persist on
|
|
382
|
+
// disk, so toggling ownCadre back on re-spawns it from saved config.
|
|
383
|
+
try {
|
|
384
|
+
await orchestrator.stopOwnerNode();
|
|
385
|
+
} catch (err) {
|
|
386
|
+
console.error(`owner node reap failed: ${(err as Error).message}`);
|
|
387
|
+
}
|
|
388
|
+
console.log('cadre-host: node-donor mode (host-own-cadre disabled — no owner node; /auth + /nat inactive)');
|
|
325
389
|
}
|
|
326
390
|
|
|
327
391
|
const settingsStore = new HostSettingsStore({ dataDir: cfg.dataDir });
|
|
@@ -329,9 +393,11 @@ program
|
|
|
329
393
|
uiPort: cfg.uiPort,
|
|
330
394
|
dataDir: cfg.dataDir,
|
|
331
395
|
orchestrator,
|
|
332
|
-
trustCircle,
|
|
333
|
-
nat: natService,
|
|
396
|
+
...(trustCircle ? { trustCircle } : {}),
|
|
397
|
+
...(natService ? { nat: natService } : {}),
|
|
334
398
|
update: updateService,
|
|
399
|
+
grants: grantService,
|
|
400
|
+
donations: donationService,
|
|
335
401
|
settingsStore,
|
|
336
402
|
});
|
|
337
403
|
const { url, port } = await server.start();
|
|
@@ -341,9 +407,10 @@ program
|
|
|
341
407
|
console.log(`cadre-host local UI: ${url}`);
|
|
342
408
|
|
|
343
409
|
await waitForTermination();
|
|
410
|
+
clearInterval(reapTimer);
|
|
344
411
|
try { await server.stop(); } catch { /* ignore */ }
|
|
345
|
-
try { await natService
|
|
346
|
-
try { await orchestrator.
|
|
412
|
+
try { await natService?.stop(); } catch { /* ignore */ }
|
|
413
|
+
try { await orchestrator.stopOwnerNode(); } catch { /* ignore */ }
|
|
347
414
|
updateService.stop();
|
|
348
415
|
console.log('cadre-host stopped.');
|
|
349
416
|
process.exit(0);
|
|
@@ -449,6 +516,22 @@ function printEnrollmentInvite(invite: { encodedInvite: string; expiresAt?: stri
|
|
|
449
516
|
}
|
|
450
517
|
}
|
|
451
518
|
|
|
519
|
+
function printGrantToken(token: string, withQr: boolean): void {
|
|
520
|
+
if (withQr) {
|
|
521
|
+
// Best-effort QR render — fall back to the bare token if the lib chokes.
|
|
522
|
+
try {
|
|
523
|
+
const qr = requireForQr('qrcode-terminal') as { generate: (text: string, opts?: { small?: boolean }, cb?: (s: string) => void) => void };
|
|
524
|
+
qr.generate(token, { small: true }, (rendered) => {
|
|
525
|
+
console.error(rendered);
|
|
526
|
+
});
|
|
527
|
+
} catch (err) {
|
|
528
|
+
console.error(`(qrcode-terminal unavailable: ${(err as Error).message})`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// The token goes to stdout alone so it can be piped/copied; metadata to stderr.
|
|
532
|
+
console.log(token);
|
|
533
|
+
}
|
|
534
|
+
|
|
452
535
|
program
|
|
453
536
|
.command('invite')
|
|
454
537
|
.description('Generate an invite to add a member to the trust circle')
|
|
@@ -589,6 +672,143 @@ trust
|
|
|
589
672
|
process.exit(0);
|
|
590
673
|
});
|
|
591
674
|
|
|
675
|
+
// ============================================================================
|
|
676
|
+
// grant subcommands — donation grant tokens (who may ask this host for a node)
|
|
677
|
+
// ============================================================================
|
|
678
|
+
//
|
|
679
|
+
// A grant token lets one grantee (friend/family) present a Bearer credential to
|
|
680
|
+
// ask this host to donate cadre nodes, up to a per-grantee cap. These commands
|
|
681
|
+
// are thin HTTP clients of the loopback `/grants-admin` admin surface — no
|
|
682
|
+
// bearer (same-machine admin), same posture as `invite` / `trust`. The
|
|
683
|
+
// grantee-facing provisioning surface lands in the donation-service ticket.
|
|
684
|
+
|
|
685
|
+
const grant = program
|
|
686
|
+
.command('grant')
|
|
687
|
+
.description('Manage donation grant tokens (who may ask this host to donate a node)');
|
|
688
|
+
|
|
689
|
+
grant
|
|
690
|
+
.command('issue')
|
|
691
|
+
.description('Issue a grant token for one grantee (prints the token + QR)')
|
|
692
|
+
.argument('<label>', 'Display label for the grantee (e.g. "Alice\'s cadre")')
|
|
693
|
+
.option('--max-nodes <n>', 'Max concurrently-live donated nodes this grant may hold', parseIntArg)
|
|
694
|
+
.option('--ttl <duration>', 'Grant lifetime (e.g. 30d, 12h); omit for no expiry')
|
|
695
|
+
.option('--no-qr', 'Print only the token, no QR code')
|
|
696
|
+
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
697
|
+
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
698
|
+
.action(async (label: string, opts: {
|
|
699
|
+
maxNodes?: number;
|
|
700
|
+
ttl?: string;
|
|
701
|
+
qr?: boolean;
|
|
702
|
+
port: string;
|
|
703
|
+
host: string;
|
|
704
|
+
}) => {
|
|
705
|
+
const payload: { label: string; maxNodes?: number; ttlMs?: number } = { label };
|
|
706
|
+
if (typeof opts.maxNodes === 'number') payload.maxNodes = opts.maxNodes;
|
|
707
|
+
if (opts.ttl) {
|
|
708
|
+
try {
|
|
709
|
+
payload.ttlMs = parseDuration(opts.ttl);
|
|
710
|
+
} catch (err) {
|
|
711
|
+
console.error(`Invalid --ttl: ${(err as Error).message}`);
|
|
712
|
+
process.exit(1);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const url = `http://${opts.host}:${resolvePort(opts.port)}/grants-admin`;
|
|
718
|
+
let response: Response;
|
|
719
|
+
try {
|
|
720
|
+
response = await fetch(url, {
|
|
721
|
+
method: 'POST',
|
|
722
|
+
headers: { 'content-type': 'application/json' },
|
|
723
|
+
body: JSON.stringify(payload),
|
|
724
|
+
});
|
|
725
|
+
} catch (err) {
|
|
726
|
+
console.error(
|
|
727
|
+
`Failed to reach cadre-host at ${url}: ${(err as Error).message}\n` +
|
|
728
|
+
`Hint: is cadre-host running? Try \`cadre-host start\`.`,
|
|
729
|
+
);
|
|
730
|
+
process.exit(2);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (!response.ok) {
|
|
734
|
+
const text = await response.text().catch(() => '');
|
|
735
|
+
console.error(`cadre-host returned ${response.status}: ${text || response.statusText}`);
|
|
736
|
+
process.exit(1);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const body = await response.json() as { grant?: { token?: string; maxNodes?: number; expiresAt?: string } };
|
|
740
|
+
const issued = body.grant;
|
|
741
|
+
if (!issued?.token) {
|
|
742
|
+
console.error('cadre-host returned malformed response (missing grant token)');
|
|
743
|
+
process.exit(1);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
printGrantToken(issued.token, opts.qr !== false);
|
|
747
|
+
console.error(`(maxNodes ${issued.maxNodes ?? '?'}${issued.expiresAt ? `, expires ${issued.expiresAt}` : ', no expiry'})`);
|
|
748
|
+
process.exit(0);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
grant
|
|
752
|
+
.command('list')
|
|
753
|
+
.description('List issued grant tokens')
|
|
754
|
+
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
755
|
+
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
756
|
+
.action(async (opts: { port: string; host: string }) => {
|
|
757
|
+
const url = `http://${opts.host}:${resolvePort(opts.port)}/grants-admin`;
|
|
758
|
+
let response: Response;
|
|
759
|
+
try {
|
|
760
|
+
response = await fetch(url);
|
|
761
|
+
} catch (err) {
|
|
762
|
+
console.error(`Failed to reach cadre-host at ${url}: ${(err as Error).message}`);
|
|
763
|
+
process.exit(2);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (!response.ok) {
|
|
767
|
+
console.error(`cadre-host returned ${response.status}: ${response.statusText}`);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const body = await response.json() as {
|
|
772
|
+
grants: Array<{ token: string; label: string; maxNodes: number; expiresAt?: string; revokedAt?: string }>;
|
|
773
|
+
};
|
|
774
|
+
console.log('Grants:');
|
|
775
|
+
if (body.grants.length === 0) {
|
|
776
|
+
console.log(' (none)');
|
|
777
|
+
} else {
|
|
778
|
+
for (const g of body.grants) {
|
|
779
|
+
const state = g.revokedAt ? ' [revoked]' : (g.expiresAt ? ` (expires ${g.expiresAt})` : '');
|
|
780
|
+
console.log(` ${g.token} ${g.label} max=${g.maxNodes}${state}`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
process.exit(0);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
grant
|
|
787
|
+
.command('revoke')
|
|
788
|
+
.description('Revoke a grant token (blocks future requests; live nodes are not torn down)')
|
|
789
|
+
.argument('<token>', 'Grant token to revoke')
|
|
790
|
+
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
791
|
+
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
792
|
+
.action(async (token: string, opts: { port: string; host: string }) => {
|
|
793
|
+
const base = `http://${opts.host}:${resolvePort(opts.port)}`;
|
|
794
|
+
let response: Response;
|
|
795
|
+
try {
|
|
796
|
+
response = await fetch(`${base}/grants-admin/${encodeURIComponent(token)}`, { method: 'DELETE' });
|
|
797
|
+
} catch (err) {
|
|
798
|
+
console.error(`Failed to reach cadre-host at ${base}: ${(err as Error).message}`);
|
|
799
|
+
process.exit(2);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (!response.ok) {
|
|
803
|
+
const text = await response.text().catch(() => '');
|
|
804
|
+
console.error(`cadre-host returned ${response.status}: ${text || response.statusText}`);
|
|
805
|
+
process.exit(1);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
console.log(`revoked grant: ${token}`);
|
|
809
|
+
process.exit(0);
|
|
810
|
+
});
|
|
811
|
+
|
|
592
812
|
// ============================================================================
|
|
593
813
|
// nat subcommands
|
|
594
814
|
// ============================================================================
|
|
@@ -741,7 +961,7 @@ ddns
|
|
|
741
961
|
// do NOT go through the running management API). Private keys land in the OS
|
|
742
962
|
// keychain (keytar) or the 0600 file-store fallback; the non-secret bits (APNs
|
|
743
963
|
// bundle id / sandbox toggle, cooldown/debounce) land in host.config.json. New
|
|
744
|
-
// credentials take effect on the next
|
|
964
|
+
// credentials take effect on the next owner-node (re)spawn — run
|
|
745
965
|
// `cadre-host` restart (or restart the service) to apply them immediately.
|
|
746
966
|
|
|
747
967
|
const push = program
|
|
@@ -771,7 +991,7 @@ push
|
|
|
771
991
|
clientEmail: opts.clientEmail,
|
|
772
992
|
privateKey,
|
|
773
993
|
});
|
|
774
|
-
console.log('✓ FCM credentials stored. Restart cadre-host to apply on the
|
|
994
|
+
console.log('✓ FCM credentials stored. Restart cadre-host to apply on the owner node.');
|
|
775
995
|
process.exit(0);
|
|
776
996
|
});
|
|
777
997
|
|
|
@@ -808,7 +1028,7 @@ push
|
|
|
808
1028
|
});
|
|
809
1029
|
console.log(
|
|
810
1030
|
`✓ APNs credentials stored (${opts.production ? 'production' : 'sandbox'}). ` +
|
|
811
|
-
`Restart cadre-host to apply on the
|
|
1031
|
+
`Restart cadre-host to apply on the owner node.`,
|
|
812
1032
|
);
|
|
813
1033
|
process.exit(0);
|
|
814
1034
|
});
|