@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
|
@@ -30,7 +30,7 @@ import type { PushCredentials } from '@serfab/cadre-core';
|
|
|
30
30
|
import {
|
|
31
31
|
decodeDockerId,
|
|
32
32
|
encodeDockerId,
|
|
33
|
-
type
|
|
33
|
+
type OwnerSpawnConfig,
|
|
34
34
|
type Handle,
|
|
35
35
|
type HostProcessConfig,
|
|
36
36
|
type ManagedNodeInfo,
|
|
@@ -53,11 +53,11 @@ const STARTUP_TOKEN_FILE = '.startup-token';
|
|
|
53
53
|
const CONFIG_FILE = 'cadre.json';
|
|
54
54
|
const LIVENESS_POLL_MS = 100;
|
|
55
55
|
|
|
56
|
-
/** Fixed friendly id for the admin's
|
|
57
|
-
export const
|
|
56
|
+
/** Fixed friendly id for the admin's owner node. */
|
|
57
|
+
export const OWNER_CONTAINER_ID = 'owner';
|
|
58
58
|
|
|
59
|
-
/** Endpoint + bearer for the
|
|
60
|
-
export interface
|
|
59
|
+
/** Endpoint + bearer for the owner node's loopback admin channel (6.6). */
|
|
60
|
+
export interface OwnerAdminEndpoint {
|
|
61
61
|
/** e.g. `http://127.0.0.1:<adminPort>`. */
|
|
62
62
|
baseUrl: string;
|
|
63
63
|
/** The child's `CADRE_STARTUP_TOKEN`, used as the admin bearer secret. */
|
|
@@ -108,8 +108,8 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
108
108
|
private readonly pushResolver?: PushCredentialsResolver;
|
|
109
109
|
/** State-change listeners — invoked when a handle's alive state changes. */
|
|
110
110
|
private readonly stateListeners = new Set<NodeStateListener>();
|
|
111
|
-
/** Persisted spawn config for the
|
|
112
|
-
private
|
|
111
|
+
/** Persisted spawn config for the owner node (re-spawn on demand). */
|
|
112
|
+
private ownerConfig?: OwnerSpawnConfig;
|
|
113
113
|
|
|
114
114
|
constructor(private readonly cfg: HostProcessConfig) {
|
|
115
115
|
this.rootDir = resolvePath(cfg.rootDir);
|
|
@@ -135,7 +135,7 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
135
135
|
*/
|
|
136
136
|
async init(): Promise<void> {
|
|
137
137
|
const state = this.stateStore.load();
|
|
138
|
-
this.
|
|
138
|
+
this.ownerConfig = state.ownerConfig;
|
|
139
139
|
for (const persisted of state.handles) {
|
|
140
140
|
const alive = isPidAlive(persisted.pid) && tokenMatches(persisted.workdir, persisted.startupToken);
|
|
141
141
|
const handle: Handle = {
|
|
@@ -148,7 +148,7 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
148
148
|
spawnedAt: persisted.spawnedAt,
|
|
149
149
|
partyId: persisted.partyId,
|
|
150
150
|
profile: persisted.profile,
|
|
151
|
-
...(persisted.
|
|
151
|
+
...(persisted.owner ? { owner: true } : {}),
|
|
152
152
|
alive,
|
|
153
153
|
};
|
|
154
154
|
this.handles.set(handle.dockerId, handle);
|
|
@@ -224,17 +224,30 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
224
224
|
p2p: this.portAllocator.allocate(),
|
|
225
225
|
admin: this.portAllocator.allocate(),
|
|
226
226
|
};
|
|
227
|
+
// A node started with pinned owner keys belongs to a FOREIGN cadre (the
|
|
228
|
+
// node-donation flow: it trusts the requester's owner key, not the host's).
|
|
229
|
+
// The host's FCM/APNs credentials are minted for the host owner's own app
|
|
230
|
+
// and are meaningless to a foreign cadre — so donated nodes get NO push
|
|
231
|
+
// block. (Per-grantee push creds would be a future ticket; none in v1.)
|
|
232
|
+
const foreignParty = (request.pinnedOwnerKeys?.length ?? 0) > 0;
|
|
227
233
|
// Only storage-profile nodes participate in strands and thus fan out push
|
|
228
234
|
// wakes — a transaction-only node need not carry credentials.
|
|
229
|
-
const push = request.profile === 'storage' ? await this.resolvePush() : undefined;
|
|
235
|
+
const push = request.profile === 'storage' && !foreignParty ? await this.resolvePush() : undefined;
|
|
236
|
+
// Pinned owner key(s) reach the child via CADRE_OWNER_KEYS (comma-separated);
|
|
237
|
+
// `cadre-cli start` unions it into its cold-start pinnedKeyTrustPolicy so the
|
|
238
|
+
// node will accept the foreign-authority-signed seed presented via POST /seed.
|
|
239
|
+
const extraEnv = request.pinnedOwnerKeys?.length
|
|
240
|
+
? { CADRE_OWNER_KEYS: request.pinnedOwnerKeys.join(',') }
|
|
241
|
+
: undefined;
|
|
230
242
|
return this.launchChild({
|
|
231
243
|
containerId: request.containerId,
|
|
232
244
|
partyId: request.partyId,
|
|
233
245
|
profile: request.profile,
|
|
234
246
|
ports,
|
|
235
|
-
|
|
247
|
+
owner: false,
|
|
236
248
|
buildConfig: (workdir) => this.buildChildConfig(request, workdir, push),
|
|
237
249
|
extraArgs: [],
|
|
250
|
+
...(extraEnv ? { extraEnv } : {}),
|
|
238
251
|
...(request.resources?.memoryLimit ? { memoryLimit: request.resources.memoryLimit } : {}),
|
|
239
252
|
});
|
|
240
253
|
}
|
|
@@ -261,32 +274,32 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
261
274
|
}
|
|
262
275
|
|
|
263
276
|
/**
|
|
264
|
-
* Ensure the admin's
|
|
277
|
+
* Ensure the admin's owner node is running, spawning it if absent or
|
|
265
278
|
* dead. Idempotent: a second call with the node already alive returns the
|
|
266
279
|
* existing handle without launching a second child. The spawn parameters
|
|
267
280
|
* are persisted so a later `/api/nodes/:id/{start,restart}` (or an
|
|
268
281
|
* orchestrator restart) can re-spawn it without re-supplying them.
|
|
269
282
|
*
|
|
270
|
-
* The
|
|
283
|
+
* The owner node is spawned as `cadre-cli start --owner
|
|
271
284
|
* --admin-port <p> --identity-protobuf <identityPath>` so it carries the
|
|
272
285
|
* host's libp2p identity, founds/joins the control network, and exposes the
|
|
273
286
|
* loopback admin channel the manager delegates to.
|
|
274
287
|
*/
|
|
275
|
-
async
|
|
276
|
-
const config = cfg ?? this.
|
|
288
|
+
async ensureOwnerNode(cfg?: OwnerSpawnConfig): Promise<ManagedNodeInfo> {
|
|
289
|
+
const config = cfg ?? this.ownerConfig;
|
|
277
290
|
if (!config) {
|
|
278
|
-
throw new Error('
|
|
291
|
+
throw new Error('ensureOwnerNode: no owner config supplied and none persisted');
|
|
279
292
|
}
|
|
280
|
-
this.
|
|
293
|
+
this.ownerConfig = config;
|
|
281
294
|
|
|
282
|
-
const existing = this.
|
|
295
|
+
const existing = this.findOwnerHandle();
|
|
283
296
|
if (existing && isPidAlive(existing.pid) && tokenMatches(existing.workdir, existing.startupToken)) {
|
|
284
297
|
this.persist();
|
|
285
298
|
return toNodeInfo(existing);
|
|
286
299
|
}
|
|
287
300
|
if (existing) {
|
|
288
301
|
// Drop the stale handle and release its ports — but DO NOT delete the
|
|
289
|
-
// workdir: the
|
|
302
|
+
// workdir: the owner node's control-DB storage lives there and must
|
|
290
303
|
// survive a restart. launchChild reuses the same workdir.
|
|
291
304
|
this.releasePorts(existing.ports);
|
|
292
305
|
this.handles.delete(existing.dockerId);
|
|
@@ -300,7 +313,7 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
300
313
|
health: this.portAllocator.allocate(),
|
|
301
314
|
metrics: this.portAllocator.allocate(),
|
|
302
315
|
admin: this.portAllocator.allocate(),
|
|
303
|
-
// The
|
|
316
|
+
// The owner node must listen on the configured libp2p port so the
|
|
304
317
|
// NAT mapping (external → internal) lands on it. That port is outside
|
|
305
318
|
// the managed range, so markUsed is a harmless no-op.
|
|
306
319
|
p2p: config.libp2pPort,
|
|
@@ -308,21 +321,21 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
308
321
|
this.portAllocator.markUsed(config.libp2pPort);
|
|
309
322
|
|
|
310
323
|
const result = this.launchChild({
|
|
311
|
-
containerId:
|
|
324
|
+
containerId: OWNER_CONTAINER_ID,
|
|
312
325
|
partyId: config.partyId,
|
|
313
326
|
profile,
|
|
314
327
|
ports,
|
|
315
|
-
|
|
316
|
-
buildConfig: (workdir) => this.
|
|
317
|
-
extraArgs: ['--
|
|
328
|
+
owner: true,
|
|
329
|
+
buildConfig: (workdir) => this.buildOwnerChildConfig(config, profile, workdir, push),
|
|
330
|
+
extraArgs: ['--owner', '--admin-port', String(ports.admin), '--identity-protobuf', config.identityPath],
|
|
318
331
|
});
|
|
319
332
|
const handle = this.handles.get(result.dockerId)!;
|
|
320
333
|
return toNodeInfo(handle);
|
|
321
334
|
}
|
|
322
335
|
|
|
323
|
-
/** Endpoint + bearer for the
|
|
324
|
-
|
|
325
|
-
const handle = this.
|
|
336
|
+
/** Endpoint + bearer for the owner node's admin channel, if spawned. */
|
|
337
|
+
getOwnerAdminEndpoint(): OwnerAdminEndpoint | undefined {
|
|
338
|
+
const handle = this.findOwnerHandle();
|
|
326
339
|
if (!handle) return undefined;
|
|
327
340
|
return {
|
|
328
341
|
baseUrl: `http://127.0.0.1:${handle.ports.admin}`,
|
|
@@ -330,41 +343,41 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
330
343
|
};
|
|
331
344
|
}
|
|
332
345
|
|
|
333
|
-
/** Whether spawn parameters for the
|
|
334
|
-
|
|
335
|
-
return this.
|
|
346
|
+
/** Whether spawn parameters for the owner node are known (persisted). */
|
|
347
|
+
hasOwnerConfig(): boolean {
|
|
348
|
+
return this.ownerConfig !== undefined;
|
|
336
349
|
}
|
|
337
350
|
|
|
338
|
-
/** Whether the given friendly/docker id refers to the
|
|
339
|
-
|
|
340
|
-
if (idOrDockerId ===
|
|
351
|
+
/** Whether the given friendly/docker id refers to the owner node. */
|
|
352
|
+
isOwnerNode(idOrDockerId: string): boolean {
|
|
353
|
+
if (idOrDockerId === OWNER_CONTAINER_ID) return true;
|
|
341
354
|
const direct = this.handles.get(idOrDockerId);
|
|
342
|
-
if (direct?.
|
|
355
|
+
if (direct?.owner) return true;
|
|
343
356
|
for (const h of this.handles.values()) {
|
|
344
|
-
if (h.
|
|
357
|
+
if (h.owner && h.containerId === idOrDockerId) return true;
|
|
345
358
|
}
|
|
346
359
|
return false;
|
|
347
360
|
}
|
|
348
361
|
|
|
349
|
-
/** Stop the
|
|
350
|
-
async
|
|
351
|
-
const handle = this.
|
|
362
|
+
/** Stop the owner node (if running). Used on graceful shutdown. */
|
|
363
|
+
async stopOwnerNode(): Promise<void> {
|
|
364
|
+
const handle = this.findOwnerHandle();
|
|
352
365
|
if (handle && isPidAlive(handle.pid)) {
|
|
353
366
|
await this.stopContainer(handle.dockerId);
|
|
354
367
|
}
|
|
355
368
|
}
|
|
356
369
|
|
|
357
|
-
/** Stop the
|
|
358
|
-
async
|
|
359
|
-
const handle = this.
|
|
370
|
+
/** Stop the owner node (if running), then re-spawn it from saved config. */
|
|
371
|
+
async restartOwnerNode(): Promise<ManagedNodeInfo> {
|
|
372
|
+
const handle = this.findOwnerHandle();
|
|
360
373
|
if (handle && isPidAlive(handle.pid)) {
|
|
361
374
|
await this.stopContainer(handle.dockerId);
|
|
362
375
|
}
|
|
363
|
-
return this.
|
|
376
|
+
return this.ensureOwnerNode();
|
|
364
377
|
}
|
|
365
378
|
|
|
366
379
|
/**
|
|
367
|
-
* Core spawn mechanics shared by `createContainer` and `
|
|
380
|
+
* Core spawn mechanics shared by `createContainer` and `ensureOwnerNode`.
|
|
368
381
|
* The caller has already allocated `ports`. Releases the ports on a
|
|
369
382
|
* synchronous spawn failure.
|
|
370
383
|
*/
|
|
@@ -373,9 +386,11 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
373
386
|
partyId: string;
|
|
374
387
|
profile: 'storage' | 'transaction';
|
|
375
388
|
ports: NodePorts;
|
|
376
|
-
|
|
389
|
+
owner: boolean;
|
|
377
390
|
buildConfig: (workdir: string) => Record<string, unknown>;
|
|
378
391
|
extraArgs: string[];
|
|
392
|
+
/** Extra env vars merged into the child's environment (e.g. CADRE_OWNER_KEYS). */
|
|
393
|
+
extraEnv?: Record<string, string>;
|
|
379
394
|
memoryLimit?: string;
|
|
380
395
|
}): OrchestratorCreateResult {
|
|
381
396
|
const { containerId, ports } = opts;
|
|
@@ -405,6 +420,9 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
405
420
|
|
|
406
421
|
const env: NodeJS.ProcessEnv = {
|
|
407
422
|
...process.env,
|
|
423
|
+
// Caller-supplied extras first — the fixed per-child vars below always win
|
|
424
|
+
// so extraEnv can never clobber the seed/startup tokens or port wiring.
|
|
425
|
+
...opts.extraEnv,
|
|
408
426
|
CADRE_STARTUP_TOKEN: token,
|
|
409
427
|
CADRE_HEALTH_PORT: String(ports.health),
|
|
410
428
|
CADRE_METRICS_PORT: String(ports.metrics),
|
|
@@ -472,7 +490,7 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
472
490
|
spawnedAt: new Date().toISOString(),
|
|
473
491
|
partyId: opts.partyId,
|
|
474
492
|
profile: opts.profile,
|
|
475
|
-
...(opts.
|
|
493
|
+
...(opts.owner ? { owner: true } : {}),
|
|
476
494
|
child,
|
|
477
495
|
alive: true,
|
|
478
496
|
};
|
|
@@ -502,10 +520,10 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
502
520
|
};
|
|
503
521
|
}
|
|
504
522
|
|
|
505
|
-
/** Locate the
|
|
506
|
-
private
|
|
523
|
+
/** Locate the owner node's handle, if one has been spawned. */
|
|
524
|
+
private findOwnerHandle(): Handle | undefined {
|
|
507
525
|
for (const h of this.handles.values()) {
|
|
508
|
-
if (h.
|
|
526
|
+
if (h.owner || h.containerId === OWNER_CONTAINER_ID) return h;
|
|
509
527
|
}
|
|
510
528
|
return undefined;
|
|
511
529
|
}
|
|
@@ -688,12 +706,20 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
688
706
|
}
|
|
689
707
|
|
|
690
708
|
/**
|
|
691
|
-
* Child config for the
|
|
692
|
-
*
|
|
693
|
-
*
|
|
709
|
+
* Child config for the **host's own personal cadre** owner node — the opt-in
|
|
710
|
+
* "founder" persona spawned only when `ownCadre.enabled` (see
|
|
711
|
+
* docs/cadre-host.md § Two roles: donor and founder). It founds/joins the
|
|
712
|
+
* control network for the host's own `partyId` (no bootstrap peers — it is the
|
|
713
|
+
* founding node of the host's *own* cadre) and carries the host identity via
|
|
714
|
+
* `--identity-protobuf` (passed as a spawn arg).
|
|
715
|
+
*
|
|
716
|
+
* This is NOT the node cadre-host donates to a requester. Donated nodes are
|
|
717
|
+
* generic (`createContainer` / `buildChildConfig`): they join the
|
|
718
|
+
* **requester's** cadre via bootstrap peers and pin the *requester's* owner
|
|
719
|
+
* key — they never run a host genesis.
|
|
694
720
|
*/
|
|
695
|
-
private
|
|
696
|
-
cfg:
|
|
721
|
+
private buildOwnerChildConfig(
|
|
722
|
+
cfg: OwnerSpawnConfig,
|
|
697
723
|
profile: 'storage' | 'transaction',
|
|
698
724
|
workdir: string,
|
|
699
725
|
push?: PushCredentials,
|
|
@@ -709,10 +735,12 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
709
735
|
if (profile === 'storage') {
|
|
710
736
|
config.storage = { type: 'file', path: join(workdir, 'storage') };
|
|
711
737
|
}
|
|
712
|
-
// The
|
|
713
|
-
// the push-wake fan-out when credentials are configured.
|
|
714
|
-
//
|
|
715
|
-
//
|
|
738
|
+
// The host-own-cadre owner/storage node participates in strands, so it owns
|
|
739
|
+
// the push-wake fan-out when credentials are configured. (Donated nodes
|
|
740
|
+
// belong to foreign cadres and get NO host push block — see the donor path.)
|
|
741
|
+
// Written into cadre.json (same host trust boundary as the workdir's
|
|
742
|
+
// control-DB); keys are never logged — only the redacted presence line in
|
|
743
|
+
// resolvePush.
|
|
716
744
|
if (push) config.push = push;
|
|
717
745
|
return config;
|
|
718
746
|
}
|
|
@@ -721,7 +749,7 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
721
749
|
const state: PersistedState = {
|
|
722
750
|
version: 1,
|
|
723
751
|
handles: [...this.handles.values()].map((h) => this.toPersisted(h)),
|
|
724
|
-
...(this.
|
|
752
|
+
...(this.ownerConfig ? { ownerConfig: this.ownerConfig } : {}),
|
|
725
753
|
};
|
|
726
754
|
this.stateStore.save(state);
|
|
727
755
|
}
|
|
@@ -749,7 +777,7 @@ export class HostProcessOrchestrator implements Orchestrator {
|
|
|
749
777
|
spawnedAt: h.spawnedAt,
|
|
750
778
|
partyId: h.partyId,
|
|
751
779
|
profile: h.profile,
|
|
752
|
-
...(h.
|
|
780
|
+
...(h.owner ? { owner: true } : {}),
|
|
753
781
|
};
|
|
754
782
|
}
|
|
755
783
|
}
|
|
@@ -764,7 +792,7 @@ function toNodeInfo(h: Handle): ManagedNodeInfo {
|
|
|
764
792
|
spawnedAt: h.spawnedAt,
|
|
765
793
|
workdir: h.workdir,
|
|
766
794
|
ports: { ...h.ports },
|
|
767
|
-
...(h.
|
|
795
|
+
...(h.owner ? { owner: true } : {}),
|
|
768
796
|
};
|
|
769
797
|
}
|
|
770
798
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { HostProcessOrchestrator,
|
|
2
|
-
export type {
|
|
1
|
+
export { HostProcessOrchestrator, OWNER_CONTAINER_ID } from './host-process-orchestrator.js';
|
|
2
|
+
export type { OwnerAdminEndpoint } from './host-process-orchestrator.js';
|
|
3
3
|
export type {
|
|
4
4
|
HostProcessConfig,
|
|
5
5
|
ManagedNodeInfo,
|
|
6
6
|
NodeStateListener,
|
|
7
7
|
NodePorts,
|
|
8
|
-
|
|
8
|
+
OwnerSpawnConfig,
|
|
9
9
|
} from './types.js';
|
|
10
10
|
export type { PersistedHandle } from './state-store.js';
|
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, renameSync, writeFileSync, mkdirSync } from '
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import debug from 'debug';
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { OwnerSpawnConfig, NodePorts } from './types.js';
|
|
6
6
|
|
|
7
7
|
const log = debug('cadre:host:state-store');
|
|
8
8
|
|
|
@@ -21,15 +21,15 @@ export interface PersistedHandle {
|
|
|
21
21
|
spawnedAt: string; // ISO timestamp
|
|
22
22
|
partyId: string;
|
|
23
23
|
profile: 'storage' | 'transaction';
|
|
24
|
-
/** True for the admin's
|
|
25
|
-
|
|
24
|
+
/** True for the admin's owner node. */
|
|
25
|
+
owner?: boolean;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface PersistedState {
|
|
29
29
|
version: 1;
|
|
30
30
|
handles: PersistedHandle[];
|
|
31
|
-
/** Spawn parameters for the
|
|
32
|
-
|
|
31
|
+
/** Spawn parameters for the owner node, so it can be re-spawned on demand. */
|
|
32
|
+
ownerConfig?: OwnerSpawnConfig;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const STATE_VERSION = 1;
|
|
@@ -57,7 +57,7 @@ export class StateStore {
|
|
|
57
57
|
return {
|
|
58
58
|
version: STATE_VERSION,
|
|
59
59
|
handles: parsed.handles,
|
|
60
|
-
...(parsed.
|
|
60
|
+
...(parsed.ownerConfig ? { ownerConfig: parsed.ownerConfig } : {}),
|
|
61
61
|
};
|
|
62
62
|
} catch (err) {
|
|
63
63
|
// A corrupt state file is operationally significant — surviving children
|
|
@@ -74,7 +74,7 @@ export class StateStore {
|
|
|
74
74
|
{
|
|
75
75
|
version: STATE_VERSION,
|
|
76
76
|
handles: state.handles,
|
|
77
|
-
...(state.
|
|
77
|
+
...(state.ownerConfig ? { ownerConfig: state.ownerConfig } : {}),
|
|
78
78
|
},
|
|
79
79
|
null,
|
|
80
80
|
2,
|
|
@@ -47,7 +47,7 @@ export interface NodePorts {
|
|
|
47
47
|
health: number;
|
|
48
48
|
metrics: number;
|
|
49
49
|
p2p: number;
|
|
50
|
-
/** Loopback admin-channel port. Bound only by the
|
|
50
|
+
/** Loopback admin-channel port. Bound only by the owner node. */
|
|
51
51
|
admin: number;
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -62,8 +62,8 @@ export interface Handle {
|
|
|
62
62
|
spawnedAt: string;
|
|
63
63
|
partyId: string;
|
|
64
64
|
profile: 'storage' | 'transaction';
|
|
65
|
-
/** True for the admin's
|
|
66
|
-
|
|
65
|
+
/** True for the admin's owner node (binds the admin channel). */
|
|
66
|
+
owner?: boolean;
|
|
67
67
|
/** Live ChildProcess reference; absent after re-attach via init(). */
|
|
68
68
|
child?: ChildProcess;
|
|
69
69
|
/** Marked false when init() finds the PID dead or token mismatched. */
|
|
@@ -83,16 +83,16 @@ export interface ManagedNodeInfo {
|
|
|
83
83
|
spawnedAt: string;
|
|
84
84
|
workdir: string;
|
|
85
85
|
ports: NodePorts;
|
|
86
|
-
/** True for the admin's
|
|
87
|
-
|
|
86
|
+
/** True for the admin's owner node. */
|
|
87
|
+
owner?: boolean;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
* Persisted spawn parameters for the admin's
|
|
92
|
-
* the handles so an orchestrator restart can re-spawn the
|
|
91
|
+
* Persisted spawn parameters for the admin's owner node. Stored alongside
|
|
92
|
+
* the handles so an orchestrator restart can re-spawn the owner node from
|
|
93
93
|
* `/api/nodes/:id/{start,restart}` without the caller re-supplying them.
|
|
94
94
|
*/
|
|
95
|
-
export interface
|
|
95
|
+
export interface OwnerSpawnConfig {
|
|
96
96
|
/** Absolute path to the host's libp2p protobuf identity (`identity.key`). */
|
|
97
97
|
identityPath: string;
|
|
98
98
|
/** Control-network partyId (the host's installId). */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
2
|
import http from 'node:http';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
4
|
+
import { OwnerNodeClient, OwnerNodeUnavailableError } from '../owner-node-client.js';
|
|
5
|
+
import type { OwnerAdminEndpoint } from '../../orchestrator/index.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Stub HTTP server matching the 6.6 admin contract. Records the last request
|
|
@@ -52,16 +52,16 @@ afterEach(async () => {
|
|
|
52
52
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
function makeClient():
|
|
56
|
-
const endpoint:
|
|
57
|
-
return new
|
|
55
|
+
function makeClient(): OwnerNodeClient {
|
|
56
|
+
const endpoint: OwnerAdminEndpoint = { baseUrl, token: TOKEN };
|
|
57
|
+
return new OwnerNodeClient(endpoint);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
describe('
|
|
60
|
+
describe('OwnerNodeClient', () => {
|
|
61
61
|
it('createInvite POSTs /admin/invites with bearer + body and unwraps data', async () => {
|
|
62
62
|
responder = () => ({
|
|
63
63
|
status: 200,
|
|
64
|
-
payload: { ok: true, data: { invite: { partyId: 'p',
|
|
64
|
+
payload: { ok: true, data: { invite: { partyId: 'p', ownerAddrs: [], token: 't', createdAt: 1 }, encodedInvite: 'enc' } },
|
|
65
65
|
});
|
|
66
66
|
const client = makeClient();
|
|
67
67
|
const result = await client.createInvite('t', 60_000);
|
|
@@ -75,7 +75,7 @@ describe('AuthorityNodeClient', () => {
|
|
|
75
75
|
|
|
76
76
|
it('acceptPhone POSTs /admin/accept-phone', async () => {
|
|
77
77
|
const client = makeClient();
|
|
78
|
-
await client.acceptPhone({ phonePeerId: '12D3KooWX', token: 't' }, { partyId: 'p',
|
|
78
|
+
await client.acceptPhone({ phonePeerId: '12D3KooWX', token: 't' }, { partyId: 'p', ownerAddrs: [], token: 't', createdAt: 1 });
|
|
79
79
|
expect(last?.method).toBe('POST');
|
|
80
80
|
expect(last?.path).toBe('/admin/accept-phone');
|
|
81
81
|
const body = JSON.parse(last!.body);
|
|
@@ -106,6 +106,22 @@ describe('AuthorityNodeClient', () => {
|
|
|
106
106
|
expect(last?.path).toBe('/admin/members/peer-1');
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
+
it('listAuthorizedMembers GETs /admin/authorized-members and returns the array', async () => {
|
|
110
|
+
responder = () => ({ status: 200, payload: { ok: true, data: { members: [{ peerId: 'b', multiaddr: null }] } } });
|
|
111
|
+
const client = makeClient();
|
|
112
|
+
const members = await client.listAuthorizedMembers();
|
|
113
|
+
expect(last?.method).toBe('GET');
|
|
114
|
+
expect(last?.path).toBe('/admin/authorized-members');
|
|
115
|
+
expect(members).toEqual([{ peerId: 'b', multiaddr: null }]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('isAuthorizedMember GETs /admin/authorized-members/:peerId and returns the boolean', async () => {
|
|
119
|
+
responder = () => ({ status: 200, payload: { ok: true, data: { member: false } } });
|
|
120
|
+
const client = makeClient();
|
|
121
|
+
expect(await client.isAuthorizedMember('peer-2')).toBe(false);
|
|
122
|
+
expect(last?.path).toBe('/admin/authorized-members/peer-2');
|
|
123
|
+
});
|
|
124
|
+
|
|
109
125
|
it('getPeerId GETs /admin/identity', async () => {
|
|
110
126
|
responder = () => ({ status: 200, payload: { ok: true, data: { peerId: '12D3KooWHost', partyId: 'party' } } });
|
|
111
127
|
const client = makeClient();
|
|
@@ -128,22 +144,34 @@ describe('AuthorityNodeClient', () => {
|
|
|
128
144
|
expect(JSON.parse(last!.body)).toEqual({ addresses: ['/dns4/h/tcp/1/p2p/x'] });
|
|
129
145
|
});
|
|
130
146
|
|
|
147
|
+
it('addDrone POSTs /admin/add-drone with body and unwraps the seed result', async () => {
|
|
148
|
+
responder = () => ({ status: 200, payload: { ok: true, data: { seed: { partyId: 'p', peers: [] }, encodedSeed: 'enc-seed' } } });
|
|
149
|
+
const client = makeClient();
|
|
150
|
+
const result = await client.addDrone({ dronePeerId: '12D3KooWDrone', droneMultiaddrs: ['/ip4/10.0.0.9/tcp/4001'] });
|
|
151
|
+
|
|
152
|
+
expect(result.encodedSeed).toBe('enc-seed');
|
|
153
|
+
expect(last?.method).toBe('POST');
|
|
154
|
+
expect(last?.path).toBe('/admin/add-drone');
|
|
155
|
+
expect(last?.auth).toBe(`Bearer ${TOKEN}`);
|
|
156
|
+
expect(JSON.parse(last!.body)).toEqual({ dronePeerId: '12D3KooWDrone', droneMultiaddrs: ['/ip4/10.0.0.9/tcp/4001'] });
|
|
157
|
+
});
|
|
158
|
+
|
|
131
159
|
it('encodeInvite mirrors cadre-core base64url(JSON)', () => {
|
|
132
160
|
const client = makeClient();
|
|
133
|
-
const invite = { partyId: 'p',
|
|
161
|
+
const invite = { partyId: 'p', ownerAddrs: [], token: 't', createdAt: 1 };
|
|
134
162
|
const encoded = client.encodeInvite(invite);
|
|
135
163
|
const decoded = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
|
|
136
164
|
expect(decoded).toEqual(invite);
|
|
137
165
|
});
|
|
138
166
|
|
|
139
|
-
it('maps a non-2xx response to
|
|
167
|
+
it('maps a non-2xx response to OwnerNodeUnavailableError carrying the code', async () => {
|
|
140
168
|
responder = () => ({ status: 503, payload: { ok: false, error: { code: 'not_ready', message: 'node booting' } } });
|
|
141
169
|
const client = makeClient();
|
|
142
|
-
await expect(client.getPeerId()).rejects.toBeInstanceOf(
|
|
170
|
+
await expect(client.getPeerId()).rejects.toBeInstanceOf(OwnerNodeUnavailableError);
|
|
143
171
|
await expect(client.getPeerId()).rejects.toMatchObject({ nodeCode: 'not_ready' });
|
|
144
172
|
});
|
|
145
173
|
|
|
146
|
-
it('maps a refused connection to
|
|
174
|
+
it('maps a refused connection to OwnerNodeUnavailableError', async () => {
|
|
147
175
|
// Bind a throwaway server to claim a port, then close it so the address
|
|
148
176
|
// is guaranteed refused.
|
|
149
177
|
const tmp = http.createServer();
|
|
@@ -155,12 +183,12 @@ describe('AuthorityNodeClient', () => {
|
|
|
155
183
|
});
|
|
156
184
|
await new Promise<void>((resolve) => tmp.close(() => resolve()));
|
|
157
185
|
|
|
158
|
-
const client = new
|
|
159
|
-
await expect(client.getPeerId()).rejects.toBeInstanceOf(
|
|
186
|
+
const client = new OwnerNodeClient({ baseUrl: `http://127.0.0.1:${closedPort}`, token: TOKEN });
|
|
187
|
+
await expect(client.getPeerId()).rejects.toBeInstanceOf(OwnerNodeUnavailableError);
|
|
160
188
|
});
|
|
161
189
|
|
|
162
|
-
it('throws
|
|
163
|
-
const client = new
|
|
164
|
-
await expect(client.getPeerId()).rejects.toBeInstanceOf(
|
|
190
|
+
it('throws OwnerNodeUnavailableError when the endpoint is undefined', async () => {
|
|
191
|
+
const client = new OwnerNodeClient(() => undefined);
|
|
192
|
+
await expect(client.getPeerId()).rejects.toBeInstanceOf(OwnerNodeUnavailableError);
|
|
165
193
|
});
|
|
166
194
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner-node delegation surface for cadre-host.
|
|
3
|
+
*
|
|
4
|
+
* The manager process spawns the admin's owner cadre node (via
|
|
5
|
+
* `HostProcessOrchestrator`) and delegates owner/membership/identity
|
|
6
|
+
* operations to it over the loopback admin channel using
|
|
7
|
+
* {@link OwnerNodeClient}. See `docs/cadre-host.md` § Control-plane
|
|
8
|
+
* separation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { OwnerNodeClient, OwnerNodeUnavailableError } from './owner-node-client.js';
|
|
12
|
+
export type { OwnerNodeClientOptions } from './owner-node-client.js';
|