@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.
Files changed (136) hide show
  1. package/dist/auth/trust-circle.d.ts +5 -5
  2. package/dist/auth/trust-circle.js +10 -10
  3. package/dist/auth/trust-circle.js.map +1 -1
  4. package/dist/auth/types.d.ts +2 -2
  5. package/dist/bin/host.js +255 -54
  6. package/dist/bin/host.js.map +1 -1
  7. package/dist/donation/donation-service.d.ts +136 -0
  8. package/dist/donation/donation-service.js +306 -0
  9. package/dist/donation/donation-service.js.map +1 -0
  10. package/dist/donation/donation-store.d.ts +42 -0
  11. package/dist/donation/donation-store.js +141 -0
  12. package/dist/donation/donation-store.js.map +1 -0
  13. package/dist/donation/grant-service.d.ts +71 -0
  14. package/dist/donation/grant-service.js +163 -0
  15. package/dist/donation/grant-service.js.map +1 -0
  16. package/dist/donation/grant-store.d.ts +38 -0
  17. package/dist/donation/grant-store.js +135 -0
  18. package/dist/donation/grant-store.js.map +1 -0
  19. package/dist/donation/index.d.ts +18 -0
  20. package/dist/donation/index.js +16 -0
  21. package/dist/donation/index.js.map +1 -0
  22. package/dist/donation/types.d.ts +175 -0
  23. package/dist/donation/types.js +40 -0
  24. package/dist/donation/types.js.map +1 -0
  25. package/dist/index.d.ts +6 -4
  26. package/dist/index.js +4 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/installer/config.d.ts +26 -0
  29. package/dist/installer/config.js +13 -0
  30. package/dist/installer/config.js.map +1 -1
  31. package/dist/installer/index.d.ts +2 -0
  32. package/dist/installer/index.js +3 -0
  33. package/dist/installer/index.js.map +1 -1
  34. package/dist/installer/wizard.d.ts +4 -0
  35. package/dist/installer/wizard.js +5 -1
  36. package/dist/installer/wizard.js.map +1 -1
  37. package/dist/nat/nat-service.d.ts +5 -5
  38. package/dist/nat/nat-service.js +9 -9
  39. package/dist/nat/nat-service.js.map +1 -1
  40. package/dist/nat/types.d.ts +1 -1
  41. package/dist/orchestrator/host-process-orchestrator.d.ts +35 -27
  42. package/dist/orchestrator/host-process-orchestrator.js +79 -53
  43. package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
  44. package/dist/orchestrator/index.d.ts +3 -3
  45. package/dist/orchestrator/index.js +1 -1
  46. package/dist/orchestrator/index.js.map +1 -1
  47. package/dist/orchestrator/state-store.d.ts +5 -5
  48. package/dist/orchestrator/state-store.js +2 -2
  49. package/dist/orchestrator/state-store.js.map +1 -1
  50. package/dist/orchestrator/types.d.ts +8 -8
  51. package/dist/owner/index.d.ts +11 -0
  52. package/dist/owner/index.js +11 -0
  53. package/dist/owner/index.js.map +1 -0
  54. package/dist/{authority/authority-node-client.d.ts → owner/owner-node-client.d.ts} +32 -14
  55. package/dist/{authority/authority-node-client.js → owner/owner-node-client.js} +39 -15
  56. package/dist/owner/owner-node-client.js.map +1 -0
  57. package/dist/server/error-handler.d.ts +15 -0
  58. package/dist/server/error-handler.js +41 -0
  59. package/dist/server/error-handler.js.map +1 -1
  60. package/dist/server/index.d.ts +26 -2
  61. package/dist/server/index.js +37 -16
  62. package/dist/server/index.js.map +1 -1
  63. package/dist/server/routes/grants-admin.d.ts +17 -0
  64. package/dist/server/routes/grants-admin.js +32 -0
  65. package/dist/server/routes/grants-admin.js.map +1 -0
  66. package/dist/server/routes/grants.d.ts +28 -0
  67. package/dist/server/routes/grants.js +163 -0
  68. package/dist/server/routes/grants.js.map +1 -0
  69. package/dist/server/routes/nodes.d.ts +5 -4
  70. package/dist/server/routes/nodes.js +19 -18
  71. package/dist/server/routes/nodes.js.map +1 -1
  72. package/dist/server/routes/settings.d.ts +6 -1
  73. package/dist/server/routes/settings.js +5 -2
  74. package/dist/server/routes/settings.js.map +1 -1
  75. package/dist/server/routes/status.d.ts +8 -4
  76. package/dist/server/routes/status.js +9 -4
  77. package/dist/server/routes/status.js.map +1 -1
  78. package/dist/update/release-key.js +7 -5
  79. package/dist/update/release-key.js.map +1 -1
  80. package/package.json +4 -4
  81. package/src/__tests__/{host-authority.smoke.test.ts → host-owner.smoke.test.ts} +7 -7
  82. package/src/__tests__/{orchestrator-authority.test.ts → orchestrator-owner.test.ts} +33 -33
  83. package/src/__tests__/orchestrator-pin-keys.test.ts +153 -0
  84. package/src/__tests__/orchestrator-push.test.ts +10 -10
  85. package/src/auth/__tests__/trust-circle-integration.test.ts +11 -11
  86. package/src/auth/__tests__/trust-circle.test.ts +10 -10
  87. package/src/auth/trust-circle.ts +10 -10
  88. package/src/auth/types.ts +2 -2
  89. package/src/bin/host.ts +273 -53
  90. package/src/donation/__tests__/donation-service.test.ts +290 -0
  91. package/src/donation/__tests__/grant-service.test.ts +213 -0
  92. package/src/donation/__tests__/grant-store.test.ts +113 -0
  93. package/src/donation/donation-service.ts +392 -0
  94. package/src/donation/donation-store.ts +164 -0
  95. package/src/donation/grant-service.ts +200 -0
  96. package/src/donation/grant-store.ts +152 -0
  97. package/src/donation/index.ts +49 -0
  98. package/src/donation/types.ts +220 -0
  99. package/src/index.ts +37 -5
  100. package/src/installer/__tests__/config.test.ts +27 -1
  101. package/src/installer/__tests__/installer.smoke.test.ts +31 -1
  102. package/src/installer/__tests__/wizard.test.ts +5 -1
  103. package/src/installer/config.ts +34 -0
  104. package/src/installer/index.ts +5 -0
  105. package/src/installer/wizard.ts +12 -1
  106. package/src/nat/__tests__/nat-service.test.ts +5 -5
  107. package/src/nat/nat-service.ts +10 -10
  108. package/src/nat/types.ts +1 -1
  109. package/src/orchestrator/host-process-orchestrator.ts +88 -60
  110. package/src/orchestrator/index.ts +3 -3
  111. package/src/orchestrator/state-store.ts +7 -7
  112. package/src/orchestrator/types.ts +8 -8
  113. package/src/{authority/__tests__/authority-node-client.test.ts → owner/__tests__/owner-node-client.test.ts} +45 -17
  114. package/src/owner/index.ts +12 -0
  115. package/src/{authority/authority-node-client.ts → owner/owner-node-client.ts} +55 -21
  116. package/src/server/__tests__/error-handler.test.ts +22 -0
  117. package/src/server/__tests__/grants-admin-route.test.ts +73 -0
  118. package/src/server/__tests__/grants-route.test.ts +189 -0
  119. package/src/server/__tests__/nodes-route.test.ts +25 -25
  120. package/src/server/__tests__/server.smoke.test.ts +75 -0
  121. package/src/server/__tests__/settings-route.test.ts +17 -0
  122. package/src/server/__tests__/status-route.test.ts +21 -0
  123. package/src/server/error-handler.ts +43 -0
  124. package/src/server/index.ts +62 -17
  125. package/src/server/routes/grants-admin.ts +41 -0
  126. package/src/server/routes/grants.ts +186 -0
  127. package/src/server/routes/nodes.ts +19 -18
  128. package/src/server/routes/settings.ts +11 -3
  129. package/src/server/routes/status.ts +18 -8
  130. package/src/update/__tests__/release-key.test.ts +16 -3
  131. package/src/update/release-key.ts +7 -5
  132. package/dist/authority/authority-node-client.js.map +0 -1
  133. package/dist/authority/index.d.ts +0 -11
  134. package/dist/authority/index.js +0 -11
  135. package/dist/authority/index.js.map +0 -1
  136. 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 AuthoritySpawnConfig,
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 authority node. */
57
- export const AUTHORITY_CONTAINER_ID = 'authority';
56
+ /** Fixed friendly id for the admin's owner node. */
57
+ export const OWNER_CONTAINER_ID = 'owner';
58
58
 
59
- /** Endpoint + bearer for the authority node's loopback admin channel (6.6). */
60
- export interface AuthorityAdminEndpoint {
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 authority node (re-spawn on demand). */
112
- private authorityConfig?: AuthoritySpawnConfig;
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.authorityConfig = state.authorityConfig;
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.authority ? { authority: true } : {}),
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
- authority: false,
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 authority node is running, spawning it if absent or
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 authority node is spawned as `cadre-cli start --authority
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 ensureAuthorityNode(cfg?: AuthoritySpawnConfig): Promise<ManagedNodeInfo> {
276
- const config = cfg ?? this.authorityConfig;
288
+ async ensureOwnerNode(cfg?: OwnerSpawnConfig): Promise<ManagedNodeInfo> {
289
+ const config = cfg ?? this.ownerConfig;
277
290
  if (!config) {
278
- throw new Error('ensureAuthorityNode: no authority config supplied and none persisted');
291
+ throw new Error('ensureOwnerNode: no owner config supplied and none persisted');
279
292
  }
280
- this.authorityConfig = config;
293
+ this.ownerConfig = config;
281
294
 
282
- const existing = this.findAuthorityHandle();
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 authority node's control-DB storage lives there and must
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 authority node must listen on the configured libp2p port so 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: AUTHORITY_CONTAINER_ID,
324
+ containerId: OWNER_CONTAINER_ID,
312
325
  partyId: config.partyId,
313
326
  profile,
314
327
  ports,
315
- authority: true,
316
- buildConfig: (workdir) => this.buildAuthorityChildConfig(config, profile, workdir, push),
317
- extraArgs: ['--authority', '--admin-port', String(ports.admin), '--identity-protobuf', config.identityPath],
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 authority node's admin channel, if spawned. */
324
- getAuthorityAdminEndpoint(): AuthorityAdminEndpoint | undefined {
325
- const handle = this.findAuthorityHandle();
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 authority node are known (persisted). */
334
- hasAuthorityConfig(): boolean {
335
- return this.authorityConfig !== undefined;
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 authority node. */
339
- isAuthorityNode(idOrDockerId: string): boolean {
340
- if (idOrDockerId === AUTHORITY_CONTAINER_ID) return true;
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?.authority) return true;
355
+ if (direct?.owner) return true;
343
356
  for (const h of this.handles.values()) {
344
- if (h.authority && h.containerId === idOrDockerId) return true;
357
+ if (h.owner && h.containerId === idOrDockerId) return true;
345
358
  }
346
359
  return false;
347
360
  }
348
361
 
349
- /** Stop the authority node (if running). Used on graceful shutdown. */
350
- async stopAuthorityNode(): Promise<void> {
351
- const handle = this.findAuthorityHandle();
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 authority node (if running), then re-spawn it from saved config. */
358
- async restartAuthorityNode(): Promise<ManagedNodeInfo> {
359
- const handle = this.findAuthorityHandle();
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.ensureAuthorityNode();
376
+ return this.ensureOwnerNode();
364
377
  }
365
378
 
366
379
  /**
367
- * Core spawn mechanics shared by `createContainer` and `ensureAuthorityNode`.
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
- authority: boolean;
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.authority ? { authority: true } : {}),
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 authority node's handle, if one has been spawned. */
506
- private findAuthorityHandle(): Handle | undefined {
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.authority || h.containerId === AUTHORITY_CONTAINER_ID) return 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 authority node. It founds/joins the control network
692
- * for `partyId` (no bootstrap peers — it is the founding node), and carries
693
- * the host identity via `--identity-protobuf` (passed as a spawn arg).
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 buildAuthorityChildConfig(
696
- cfg: AuthoritySpawnConfig,
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 always-on authority/storage node participates in strands, so it owns
713
- // the push-wake fan-out when credentials are configured. Written into
714
- // cadre.json (same host trust boundary as the workdir's control-DB); keys
715
- // are never logged — only the redacted presence line in resolvePush.
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.authorityConfig ? { authorityConfig: this.authorityConfig } : {}),
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.authority ? { authority: true } : {}),
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.authority ? { authority: true } : {}),
795
+ ...(h.owner ? { owner: true } : {}),
768
796
  };
769
797
  }
770
798
 
@@ -1,10 +1,10 @@
1
- export { HostProcessOrchestrator, AUTHORITY_CONTAINER_ID } from './host-process-orchestrator.js';
2
- export type { AuthorityAdminEndpoint } from './host-process-orchestrator.js';
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
- AuthoritySpawnConfig,
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 { AuthoritySpawnConfig, NodePorts } from './types.js';
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 authority node. */
25
- authority?: boolean;
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 authority node, so it can be re-spawned on demand. */
32
- authorityConfig?: AuthoritySpawnConfig;
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.authorityConfig ? { authorityConfig: parsed.authorityConfig } : {}),
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.authorityConfig ? { authorityConfig: state.authorityConfig } : {}),
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 authority node. */
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 authority node (binds the admin channel). */
66
- authority?: boolean;
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 authority node. */
87
- authority?: boolean;
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 authority node. Stored alongside
92
- * the handles so an orchestrator restart can re-spawn the authority node from
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 AuthoritySpawnConfig {
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 { AuthorityNodeClient, AuthorityNodeUnavailableError } from '../authority-node-client.js';
5
- import type { AuthorityAdminEndpoint } from '../../orchestrator/index.js';
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(): AuthorityNodeClient {
56
- const endpoint: AuthorityAdminEndpoint = { baseUrl, token: TOKEN };
57
- return new AuthorityNodeClient(endpoint);
55
+ function makeClient(): OwnerNodeClient {
56
+ const endpoint: OwnerAdminEndpoint = { baseUrl, token: TOKEN };
57
+ return new OwnerNodeClient(endpoint);
58
58
  }
59
59
 
60
- describe('AuthorityNodeClient', () => {
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', authorityAddrs: [], token: 't', createdAt: 1 }, encodedInvite: 'enc' } },
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', authorityAddrs: [], token: 't', createdAt: 1 });
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', authorityAddrs: [], token: 't', createdAt: 1 };
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 AuthorityNodeUnavailableError carrying the code', async () => {
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(AuthorityNodeUnavailableError);
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 AuthorityNodeUnavailableError', async () => {
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 AuthorityNodeClient({ baseUrl: `http://127.0.0.1:${closedPort}`, token: TOKEN });
159
- await expect(client.getPeerId()).rejects.toBeInstanceOf(AuthorityNodeUnavailableError);
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 AuthorityNodeUnavailableError when the endpoint is undefined', async () => {
163
- const client = new AuthorityNodeClient(() => undefined);
164
- await expect(client.getPeerId()).rejects.toBeInstanceOf(AuthorityNodeUnavailableError);
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';