@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
|
@@ -41,9 +41,9 @@ export interface CadreNodeLike {
|
|
|
41
41
|
* membership management.
|
|
42
42
|
*
|
|
43
43
|
* Membership is canonical in the cadre control DB (`CadrePeer` table),
|
|
44
|
-
* reached over the
|
|
44
|
+
* reached over the owner node's admin channel; labels and pending tokens
|
|
45
45
|
* live in the host-local `TrustCircleStore`. When the node is unreachable,
|
|
46
|
-
*
|
|
46
|
+
* owner operations surface `node_unavailable` (→ 503) and `list()`
|
|
47
47
|
* degrades to the local labels file.
|
|
48
48
|
*/
|
|
49
49
|
export declare class TrustCircleService {
|
|
@@ -60,7 +60,7 @@ export declare class TrustCircleService {
|
|
|
60
60
|
constructor(opts: TrustCircleServiceOptions);
|
|
61
61
|
/**
|
|
62
62
|
* Issue a new invite. Generates a token, persists a pending row, and
|
|
63
|
-
* delegates to cadre-core to embed the host's
|
|
63
|
+
* delegates to cadre-core to embed the host's owner addresses.
|
|
64
64
|
*/
|
|
65
65
|
issueInvite(opts: {
|
|
66
66
|
label: string;
|
|
@@ -85,7 +85,7 @@ export declare class TrustCircleService {
|
|
|
85
85
|
*
|
|
86
86
|
* Crash-safety tradeoff: if the host crashes between `acceptPhone` success
|
|
87
87
|
* and the durable `removePending`, a retry will dial in fine but the
|
|
88
|
-
*
|
|
88
|
+
* owner node's `acceptPhone` will reject the second `CadrePeer` insert
|
|
89
89
|
* with a PK constraint error (cadre-core does not upsert). That surfaces to
|
|
90
90
|
* the redeemer as a non-`node_unavailable` failure; the admin can revoke
|
|
91
91
|
* the lingering pending row. This is a narrower window than the prior
|
|
@@ -110,7 +110,7 @@ export declare class TrustCircleService {
|
|
|
110
110
|
/**
|
|
111
111
|
* UI snapshot. Joins the canonical CadrePeer list (fetched over the admin
|
|
112
112
|
* channel) with local labels; prunes any orphan labels (peer is no longer
|
|
113
|
-
* authorised). When the
|
|
113
|
+
* authorised). When the owner node is unreachable, degrades to the
|
|
114
114
|
* local labels file so listing keeps working while the node is down.
|
|
115
115
|
*/
|
|
116
116
|
list(): Promise<TrustCircleSnapshot>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import debug from 'debug';
|
|
3
|
-
import {
|
|
3
|
+
import { OwnerNodeUnavailableError } from '../owner/owner-node-client.js';
|
|
4
4
|
import { TrustCircleError } from './types.js';
|
|
5
5
|
const log = debug('cadre:host:trust-circle');
|
|
6
6
|
/** Default invite lifetime: 24 hours. */
|
|
@@ -12,9 +12,9 @@ const TOKEN_BYTES = 32;
|
|
|
12
12
|
* membership management.
|
|
13
13
|
*
|
|
14
14
|
* Membership is canonical in the cadre control DB (`CadrePeer` table),
|
|
15
|
-
* reached over the
|
|
15
|
+
* reached over the owner node's admin channel; labels and pending tokens
|
|
16
16
|
* live in the host-local `TrustCircleStore`. When the node is unreachable,
|
|
17
|
-
*
|
|
17
|
+
* owner operations surface `node_unavailable` (→ 503) and `list()`
|
|
18
18
|
* degrades to the local labels file.
|
|
19
19
|
*/
|
|
20
20
|
export class TrustCircleService {
|
|
@@ -32,7 +32,7 @@ export class TrustCircleService {
|
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* Issue a new invite. Generates a token, persists a pending row, and
|
|
35
|
-
* delegates to cadre-core to embed the host's
|
|
35
|
+
* delegates to cadre-core to embed the host's owner addresses.
|
|
36
36
|
*/
|
|
37
37
|
async issueInvite(opts) {
|
|
38
38
|
const label = normaliseLabel(opts.label);
|
|
@@ -70,7 +70,7 @@ export class TrustCircleService {
|
|
|
70
70
|
*
|
|
71
71
|
* Crash-safety tradeoff: if the host crashes between `acceptPhone` success
|
|
72
72
|
* and the durable `removePending`, a retry will dial in fine but the
|
|
73
|
-
*
|
|
73
|
+
* owner node's `acceptPhone` will reject the second `CadrePeer` insert
|
|
74
74
|
* with a PK constraint error (cadre-core does not upsert). That surfaces to
|
|
75
75
|
* the redeemer as a non-`node_unavailable` failure; the admin can revoke
|
|
76
76
|
* the lingering pending row. This is a narrower window than the prior
|
|
@@ -106,7 +106,7 @@ export class TrustCircleService {
|
|
|
106
106
|
// matches by construction) and re-checks expiration.
|
|
107
107
|
const reconstructed = {
|
|
108
108
|
partyId: '',
|
|
109
|
-
|
|
109
|
+
ownerAddrs: [],
|
|
110
110
|
token: pending.token,
|
|
111
111
|
createdAt: new Date(pending.createdAt).getTime(),
|
|
112
112
|
...(pending.expiresAt ? { expiresAt: new Date(pending.expiresAt).getTime() } : {}),
|
|
@@ -172,7 +172,7 @@ export class TrustCircleService {
|
|
|
172
172
|
/**
|
|
173
173
|
* UI snapshot. Joins the canonical CadrePeer list (fetched over the admin
|
|
174
174
|
* channel) with local labels; prunes any orphan labels (peer is no longer
|
|
175
|
-
* authorised). When the
|
|
175
|
+
* authorised). When the owner node is unreachable, degrades to the
|
|
176
176
|
* local labels file so listing keeps working while the node is down.
|
|
177
177
|
*/
|
|
178
178
|
async list() {
|
|
@@ -189,7 +189,7 @@ export class TrustCircleService {
|
|
|
189
189
|
controlMembers = await this.cadreNode.listMembers();
|
|
190
190
|
}
|
|
191
191
|
catch (err) {
|
|
192
|
-
if (!(err instanceof
|
|
192
|
+
if (!(err instanceof OwnerNodeUnavailableError))
|
|
193
193
|
throw err;
|
|
194
194
|
controlMembers = null;
|
|
195
195
|
}
|
|
@@ -232,8 +232,8 @@ export class TrustCircleService {
|
|
|
232
232
|
* `never` so callers can `.catch(err => this.toDomainError(err))`.
|
|
233
233
|
*/
|
|
234
234
|
toDomainError(err) {
|
|
235
|
-
if (err instanceof
|
|
236
|
-
throw new TrustCircleError('node_unavailable', `
|
|
235
|
+
if (err instanceof OwnerNodeUnavailableError) {
|
|
236
|
+
throw new TrustCircleError('node_unavailable', `Owner node unavailable: ${err.message}`);
|
|
237
237
|
}
|
|
238
238
|
throw err instanceof Error ? err : new Error(String(err));
|
|
239
239
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trust-circle.js","sourceRoot":"","sources":["../../src/auth/trust-circle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"trust-circle.js","sourceRoot":"","sources":["../../src/auth/trust-circle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAS1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,GAAG,GAAG,KAAK,CAAC,yBAAyB,CAAC,CAAC;AAE7C,yCAAyC;AACzC,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEzD,wDAAwD;AACxD,MAAM,WAAW,GAAG,EAAE,CAAC;AA4BvB;;;;;;;;;GASG;AACH,MAAM,OAAO,kBAAkB;IAY7B,YAAY,IAA+B;QAR3C;;;;;WAKG;QACc,wBAAmB,GAAG,IAAI,GAAG,EAAU,CAAC;QAGvD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACxB,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,WAAW,CAAC,IAAuC;QAMvD,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,qBAAqB,CAAC;QAClD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,gBAAgB,CAAC,eAAe,EAAE,kBAAkB,KAAK,EAAE,CAAC,CAAC;QACzE,CAAC;QAED,MAAM,KAAK,GAAG,aAAa,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC3B,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,CAAC;QAEtD,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,IAAI,CAAC,SAAS;aACnD,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC;aAC1B,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;QAE3C,MAAM,OAAO,GAAkB;YAC7B,KAAK;YACL,KAAK;YACL,SAAS,EAAE,OAAO,CAAC,WAAW,EAAE;YAChC,SAAS,EAAE,SAAS,CAAC,WAAW,EAAE;SACnC,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAE/B,GAAG,CAAC,+CAA+C,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC;QAE5F,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IACrD,CAAC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,KAAK,CAAC,YAAY,CAAC,IAAuC;QACxD,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAC/B,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;YACtB,MAAM,IAAI,gBAAgB,CAAC,eAAe,EAAE,+BAA+B,CAAC,CAAC;QAC/E,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAC7C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,gBAAgB,CAAC,kBAAkB,EAAE,sCAAsC,CAAC,CAAC;QACzF,CAAC;QAED,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;YACtB,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAC9C,IAAI,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,EAAE,CAAC;gBACnE,sDAAsD;gBACtD,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;gBAChC,MAAM,IAAI,gBAAgB,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAC;YAC9D,CAAC;QACH,CAAC;QAED,sEAAsE;QACtE,uEAAuE;QACvE,8DAA8D;QAC9D,IAAI,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACxC,MAAM,IAAI,gBAAgB,CAAC,kBAAkB,EAAE,sCAAsC,CAAC,CAAC;QACzF,CAAC;QACD,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAEpC,IAAI,CAAC;YACH,uEAAuE;YACvE,wEAAwE;YACxE,qDAAqD;YACrD,MAAM,aAAa,GAAgB;gBACjC,OAAO,EAAE,EAAE;gBACX,UAAU,EAAE,EAAE;gBACd,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,SAAS,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;gBAChD,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACnF,CAAC;YAEF,MAAM,IAAI,CAAC,SAAS;iBACjB,WAAW,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,aAAa,CAAC;iBAC1D,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;YAE3C,uEAAuE;YACvE,qEAAqE;YACrE,kEAAkE;YAClE,+CAA+C;YAC/C,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAEhC,wEAAwE;YACxE,kEAAkE;YAClE,mCAAmC;YACnC,MAAM,MAAM,GAAsB;gBAChC,MAAM;gBACN,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;aAClC,CAAC;YACF,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YAE7B,GAAG,CAAC,gDAAgD,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;YAEpF,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QAC1C,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;IAED,4DAA4D;IAC5D,KAAK,CAAC,aAAa,CAAC,KAAa;QAC/B,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;YACrC,MAAM,IAAI,gBAAgB,CAAC,WAAW,EAAE,2BAA2B,KAAK,EAAE,CAAC,CAAC;QAC9E,CAAC;QACD,GAAG,CAAC,mCAAmC,EAAE,KAAK,CAAC,CAAC;IAClD,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,YAAY,CAAC,MAAc;QAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAE9C,IAAI,CAAC,OAAO,IAAI,CAAC,SAAS,EAAE,CAAC;YAC3B,MAAM,IAAI,gBAAgB,CAAC,WAAW,EAAE,mBAAmB,MAAM,EAAE,CAAC,CAAC;QACvE,CAAC;QAED,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;QAClF,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QAClC,CAAC;QACD,GAAG,CAAC,0BAA0B,EAAE,MAAM,CAAC,CAAC;IAC1C,CAAC;IAED,kEAAkE;IAClE,KAAK,CAAC,QAAQ,CAAC,MAAc;QAC3B,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC/C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,IAAI;QACR,MAAM,MAAM,GAAG,IAAI,GAAG,EAA6C,CAAC;QACpE,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACzC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAClG,CAAC;QAED,MAAM,OAAO,GAAwB,EAAE,CAAC;QAExC,0EAA0E;QAC1E,qEAAqE;QACrE,qBAAqB;QACrB,IAAI,cAAgD,CAAC;QACrD,IAAI,CAAC;YACH,cAAc,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC;QACtD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,CAAC,GAAG,YAAY,yBAAyB,CAAC;gBAAE,MAAM,GAAG,CAAC;YAC3D,cAAc,GAAG,IAAI,CAAC;QACxB,CAAC;QAED,IAAI,cAAc,EAAE,CAAC;YACnB,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;YACtC,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,cAAc,EAAE,CAAC;gBACxC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBACxB,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBACjC,OAAO,CAAC,IAAI,CAAC;oBACX,MAAM;oBACN,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI,MAAM;oBAC7B,OAAO,EAAE,KAAK,EAAE,OAAO,IAAI,EAAE;oBAC7B,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBACvC,CAAC,CAAC;YACL,CAAC;YACD,yEAAyE;YACzE,uEAAuE;YACvE,eAAe;YACf,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;gBACnC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC7B,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;gBAClC,CAAC;YACH,CAAC;QACH,CAAC;aAAM,CAAC;YACN,4DAA4D;YAC5D,KAAK,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,MAAM,EAAE,CAAC;gBACnC,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;QAEzC,+BAA+B;QAC/B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;QAE/D,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;IAC9B,CAAC;IAED;;;;OAIG;IACK,aAAa,CAAC,GAAY;QAChC,IAAI,GAAG,YAAY,yBAAyB,EAAE,CAAC;YAC7C,MAAM,IAAI,gBAAgB,CAAC,kBAAkB,EAAE,2BAA2B,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3F,CAAC;QACD,MAAM,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5D,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAA2B;IACnE,OAAO;QACL,KAAK,CAAC,UAAU,CAAC,IAAI;YACnB,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;gBAC5C,MAAM,IAAI,gBAAgB,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAC;YACnE,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;YACnF,OAAO;gBACL,aAAa,EAAE,MAAM,CAAC,aAAa;gBACnC,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC3E,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,YAAY,CAAC,KAAK;YACtB,MAAM,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACrC,CAAC;QACD,KAAK,CAAC,YAAY,CAAC,MAAM;YACvB,MAAM,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACrC,CAAC;QACD,KAAK,CAAC,eAAe;YACnB,OAAO,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;QAC9B,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,aAAa;IACpB,mEAAmE;IACnE,OAAO,WAAW,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACxD,CAAC;AAED,SAAS,cAAc,CAAC,KAAc;IACpC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,IAAI,gBAAgB,CAAC,eAAe,EAAE,wBAAwB,CAAC,CAAC;IACxE,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,gBAAgB,CAAC,eAAe,EAAE,yBAAyB,CAAC,CAAC;IACzE,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QACzB,MAAM,IAAI,gBAAgB,CAAC,eAAe,EAAE,uCAAuC,CAAC,CAAC;IACvF,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -57,7 +57,7 @@ export interface TrustCircleSnapshot {
|
|
|
57
57
|
* - `not_found` → 404 (unknown token / unknown peer)
|
|
58
58
|
* - `expired` → 410 (invite past expiresAt)
|
|
59
59
|
* - `already_redeemed` → 409 (token was used and is gone)
|
|
60
|
-
* - `node_unavailable` → 503 (
|
|
60
|
+
* - `node_unavailable` → 503 (owner node admin channel down/not ready)
|
|
61
61
|
* - default → 500
|
|
62
62
|
*/
|
|
63
63
|
export interface TrustCircleHandlers {
|
|
@@ -75,7 +75,7 @@ export interface TrustCircleHandlers {
|
|
|
75
75
|
}
|
|
76
76
|
/** Error codes thrown by TrustCircleService. */
|
|
77
77
|
export type TrustCircleErrorCode = 'not_found' | 'expired' | 'already_redeemed' | 'invalid_label' | 'invalid_token' | 'storage_error'
|
|
78
|
-
/** The
|
|
78
|
+
/** The owner node's admin channel is unreachable / not ready. */
|
|
79
79
|
| 'node_unavailable';
|
|
80
80
|
/** Typed error carrying a stable `code` for HTTP mapping. */
|
|
81
81
|
export declare class TrustCircleError extends Error {
|
package/dist/bin/host.js
CHANGED
|
@@ -17,17 +17,18 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
17
17
|
import { Command } from 'commander';
|
|
18
18
|
import { parseDuration } from '../auth/duration.js';
|
|
19
19
|
import { Installer } from '../installer/index.js';
|
|
20
|
-
import { readHostConfig, updateHostConfig } from '../installer/config.js';
|
|
20
|
+
import { readHostConfig, updateHostConfig, hostOwnsCadre } from '../installer/config.js';
|
|
21
21
|
import { configPath as resolveConfigPath, defaultDataDir, defaultHostJs, defaultServiceDir, identityPath as resolveIdentityPath, } from '../installer/paths.js';
|
|
22
22
|
import { detectPlatform } from '../installer/platform.js';
|
|
23
23
|
import { createServiceHost } from '../installer/service-host/index.js';
|
|
24
24
|
import { UpdateService } from '../update/index.js';
|
|
25
25
|
import { HostProcessOrchestrator } from '../orchestrator/index.js';
|
|
26
26
|
import { TrustCircleService, TrustCircleStore } from '../auth/index.js';
|
|
27
|
+
import { GrantService, GrantStore, DonationService, DonationStore, DONATION_AWAITING_SEED_TTL_MS, DONATION_REAP_SWEEP_MS, } from '../donation/index.js';
|
|
27
28
|
import { NatService } from '../nat/index.js';
|
|
28
29
|
import { createSecretsStore } from '../nat/secrets/index.js';
|
|
29
30
|
import { resolvePushCredentials, setFcmSecret, setApnsSecret, clearPushSecret, pushStatus, } from '../push/index.js';
|
|
30
|
-
import {
|
|
31
|
+
import { OwnerNodeClient } from '../owner/index.js';
|
|
31
32
|
import { createLocalUiServer, HostSettingsStore } from '../server/index.js';
|
|
32
33
|
import { openBrowser } from '../installer/browser.js';
|
|
33
34
|
const DEFAULT_PORT = Number(process.env.CADRE_HOST_PORT ?? '8765');
|
|
@@ -62,6 +63,7 @@ program
|
|
|
62
63
|
.option('--no-upnp', 'Disable UPnP/NAT-PMP probing on first run')
|
|
63
64
|
.option('--no-browser', 'Do not open a browser after install')
|
|
64
65
|
.option('--no-invite', 'Skip generating the first enrollment invite')
|
|
66
|
+
.option('--own-cadre', 'Also run the host\'s own personal cadre here (founder persona; default: donor-only)')
|
|
65
67
|
.option('--system', 'System-wide install (not yet supported in v1)')
|
|
66
68
|
.option('--node-path <path>', 'Override the node binary embedded in the service unit')
|
|
67
69
|
.action(async (opts) => {
|
|
@@ -75,6 +77,7 @@ program
|
|
|
75
77
|
noUpnp: opts.upnp === false,
|
|
76
78
|
openBrowser: opts.browser !== false,
|
|
77
79
|
noInvite: opts.invite === false,
|
|
80
|
+
ownCadre: opts.ownCadre === true,
|
|
78
81
|
system: Boolean(opts.system),
|
|
79
82
|
...(opts.nodePath ? { nodePath: opts.nodePath } : {}),
|
|
80
83
|
});
|
|
@@ -218,11 +221,14 @@ program
|
|
|
218
221
|
// Don't block startup on the network round-trip — call and forget.
|
|
219
222
|
void updateService.check();
|
|
220
223
|
updateService.start();
|
|
221
|
-
// Wire the long-lived HTTP management server.
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
224
|
+
// Wire the long-lived HTTP management server. cadre-host's primary role
|
|
225
|
+
// is **node donor**: it spawns generic cadre nodes for *other people's*
|
|
226
|
+
// cadres on request (the donation grant layer below) and never needs an
|
|
227
|
+
// owner node of its own for that. Running the host's **own** personal
|
|
228
|
+
// cadre here — the "founder" persona — is opt-in via `ownCadre.enabled`
|
|
229
|
+
// (see docs/cadre-host.md § Two roles: donor and founder). Only when it
|
|
230
|
+
// is enabled do we spawn the owner node and bring up trust-circle + NAT.
|
|
231
|
+
// The manager never joins the control network (docs/cadre-host.md
|
|
226
232
|
// § Control-plane separation).
|
|
227
233
|
// Push (FCM/APNs) credentials are resolved fresh on every node spawn so a
|
|
228
234
|
// restart re-reads the secret store (rotated keys) and nothing raw is
|
|
@@ -234,61 +240,112 @@ program
|
|
|
234
240
|
pushResolver: () => resolvePushCredentials(pushSecrets, readHostConfig(cfgPath).push),
|
|
235
241
|
});
|
|
236
242
|
await orchestrator.init();
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
console.error(`authority node spawn failed: ${err.message}`);
|
|
250
|
-
}
|
|
251
|
-
// The client reads the admin endpoint lazily so a node restart's fresh
|
|
252
|
-
// bearer token is picked up automatically.
|
|
253
|
-
const authority = new AuthorityNodeClient(() => orchestrator.getAuthorityAdminEndpoint());
|
|
254
|
-
const trustCircle = new TrustCircleService({
|
|
255
|
-
cadreNode: authority,
|
|
256
|
-
store: new TrustCircleStore(cfg.dataDir),
|
|
257
|
-
});
|
|
258
|
-
const natService = new NatService({
|
|
259
|
-
rootDir: cfg.dataDir,
|
|
260
|
-
cadreNode: authority,
|
|
243
|
+
// Donation grant layer — the always-on donor surface. Local-only:
|
|
244
|
+
// issue/validate/revoke are pure store ops (no node round-trip), so it
|
|
245
|
+
// needs no owner-node handle. The loopback `/grants-admin` surface is
|
|
246
|
+
// mounted by createLocalUiServer regardless of the founder persona.
|
|
247
|
+
const grantService = new GrantService({ store: new GrantStore(cfg.dataDir) });
|
|
248
|
+
// Donation lifecycle service — consumes a validated grant to actually
|
|
249
|
+
// spawn a donated node into the requester's cadre. Drives the
|
|
250
|
+
// grantee-facing `/grants` surface (mounted by createLocalUiServer below).
|
|
251
|
+
const donationService = new DonationService({
|
|
252
|
+
orchestrator,
|
|
253
|
+
grants: grantService,
|
|
254
|
+
store: new DonationStore(cfg.dataDir),
|
|
261
255
|
});
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
256
|
+
// Reap orphaned donations: a requester that provisioned a node but never
|
|
257
|
+
// presented a seed leaves an `awaiting_seed` child holding host ports.
|
|
258
|
+
// Sweep once at startup (for records recovered from disk by
|
|
259
|
+
// orchestrator.init()), then periodically.
|
|
260
|
+
const reapStale = () => {
|
|
261
|
+
void donationService
|
|
262
|
+
.reapStaleAwaitingSeed(DONATION_AWAITING_SEED_TTL_MS)
|
|
263
|
+
.catch((err) => console.error(`donation reap failed: ${err.message}`));
|
|
264
|
+
};
|
|
265
|
+
reapStale();
|
|
266
|
+
const reapTimer = setInterval(reapStale, DONATION_REAP_SWEEP_MS);
|
|
267
|
+
reapTimer.unref();
|
|
268
|
+
// Founder stack (opt-in). Absent `ownCadre.enabled`, cadre-host is a pure
|
|
269
|
+
// donor: no owner node, and the /auth + /nat surfaces stay unmounted
|
|
270
|
+
// (they 404). Per-donated-node WAN reachability is deferred to
|
|
271
|
+
// backlog/feat-cadre-host-wan-grant-reachability, so v1 donor mode is
|
|
272
|
+
// loopback-only — nothing for NatService to map without an owner node.
|
|
273
|
+
let trustCircle;
|
|
274
|
+
let natService;
|
|
275
|
+
if (hostOwnsCadre(cfg)) {
|
|
276
|
+
// Spawn the owner node. Best-effort: a spawn failure leaves the
|
|
277
|
+
// management API up (trust-circle listing degrades to local labels,
|
|
278
|
+
// owner ops return 503) rather than taking down the whole process.
|
|
269
279
|
try {
|
|
270
|
-
await
|
|
280
|
+
await orchestrator.ensureOwnerNode({
|
|
281
|
+
identityPath: idPath,
|
|
282
|
+
partyId: cfg.installId,
|
|
283
|
+
libp2pPort: cfg.libp2pPort,
|
|
284
|
+
});
|
|
285
|
+
console.log('cadre-host: owner node spawned (host-own-cadre enabled)');
|
|
271
286
|
}
|
|
272
287
|
catch (err) {
|
|
273
|
-
console.error(`
|
|
288
|
+
console.error(`owner node spawn failed: ${err.message}`);
|
|
289
|
+
}
|
|
290
|
+
// The client reads the admin endpoint lazily so a node restart's fresh
|
|
291
|
+
// bearer token is picked up automatically.
|
|
292
|
+
const owner = new OwnerNodeClient(() => orchestrator.getOwnerAdminEndpoint());
|
|
293
|
+
trustCircle = new TrustCircleService({
|
|
294
|
+
cadreNode: owner,
|
|
295
|
+
store: new TrustCircleStore(cfg.dataDir),
|
|
296
|
+
});
|
|
297
|
+
natService = new NatService({
|
|
298
|
+
rootDir: cfg.dataDir,
|
|
299
|
+
cadreNode: owner,
|
|
300
|
+
});
|
|
301
|
+
// Push NAT-resolved invite addresses to the node on every NAT change.
|
|
302
|
+
// NatService.start() also fires this once as an initial push, retried
|
|
303
|
+
// until the freshly spawned owner node accepts it (bounded; the
|
|
304
|
+
// management API that mints invites comes up only after start() resolves).
|
|
305
|
+
natService.onAddressesChanged(async (addresses) => {
|
|
306
|
+
if (addresses.length === 0)
|
|
307
|
+
return;
|
|
308
|
+
try {
|
|
309
|
+
await owner.pushInviteAddresses(addresses);
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
console.error(`invite-address push failed: ${err.message}`);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
// Best-effort NAT start. Failures here aren't fatal — the local UI
|
|
316
|
+
// can still serve the trust circle, settings, etc.
|
|
317
|
+
try {
|
|
318
|
+
await natService.start();
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
console.error(`NAT start failed: ${err.message}`);
|
|
274
322
|
}
|
|
275
|
-
});
|
|
276
|
-
// Best-effort NAT start. Failures here aren't fatal — the local UI
|
|
277
|
-
// can still serve the trust circle, settings, etc.
|
|
278
|
-
try {
|
|
279
|
-
await natService.start();
|
|
280
323
|
}
|
|
281
|
-
|
|
282
|
-
|
|
324
|
+
else {
|
|
325
|
+
// Donor-only. If ownCadre was toggled off after a prior founder run,
|
|
326
|
+
// orchestrator.init() re-attaches the still-running owner child (it would
|
|
327
|
+
// otherwise linger in listNodes with no trustCircle/nat wired, serving
|
|
328
|
+
// the host's own cadre despite being disabled). Reap it now so a disabled
|
|
329
|
+
// own-cadre is actually stopped — its workdir + control-DB persist on
|
|
330
|
+
// disk, so toggling ownCadre back on re-spawns it from saved config.
|
|
331
|
+
try {
|
|
332
|
+
await orchestrator.stopOwnerNode();
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
console.error(`owner node reap failed: ${err.message}`);
|
|
336
|
+
}
|
|
337
|
+
console.log('cadre-host: node-donor mode (host-own-cadre disabled — no owner node; /auth + /nat inactive)');
|
|
283
338
|
}
|
|
284
339
|
const settingsStore = new HostSettingsStore({ dataDir: cfg.dataDir });
|
|
285
340
|
const server = createLocalUiServer({
|
|
286
341
|
uiPort: cfg.uiPort,
|
|
287
342
|
dataDir: cfg.dataDir,
|
|
288
343
|
orchestrator,
|
|
289
|
-
trustCircle,
|
|
290
|
-
nat: natService,
|
|
344
|
+
...(trustCircle ? { trustCircle } : {}),
|
|
345
|
+
...(natService ? { nat: natService } : {}),
|
|
291
346
|
update: updateService,
|
|
347
|
+
grants: grantService,
|
|
348
|
+
donations: donationService,
|
|
292
349
|
settingsStore,
|
|
293
350
|
});
|
|
294
351
|
const { url, port } = await server.start();
|
|
@@ -297,16 +354,17 @@ program
|
|
|
297
354
|
}
|
|
298
355
|
console.log(`cadre-host local UI: ${url}`);
|
|
299
356
|
await waitForTermination();
|
|
357
|
+
clearInterval(reapTimer);
|
|
300
358
|
try {
|
|
301
359
|
await server.stop();
|
|
302
360
|
}
|
|
303
361
|
catch { /* ignore */ }
|
|
304
362
|
try {
|
|
305
|
-
await natService
|
|
363
|
+
await natService?.stop();
|
|
306
364
|
}
|
|
307
365
|
catch { /* ignore */ }
|
|
308
366
|
try {
|
|
309
|
-
await orchestrator.
|
|
367
|
+
await orchestrator.stopOwnerNode();
|
|
310
368
|
}
|
|
311
369
|
catch { /* ignore */ }
|
|
312
370
|
updateService.stop();
|
|
@@ -411,6 +469,22 @@ function printEnrollmentInvite(invite) {
|
|
|
411
469
|
console.log(` (expires ${invite.expiresAt})`);
|
|
412
470
|
}
|
|
413
471
|
}
|
|
472
|
+
function printGrantToken(token, withQr) {
|
|
473
|
+
if (withQr) {
|
|
474
|
+
// Best-effort QR render — fall back to the bare token if the lib chokes.
|
|
475
|
+
try {
|
|
476
|
+
const qr = requireForQr('qrcode-terminal');
|
|
477
|
+
qr.generate(token, { small: true }, (rendered) => {
|
|
478
|
+
console.error(rendered);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
console.error(`(qrcode-terminal unavailable: ${err.message})`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// The token goes to stdout alone so it can be piped/copied; metadata to stderr.
|
|
486
|
+
console.log(token);
|
|
487
|
+
}
|
|
414
488
|
program
|
|
415
489
|
.command('invite')
|
|
416
490
|
.description('Generate an invite to add a member to the trust circle')
|
|
@@ -543,6 +617,133 @@ trust
|
|
|
543
617
|
process.exit(0);
|
|
544
618
|
});
|
|
545
619
|
// ============================================================================
|
|
620
|
+
// grant subcommands — donation grant tokens (who may ask this host for a node)
|
|
621
|
+
// ============================================================================
|
|
622
|
+
//
|
|
623
|
+
// A grant token lets one grantee (friend/family) present a Bearer credential to
|
|
624
|
+
// ask this host to donate cadre nodes, up to a per-grantee cap. These commands
|
|
625
|
+
// are thin HTTP clients of the loopback `/grants-admin` admin surface — no
|
|
626
|
+
// bearer (same-machine admin), same posture as `invite` / `trust`. The
|
|
627
|
+
// grantee-facing provisioning surface lands in the donation-service ticket.
|
|
628
|
+
const grant = program
|
|
629
|
+
.command('grant')
|
|
630
|
+
.description('Manage donation grant tokens (who may ask this host to donate a node)');
|
|
631
|
+
grant
|
|
632
|
+
.command('issue')
|
|
633
|
+
.description('Issue a grant token for one grantee (prints the token + QR)')
|
|
634
|
+
.argument('<label>', 'Display label for the grantee (e.g. "Alice\'s cadre")')
|
|
635
|
+
.option('--max-nodes <n>', 'Max concurrently-live donated nodes this grant may hold', parseIntArg)
|
|
636
|
+
.option('--ttl <duration>', 'Grant lifetime (e.g. 30d, 12h); omit for no expiry')
|
|
637
|
+
.option('--no-qr', 'Print only the token, no QR code')
|
|
638
|
+
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
639
|
+
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
640
|
+
.action(async (label, opts) => {
|
|
641
|
+
const payload = { label };
|
|
642
|
+
if (typeof opts.maxNodes === 'number')
|
|
643
|
+
payload.maxNodes = opts.maxNodes;
|
|
644
|
+
if (opts.ttl) {
|
|
645
|
+
try {
|
|
646
|
+
payload.ttlMs = parseDuration(opts.ttl);
|
|
647
|
+
}
|
|
648
|
+
catch (err) {
|
|
649
|
+
console.error(`Invalid --ttl: ${err.message}`);
|
|
650
|
+
process.exit(1);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const url = `http://${opts.host}:${resolvePort(opts.port)}/grants-admin`;
|
|
655
|
+
let response;
|
|
656
|
+
try {
|
|
657
|
+
response = await fetch(url, {
|
|
658
|
+
method: 'POST',
|
|
659
|
+
headers: { 'content-type': 'application/json' },
|
|
660
|
+
body: JSON.stringify(payload),
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
catch (err) {
|
|
664
|
+
console.error(`Failed to reach cadre-host at ${url}: ${err.message}\n` +
|
|
665
|
+
`Hint: is cadre-host running? Try \`cadre-host start\`.`);
|
|
666
|
+
process.exit(2);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (!response.ok) {
|
|
670
|
+
const text = await response.text().catch(() => '');
|
|
671
|
+
console.error(`cadre-host returned ${response.status}: ${text || response.statusText}`);
|
|
672
|
+
process.exit(1);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const body = await response.json();
|
|
676
|
+
const issued = body.grant;
|
|
677
|
+
if (!issued?.token) {
|
|
678
|
+
console.error('cadre-host returned malformed response (missing grant token)');
|
|
679
|
+
process.exit(1);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
printGrantToken(issued.token, opts.qr !== false);
|
|
683
|
+
console.error(`(maxNodes ${issued.maxNodes ?? '?'}${issued.expiresAt ? `, expires ${issued.expiresAt}` : ', no expiry'})`);
|
|
684
|
+
process.exit(0);
|
|
685
|
+
});
|
|
686
|
+
grant
|
|
687
|
+
.command('list')
|
|
688
|
+
.description('List issued grant tokens')
|
|
689
|
+
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
690
|
+
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
691
|
+
.action(async (opts) => {
|
|
692
|
+
const url = `http://${opts.host}:${resolvePort(opts.port)}/grants-admin`;
|
|
693
|
+
let response;
|
|
694
|
+
try {
|
|
695
|
+
response = await fetch(url);
|
|
696
|
+
}
|
|
697
|
+
catch (err) {
|
|
698
|
+
console.error(`Failed to reach cadre-host at ${url}: ${err.message}`);
|
|
699
|
+
process.exit(2);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (!response.ok) {
|
|
703
|
+
console.error(`cadre-host returned ${response.status}: ${response.statusText}`);
|
|
704
|
+
process.exit(1);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const body = await response.json();
|
|
708
|
+
console.log('Grants:');
|
|
709
|
+
if (body.grants.length === 0) {
|
|
710
|
+
console.log(' (none)');
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
for (const g of body.grants) {
|
|
714
|
+
const state = g.revokedAt ? ' [revoked]' : (g.expiresAt ? ` (expires ${g.expiresAt})` : '');
|
|
715
|
+
console.log(` ${g.token} ${g.label} max=${g.maxNodes}${state}`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
process.exit(0);
|
|
719
|
+
});
|
|
720
|
+
grant
|
|
721
|
+
.command('revoke')
|
|
722
|
+
.description('Revoke a grant token (blocks future requests; live nodes are not torn down)')
|
|
723
|
+
.argument('<token>', 'Grant token to revoke')
|
|
724
|
+
.option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
|
|
725
|
+
.option('--host <host>', 'cadre-host management API host', '127.0.0.1')
|
|
726
|
+
.action(async (token, opts) => {
|
|
727
|
+
const base = `http://${opts.host}:${resolvePort(opts.port)}`;
|
|
728
|
+
let response;
|
|
729
|
+
try {
|
|
730
|
+
response = await fetch(`${base}/grants-admin/${encodeURIComponent(token)}`, { method: 'DELETE' });
|
|
731
|
+
}
|
|
732
|
+
catch (err) {
|
|
733
|
+
console.error(`Failed to reach cadre-host at ${base}: ${err.message}`);
|
|
734
|
+
process.exit(2);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (!response.ok) {
|
|
738
|
+
const text = await response.text().catch(() => '');
|
|
739
|
+
console.error(`cadre-host returned ${response.status}: ${text || response.statusText}`);
|
|
740
|
+
process.exit(1);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
console.log(`revoked grant: ${token}`);
|
|
744
|
+
process.exit(0);
|
|
745
|
+
});
|
|
746
|
+
// ============================================================================
|
|
546
747
|
// nat subcommands
|
|
547
748
|
// ============================================================================
|
|
548
749
|
const nat = program
|
|
@@ -678,7 +879,7 @@ ddns
|
|
|
678
879
|
// do NOT go through the running management API). Private keys land in the OS
|
|
679
880
|
// keychain (keytar) or the 0600 file-store fallback; the non-secret bits (APNs
|
|
680
881
|
// bundle id / sandbox toggle, cooldown/debounce) land in host.config.json. New
|
|
681
|
-
// credentials take effect on the next
|
|
882
|
+
// credentials take effect on the next owner-node (re)spawn — run
|
|
682
883
|
// `cadre-host` restart (or restart the service) to apply them immediately.
|
|
683
884
|
const push = program
|
|
684
885
|
.command('push')
|
|
@@ -700,7 +901,7 @@ push
|
|
|
700
901
|
clientEmail: opts.clientEmail,
|
|
701
902
|
privateKey,
|
|
702
903
|
});
|
|
703
|
-
console.log('✓ FCM credentials stored. Restart cadre-host to apply on the
|
|
904
|
+
console.log('✓ FCM credentials stored. Restart cadre-host to apply on the owner node.');
|
|
704
905
|
process.exit(0);
|
|
705
906
|
});
|
|
706
907
|
push
|
|
@@ -727,7 +928,7 @@ push
|
|
|
727
928
|
},
|
|
728
929
|
});
|
|
729
930
|
console.log(`✓ APNs credentials stored (${opts.production ? 'production' : 'sandbox'}). ` +
|
|
730
|
-
`Restart cadre-host to apply on the
|
|
931
|
+
`Restart cadre-host to apply on the owner node.`);
|
|
731
932
|
process.exit(0);
|
|
732
933
|
});
|
|
733
934
|
push
|