@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/src/cadre-node.ts
CHANGED
|
@@ -34,7 +34,7 @@ import type {
|
|
|
34
34
|
} from './types.js';
|
|
35
35
|
import { DEFAULT_CHECKIN_WINDOW_MS } from './types.js';
|
|
36
36
|
import { sign } from '@optimystic/quereus-plugin-crypto';
|
|
37
|
-
import {
|
|
37
|
+
import { ed25519KeyPairFromLibp2p, type Ed25519KeyPair } from './ed25519-key.js';
|
|
38
38
|
import { DEFAULT_IDENTITY_KEY_ID } from './key-store.js';
|
|
39
39
|
import { ed25519PublicKeyB64FromPeerId } from './seed-bootstrap.js';
|
|
40
40
|
import {
|
|
@@ -109,7 +109,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
109
109
|
* The resolved node identity key, set once by {@link resolveIdentityKey}
|
|
110
110
|
* during {@link start} (from `config.keyStore`, else `config.privateKey`).
|
|
111
111
|
* Left undefined when neither is configured — libp2p then generates an
|
|
112
|
-
* ephemeral key internally and there is no exposed
|
|
112
|
+
* ephemeral key internally and there is no exposed owner key. Every
|
|
113
113
|
* identity-dependent path (control node creation, self-record signing, strand
|
|
114
114
|
* launch) reads this resolved field, never `config.privateKey` directly.
|
|
115
115
|
*/
|
|
@@ -132,9 +132,9 @@ export class CadreNode implements SAppIdLookup {
|
|
|
132
132
|
private strandAddrService: StrandAddrService | null = null;
|
|
133
133
|
/**
|
|
134
134
|
* Server-side push-wake fan-out. Constructed by {@link start} only when
|
|
135
|
-
* `config.push` (
|
|
136
|
-
* exactly as before (no notifier, no fan-out). Owns who/when
|
|
137
|
-
* hibernating mobile peers on strand activity.
|
|
135
|
+
* `config.push` (an injected `PushNotifier` + policy) is present — without it
|
|
136
|
+
* the node behaves exactly as before (no notifier, no fan-out). Owns who/when
|
|
137
|
+
* to wake hibernating mobile peers on strand activity.
|
|
138
138
|
*/
|
|
139
139
|
private pushFanoutService: PushFanoutService | null = null;
|
|
140
140
|
/** Backing field for the {@link running} / {@link isRunning} getters. */
|
|
@@ -187,7 +187,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
187
187
|
private reconcileControlCohortInFlight: Promise<void> | null = null;
|
|
188
188
|
/**
|
|
189
189
|
* Single-flight guard for {@link registerSelf}. Concurrent callers (the explicit
|
|
190
|
-
* CLI `--
|
|
190
|
+
* CLI `--owner` publish, the 1s startup timer, the TTL heartbeat, and the
|
|
191
191
|
* address-change listener) share one in-flight publish so two of them can never
|
|
192
192
|
* both read "no row yet" and race a duplicate INSERT (a `CadrePeer` PK conflict).
|
|
193
193
|
*/
|
|
@@ -195,7 +195,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
195
195
|
|
|
196
196
|
// ── Write-while-alone re-replication (control-write-ensure-replicated) ──────
|
|
197
197
|
/**
|
|
198
|
-
* Re-replication queue for
|
|
198
|
+
* Re-replication queue for owner control writes that committed while this
|
|
199
199
|
* node was alone — `controlNode.getConnections().length === 0` at write time
|
|
200
200
|
* means the Optimystic commit was local-only (the block's cluster was ≤1) and
|
|
201
201
|
* never broadcast. Maps the affected subject peerId → the write to re-issue once
|
|
@@ -209,7 +209,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
209
209
|
/** This node's own `DeviceToken` self-write committed local-only (re-touched on growth). */
|
|
210
210
|
private pendingSelfDeviceWrite = false;
|
|
211
211
|
/**
|
|
212
|
-
* Guards the one-shot, first-cohort-growth reconstruction: an
|
|
212
|
+
* Guards the one-shot, first-cohort-growth reconstruction: an owner re-touches
|
|
213
213
|
* every membership row it may have authored that could be unreplicated (covering
|
|
214
214
|
* writes made before this process started, which the in-memory queue cannot know).
|
|
215
215
|
* Set true after the first drain so later passes only drain the in-memory queue.
|
|
@@ -440,10 +440,11 @@ export class CadreNode implements SAppIdLookup {
|
|
|
440
440
|
this.hibernationManager.start();
|
|
441
441
|
|
|
442
442
|
// Register the control-network push-wake receiver: a same-cadre peer can
|
|
443
|
-
// signal us to bring a hibernating strand online. Gated on
|
|
444
|
-
// membership; the wake routes through the same
|
|
443
|
+
// signal us to bring a hibernating strand online. Gated on AUTHORIZED
|
|
444
|
+
// membership (not the addressable surface); the wake routes through the same
|
|
445
|
+
// path as a local wake.
|
|
445
446
|
this.strandWakeService = new StrandWakeService({
|
|
446
|
-
isMember: (peerId) => this.
|
|
447
|
+
isMember: (peerId) => this.isAuthorizedMember(peerId),
|
|
447
448
|
getStrand: (strandId) => this.strandManager.getInstance(strandId),
|
|
448
449
|
wake: (strandId) => this.wakeStrand(strandId),
|
|
449
450
|
});
|
|
@@ -452,33 +453,17 @@ export class CadreNode implements SAppIdLookup {
|
|
|
452
453
|
// Register the control-network strand-address responder: a same-cadre peer
|
|
453
454
|
// resolving a strand's bootstrap seed asks us for our live strand-network
|
|
454
455
|
// multiaddrs (its CadrePeer row only knows our *control* address). Gated on
|
|
455
|
-
//
|
|
456
|
+
// AUTHORIZED membership; answers only for strands we are actively meshing.
|
|
456
457
|
this.strandAddrService = new StrandAddrService({
|
|
457
|
-
isMember: (peerId) => this.
|
|
458
|
+
isMember: (peerId) => this.isAuthorizedMember(peerId),
|
|
458
459
|
getStrandMultiaddrs: (strandId) => this.getStrandMultiaddrs(strandId),
|
|
459
460
|
});
|
|
460
461
|
this.strandAddrService.initialize(this.controlNode);
|
|
461
462
|
|
|
462
|
-
// Server-side push-wake fan-out: only when push
|
|
463
|
-
// The PushNotifier reaches for node:http2/node:crypto, so it is loaded via a
|
|
464
|
-
// guarded DYNAMIC import — a cross-platform (RN/browser) node that never sets
|
|
465
|
-
// config.push never pulls those modules into its static graph. The fan-out
|
|
466
|
-
// service itself is import-clean (PushNotifier type only) and statically imported.
|
|
463
|
+
// Server-side push-wake fan-out: only when push is configured.
|
|
467
464
|
if (this.config.push) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
this.pushFanoutService = new PushFanoutService({
|
|
471
|
-
listMembers: () => this.listMembers(),
|
|
472
|
-
getStrand: (strandId) => this.strandManager.getInstance(strandId),
|
|
473
|
-
selfPeerId: () => this.controlNode?.peerId.toString(),
|
|
474
|
-
pushWake: (peerId, strandId, reason) => this.pushWake(peerId, strandId, reason),
|
|
475
|
-
resolveDeviceToken: (peerId) => this.resolveDeviceToken(peerId),
|
|
476
|
-
expireDeviceToken: (peerId) => this.expireDeviceToken(peerId),
|
|
477
|
-
notifier,
|
|
478
|
-
cooldownMs: this.config.push.cooldownMs,
|
|
479
|
-
debounceMs: this.config.push.debounceMs,
|
|
480
|
-
});
|
|
481
|
-
log('Push-wake fan-out enabled (FCM=%s, APNs=%s)', !!this.config.push.fcm, !!this.config.push.apns);
|
|
465
|
+
this.pushFanoutService = this.buildPushFanout(this.config.push);
|
|
466
|
+
log('Push-wake fan-out enabled (injected notifier)');
|
|
482
467
|
}
|
|
483
468
|
|
|
484
469
|
this._running = true;
|
|
@@ -501,6 +486,32 @@ export class CadreNode implements SAppIdLookup {
|
|
|
501
486
|
}
|
|
502
487
|
}
|
|
503
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Build the server-side push-wake fan-out over an injected {@link PushNotifier}.
|
|
491
|
+
*
|
|
492
|
+
* The notifier reaches for `node:http2`/`node:crypto`, so the cross-platform
|
|
493
|
+
* core never constructs it — the Node host builds it from
|
|
494
|
+
* `@serfab/cadre-core/push-node` and passes the instance in
|
|
495
|
+
* `CadreNodeConfig.push`. This node owns that instance's lifecycle from here:
|
|
496
|
+
* {@link cleanup} closes the fan-out, which closes the notifier (freeing the
|
|
497
|
+
* APNs HTTP/2 session). Every other primitive the fan-out needs (member
|
|
498
|
+
* enumeration, participation, direct dial, token resolve/expire) is an
|
|
499
|
+
* import-clean closure over this node.
|
|
500
|
+
*/
|
|
501
|
+
private buildPushFanout(push: NonNullable<CadreNodeConfig['push']>): PushFanoutService {
|
|
502
|
+
return new PushFanoutService({
|
|
503
|
+
listMembers: () => this.listMembers(),
|
|
504
|
+
getStrand: (strandId) => this.strandManager.getInstance(strandId),
|
|
505
|
+
selfPeerId: () => this.controlNode?.peerId.toString(),
|
|
506
|
+
pushWake: (peerId, strandId, reason) => this.pushWake(peerId, strandId, reason),
|
|
507
|
+
resolveDeviceToken: (peerId) => this.resolveDeviceToken(peerId),
|
|
508
|
+
expireDeviceToken: (peerId) => this.expireDeviceToken(peerId),
|
|
509
|
+
notifier: push.notifier,
|
|
510
|
+
cooldownMs: push.cooldownMs,
|
|
511
|
+
debounceMs: push.debounceMs,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
504
515
|
/**
|
|
505
516
|
* Stop the cadre node
|
|
506
517
|
*/
|
|
@@ -657,7 +668,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
657
668
|
/**
|
|
658
669
|
* Schedule the node's initial self-record publish + ongoing refresh shortly
|
|
659
670
|
* after start (non-blocking). {@link registerSelf} is idempotent and safely
|
|
660
|
-
* no-ops when it cannot yet sign/insert (e.g.
|
|
671
|
+
* no-ops when it cannot yet sign/insert (e.g. owner key not installed),
|
|
661
672
|
* so the timer is harmless even when registration only becomes possible later.
|
|
662
673
|
*/
|
|
663
674
|
private scheduleSelfRegistration(): void {
|
|
@@ -690,10 +701,10 @@ export class CadreNode implements SAppIdLookup {
|
|
|
690
701
|
* (signaling/`p2p-circuit` first), signed with the ed25519 key behind its
|
|
691
702
|
* PeerId (the resolved node identity from `keyStore`/`config.privateKey`).
|
|
692
703
|
* - If the row already exists: a self-signed UPDATE bumping `UpdatedAt`.
|
|
693
|
-
* - If not, and the node is its own
|
|
704
|
+
* - If not, and the node is its own owner: an owner-signed INSERT that
|
|
694
705
|
* also carries the self-signature.
|
|
695
|
-
* - Otherwise: logs and returns (a non-
|
|
696
|
-
* wait for an
|
|
706
|
+
* - Otherwise: logs and returns (a non-owner node with no row yet must
|
|
707
|
+
* wait for an owner to insert it; it can then self-refresh).
|
|
697
708
|
*
|
|
698
709
|
* Safe to call repeatedly (heartbeat / address-change driven); each successful
|
|
699
710
|
* publish strictly increases `UpdatedAt`. Concurrent calls are collapsed into a
|
|
@@ -749,14 +760,14 @@ export class CadreNode implements SAppIdLookup {
|
|
|
749
760
|
return 'refreshed';
|
|
750
761
|
}
|
|
751
762
|
if (this.seedBootstrapService) {
|
|
752
|
-
// First-time row: requires an
|
|
753
|
-
//
|
|
763
|
+
// First-time row: requires an owner signature (the node is its own
|
|
764
|
+
// owner). insertSelfPeerRecord throws if no owner key is present.
|
|
754
765
|
await this.seedBootstrapService.insertSelfPeerRecord(record);
|
|
755
766
|
if (this.committedAlone()) this.pendingSelfPeerWrite = true;
|
|
756
|
-
log('registerSelf: inserted own CadrePeer record (
|
|
767
|
+
log('registerSelf: inserted own CadrePeer record (owner-signed, updatedAt=%d, %d addrs)', updatedAt, addrs.length);
|
|
757
768
|
return 'inserted';
|
|
758
769
|
}
|
|
759
|
-
log('registerSelf: not yet a CadrePeer member and no
|
|
770
|
+
log('registerSelf: not yet a CadrePeer member and no owner service to self-insert; skipping (an owner must add this peer first)');
|
|
760
771
|
return 'skipped';
|
|
761
772
|
}
|
|
762
773
|
|
|
@@ -774,43 +785,43 @@ export class CadreNode implements SAppIdLookup {
|
|
|
774
785
|
return null;
|
|
775
786
|
}
|
|
776
787
|
try {
|
|
777
|
-
const { privateKeyB64, publicKeyB64 } =
|
|
788
|
+
const { privateKeyB64, publicKeyB64 } = ed25519KeyPairFromLibp2p(this.identityKey);
|
|
778
789
|
if (publicKeyB64 === ed25519PublicKeyB64FromPeerId(peerId)) {
|
|
779
790
|
return { privateKeyB64, publicKeyB64 };
|
|
780
791
|
}
|
|
781
792
|
log('getSelfSigningKey: resolved identity key does not match control node peerId; cannot self-sign');
|
|
782
793
|
} catch (error) {
|
|
783
|
-
// Logs the error shape only;
|
|
794
|
+
// Logs the error shape only; ed25519KeyPairFromLibp2p never embeds key material.
|
|
784
795
|
log('getSelfSigningKey: failed to derive ed25519 key from identity key: %o', error);
|
|
785
796
|
}
|
|
786
797
|
return null;
|
|
787
798
|
}
|
|
788
799
|
|
|
789
800
|
/**
|
|
790
|
-
* The
|
|
791
|
-
* identity key. In the single-key reference model the
|
|
792
|
-
* *derived from* the node identity (see {@link
|
|
801
|
+
* The owner keypair (base64url Ed25519) derived from this node's resolved
|
|
802
|
+
* identity key. In the single-key reference model the owner signing key is
|
|
803
|
+
* *derived from* the node identity (see {@link ed25519KeyPairFromLibp2p}), so the
|
|
793
804
|
* same key material protected in a secure enclave backs both.
|
|
794
805
|
*
|
|
795
|
-
* Exposed so the hosting app retains control of
|
|
806
|
+
* Exposed so the hosting app retains control of owner genesis: cadre-core
|
|
796
807
|
* resolves + protects the identity, then the app sources this pair to drive
|
|
797
|
-
* `
|
|
798
|
-
* never silently runs genesis. A future separate-
|
|
808
|
+
* `ensureOwnerKey(pub)` + `initializeSeedBootstrap(priv)` itself — cadre-core
|
|
809
|
+
* never silently runs genesis. A future separate-owner slot would return a
|
|
799
810
|
* distinct key here instead of the identity-derived one.
|
|
800
811
|
*
|
|
801
|
-
* @returns The base64url seed/public-key
|
|
812
|
+
* @returns The base64url seed/public-key owner pair.
|
|
802
813
|
* @throws If called before {@link start} has resolved the identity, or when the
|
|
803
814
|
* node runs on an ephemeral libp2p key (no `keyStore`/`privateKey` configured),
|
|
804
815
|
* since that key is internal to libp2p and not exposed.
|
|
805
816
|
*/
|
|
806
|
-
|
|
817
|
+
getIdentityOwnerKey(): Ed25519KeyPair {
|
|
807
818
|
if (!this.identityKey) {
|
|
808
819
|
throw new Error(
|
|
809
|
-
'
|
|
810
|
-
'configure `keyStore` or `privateKey` (an ephemeral libp2p identity exposes no
|
|
820
|
+
'getIdentityOwnerKey: node identity not resolved — call start() first, and ' +
|
|
821
|
+
'configure `keyStore` or `privateKey` (an ephemeral libp2p identity exposes no owner key)'
|
|
811
822
|
);
|
|
812
823
|
}
|
|
813
|
-
return
|
|
824
|
+
return ed25519KeyPairFromLibp2p(this.identityKey);
|
|
814
825
|
}
|
|
815
826
|
|
|
816
827
|
/**
|
|
@@ -904,7 +915,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
904
915
|
|
|
905
916
|
// Reset the write-while-alone re-replication state so a stop()→start() cycle
|
|
906
917
|
// re-arms the growth edge and re-runs the first-growth reconstruction. Any
|
|
907
|
-
// queued local-only writes are moot once the node is torn down;
|
|
918
|
+
// queued local-only writes are moot once the node is torn down; owner
|
|
908
919
|
// inserts are re-covered by reconstruction on the next start, and the
|
|
909
920
|
// (already-loudly-logged) delete gap is tracked for follow-up.
|
|
910
921
|
this.hasControlConnection = false;
|
|
@@ -1062,18 +1073,18 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1062
1073
|
return;
|
|
1063
1074
|
}
|
|
1064
1075
|
|
|
1065
|
-
// 2. Classify backbone (
|
|
1066
|
-
const
|
|
1076
|
+
// 2. Classify backbone (owner) members and select a bounded dial set.
|
|
1077
|
+
const ownerKeys = await this.controlDatabase.getOwnerKeys();
|
|
1067
1078
|
if (!this._running || !this.controlNode) {
|
|
1068
1079
|
return;
|
|
1069
1080
|
}
|
|
1070
1081
|
const targetDegree = this.config.network?.controlCohort?.targetDegree
|
|
1071
1082
|
?? DEFAULT_CONTROL_COHORT_TARGET_DEGREE;
|
|
1072
|
-
const { dials,
|
|
1073
|
-
if (
|
|
1083
|
+
const { dials, cappedNonOwner } = selectControlCohortDials(siblings, ownerKeys, targetDegree);
|
|
1084
|
+
if (cappedNonOwner > 0) {
|
|
1074
1085
|
// Don't silently bound coverage — surface what the out-degree cap dropped.
|
|
1075
|
-
log('reconcileControlCohort: capped %d non-
|
|
1076
|
-
|
|
1086
|
+
log('reconcileControlCohort: capped %d non-owner sibling(s) at targetDegree=%d',
|
|
1087
|
+
cappedNonOwner, targetDegree);
|
|
1077
1088
|
}
|
|
1078
1089
|
|
|
1079
1090
|
// 3. Skip already-connected peers (no re-dial / churn for live connections).
|
|
@@ -1102,7 +1113,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1102
1113
|
*
|
|
1103
1114
|
* A per-peer failure (NAT, offline, relay down, connection-gater denial) is
|
|
1104
1115
|
* logged and swallowed so one unreachable sibling never aborts the pass —
|
|
1105
|
-
* exactly like {@link SeedBootstrapService.applySeed}'s
|
|
1116
|
+
* exactly like {@link SeedBootstrapService.applySeed}'s owner-dial loop. A
|
|
1106
1117
|
* failed dial is simply retried on the next pass.
|
|
1107
1118
|
*/
|
|
1108
1119
|
private async dialControlSibling(sibling: CohortPeerRow): Promise<boolean> {
|
|
@@ -1166,7 +1177,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1166
1177
|
// Write-while-alone re-replication (control-write-ensure-replicated)
|
|
1167
1178
|
//
|
|
1168
1179
|
// Optimystic's coordinator commits a control write WITHOUT broadcasting when the
|
|
1169
|
-
// block's cluster has ≤1 member. An
|
|
1180
|
+
// block's cluster has ≤1 member. An owner that authorizes/removes a peer or
|
|
1170
1181
|
// (re)publishes its own record while no sibling is connected therefore writes a
|
|
1171
1182
|
// row that exists ONLY in its local control DB — a sibling that connects later
|
|
1172
1183
|
// never observes it (pull-on-read routes to the block's cluster, which never
|
|
@@ -1193,7 +1204,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1193
1204
|
}
|
|
1194
1205
|
|
|
1195
1206
|
/**
|
|
1196
|
-
* Record (or clear) a just-committed
|
|
1207
|
+
* Record (or clear) a just-committed owner membership write in the
|
|
1197
1208
|
* write-while-alone re-replication queue. If the control node had no connections
|
|
1198
1209
|
* at commit, the Optimystic write was local-only and must be re-issued once the
|
|
1199
1210
|
* cohort grows; otherwise the write replicated and any stale queue entry for this
|
|
@@ -1267,7 +1278,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1267
1278
|
this.pendingSelfDeviceWrite = false;
|
|
1268
1279
|
}
|
|
1269
1280
|
|
|
1270
|
-
// 2. First growth: an
|
|
1281
|
+
// 2. First growth: an owner reconstructs membership rows it may have
|
|
1271
1282
|
// authored that never replicated (covers writes from before this process
|
|
1272
1283
|
// started). Rows already tracked in the in-memory queue are handled by
|
|
1273
1284
|
// step 3 — skipped here to avoid a double re-touch.
|
|
@@ -1276,7 +1287,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1276
1287
|
await this.reconstructAuthoredMembership();
|
|
1277
1288
|
}
|
|
1278
1289
|
|
|
1279
|
-
// 3. In-session pending
|
|
1290
|
+
// 3. In-session pending owner membership writes (authorize / remove).
|
|
1280
1291
|
await this.drainPendingPeerWrites();
|
|
1281
1292
|
}
|
|
1282
1293
|
|
|
@@ -1305,16 +1316,16 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1305
1316
|
|
|
1306
1317
|
/**
|
|
1307
1318
|
* One-shot, first-cohort-growth reconstruction of the write-while-alone queue for
|
|
1308
|
-
* an
|
|
1309
|
-
*
|
|
1319
|
+
* an OWNER node: re-touch every membership row that may be an unreplicated
|
|
1320
|
+
* owner insert. A row is a candidate iff it is not self (handled by
|
|
1310
1321
|
* {@link registerSelf}), is not already tracked in the in-memory queue (handled by
|
|
1311
1322
|
* {@link drainPendingPeerWrites}), and carries no self-`Sig` yet (an
|
|
1312
|
-
*
|
|
1323
|
+
* owner-authored row the peer has not self-published — the only kind safe to
|
|
1313
1324
|
* bump without invalidating a self-signature). O(rows) on the small control
|
|
1314
|
-
* tables; safe to over-apply (a monotonic
|
|
1325
|
+
* tables; safe to over-apply (a monotonic owner bump on an already-replicated
|
|
1315
1326
|
* row is a no-op-equivalent).
|
|
1316
1327
|
*
|
|
1317
|
-
* A node with no
|
|
1328
|
+
* A node with no owner private key skips this entirely — it cannot re-sign
|
|
1318
1329
|
* rows for other peers, and rows it merely holds are not its to re-issue. DELETEs
|
|
1319
1330
|
* cannot be reconstructed here (a removed row leaves no local trace and the schema
|
|
1320
1331
|
* carries no tombstone) — see the delete-path note in {@link drainPendingPeerWrites}.
|
|
@@ -1349,17 +1360,17 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1349
1360
|
}
|
|
1350
1361
|
}
|
|
1351
1362
|
if (touched > 0) {
|
|
1352
|
-
log('drain: reconstructed %d
|
|
1363
|
+
log('drain: reconstructed %d owner-authored membership row(s) on first cohort growth', touched);
|
|
1353
1364
|
}
|
|
1354
1365
|
}
|
|
1355
1366
|
|
|
1356
1367
|
/**
|
|
1357
|
-
* Drain the in-session write-while-alone queue: re-issue each pending
|
|
1368
|
+
* Drain the in-session write-while-alone queue: re-issue each pending owner
|
|
1358
1369
|
* membership write now that the cohort can broadcast it. Sequential (no fan-out)
|
|
1359
1370
|
* to avoid a thundering re-touch; an entry is cleared only on success, so a
|
|
1360
1371
|
* failure (or a still-alone re-commit) leaves it queued for the next growth.
|
|
1361
1372
|
*
|
|
1362
|
-
* - `authorize`: re-issue as an idempotent monotonic
|
|
1373
|
+
* - `authorize`: re-issue as an idempotent monotonic owner UPDATE
|
|
1363
1374
|
* ({@link reissuePeerAuthorize}). Skipped (and cleared) if the row vanished
|
|
1364
1375
|
* (raced a delete) or now carries a self-`Sig` (the peer self-published and owns
|
|
1365
1376
|
* its republish).
|
|
@@ -1375,7 +1386,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1375
1386
|
return;
|
|
1376
1387
|
}
|
|
1377
1388
|
if (!this.seedBootstrapService?.canAuthorize()) {
|
|
1378
|
-
// A non-
|
|
1389
|
+
// A non-owner node cannot have authored these writes; drop any stray entries.
|
|
1379
1390
|
this.pendingPeerWrites.clear();
|
|
1380
1391
|
return;
|
|
1381
1392
|
}
|
|
@@ -1387,7 +1398,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1387
1398
|
if (kind === 'authorize') {
|
|
1388
1399
|
const record = await this.controlDatabase.queryPeerRecord(peerId);
|
|
1389
1400
|
if (!record || record.sig) {
|
|
1390
|
-
// Removed since, or peer self-published — nothing for the
|
|
1401
|
+
// Removed since, or peer self-published — nothing for the owner to re-issue.
|
|
1391
1402
|
this.pendingPeerWrites.delete(peerId);
|
|
1392
1403
|
continue;
|
|
1393
1404
|
}
|
|
@@ -1405,9 +1416,9 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1405
1416
|
}
|
|
1406
1417
|
|
|
1407
1418
|
/**
|
|
1408
|
-
* Re-issue an
|
|
1419
|
+
* Re-issue an owner membership row as a monotonic owner UPDATE: bump
|
|
1409
1420
|
* `UpdatedAt` strictly above the stored value (and the wall clock) and re-sign via
|
|
1410
|
-
* the
|
|
1421
|
+
* the owner branch of `CadrePeer.AuthorizedUpdate`. The row already exists
|
|
1411
1422
|
* locally (it committed there, just local-only), so this is an UPDATE, not the
|
|
1412
1423
|
* original INSERT.
|
|
1413
1424
|
*/
|
|
@@ -1526,11 +1537,11 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1526
1537
|
* for any member — the `AuthorizedUpdate` self-branch verifies the new `Sig`
|
|
1527
1538
|
* against the bound `CadrePeer.PublicKey`). Platform/Token may change here
|
|
1528
1539
|
* (rotation / platform switch / reinstall are all normal self-updates).
|
|
1529
|
-
* - If not, and the node holds an
|
|
1540
|
+
* - If not, and the node holds an owner service: an owner-signed INSERT
|
|
1530
1541
|
* that also carries the self-signature.
|
|
1531
1542
|
* - Otherwise: throws. Like `CadrePeer`, the first `DeviceToken` row requires an
|
|
1532
|
-
*
|
|
1533
|
-
* seeded by an
|
|
1543
|
+
* owner signature; a non-owner peer (e.g. a phone) must have its row
|
|
1544
|
+
* seeded by an owner — typically the server it enrolled with — before it
|
|
1534
1545
|
* can self-refresh. (Establishing that phone→server registration handshake is
|
|
1535
1546
|
* the downstream "RN registration" ticket; this node only owns the cadre-core
|
|
1536
1547
|
* write path.)
|
|
@@ -1541,7 +1552,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1541
1552
|
* @param platform - `'fcm'` (Android/Firebase) or `'apns'` (Apple).
|
|
1542
1553
|
* @param token - the opaque platform device/registration token.
|
|
1543
1554
|
* @throws if the node is not started, exposes no self-signing key, or has no
|
|
1544
|
-
* existing row and no
|
|
1555
|
+
* existing row and no owner service to self-insert.
|
|
1545
1556
|
*/
|
|
1546
1557
|
async registerDeviceToken(platform: PushPlatform, token: string): Promise<void> {
|
|
1547
1558
|
if (!this._running || !this.controlNode || !this.controlDatabase) {
|
|
@@ -1573,12 +1584,12 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1573
1584
|
if (this.seedBootstrapService) {
|
|
1574
1585
|
await this.seedBootstrapService.insertSelfDeviceToken(record);
|
|
1575
1586
|
if (this.committedAlone()) this.pendingSelfDeviceWrite = true;
|
|
1576
|
-
log('registerDeviceToken: inserted own DeviceToken (
|
|
1587
|
+
log('registerDeviceToken: inserted own DeviceToken (owner-signed, platform=%s, updatedAt=%d)', platform, updatedAt);
|
|
1577
1588
|
return;
|
|
1578
1589
|
}
|
|
1579
1590
|
throw new Error(
|
|
1580
1591
|
`Cannot register device token for ${peerId}: no existing row to self-update and no ` +
|
|
1581
|
-
'
|
|
1592
|
+
'owner service to self-insert. An owner must seed this peer\'s DeviceToken ' +
|
|
1582
1593
|
'row first (mirrors CadrePeer enrollment).'
|
|
1583
1594
|
);
|
|
1584
1595
|
}
|
|
@@ -1649,11 +1660,11 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1649
1660
|
/**
|
|
1650
1661
|
* Delete this node's own `DeviceToken` row (logout / token invalidation). No-op
|
|
1651
1662
|
* when no row exists. Like {@link registerDeviceToken}'s first insert, the delete
|
|
1652
|
-
* is gated on an
|
|
1653
|
-
* AND delete), so it requires this node's
|
|
1654
|
-
* must route the clear through its
|
|
1663
|
+
* is gated on an owner signature (`DeviceToken.AuthorizedInsert` covers insert
|
|
1664
|
+
* AND delete), so it requires this node's owner service; a non-owner peer
|
|
1665
|
+
* must route the clear through its owner (downstream RN registration path).
|
|
1655
1666
|
*
|
|
1656
|
-
* @throws if the node is not started, or a row exists but no
|
|
1667
|
+
* @throws if the node is not started, or a row exists but no owner service is
|
|
1657
1668
|
* available to sign the delete.
|
|
1658
1669
|
*/
|
|
1659
1670
|
async clearDeviceToken(): Promise<void> {
|
|
@@ -1668,8 +1679,8 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1668
1679
|
}
|
|
1669
1680
|
if (!this.seedBootstrapService) {
|
|
1670
1681
|
throw new Error(
|
|
1671
|
-
`Cannot clear device token for ${peerId}: delete requires an
|
|
1672
|
-
'and no
|
|
1682
|
+
`Cannot clear device token for ${peerId}: delete requires an owner signature ` +
|
|
1683
|
+
'and no owner service is initialized.'
|
|
1673
1684
|
);
|
|
1674
1685
|
}
|
|
1675
1686
|
await this.seedBootstrapService.deleteDeviceToken(peerId);
|
|
@@ -1681,10 +1692,10 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1681
1692
|
* unregistered during a push-wake fan-out. Unlike {@link clearDeviceToken}
|
|
1682
1693
|
* (self-only — it hardcodes the local peerId), this takes an arbitrary peerId.
|
|
1683
1694
|
*
|
|
1684
|
-
* - When this node holds an
|
|
1685
|
-
* (`deleteDeviceToken` is
|
|
1695
|
+
* - When this node holds an owner seed service, it deletes the row
|
|
1696
|
+
* (`deleteDeviceToken` is owner-gated and accepts any peerId), so the peer
|
|
1686
1697
|
* is not retried until it re-registers.
|
|
1687
|
-
* - When this node is NOT an
|
|
1698
|
+
* - When this node is NOT an owner it cannot delete the row, so it only logs
|
|
1688
1699
|
* that a re-registration is needed. The fan-out's own in-memory dead-token set
|
|
1689
1700
|
* is what actually stops re-pushing to the dead token this process — see
|
|
1690
1701
|
* {@link PushFanoutService}. That set is acceptably lossy across restarts (a
|
|
@@ -1696,13 +1707,13 @@ export class CadreNode implements SAppIdLookup {
|
|
|
1696
1707
|
if (this.seedBootstrapService) {
|
|
1697
1708
|
try {
|
|
1698
1709
|
await this.seedBootstrapService.deleteDeviceToken(peerId);
|
|
1699
|
-
log('expireDeviceToken:
|
|
1710
|
+
log('expireDeviceToken: owner-deleted stale DeviceToken for %s', peerId);
|
|
1700
1711
|
} catch (error) {
|
|
1701
|
-
log('expireDeviceToken:
|
|
1712
|
+
log('expireDeviceToken: owner delete for %s failed: %o', peerId, error);
|
|
1702
1713
|
}
|
|
1703
1714
|
return;
|
|
1704
1715
|
}
|
|
1705
|
-
log('expireDeviceToken: %s token is stale but this node is not an
|
|
1716
|
+
log('expireDeviceToken: %s token is stale but this node is not an owner; re-registration required', peerId);
|
|
1706
1717
|
}
|
|
1707
1718
|
|
|
1708
1719
|
private async handleStrandAdded(strand: StrandRow): Promise<void> {
|
|
@@ -2070,19 +2081,19 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2070
2081
|
|
|
2071
2082
|
/**
|
|
2072
2083
|
* Publish a strand row to the shared control database under this node's own
|
|
2073
|
-
*
|
|
2084
|
+
* owner identity, so other cadre members discover it via control-network
|
|
2074
2085
|
* sync (their {@link StrandWatcher} fires `strand:discovered`).
|
|
2075
2086
|
*
|
|
2076
|
-
* This is the
|
|
2087
|
+
* This is the owner-signed `Strand` INSERT that {@link addStrand}
|
|
2077
2088
|
* deliberately omits: `addStrand` only starts the LOCAL strand instance,
|
|
2078
2089
|
* whereas publishing makes the strand visible cadre-wide. A typical creator
|
|
2079
2090
|
* does both (start locally + publish); a discovering peer only does
|
|
2080
2091
|
* `addStrand` (the row already exists).
|
|
2081
2092
|
*
|
|
2082
2093
|
* The insert is signed with the ed25519 key behind this node's PeerId — which
|
|
2083
|
-
* {@link
|
|
2084
|
-
* so peer identity and
|
|
2085
|
-
* enrolled in `
|
|
2094
|
+
* {@link ed25519KeyPairFromLibp2p} also exposes as the node's owner keypair,
|
|
2095
|
+
* so peer identity and owner key are one and the same. That key must be
|
|
2096
|
+
* enrolled in `OwnerKey` (e.g. via {@link ControlDatabase.ensureOwnerKey}
|
|
2086
2097
|
* at genesis) or the schema's `Strand.Authorized` constraint rejects the write.
|
|
2087
2098
|
* Failing loudly here is intentional: a silently-unpublished strand would run
|
|
2088
2099
|
* as a local-only island that no peer could ever discover or join.
|
|
@@ -2091,7 +2102,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2091
2102
|
* {@link addStrand}).
|
|
2092
2103
|
* @param type - `'o'` for open (default) or `'c'` for closed.
|
|
2093
2104
|
* @param memberPrivateKey - Optional membership key for a closed strand.
|
|
2094
|
-
* @throws if the node is not started, exposes no
|
|
2105
|
+
* @throws if the node is not started, exposes no owner signing key, or the
|
|
2095
2106
|
* control DB rejects the (unauthorized) insert.
|
|
2096
2107
|
*/
|
|
2097
2108
|
async publishStrand(strandId: string, type: 'o' | 'c' = 'o', memberPrivateKey?: string): Promise<void> {
|
|
@@ -2101,22 +2112,22 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2101
2112
|
const signingKey = this.getSelfSigningKey();
|
|
2102
2113
|
if (!signingKey) {
|
|
2103
2114
|
throw new Error(
|
|
2104
|
-
`Cannot publish strand ${strandId}: no
|
|
2105
|
-
'(node identity is unavailable or does not match the node PeerId). Run
|
|
2106
|
-
'genesis (
|
|
2115
|
+
`Cannot publish strand ${strandId}: no owner signing key available ` +
|
|
2116
|
+
'(node identity is unavailable or does not match the node PeerId). Run owner ' +
|
|
2117
|
+
'genesis (ensureOwnerKey + initializeSeedBootstrap) before publishing.'
|
|
2107
2118
|
);
|
|
2108
2119
|
}
|
|
2109
2120
|
// insertStrand hands this callback the canonical row-bound message BYTES (see
|
|
2110
2121
|
// buildAuthorizationMessage); ed25519-sign them directly (no pre-hash) with
|
|
2111
|
-
// the
|
|
2122
|
+
// the owner private key, returning a base64url signature.
|
|
2112
2123
|
const signMessage = (message: Uint8Array): string =>
|
|
2113
2124
|
sign(message, signingKey.privateKeyB64, 'ed25519', 'bytes', 'base64url', 'base64url') as string;
|
|
2114
2125
|
await this.controlDatabase.insertStrand(strandId, type, signingKey.publicKeyB64, signMessage, memberPrivateKey);
|
|
2115
|
-
log('Published strand %s (type %s) to control DB under
|
|
2126
|
+
log('Published strand %s (type %s) to control DB under owner %s', strandId, type, signingKey.publicKeyB64);
|
|
2116
2127
|
}
|
|
2117
2128
|
|
|
2118
2129
|
/**
|
|
2119
|
-
* Publish an
|
|
2130
|
+
* Publish an owner-signed `FormationInvite` (open-invitation token) to the
|
|
2120
2131
|
* shared control database, so a later {@link formStrand} redemption can be
|
|
2121
2132
|
* validated against it (the consent branch of `Strand.Authorized`).
|
|
2122
2133
|
*
|
|
@@ -2127,8 +2138,8 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2127
2138
|
* from this row. A host minting a closed-strand invite does both (mint +
|
|
2128
2139
|
* publish), exactly as the integration harness's `createInvitation` does.
|
|
2129
2140
|
*
|
|
2130
|
-
* Signs with the same self-
|
|
2131
|
-
* key behind this node's PeerId, which must be an enrolled `
|
|
2141
|
+
* Signs with the same self-owner key as {@link publishStrand} (the ed25519
|
|
2142
|
+
* key behind this node's PeerId, which must be an enrolled `OwnerKey`).
|
|
2132
2143
|
* Throws loudly if the node isn't started or exposes no signing key.
|
|
2133
2144
|
*
|
|
2134
2145
|
* @param token - Invitation token (the `FormationInvite` primary key); use the
|
|
@@ -2148,15 +2159,15 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2148
2159
|
const signingKey = this.getSelfSigningKey();
|
|
2149
2160
|
if (!signingKey) {
|
|
2150
2161
|
throw new Error(
|
|
2151
|
-
`Cannot publish formation invite ${token}: no
|
|
2152
|
-
'(node identity is unavailable or does not match the node PeerId). Run
|
|
2153
|
-
'genesis (
|
|
2162
|
+
`Cannot publish formation invite ${token}: no owner signing key available ` +
|
|
2163
|
+
'(node identity is unavailable or does not match the node PeerId). Run owner ' +
|
|
2164
|
+
'genesis (ensureOwnerKey + initializeSeedBootstrap) before publishing.'
|
|
2154
2165
|
);
|
|
2155
2166
|
}
|
|
2156
2167
|
const signMessage = (message: Uint8Array): string =>
|
|
2157
2168
|
sign(message, signingKey.privateKeyB64, 'ed25519', 'bytes', 'base64url', 'base64url') as string;
|
|
2158
2169
|
await this.controlDatabase.insertFormationInvite(token, sAppId, signingKey.publicKeyB64, signMessage, options);
|
|
2159
|
-
log('Published formation invite %s (sApp %s) under
|
|
2170
|
+
log('Published formation invite %s (sApp %s) under owner %s', token, sAppId, signingKey.publicKeyB64);
|
|
2160
2171
|
}
|
|
2161
2172
|
|
|
2162
2173
|
/**
|
|
@@ -2475,19 +2486,19 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2475
2486
|
// ============================================================================
|
|
2476
2487
|
|
|
2477
2488
|
/**
|
|
2478
|
-
* Initialize the seed bootstrap service with an
|
|
2489
|
+
* Initialize the seed bootstrap service with an owner key.
|
|
2479
2490
|
* Must be called before using seed-related methods that require signing.
|
|
2480
2491
|
*
|
|
2481
|
-
* @param
|
|
2492
|
+
* @param ownerPrivateKey - The owner's private key (base64url encoded)
|
|
2482
2493
|
*/
|
|
2483
|
-
initializeSeedBootstrap(
|
|
2494
|
+
initializeSeedBootstrap(ownerPrivateKey: string): void {
|
|
2484
2495
|
if (!this.controlNode || !this.controlDatabase) {
|
|
2485
2496
|
throw new Error('CadreNode must be started before initializing seed bootstrap');
|
|
2486
2497
|
}
|
|
2487
2498
|
|
|
2488
2499
|
this.seedBootstrapService = new SeedBootstrapService({
|
|
2489
2500
|
partyId: this.config.controlNetwork.partyId,
|
|
2490
|
-
|
|
2501
|
+
ownerPrivateKey,
|
|
2491
2502
|
inviteAddressResolver: () => this.resolveInviteAddresses(),
|
|
2492
2503
|
trustPolicy: this.config.seedTrustPolicy,
|
|
2493
2504
|
});
|
|
@@ -2538,12 +2549,45 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2538
2549
|
|
|
2539
2550
|
/**
|
|
2540
2551
|
* Probe whether a given peer is a `CadrePeer` member.
|
|
2552
|
+
*
|
|
2553
|
+
* This is the ADDRESSABLE surface ("do I have a dialable address record for this
|
|
2554
|
+
* peer") — it includes this node's own self-published row. Address resolution and
|
|
2555
|
+
* push fan-out use this. The trust-facing gate is {@link isAuthorizedMember}.
|
|
2541
2556
|
*/
|
|
2542
2557
|
async isMember(peerId: string): Promise<boolean> {
|
|
2543
2558
|
const members = await this.listMembers();
|
|
2544
2559
|
return members.some(m => m.peerId === peerId);
|
|
2545
2560
|
}
|
|
2546
2561
|
|
|
2562
|
+
/**
|
|
2563
|
+
* Enumerate the party's AUTHORIZED members — the trust-facing set, distinct from
|
|
2564
|
+
* the addressable set ({@link listMembers}). Excludes this node itself: a node
|
|
2565
|
+
* publishes its own `CadrePeer` address row so its dialable address rides in seeds,
|
|
2566
|
+
* but "self" is not a peer this node authorized. The control-network wake and
|
|
2567
|
+
* strand-address gates consult this set, NOT the addressable one.
|
|
2568
|
+
*
|
|
2569
|
+
* NOTE: this is the addressable set minus self for now. The real trust predicate
|
|
2570
|
+
* (voucher recorded on the row ∈ node-local trusted-owner anchor, signature
|
|
2571
|
+
* verified) lands with the `membership-node-local-owner-anchor` /
|
|
2572
|
+
* `membership-authorized-predicate-and-gates` tickets; keeping the filter in this one
|
|
2573
|
+
* method means that change touches a single place.
|
|
2574
|
+
*/
|
|
2575
|
+
async listAuthorizedMembers(): Promise<Array<{ peerId: string; multiaddr: string | null }>> {
|
|
2576
|
+
const selfPeerId = this.peerId?.toString();
|
|
2577
|
+
const members = await this.listMembers();
|
|
2578
|
+
return members.filter(m => m.peerId !== selfPeerId);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
/**
|
|
2582
|
+
* Probe whether a given peer is an AUTHORIZED party member (see
|
|
2583
|
+
* {@link listAuthorizedMembers}) — the gate the control-network wake and
|
|
2584
|
+
* strand-address responders consult, NOT {@link isMember} (the addressable surface).
|
|
2585
|
+
*/
|
|
2586
|
+
async isAuthorizedMember(peerId: string): Promise<boolean> {
|
|
2587
|
+
const members = await this.listAuthorizedMembers();
|
|
2588
|
+
return members.some(m => m.peerId === peerId);
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2547
2591
|
/**
|
|
2548
2592
|
* Push-wake a hibernating cadre peer over the control network.
|
|
2549
2593
|
*
|
|
@@ -2573,8 +2617,8 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2573
2617
|
|
|
2574
2618
|
/**
|
|
2575
2619
|
* Enable the seed listener for receiving seeds via the /sereus/seed/1.0.0 protocol.
|
|
2576
|
-
* This is for drone nodes that need to receive seeds without being an
|
|
2577
|
-
* Does not require an
|
|
2620
|
+
* This is for drone nodes that need to receive seeds without being an owner.
|
|
2621
|
+
* Does not require an owner key.
|
|
2578
2622
|
*/
|
|
2579
2623
|
enableSeedListener(): void {
|
|
2580
2624
|
if (!this.controlNode || !this.controlDatabase) {
|
|
@@ -2589,7 +2633,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2589
2633
|
|
|
2590
2634
|
this.seedBootstrapService = new SeedBootstrapService({
|
|
2591
2635
|
partyId: this.config.controlNetwork.partyId,
|
|
2592
|
-
// No
|
|
2636
|
+
// No owner key - this node only receives seeds
|
|
2593
2637
|
inviteAddressResolver: () => this.resolveInviteAddresses(),
|
|
2594
2638
|
trustPolicy: this.config.seedTrustPolicy,
|
|
2595
2639
|
});
|
|
@@ -2613,7 +2657,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2613
2657
|
|
|
2614
2658
|
/**
|
|
2615
2659
|
* Authorize a new peer to join the cadre.
|
|
2616
|
-
* Signs the peer ID with the
|
|
2660
|
+
* Signs the peer ID with the owner key and inserts into CadrePeer table.
|
|
2617
2661
|
*
|
|
2618
2662
|
* @param peerId - The peer ID to authorize
|
|
2619
2663
|
* @param multiaddrs - Optional multiaddrs for the peer
|
|
@@ -2629,7 +2673,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2629
2673
|
|
|
2630
2674
|
/**
|
|
2631
2675
|
* Remove a previously-authorized peer from the cadre.
|
|
2632
|
-
* Signs the peer ID with the
|
|
2676
|
+
* Signs the peer ID with the owner key and deletes the CadrePeer row.
|
|
2633
2677
|
*
|
|
2634
2678
|
* @param peerId - The peer ID to remove
|
|
2635
2679
|
*/
|
|
@@ -2644,7 +2688,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2644
2688
|
|
|
2645
2689
|
/**
|
|
2646
2690
|
* Create a seed from the current control network state.
|
|
2647
|
-
* The seed contains peer information and is signed by an
|
|
2691
|
+
* The seed contains peer information and is signed by an owner.
|
|
2648
2692
|
*/
|
|
2649
2693
|
async createSeed(): Promise<ControlNetworkSeed> {
|
|
2650
2694
|
if (!this.seedBootstrapService) {
|
|
@@ -2659,7 +2703,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2659
2703
|
* Validates the seed signature, then evaluates a trust anchor for the signer
|
|
2660
2704
|
* key (see `SeedTrustPolicy`). An enrollment caller can pass a per-seed
|
|
2661
2705
|
* `trustPolicy` override — e.g. a `pinnedKeyTrustPolicy` built from a
|
|
2662
|
-
* `CadreInvite.
|
|
2706
|
+
* `CadreInvite.ownerKeys` — so a cold-start node can accept its first
|
|
2663
2707
|
* seed without reconfiguring the service.
|
|
2664
2708
|
*/
|
|
2665
2709
|
async applySeed(
|
|
@@ -2667,7 +2711,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2667
2711
|
options?: { trustPolicy?: SeedTrustPolicy }
|
|
2668
2712
|
): Promise<ApplySeedResult> {
|
|
2669
2713
|
if (!this.seedBootstrapService) {
|
|
2670
|
-
// Create a temporary service for applying seeds (doesn't need
|
|
2714
|
+
// Create a temporary service for applying seeds (doesn't need owner key).
|
|
2671
2715
|
// partyId is the attacker-influenced seed.partyId — it only labels logs; the
|
|
2672
2716
|
// trust decision rests solely on signerKey vs the anchor set (configured
|
|
2673
2717
|
// default below, or the per-call options.trustPolicy override).
|
|
@@ -2799,7 +2843,7 @@ export class CadreNode implements SAppIdLookup {
|
|
|
2799
2843
|
}
|
|
2800
2844
|
|
|
2801
2845
|
/**
|
|
2802
|
-
* Dial an
|
|
2846
|
+
* Dial an owner from an invite (for phone joining via invite).
|
|
2803
2847
|
*/
|
|
2804
2848
|
async dialInvite(invite: CadreInvite): Promise<void> {
|
|
2805
2849
|
if (!this.seedBootstrapService) {
|