@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.
- package/README.md +2 -2
- package/dist/cadre-node.d.ts +101 -61
- package/dist/cadre-node.js +165 -124
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-cohort.d.ts +15 -15
- package/dist/control-cohort.js +14 -14
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +51 -34
- package/dist/control-database.js +72 -56
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +2 -2
- package/dist/control-formation-recorder.js +2 -2
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +57 -33
- package/dist/control-schema.js.map +1 -1
- package/dist/device-token.d.ts +1 -1
- package/dist/device-token.js +1 -1
- package/dist/{authority-key.d.ts → ed25519-key.d.ts} +11 -10
- package/dist/{authority-key.js → ed25519-key.js} +9 -10
- package/dist/ed25519-key.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/key-store.d.ts +1 -1
- package/dist/key-store.js +1 -1
- package/dist/peer-authorization.d.ts +28 -6
- package/dist/peer-authorization.js +33 -7
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-record.d.ts +3 -3
- package/dist/peer-record.js +3 -3
- package/dist/push-node.d.ts +38 -0
- package/dist/push-node.js +48 -0
- package/dist/push-node.js.map +1 -0
- package/dist/push-notifier-apns.d.ts +10 -2
- package/dist/push-notifier-apns.js +14 -10
- package/dist/push-notifier-apns.js.map +1 -1
- package/dist/push-notifier-fcm.d.ts +7 -1
- package/dist/push-notifier-fcm.js +9 -10
- package/dist/push-notifier-fcm.js.map +1 -1
- package/dist/push-notifier.d.ts +16 -35
- package/dist/push-notifier.js +16 -44
- package/dist/push-notifier.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +70 -51
- package/dist/seed-bootstrap.js +168 -106
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +11 -11
- package/dist/seed-trust-policy.js +11 -11
- package/dist/seed-trust-policy.js.map +1 -1
- package/dist/strand-database.d.ts +5 -5
- package/dist/strand-database.js +4 -4
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +1 -1
- package/dist/strand-formation-protocol.js +1 -1
- package/dist/strand-instance-manager.d.ts +1 -1
- package/dist/strand-instance-manager.js +1 -1
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-key.d.ts +6 -6
- package/dist/strand-member-key.js +6 -6
- package/dist/strand-member-key.js.map +1 -1
- package/dist/strand-member-registry.d.ts +7 -7
- package/dist/strand-member-registry.js +5 -5
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +84 -84
- package/dist/strand-membership-writer.js +90 -90
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/types.d.ts +52 -32
- package/dist/types.js.map +1 -1
- package/package.json +7 -3
- package/src/cadre-node.ts +170 -126
- package/src/control-cohort.ts +17 -17
- package/src/control-database.ts +76 -59
- package/src/control-formation-recorder.ts +2 -2
- package/src/control-schema.ts +57 -33
- package/src/device-token.ts +1 -1
- package/src/{authority-key.ts → ed25519-key.ts} +12 -11
- package/src/index.ts +21 -19
- package/src/key-store.ts +1 -1
- package/src/peer-authorization.ts +35 -7
- package/src/peer-record.ts +3 -3
- package/src/push-node.ts +64 -0
- package/src/push-notifier-apns.ts +16 -12
- package/src/push-notifier-fcm.ts +9 -10
- package/src/push-notifier.ts +16 -57
- package/src/seed-bootstrap.ts +179 -115
- package/src/seed-trust-policy.ts +18 -18
- package/src/strand-database.ts +6 -6
- package/src/strand-formation-protocol.ts +1 -1
- package/src/strand-instance-manager.ts +2 -2
- package/src/strand-member-key.ts +7 -7
- package/src/strand-member-registry.ts +10 -10
- package/src/strand-membership-writer.ts +127 -127
- package/src/types.ts +52 -32
- package/dist/authority-key.js.map +0 -1
package/dist/cadre-node.js
CHANGED
|
@@ -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 {
|
|
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` (
|
|
63
|
-
* exactly as before (no notifier, no fan-out). Owns who/when
|
|
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 `--
|
|
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
|
|
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
|
|
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
|
|
331
|
-
// membership; the wake routes through the same
|
|
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.
|
|
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
|
-
//
|
|
342
|
+
// AUTHORIZED membership; answers only for strands we are actively meshing.
|
|
342
343
|
this.strandAddrService = new StrandAddrService({
|
|
343
|
-
isMember: (peerId) => this.
|
|
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
|
|
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
|
-
|
|
354
|
-
|
|
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.
|
|
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
|
|
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-
|
|
553
|
-
* wait for an
|
|
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
|
|
604
|
-
//
|
|
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 (
|
|
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
|
|
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 } =
|
|
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;
|
|
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
|
|
642
|
-
* identity key. In the single-key reference model the
|
|
643
|
-
* *derived from* the node identity (see {@link
|
|
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
|
|
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
|
-
* `
|
|
649
|
-
* never silently runs genesis. A future separate-
|
|
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
|
|
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
|
-
|
|
667
|
+
getIdentityOwnerKey() {
|
|
658
668
|
if (!this.identityKey) {
|
|
659
|
-
throw new Error('
|
|
660
|
-
'configure `keyStore` or `privateKey` (an ephemeral libp2p identity exposes no
|
|
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
|
|
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;
|
|
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 (
|
|
893
|
-
const
|
|
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,
|
|
900
|
-
if (
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1123
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
1342
|
-
* seeded by an
|
|
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
|
|
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 (
|
|
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
|
-
'
|
|
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
|
|
1448
|
-
* AND delete), so it requires this node's
|
|
1449
|
-
* must route the clear through its
|
|
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
|
|
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
|
|
1466
|
-
'and no
|
|
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
|
|
1477
|
-
* (`deleteDeviceToken` is
|
|
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
|
|
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:
|
|
1501
|
+
log('expireDeviceToken: owner-deleted stale DeviceToken for %s', peerId);
|
|
1492
1502
|
}
|
|
1493
1503
|
catch (error) {
|
|
1494
|
-
log('expireDeviceToken:
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
1837
|
-
* so peer identity and
|
|
1838
|
-
* enrolled in `
|
|
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
|
|
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
|
|
1857
|
-
'(node identity is unavailable or does not match the node PeerId). Run
|
|
1858
|
-
'genesis (
|
|
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
|
|
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
|
|
1875
|
+
log('Published strand %s (type %s) to control DB under owner %s', strandId, type, signingKey.publicKeyB64);
|
|
1866
1876
|
}
|
|
1867
1877
|
/**
|
|
1868
|
-
* Publish an
|
|
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-
|
|
1880
|
-
* key behind this node's PeerId, which must be an enrolled `
|
|
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
|
|
1896
|
-
'(node identity is unavailable or does not match the node PeerId). Run
|
|
1897
|
-
'genesis (
|
|
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
|
|
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
|
|
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
|
|
2203
|
+
* @param ownerPrivateKey - The owner's private key (base64url encoded)
|
|
2194
2204
|
*/
|
|
2195
|
-
initializeSeedBootstrap(
|
|
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
|
-
|
|
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
|
|
2280
|
-
* Does not require an
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
2521
|
+
* Dial an owner from an invite (for phone joining via invite).
|
|
2481
2522
|
*/
|
|
2482
2523
|
async dialInvite(invite) {
|
|
2483
2524
|
if (!this.seedBootstrapService) {
|