@serfab/cadre-core 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 (93) hide show
  1. package/README.md +2 -2
  2. package/dist/cadre-node.d.ts +101 -61
  3. package/dist/cadre-node.js +165 -124
  4. package/dist/cadre-node.js.map +1 -1
  5. package/dist/control-cohort.d.ts +15 -15
  6. package/dist/control-cohort.js +14 -14
  7. package/dist/control-cohort.js.map +1 -1
  8. package/dist/control-database.d.ts +51 -34
  9. package/dist/control-database.js +72 -56
  10. package/dist/control-database.js.map +1 -1
  11. package/dist/control-formation-recorder.d.ts +2 -2
  12. package/dist/control-formation-recorder.js +2 -2
  13. package/dist/control-schema.d.ts +1 -1
  14. package/dist/control-schema.js +57 -33
  15. package/dist/control-schema.js.map +1 -1
  16. package/dist/device-token.d.ts +1 -1
  17. package/dist/device-token.js +1 -1
  18. package/dist/{authority-key.d.ts → ed25519-key.d.ts} +11 -10
  19. package/dist/{authority-key.js → ed25519-key.js} +9 -10
  20. package/dist/ed25519-key.js.map +1 -0
  21. package/dist/index.d.ts +3 -3
  22. package/dist/index.js +8 -8
  23. package/dist/index.js.map +1 -1
  24. package/dist/key-store.d.ts +1 -1
  25. package/dist/key-store.js +1 -1
  26. package/dist/peer-authorization.d.ts +28 -6
  27. package/dist/peer-authorization.js +33 -7
  28. package/dist/peer-authorization.js.map +1 -1
  29. package/dist/peer-record.d.ts +3 -3
  30. package/dist/peer-record.js +3 -3
  31. package/dist/push-node.d.ts +38 -0
  32. package/dist/push-node.js +48 -0
  33. package/dist/push-node.js.map +1 -0
  34. package/dist/push-notifier-apns.d.ts +10 -2
  35. package/dist/push-notifier-apns.js +14 -10
  36. package/dist/push-notifier-apns.js.map +1 -1
  37. package/dist/push-notifier-fcm.d.ts +7 -1
  38. package/dist/push-notifier-fcm.js +9 -10
  39. package/dist/push-notifier-fcm.js.map +1 -1
  40. package/dist/push-notifier.d.ts +16 -35
  41. package/dist/push-notifier.js +16 -44
  42. package/dist/push-notifier.js.map +1 -1
  43. package/dist/seed-bootstrap.d.ts +70 -51
  44. package/dist/seed-bootstrap.js +168 -106
  45. package/dist/seed-bootstrap.js.map +1 -1
  46. package/dist/seed-trust-policy.d.ts +11 -11
  47. package/dist/seed-trust-policy.js +11 -11
  48. package/dist/seed-trust-policy.js.map +1 -1
  49. package/dist/strand-database.d.ts +5 -5
  50. package/dist/strand-database.js +4 -4
  51. package/dist/strand-database.js.map +1 -1
  52. package/dist/strand-formation-protocol.d.ts +1 -1
  53. package/dist/strand-formation-protocol.js +1 -1
  54. package/dist/strand-instance-manager.d.ts +1 -1
  55. package/dist/strand-instance-manager.js +1 -1
  56. package/dist/strand-instance-manager.js.map +1 -1
  57. package/dist/strand-member-key.d.ts +6 -6
  58. package/dist/strand-member-key.js +6 -6
  59. package/dist/strand-member-key.js.map +1 -1
  60. package/dist/strand-member-registry.d.ts +7 -7
  61. package/dist/strand-member-registry.js +5 -5
  62. package/dist/strand-member-registry.js.map +1 -1
  63. package/dist/strand-membership-writer.d.ts +84 -84
  64. package/dist/strand-membership-writer.js +90 -90
  65. package/dist/strand-membership-writer.js.map +1 -1
  66. package/dist/types.d.ts +52 -32
  67. package/dist/types.js.map +1 -1
  68. package/package.json +7 -3
  69. package/src/cadre-node.ts +170 -126
  70. package/src/control-cohort.ts +17 -17
  71. package/src/control-database.ts +76 -59
  72. package/src/control-formation-recorder.ts +2 -2
  73. package/src/control-schema.ts +57 -33
  74. package/src/device-token.ts +1 -1
  75. package/src/{authority-key.ts → ed25519-key.ts} +12 -11
  76. package/src/index.ts +21 -19
  77. package/src/key-store.ts +1 -1
  78. package/src/peer-authorization.ts +35 -7
  79. package/src/peer-record.ts +3 -3
  80. package/src/push-node.ts +64 -0
  81. package/src/push-notifier-apns.ts +16 -12
  82. package/src/push-notifier-fcm.ts +9 -10
  83. package/src/push-notifier.ts +16 -57
  84. package/src/seed-bootstrap.ts +179 -115
  85. package/src/seed-trust-policy.ts +18 -18
  86. package/src/strand-database.ts +6 -6
  87. package/src/strand-formation-protocol.ts +1 -1
  88. package/src/strand-instance-manager.ts +2 -2
  89. package/src/strand-member-key.ts +7 -7
  90. package/src/strand-member-registry.ts +10 -10
  91. package/src/strand-membership-writer.ts +127 -127
  92. package/src/types.ts +52 -32
  93. package/dist/authority-key.js.map +0 -1
@@ -6,7 +6,7 @@ import { createLibp2pNode } from '@optimystic/db-p2p';
6
6
  import { multiaddr } from '@multiformats/multiaddr';
7
7
  import { DEFAULT_CHECKIN_WINDOW_MS } from './types.js';
8
8
  import { sign } from '@optimystic/quereus-plugin-crypto';
9
- import { authorityKeyFromLibp2p } from './authority-key.js';
9
+ import { ed25519KeyPairFromLibp2p } from './ed25519-key.js';
10
10
  import { DEFAULT_IDENTITY_KEY_ID } from './key-store.js';
11
11
  import { ed25519PublicKeyB64FromPeerId } from './seed-bootstrap.js';
12
12
  import { signPeerRecord, verifyPeerRecordSignature, isPeerRecordFresh, orderSignalingFirst, isSignalingAddr, currentMemberTrustPolicy, DEFAULT_PEER_RECORD_MAX_AGE_MS, DEFAULT_PEER_RECORD_HEARTBEAT_MS } from './peer-record.js';
@@ -59,9 +59,9 @@ export class CadreNode {
59
59
  this.strandAddrService = null;
60
60
  /**
61
61
  * Server-side push-wake fan-out. Constructed by {@link start} only when
62
- * `config.push` (FCM/APNs credentials) is present — without it the node behaves
63
- * exactly as before (no notifier, no fan-out). Owns who/when to wake
64
- * hibernating mobile peers on strand activity.
62
+ * `config.push` (an injected `PushNotifier` + policy) is present — without it
63
+ * the node behaves exactly as before (no notifier, no fan-out). Owns who/when
64
+ * to wake hibernating mobile peers on strand activity.
65
65
  */
66
66
  this.pushFanoutService = null;
67
67
  /** Backing field for the {@link running} / {@link isRunning} getters. */
@@ -111,14 +111,14 @@ export class CadreNode {
111
111
  this.reconcileControlCohortInFlight = null;
112
112
  /**
113
113
  * Single-flight guard for {@link registerSelf}. Concurrent callers (the explicit
114
- * CLI `--authority` publish, the 1s startup timer, the TTL heartbeat, and the
114
+ * CLI `--owner` publish, the 1s startup timer, the TTL heartbeat, and the
115
115
  * address-change listener) share one in-flight publish so two of them can never
116
116
  * both read "no row yet" and race a duplicate INSERT (a `CadrePeer` PK conflict).
117
117
  */
118
118
  this.registerSelfInFlight = null;
119
119
  // ── Write-while-alone re-replication (control-write-ensure-replicated) ──────
120
120
  /**
121
- * Re-replication queue for authority control writes that committed while this
121
+ * Re-replication queue for owner control writes that committed while this
122
122
  * node was alone — `controlNode.getConnections().length === 0` at write time
123
123
  * means the Optimystic commit was local-only (the block's cluster was ≤1) and
124
124
  * never broadcast. Maps the affected subject peerId → the write to re-issue once
@@ -132,7 +132,7 @@ export class CadreNode {
132
132
  /** This node's own `DeviceToken` self-write committed local-only (re-touched on growth). */
133
133
  this.pendingSelfDeviceWrite = false;
134
134
  /**
135
- * Guards the one-shot, first-cohort-growth reconstruction: an authority re-touches
135
+ * Guards the one-shot, first-cohort-growth reconstruction: an owner re-touches
136
136
  * every membership row it may have authored that could be unreplicated (covering
137
137
  * writes made before this process started, which the in-memory queue cannot know).
138
138
  * Set true after the first drain so later passes only drain the in-memory queue.
@@ -327,10 +327,11 @@ export class CadreNode {
327
327
  // Start hibernation manager
328
328
  this.hibernationManager.start();
329
329
  // Register the control-network push-wake receiver: a same-cadre peer can
330
- // signal us to bring a hibernating strand online. Gated on CadrePeer
331
- // membership; the wake routes through the same path as a local wake.
330
+ // signal us to bring a hibernating strand online. Gated on AUTHORIZED
331
+ // membership (not the addressable surface); the wake routes through the same
332
+ // path as a local wake.
332
333
  this.strandWakeService = new StrandWakeService({
333
- isMember: (peerId) => this.isMember(peerId),
334
+ isMember: (peerId) => this.isAuthorizedMember(peerId),
334
335
  getStrand: (strandId) => this.strandManager.getInstance(strandId),
335
336
  wake: (strandId) => this.wakeStrand(strandId),
336
337
  });
@@ -338,32 +339,16 @@ export class CadreNode {
338
339
  // Register the control-network strand-address responder: a same-cadre peer
339
340
  // resolving a strand's bootstrap seed asks us for our live strand-network
340
341
  // multiaddrs (its CadrePeer row only knows our *control* address). Gated on
341
- // CadrePeer membership; answers only for strands we are actively meshing.
342
+ // AUTHORIZED membership; answers only for strands we are actively meshing.
342
343
  this.strandAddrService = new StrandAddrService({
343
- isMember: (peerId) => this.isMember(peerId),
344
+ isMember: (peerId) => this.isAuthorizedMember(peerId),
344
345
  getStrandMultiaddrs: (strandId) => this.getStrandMultiaddrs(strandId),
345
346
  });
346
347
  this.strandAddrService.initialize(this.controlNode);
347
- // Server-side push-wake fan-out: only when push credentials are configured.
348
- // The PushNotifier reaches for node:http2/node:crypto, so it is loaded via a
349
- // guarded DYNAMIC import — a cross-platform (RN/browser) node that never sets
350
- // config.push never pulls those modules into its static graph. The fan-out
351
- // service itself is import-clean (PushNotifier type only) and statically imported.
348
+ // Server-side push-wake fan-out: only when push is configured.
352
349
  if (this.config.push) {
353
- const { createPushNotifier } = await import('./push-notifier.js');
354
- const notifier = createPushNotifier(this.config.push);
355
- this.pushFanoutService = new PushFanoutService({
356
- listMembers: () => this.listMembers(),
357
- getStrand: (strandId) => this.strandManager.getInstance(strandId),
358
- selfPeerId: () => this.controlNode?.peerId.toString(),
359
- pushWake: (peerId, strandId, reason) => this.pushWake(peerId, strandId, reason),
360
- resolveDeviceToken: (peerId) => this.resolveDeviceToken(peerId),
361
- expireDeviceToken: (peerId) => this.expireDeviceToken(peerId),
362
- notifier,
363
- cooldownMs: this.config.push.cooldownMs,
364
- debounceMs: this.config.push.debounceMs,
365
- });
366
- log('Push-wake fan-out enabled (FCM=%s, APNs=%s)', !!this.config.push.fcm, !!this.config.push.apns);
350
+ this.pushFanoutService = this.buildPushFanout(this.config.push);
351
+ log('Push-wake fan-out enabled (injected notifier)');
367
352
  }
368
353
  this._running = true;
369
354
  this.emit('control:connected', undefined);
@@ -382,6 +367,31 @@ export class CadreNode {
382
367
  throw error;
383
368
  }
384
369
  }
370
+ /**
371
+ * Build the server-side push-wake fan-out over an injected {@link PushNotifier}.
372
+ *
373
+ * The notifier reaches for `node:http2`/`node:crypto`, so the cross-platform
374
+ * core never constructs it — the Node host builds it from
375
+ * `@serfab/cadre-core/push-node` and passes the instance in
376
+ * `CadreNodeConfig.push`. This node owns that instance's lifecycle from here:
377
+ * {@link cleanup} closes the fan-out, which closes the notifier (freeing the
378
+ * APNs HTTP/2 session). Every other primitive the fan-out needs (member
379
+ * enumeration, participation, direct dial, token resolve/expire) is an
380
+ * import-clean closure over this node.
381
+ */
382
+ buildPushFanout(push) {
383
+ return new PushFanoutService({
384
+ listMembers: () => this.listMembers(),
385
+ getStrand: (strandId) => this.strandManager.getInstance(strandId),
386
+ selfPeerId: () => this.controlNode?.peerId.toString(),
387
+ pushWake: (peerId, strandId, reason) => this.pushWake(peerId, strandId, reason),
388
+ resolveDeviceToken: (peerId) => this.resolveDeviceToken(peerId),
389
+ expireDeviceToken: (peerId) => this.expireDeviceToken(peerId),
390
+ notifier: push.notifier,
391
+ cooldownMs: push.cooldownMs,
392
+ debounceMs: push.debounceMs,
393
+ });
394
+ }
385
395
  /**
386
396
  * Stop the cadre node
387
397
  */
@@ -515,7 +525,7 @@ export class CadreNode {
515
525
  /**
516
526
  * Schedule the node's initial self-record publish + ongoing refresh shortly
517
527
  * after start (non-blocking). {@link registerSelf} is idempotent and safely
518
- * no-ops when it cannot yet sign/insert (e.g. authority key not installed),
528
+ * no-ops when it cannot yet sign/insert (e.g. owner key not installed),
519
529
  * so the timer is harmless even when registration only becomes possible later.
520
530
  */
521
531
  scheduleSelfRegistration() {
@@ -547,10 +557,10 @@ export class CadreNode {
547
557
  * (signaling/`p2p-circuit` first), signed with the ed25519 key behind its
548
558
  * PeerId (the resolved node identity from `keyStore`/`config.privateKey`).
549
559
  * - If the row already exists: a self-signed UPDATE bumping `UpdatedAt`.
550
- * - If not, and the node is its own authority: an authority-signed INSERT that
560
+ * - If not, and the node is its own owner: an owner-signed INSERT that
551
561
  * also carries the self-signature.
552
- * - Otherwise: logs and returns (a non-authority node with no row yet must
553
- * wait for an authority to insert it; it can then self-refresh).
562
+ * - Otherwise: logs and returns (a non-owner node with no row yet must
563
+ * wait for an owner to insert it; it can then self-refresh).
554
564
  *
555
565
  * Safe to call repeatedly (heartbeat / address-change driven); each successful
556
566
  * publish strictly increases `UpdatedAt`. Concurrent calls are collapsed into a
@@ -600,15 +610,15 @@ export class CadreNode {
600
610
  return 'refreshed';
601
611
  }
602
612
  if (this.seedBootstrapService) {
603
- // First-time row: requires an authority signature (the node is its own
604
- // authority). insertSelfPeerRecord throws if no authority key is present.
613
+ // First-time row: requires an owner signature (the node is its own
614
+ // owner). insertSelfPeerRecord throws if no owner key is present.
605
615
  await this.seedBootstrapService.insertSelfPeerRecord(record);
606
616
  if (this.committedAlone())
607
617
  this.pendingSelfPeerWrite = true;
608
- log('registerSelf: inserted own CadrePeer record (authority-signed, updatedAt=%d, %d addrs)', updatedAt, addrs.length);
618
+ log('registerSelf: inserted own CadrePeer record (owner-signed, updatedAt=%d, %d addrs)', updatedAt, addrs.length);
609
619
  return 'inserted';
610
620
  }
611
- log('registerSelf: not yet a CadrePeer member and no authority service to self-insert; skipping (an authority must add this peer first)');
621
+ log('registerSelf: not yet a CadrePeer member and no owner service to self-insert; skipping (an owner must add this peer first)');
612
622
  return 'skipped';
613
623
  }
614
624
  /**
@@ -625,41 +635,41 @@ export class CadreNode {
625
635
  return null;
626
636
  }
627
637
  try {
628
- const { privateKeyB64, publicKeyB64 } = authorityKeyFromLibp2p(this.identityKey);
638
+ const { privateKeyB64, publicKeyB64 } = ed25519KeyPairFromLibp2p(this.identityKey);
629
639
  if (publicKeyB64 === ed25519PublicKeyB64FromPeerId(peerId)) {
630
640
  return { privateKeyB64, publicKeyB64 };
631
641
  }
632
642
  log('getSelfSigningKey: resolved identity key does not match control node peerId; cannot self-sign');
633
643
  }
634
644
  catch (error) {
635
- // Logs the error shape only; authorityKeyFromLibp2p never embeds key material.
645
+ // Logs the error shape only; ed25519KeyPairFromLibp2p never embeds key material.
636
646
  log('getSelfSigningKey: failed to derive ed25519 key from identity key: %o', error);
637
647
  }
638
648
  return null;
639
649
  }
640
650
  /**
641
- * The authority keypair (base64url Ed25519) derived from this node's resolved
642
- * identity key. In the single-key reference model the authority signing key is
643
- * *derived from* the node identity (see {@link authorityKeyFromLibp2p}), so the
651
+ * The owner keypair (base64url Ed25519) derived from this node's resolved
652
+ * identity key. In the single-key reference model the owner signing key is
653
+ * *derived from* the node identity (see {@link ed25519KeyPairFromLibp2p}), so the
644
654
  * same key material protected in a secure enclave backs both.
645
655
  *
646
- * Exposed so the hosting app retains control of authority genesis: cadre-core
656
+ * Exposed so the hosting app retains control of owner genesis: cadre-core
647
657
  * resolves + protects the identity, then the app sources this pair to drive
648
- * `ensureAuthorityKey(pub)` + `initializeSeedBootstrap(priv)` itself — cadre-core
649
- * never silently runs genesis. A future separate-authority slot would return a
658
+ * `ensureOwnerKey(pub)` + `initializeSeedBootstrap(priv)` itself — cadre-core
659
+ * never silently runs genesis. A future separate-owner slot would return a
650
660
  * distinct key here instead of the identity-derived one.
651
661
  *
652
- * @returns The base64url seed/public-key authority pair.
662
+ * @returns The base64url seed/public-key owner pair.
653
663
  * @throws If called before {@link start} has resolved the identity, or when the
654
664
  * node runs on an ephemeral libp2p key (no `keyStore`/`privateKey` configured),
655
665
  * since that key is internal to libp2p and not exposed.
656
666
  */
657
- getIdentityAuthorityKey() {
667
+ getIdentityOwnerKey() {
658
668
  if (!this.identityKey) {
659
- throw new Error('getIdentityAuthorityKey: node identity not resolved — call start() first, and ' +
660
- 'configure `keyStore` or `privateKey` (an ephemeral libp2p identity exposes no authority key)');
669
+ throw new Error('getIdentityOwnerKey: node identity not resolved — call start() first, and ' +
670
+ 'configure `keyStore` or `privateKey` (an ephemeral libp2p identity exposes no owner key)');
661
671
  }
662
- return authorityKeyFromLibp2p(this.identityKey);
672
+ return ed25519KeyPairFromLibp2p(this.identityKey);
663
673
  }
664
674
  /**
665
675
  * Collect this node's current dialable addresses for publication, signaling
@@ -742,7 +752,7 @@ export class CadreNode {
742
752
  this.turnConnectionCloseHandler = null;
743
753
  // Reset the write-while-alone re-replication state so a stop()→start() cycle
744
754
  // re-arms the growth edge and re-runs the first-growth reconstruction. Any
745
- // queued local-only writes are moot once the node is torn down; authority
755
+ // queued local-only writes are moot once the node is torn down; owner
746
756
  // inserts are re-covered by reconstruction on the next start, and the
747
757
  // (already-loudly-logged) delete gap is tracked for follow-up.
748
758
  this.hasControlConnection = false;
@@ -889,17 +899,17 @@ export class CadreNode {
889
899
  if (!this._running || !this.controlNode || !this.controlDatabase) {
890
900
  return;
891
901
  }
892
- // 2. Classify backbone (authority) members and select a bounded dial set.
893
- const authorityKeys = await this.controlDatabase.getAuthorityKeys();
902
+ // 2. Classify backbone (owner) members and select a bounded dial set.
903
+ const ownerKeys = await this.controlDatabase.getOwnerKeys();
894
904
  if (!this._running || !this.controlNode) {
895
905
  return;
896
906
  }
897
907
  const targetDegree = this.config.network?.controlCohort?.targetDegree
898
908
  ?? DEFAULT_CONTROL_COHORT_TARGET_DEGREE;
899
- const { dials, cappedNonAuthority } = selectControlCohortDials(siblings, authorityKeys, targetDegree);
900
- if (cappedNonAuthority > 0) {
909
+ const { dials, cappedNonOwner } = selectControlCohortDials(siblings, ownerKeys, targetDegree);
910
+ if (cappedNonOwner > 0) {
901
911
  // Don't silently bound coverage — surface what the out-degree cap dropped.
902
- log('reconcileControlCohort: capped %d non-authority sibling(s) at targetDegree=%d', cappedNonAuthority, targetDegree);
912
+ log('reconcileControlCohort: capped %d non-owner sibling(s) at targetDegree=%d', cappedNonOwner, targetDegree);
903
913
  }
904
914
  // 3. Skip already-connected peers (no re-dial / churn for live connections).
905
915
  const connected = new Set(this.controlNode.getConnections().map((c) => c.remotePeer.toString()));
@@ -924,7 +934,7 @@ export class CadreNode {
924
934
  *
925
935
  * A per-peer failure (NAT, offline, relay down, connection-gater denial) is
926
936
  * logged and swallowed so one unreachable sibling never aborts the pass —
927
- * exactly like {@link SeedBootstrapService.applySeed}'s authority-dial loop. A
937
+ * exactly like {@link SeedBootstrapService.applySeed}'s owner-dial loop. A
928
938
  * failed dial is simply retried on the next pass.
929
939
  */
930
940
  async dialControlSibling(sibling) {
@@ -987,7 +997,7 @@ export class CadreNode {
987
997
  // Write-while-alone re-replication (control-write-ensure-replicated)
988
998
  //
989
999
  // Optimystic's coordinator commits a control write WITHOUT broadcasting when the
990
- // block's cluster has ≤1 member. An authority that authorizes/removes a peer or
1000
+ // block's cluster has ≤1 member. An owner that authorizes/removes a peer or
991
1001
  // (re)publishes its own record while no sibling is connected therefore writes a
992
1002
  // row that exists ONLY in its local control DB — a sibling that connects later
993
1003
  // never observes it (pull-on-read routes to the block's cluster, which never
@@ -1012,7 +1022,7 @@ export class CadreNode {
1012
1022
  return (this.controlNode?.getConnections().length ?? 0) === 0;
1013
1023
  }
1014
1024
  /**
1015
- * Record (or clear) a just-committed authority membership write in the
1025
+ * Record (or clear) a just-committed owner membership write in the
1016
1026
  * write-while-alone re-replication queue. If the control node had no connections
1017
1027
  * at commit, the Optimystic write was local-only and must be re-issued once the
1018
1028
  * cohort grows; otherwise the write replicated and any stale queue entry for this
@@ -1083,7 +1093,7 @@ export class CadreNode {
1083
1093
  await this.retouchSelfDeviceToken();
1084
1094
  this.pendingSelfDeviceWrite = false;
1085
1095
  }
1086
- // 2. First growth: an authority reconstructs membership rows it may have
1096
+ // 2. First growth: an owner reconstructs membership rows it may have
1087
1097
  // authored that never replicated (covers writes from before this process
1088
1098
  // started). Rows already tracked in the in-memory queue are handled by
1089
1099
  // step 3 — skipped here to avoid a double re-touch.
@@ -1091,7 +1101,7 @@ export class CadreNode {
1091
1101
  this.reconstructedLocalOnlyWrites = true;
1092
1102
  await this.reconstructAuthoredMembership();
1093
1103
  }
1094
- // 3. In-session pending authority membership writes (authorize / remove).
1104
+ // 3. In-session pending owner membership writes (authorize / remove).
1095
1105
  await this.drainPendingPeerWrites();
1096
1106
  }
1097
1107
  /**
@@ -1119,16 +1129,16 @@ export class CadreNode {
1119
1129
  }
1120
1130
  /**
1121
1131
  * One-shot, first-cohort-growth reconstruction of the write-while-alone queue for
1122
- * an AUTHORITY node: re-touch every membership row that may be an unreplicated
1123
- * authority insert. A row is a candidate iff it is not self (handled by
1132
+ * an OWNER node: re-touch every membership row that may be an unreplicated
1133
+ * owner insert. A row is a candidate iff it is not self (handled by
1124
1134
  * {@link registerSelf}), is not already tracked in the in-memory queue (handled by
1125
1135
  * {@link drainPendingPeerWrites}), and carries no self-`Sig` yet (an
1126
- * authority-authored row the peer has not self-published — the only kind safe to
1136
+ * owner-authored row the peer has not self-published — the only kind safe to
1127
1137
  * bump without invalidating a self-signature). O(rows) on the small control
1128
- * tables; safe to over-apply (a monotonic authority bump on an already-replicated
1138
+ * tables; safe to over-apply (a monotonic owner bump on an already-replicated
1129
1139
  * row is a no-op-equivalent).
1130
1140
  *
1131
- * A node with no authority private key skips this entirely — it cannot re-sign
1141
+ * A node with no owner private key skips this entirely — it cannot re-sign
1132
1142
  * rows for other peers, and rows it merely holds are not its to re-issue. DELETEs
1133
1143
  * cannot be reconstructed here (a removed row leaves no local trace and the schema
1134
1144
  * carries no tombstone) — see the delete-path note in {@link drainPendingPeerWrites}.
@@ -1164,16 +1174,16 @@ export class CadreNode {
1164
1174
  }
1165
1175
  }
1166
1176
  if (touched > 0) {
1167
- log('drain: reconstructed %d authority-authored membership row(s) on first cohort growth', touched);
1177
+ log('drain: reconstructed %d owner-authored membership row(s) on first cohort growth', touched);
1168
1178
  }
1169
1179
  }
1170
1180
  /**
1171
- * Drain the in-session write-while-alone queue: re-issue each pending authority
1181
+ * Drain the in-session write-while-alone queue: re-issue each pending owner
1172
1182
  * membership write now that the cohort can broadcast it. Sequential (no fan-out)
1173
1183
  * to avoid a thundering re-touch; an entry is cleared only on success, so a
1174
1184
  * failure (or a still-alone re-commit) leaves it queued for the next growth.
1175
1185
  *
1176
- * - `authorize`: re-issue as an idempotent monotonic authority UPDATE
1186
+ * - `authorize`: re-issue as an idempotent monotonic owner UPDATE
1177
1187
  * ({@link reissuePeerAuthorize}). Skipped (and cleared) if the row vanished
1178
1188
  * (raced a delete) or now carries a self-`Sig` (the peer self-published and owns
1179
1189
  * its republish).
@@ -1189,7 +1199,7 @@ export class CadreNode {
1189
1199
  return;
1190
1200
  }
1191
1201
  if (!this.seedBootstrapService?.canAuthorize()) {
1192
- // A non-authority node cannot have authored these writes; drop any stray entries.
1202
+ // A non-owner node cannot have authored these writes; drop any stray entries.
1193
1203
  this.pendingPeerWrites.clear();
1194
1204
  return;
1195
1205
  }
@@ -1201,7 +1211,7 @@ export class CadreNode {
1201
1211
  if (kind === 'authorize') {
1202
1212
  const record = await this.controlDatabase.queryPeerRecord(peerId);
1203
1213
  if (!record || record.sig) {
1204
- // Removed since, or peer self-published — nothing for the authority to re-issue.
1214
+ // Removed since, or peer self-published — nothing for the owner to re-issue.
1205
1215
  this.pendingPeerWrites.delete(peerId);
1206
1216
  continue;
1207
1217
  }
@@ -1220,9 +1230,9 @@ export class CadreNode {
1220
1230
  }
1221
1231
  }
1222
1232
  /**
1223
- * Re-issue an authority membership row as a monotonic authority UPDATE: bump
1233
+ * Re-issue an owner membership row as a monotonic owner UPDATE: bump
1224
1234
  * `UpdatedAt` strictly above the stored value (and the wall clock) and re-sign via
1225
- * the authority branch of `CadrePeer.AuthorizedUpdate`. The row already exists
1235
+ * the owner branch of `CadrePeer.AuthorizedUpdate`. The row already exists
1226
1236
  * locally (it committed there, just local-only), so this is an UPDATE, not the
1227
1237
  * original INSERT.
1228
1238
  */
@@ -1335,11 +1345,11 @@ export class CadreNode {
1335
1345
  * for any member — the `AuthorizedUpdate` self-branch verifies the new `Sig`
1336
1346
  * against the bound `CadrePeer.PublicKey`). Platform/Token may change here
1337
1347
  * (rotation / platform switch / reinstall are all normal self-updates).
1338
- * - If not, and the node holds an authority service: an authority-signed INSERT
1348
+ * - If not, and the node holds an owner service: an owner-signed INSERT
1339
1349
  * that also carries the self-signature.
1340
1350
  * - Otherwise: throws. Like `CadrePeer`, the first `DeviceToken` row requires an
1341
- * authority signature; a non-authority peer (e.g. a phone) must have its row
1342
- * seeded by an authority — typically the server it enrolled with — before it
1351
+ * owner signature; a non-owner peer (e.g. a phone) must have its row
1352
+ * seeded by an owner — typically the server it enrolled with — before it
1343
1353
  * can self-refresh. (Establishing that phone→server registration handshake is
1344
1354
  * the downstream "RN registration" ticket; this node only owns the cadre-core
1345
1355
  * write path.)
@@ -1350,7 +1360,7 @@ export class CadreNode {
1350
1360
  * @param platform - `'fcm'` (Android/Firebase) or `'apns'` (Apple).
1351
1361
  * @param token - the opaque platform device/registration token.
1352
1362
  * @throws if the node is not started, exposes no self-signing key, or has no
1353
- * existing row and no authority service to self-insert.
1363
+ * existing row and no owner service to self-insert.
1354
1364
  */
1355
1365
  async registerDeviceToken(platform, token) {
1356
1366
  if (!this._running || !this.controlNode || !this.controlDatabase) {
@@ -1377,11 +1387,11 @@ export class CadreNode {
1377
1387
  await this.seedBootstrapService.insertSelfDeviceToken(record);
1378
1388
  if (this.committedAlone())
1379
1389
  this.pendingSelfDeviceWrite = true;
1380
- log('registerDeviceToken: inserted own DeviceToken (authority-signed, platform=%s, updatedAt=%d)', platform, updatedAt);
1390
+ log('registerDeviceToken: inserted own DeviceToken (owner-signed, platform=%s, updatedAt=%d)', platform, updatedAt);
1381
1391
  return;
1382
1392
  }
1383
1393
  throw new Error(`Cannot register device token for ${peerId}: no existing row to self-update and no ` +
1384
- 'authority service to self-insert. An authority must seed this peer\'s DeviceToken ' +
1394
+ 'owner service to self-insert. An owner must seed this peer\'s DeviceToken ' +
1385
1395
  'row first (mirrors CadrePeer enrollment).');
1386
1396
  }
1387
1397
  /**
@@ -1444,11 +1454,11 @@ export class CadreNode {
1444
1454
  /**
1445
1455
  * Delete this node's own `DeviceToken` row (logout / token invalidation). No-op
1446
1456
  * when no row exists. Like {@link registerDeviceToken}'s first insert, the delete
1447
- * is gated on an authority signature (`DeviceToken.AuthorizedInsert` covers insert
1448
- * AND delete), so it requires this node's authority service; a non-authority peer
1449
- * must route the clear through its authority (downstream RN registration path).
1457
+ * is gated on an owner signature (`DeviceToken.AuthorizedInsert` covers insert
1458
+ * AND delete), so it requires this node's owner service; a non-owner peer
1459
+ * must route the clear through its owner (downstream RN registration path).
1450
1460
  *
1451
- * @throws if the node is not started, or a row exists but no authority service is
1461
+ * @throws if the node is not started, or a row exists but no owner service is
1452
1462
  * available to sign the delete.
1453
1463
  */
1454
1464
  async clearDeviceToken() {
@@ -1462,8 +1472,8 @@ export class CadreNode {
1462
1472
  return;
1463
1473
  }
1464
1474
  if (!this.seedBootstrapService) {
1465
- throw new Error(`Cannot clear device token for ${peerId}: delete requires an authority signature ` +
1466
- 'and no authority service is initialized.');
1475
+ throw new Error(`Cannot clear device token for ${peerId}: delete requires an owner signature ` +
1476
+ 'and no owner service is initialized.');
1467
1477
  }
1468
1478
  await this.seedBootstrapService.deleteDeviceToken(peerId);
1469
1479
  log('clearDeviceToken: deleted DeviceToken for %s', peerId);
@@ -1473,10 +1483,10 @@ export class CadreNode {
1473
1483
  * unregistered during a push-wake fan-out. Unlike {@link clearDeviceToken}
1474
1484
  * (self-only — it hardcodes the local peerId), this takes an arbitrary peerId.
1475
1485
  *
1476
- * - When this node holds an authority seed service, it deletes the row
1477
- * (`deleteDeviceToken` is authority-gated and accepts any peerId), so the peer
1486
+ * - When this node holds an owner seed service, it deletes the row
1487
+ * (`deleteDeviceToken` is owner-gated and accepts any peerId), so the peer
1478
1488
  * is not retried until it re-registers.
1479
- * - When this node is NOT an authority it cannot delete the row, so it only logs
1489
+ * - When this node is NOT an owner it cannot delete the row, so it only logs
1480
1490
  * that a re-registration is needed. The fan-out's own in-memory dead-token set
1481
1491
  * is what actually stops re-pushing to the dead token this process — see
1482
1492
  * {@link PushFanoutService}. That set is acceptably lossy across restarts (a
@@ -1488,14 +1498,14 @@ export class CadreNode {
1488
1498
  if (this.seedBootstrapService) {
1489
1499
  try {
1490
1500
  await this.seedBootstrapService.deleteDeviceToken(peerId);
1491
- log('expireDeviceToken: authority-deleted stale DeviceToken for %s', peerId);
1501
+ log('expireDeviceToken: owner-deleted stale DeviceToken for %s', peerId);
1492
1502
  }
1493
1503
  catch (error) {
1494
- log('expireDeviceToken: authority delete for %s failed: %o', peerId, error);
1504
+ log('expireDeviceToken: owner delete for %s failed: %o', peerId, error);
1495
1505
  }
1496
1506
  return;
1497
1507
  }
1498
- log('expireDeviceToken: %s token is stale but this node is not an authority; re-registration required', peerId);
1508
+ log('expireDeviceToken: %s token is stale but this node is not an owner; re-registration required', peerId);
1499
1509
  }
1500
1510
  async handleStrandAdded(strand) {
1501
1511
  log('Handling strand added from control network: %s', strand.Id);
@@ -1823,19 +1833,19 @@ export class CadreNode {
1823
1833
  }
1824
1834
  /**
1825
1835
  * Publish a strand row to the shared control database under this node's own
1826
- * authority identity, so other cadre members discover it via control-network
1836
+ * owner identity, so other cadre members discover it via control-network
1827
1837
  * sync (their {@link StrandWatcher} fires `strand:discovered`).
1828
1838
  *
1829
- * This is the authority-signed `Strand` INSERT that {@link addStrand}
1839
+ * This is the owner-signed `Strand` INSERT that {@link addStrand}
1830
1840
  * deliberately omits: `addStrand` only starts the LOCAL strand instance,
1831
1841
  * whereas publishing makes the strand visible cadre-wide. A typical creator
1832
1842
  * does both (start locally + publish); a discovering peer only does
1833
1843
  * `addStrand` (the row already exists).
1834
1844
  *
1835
1845
  * The insert is signed with the ed25519 key behind this node's PeerId — which
1836
- * {@link authorityKeyFromLibp2p} also exposes as the node's authority keypair,
1837
- * so peer identity and authority key are one and the same. That key must be
1838
- * enrolled in `AuthorityKey` (e.g. via {@link ControlDatabase.ensureAuthorityKey}
1846
+ * {@link ed25519KeyPairFromLibp2p} also exposes as the node's owner keypair,
1847
+ * so peer identity and owner key are one and the same. That key must be
1848
+ * enrolled in `OwnerKey` (e.g. via {@link ControlDatabase.ensureOwnerKey}
1839
1849
  * at genesis) or the schema's `Strand.Authorized` constraint rejects the write.
1840
1850
  * Failing loudly here is intentional: a silently-unpublished strand would run
1841
1851
  * as a local-only island that no peer could ever discover or join.
@@ -1844,7 +1854,7 @@ export class CadreNode {
1844
1854
  * {@link addStrand}).
1845
1855
  * @param type - `'o'` for open (default) or `'c'` for closed.
1846
1856
  * @param memberPrivateKey - Optional membership key for a closed strand.
1847
- * @throws if the node is not started, exposes no authority signing key, or the
1857
+ * @throws if the node is not started, exposes no owner signing key, or the
1848
1858
  * control DB rejects the (unauthorized) insert.
1849
1859
  */
1850
1860
  async publishStrand(strandId, type = 'o', memberPrivateKey) {
@@ -1853,19 +1863,19 @@ export class CadreNode {
1853
1863
  }
1854
1864
  const signingKey = this.getSelfSigningKey();
1855
1865
  if (!signingKey) {
1856
- throw new Error(`Cannot publish strand ${strandId}: no authority signing key available ` +
1857
- '(node identity is unavailable or does not match the node PeerId). Run authority ' +
1858
- 'genesis (ensureAuthorityKey + initializeSeedBootstrap) before publishing.');
1866
+ throw new Error(`Cannot publish strand ${strandId}: no owner signing key available ` +
1867
+ '(node identity is unavailable or does not match the node PeerId). Run owner ' +
1868
+ 'genesis (ensureOwnerKey + initializeSeedBootstrap) before publishing.');
1859
1869
  }
1860
1870
  // insertStrand hands this callback the canonical row-bound message BYTES (see
1861
1871
  // buildAuthorizationMessage); ed25519-sign them directly (no pre-hash) with
1862
- // the authority private key, returning a base64url signature.
1872
+ // the owner private key, returning a base64url signature.
1863
1873
  const signMessage = (message) => sign(message, signingKey.privateKeyB64, 'ed25519', 'bytes', 'base64url', 'base64url');
1864
1874
  await this.controlDatabase.insertStrand(strandId, type, signingKey.publicKeyB64, signMessage, memberPrivateKey);
1865
- log('Published strand %s (type %s) to control DB under authority %s', strandId, type, signingKey.publicKeyB64);
1875
+ log('Published strand %s (type %s) to control DB under owner %s', strandId, type, signingKey.publicKeyB64);
1866
1876
  }
1867
1877
  /**
1868
- * Publish an authority-signed `FormationInvite` (open-invitation token) to the
1878
+ * Publish an owner-signed `FormationInvite` (open-invitation token) to the
1869
1879
  * shared control database, so a later {@link formStrand} redemption can be
1870
1880
  * validated against it (the consent branch of `Strand.Authorized`).
1871
1881
  *
@@ -1876,8 +1886,8 @@ export class CadreNode {
1876
1886
  * from this row. A host minting a closed-strand invite does both (mint +
1877
1887
  * publish), exactly as the integration harness's `createInvitation` does.
1878
1888
  *
1879
- * Signs with the same self-authority key as {@link publishStrand} (the ed25519
1880
- * key behind this node's PeerId, which must be an enrolled `AuthorityKey`).
1889
+ * Signs with the same self-owner key as {@link publishStrand} (the ed25519
1890
+ * key behind this node's PeerId, which must be an enrolled `OwnerKey`).
1881
1891
  * Throws loudly if the node isn't started or exposes no signing key.
1882
1892
  *
1883
1893
  * @param token - Invitation token (the `FormationInvite` primary key); use the
@@ -1892,13 +1902,13 @@ export class CadreNode {
1892
1902
  }
1893
1903
  const signingKey = this.getSelfSigningKey();
1894
1904
  if (!signingKey) {
1895
- throw new Error(`Cannot publish formation invite ${token}: no authority signing key available ` +
1896
- '(node identity is unavailable or does not match the node PeerId). Run authority ' +
1897
- 'genesis (ensureAuthorityKey + initializeSeedBootstrap) before publishing.');
1905
+ throw new Error(`Cannot publish formation invite ${token}: no owner signing key available ` +
1906
+ '(node identity is unavailable or does not match the node PeerId). Run owner ' +
1907
+ 'genesis (ensureOwnerKey + initializeSeedBootstrap) before publishing.');
1898
1908
  }
1899
1909
  const signMessage = (message) => sign(message, signingKey.privateKeyB64, 'ed25519', 'bytes', 'base64url', 'base64url');
1900
1910
  await this.controlDatabase.insertFormationInvite(token, sAppId, signingKey.publicKeyB64, signMessage, options);
1901
- log('Published formation invite %s (sApp %s) under authority %s', token, sAppId, signingKey.publicKeyB64);
1911
+ log('Published formation invite %s (sApp %s) under owner %s', token, sAppId, signingKey.publicKeyB64);
1902
1912
  }
1903
1913
  /**
1904
1914
  * Shared strand launch path for both the explicit (`addStrand`) and the
@@ -2187,18 +2197,18 @@ export class CadreNode {
2187
2197
  // Seed Bootstrap API
2188
2198
  // ============================================================================
2189
2199
  /**
2190
- * Initialize the seed bootstrap service with an authority key.
2200
+ * Initialize the seed bootstrap service with an owner key.
2191
2201
  * Must be called before using seed-related methods that require signing.
2192
2202
  *
2193
- * @param authorityPrivateKey - The authority's private key (base64url encoded)
2203
+ * @param ownerPrivateKey - The owner's private key (base64url encoded)
2194
2204
  */
2195
- initializeSeedBootstrap(authorityPrivateKey) {
2205
+ initializeSeedBootstrap(ownerPrivateKey) {
2196
2206
  if (!this.controlNode || !this.controlDatabase) {
2197
2207
  throw new Error('CadreNode must be started before initializing seed bootstrap');
2198
2208
  }
2199
2209
  this.seedBootstrapService = new SeedBootstrapService({
2200
2210
  partyId: this.config.controlNetwork.partyId,
2201
- authorityPrivateKey,
2211
+ ownerPrivateKey,
2202
2212
  inviteAddressResolver: () => this.resolveInviteAddresses(),
2203
2213
  trustPolicy: this.config.seedTrustPolicy,
2204
2214
  });
@@ -2243,11 +2253,42 @@ export class CadreNode {
2243
2253
  }
2244
2254
  /**
2245
2255
  * Probe whether a given peer is a `CadrePeer` member.
2256
+ *
2257
+ * This is the ADDRESSABLE surface ("do I have a dialable address record for this
2258
+ * peer") — it includes this node's own self-published row. Address resolution and
2259
+ * push fan-out use this. The trust-facing gate is {@link isAuthorizedMember}.
2246
2260
  */
2247
2261
  async isMember(peerId) {
2248
2262
  const members = await this.listMembers();
2249
2263
  return members.some(m => m.peerId === peerId);
2250
2264
  }
2265
+ /**
2266
+ * Enumerate the party's AUTHORIZED members — the trust-facing set, distinct from
2267
+ * the addressable set ({@link listMembers}). Excludes this node itself: a node
2268
+ * publishes its own `CadrePeer` address row so its dialable address rides in seeds,
2269
+ * but "self" is not a peer this node authorized. The control-network wake and
2270
+ * strand-address gates consult this set, NOT the addressable one.
2271
+ *
2272
+ * NOTE: this is the addressable set minus self for now. The real trust predicate
2273
+ * (voucher recorded on the row ∈ node-local trusted-owner anchor, signature
2274
+ * verified) lands with the `membership-node-local-owner-anchor` /
2275
+ * `membership-authorized-predicate-and-gates` tickets; keeping the filter in this one
2276
+ * method means that change touches a single place.
2277
+ */
2278
+ async listAuthorizedMembers() {
2279
+ const selfPeerId = this.peerId?.toString();
2280
+ const members = await this.listMembers();
2281
+ return members.filter(m => m.peerId !== selfPeerId);
2282
+ }
2283
+ /**
2284
+ * Probe whether a given peer is an AUTHORIZED party member (see
2285
+ * {@link listAuthorizedMembers}) — the gate the control-network wake and
2286
+ * strand-address responders consult, NOT {@link isMember} (the addressable surface).
2287
+ */
2288
+ async isAuthorizedMember(peerId) {
2289
+ const members = await this.listAuthorizedMembers();
2290
+ return members.some(m => m.peerId === peerId);
2291
+ }
2251
2292
  /**
2252
2293
  * Push-wake a hibernating cadre peer over the control network.
2253
2294
  *
@@ -2276,8 +2317,8 @@ export class CadreNode {
2276
2317
  }
2277
2318
  /**
2278
2319
  * Enable the seed listener for receiving seeds via the /sereus/seed/1.0.0 protocol.
2279
- * This is for drone nodes that need to receive seeds without being an authority.
2280
- * Does not require an authority key.
2320
+ * This is for drone nodes that need to receive seeds without being an owner.
2321
+ * Does not require an owner key.
2281
2322
  */
2282
2323
  enableSeedListener() {
2283
2324
  if (!this.controlNode || !this.controlDatabase) {
@@ -2290,7 +2331,7 @@ export class CadreNode {
2290
2331
  }
2291
2332
  this.seedBootstrapService = new SeedBootstrapService({
2292
2333
  partyId: this.config.controlNetwork.partyId,
2293
- // No authority key - this node only receives seeds
2334
+ // No owner key - this node only receives seeds
2294
2335
  inviteAddressResolver: () => this.resolveInviteAddresses(),
2295
2336
  trustPolicy: this.config.seedTrustPolicy,
2296
2337
  });
@@ -2310,7 +2351,7 @@ export class CadreNode {
2310
2351
  }
2311
2352
  /**
2312
2353
  * Authorize a new peer to join the cadre.
2313
- * Signs the peer ID with the authority key and inserts into CadrePeer table.
2354
+ * Signs the peer ID with the owner key and inserts into CadrePeer table.
2314
2355
  *
2315
2356
  * @param peerId - The peer ID to authorize
2316
2357
  * @param multiaddrs - Optional multiaddrs for the peer
@@ -2325,7 +2366,7 @@ export class CadreNode {
2325
2366
  }
2326
2367
  /**
2327
2368
  * Remove a previously-authorized peer from the cadre.
2328
- * Signs the peer ID with the authority key and deletes the CadrePeer row.
2369
+ * Signs the peer ID with the owner key and deletes the CadrePeer row.
2329
2370
  *
2330
2371
  * @param peerId - The peer ID to remove
2331
2372
  */
@@ -2339,7 +2380,7 @@ export class CadreNode {
2339
2380
  }
2340
2381
  /**
2341
2382
  * Create a seed from the current control network state.
2342
- * The seed contains peer information and is signed by an authority.
2383
+ * The seed contains peer information and is signed by an owner.
2343
2384
  */
2344
2385
  async createSeed() {
2345
2386
  if (!this.seedBootstrapService) {
@@ -2353,12 +2394,12 @@ export class CadreNode {
2353
2394
  * Validates the seed signature, then evaluates a trust anchor for the signer
2354
2395
  * key (see `SeedTrustPolicy`). An enrollment caller can pass a per-seed
2355
2396
  * `trustPolicy` override — e.g. a `pinnedKeyTrustPolicy` built from a
2356
- * `CadreInvite.authorityKeys` — so a cold-start node can accept its first
2397
+ * `CadreInvite.ownerKeys` — so a cold-start node can accept its first
2357
2398
  * seed without reconfiguring the service.
2358
2399
  */
2359
2400
  async applySeed(seed, options) {
2360
2401
  if (!this.seedBootstrapService) {
2361
- // Create a temporary service for applying seeds (doesn't need authority key).
2402
+ // Create a temporary service for applying seeds (doesn't need owner key).
2362
2403
  // partyId is the attacker-influenced seed.partyId — it only labels logs; the
2363
2404
  // trust decision rests solely on signerKey vs the anchor set (configured
2364
2405
  // default below, or the per-call options.trustPolicy override).
@@ -2477,7 +2518,7 @@ export class CadreNode {
2477
2518
  return JSON.parse(json);
2478
2519
  }
2479
2520
  /**
2480
- * Dial an authority from an invite (for phone joining via invite).
2521
+ * Dial an owner from an invite (for phone joining via invite).
2481
2522
  */
2482
2523
  async dialInvite(invite) {
2483
2524
  if (!this.seedBootstrapService) {