@serfab/cadre-core 0.9.0 → 0.10.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 +4 -2
- package/dist/bootstrap-peer-store-file.d.ts +13 -0
- package/dist/bootstrap-peer-store-file.js +42 -0
- package/dist/bootstrap-peer-store-file.js.map +1 -0
- package/dist/bootstrap-peer-store.d.ts +73 -0
- package/dist/bootstrap-peer-store.js +128 -0
- package/dist/bootstrap-peer-store.js.map +1 -0
- package/dist/cadre-node.d.ts +918 -53
- package/dist/cadre-node.js +1843 -190
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +93 -0
- package/dist/control-authorization.js +57 -0
- package/dist/control-authorization.js.map +1 -0
- package/dist/control-database.d.ts +990 -50
- package/dist/control-database.js +1459 -108
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +89 -10
- package/dist/control-formation-recorder.js +129 -14
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +774 -210
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +38 -4
- package/dist/control-stream.js +70 -4
- package/dist/control-stream.js.map +1 -1
- package/dist/control-write-retry.d.ts +151 -0
- package/dist/control-write-retry.js +411 -0
- package/dist/control-write-retry.js.map +1 -0
- package/dist/delegate-admission.d.ts +134 -0
- package/dist/delegate-admission.js +244 -0
- package/dist/delegate-admission.js.map +1 -0
- package/dist/device-token.d.ts +11 -11
- package/dist/device-token.js +14 -13
- package/dist/device-token.js.map +1 -1
- package/dist/ed25519-key.d.ts +26 -0
- package/dist/ed25519-key.js +56 -1
- package/dist/ed25519-key.js.map +1 -1
- package/dist/file-durable-slot.d.ts +20 -0
- package/dist/file-durable-slot.js +53 -0
- package/dist/file-durable-slot.js.map +1 -0
- package/dist/formation-approval.d.ts +157 -0
- package/dist/formation-approval.js +375 -0
- package/dist/formation-approval.js.map +1 -0
- package/dist/fs-atomic.d.ts +38 -0
- package/dist/fs-atomic.js +97 -0
- package/dist/fs-atomic.js.map +1 -0
- package/dist/identity-key.d.ts +49 -0
- package/dist/identity-key.js +81 -0
- package/dist/identity-key.js.map +1 -0
- package/dist/index.d.ts +16 -6
- package/dist/index.js +39 -5
- package/dist/index.js.map +1 -1
- package/dist/key-store-file.d.ts +2 -24
- package/dist/key-store-file.js +10 -77
- package/dist/key-store-file.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +146 -0
- package/dist/membership-connection-gater.js +180 -0
- package/dist/membership-connection-gater.js.map +1 -0
- package/dist/node-local-snapshot.d.ts +109 -0
- package/dist/node-local-snapshot.js +182 -0
- package/dist/node-local-snapshot.js.map +1 -0
- package/dist/peer-authorization.d.ts +135 -18
- package/dist/peer-authorization.js +152 -21
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-record.d.ts +21 -8
- package/dist/peer-record.js +26 -10
- package/dist/peer-record.js.map +1 -1
- package/dist/relay-addrs.d.ts +45 -0
- package/dist/relay-addrs.js +103 -0
- package/dist/relay-addrs.js.map +1 -0
- package/dist/relay-reservation.d.ts +266 -0
- package/dist/relay-reservation.js +613 -0
- package/dist/relay-reservation.js.map +1 -0
- package/dist/seed-bootstrap.d.ts +221 -56
- package/dist/seed-bootstrap.js +328 -188
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +47 -21
- package/dist/seed-trust-policy.js +24 -15
- package/dist/seed-trust-policy.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +42 -13
- package/dist/strand-addr-protocol.js +58 -52
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-backfill.d.ts +109 -0
- package/dist/strand-backfill.js +279 -0
- package/dist/strand-backfill.js.map +1 -0
- package/dist/strand-formation-manager.d.ts +38 -9
- package/dist/strand-formation-manager.js +120 -17
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +103 -4
- package/dist/strand-formation-protocol.js +232 -4
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +39 -1
- package/dist/strand-instance-manager.js +92 -14
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-registry.d.ts +5 -5
- package/dist/strand-member-registry.js +18 -10
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +482 -88
- package/dist/strand-membership-writer.js +757 -136
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +99 -33
- package/dist/strand-solicitation.js +100 -52
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-transport-key.d.ts +40 -0
- package/dist/strand-transport-key.js +56 -0
- package/dist/strand-transport-key.js.map +1 -0
- package/dist/strand-wake-protocol.d.ts +9 -7
- package/dist/strand-wake-protocol.js +22 -46
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/strand-watcher.d.ts +17 -2
- package/dist/strand-watcher.js +47 -3
- package/dist/strand-watcher.js.map +1 -1
- package/dist/trusted-owner-store-file.d.ts +12 -0
- package/dist/trusted-owner-store-file.js +39 -0
- package/dist/trusted-owner-store-file.js.map +1 -0
- package/dist/trusted-owner-store.d.ts +64 -0
- package/dist/trusted-owner-store.js +126 -0
- package/dist/trusted-owner-store.js.map +1 -0
- package/dist/types.d.ts +275 -20
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -1
- package/package.json +19 -8
- package/src/bootstrap-peer-store-file.ts +44 -0
- package/src/bootstrap-peer-store.ts +172 -0
- package/src/cadre-node.ts +1992 -202
- package/src/control-authorization.ts +114 -0
- package/src/control-database.ts +1704 -146
- package/src/control-formation-recorder.ts +172 -19
- package/src/control-schema.ts +786 -222
- package/src/control-stream.ts +81 -4
- package/src/control-write-retry.ts +473 -0
- package/src/delegate-admission.ts +302 -0
- package/src/device-token.ts +18 -13
- package/src/ed25519-key.ts +134 -69
- package/src/file-durable-slot.ts +61 -0
- package/src/formation-approval.ts +604 -0
- package/src/fs-atomic.ts +107 -0
- package/src/identity-key.ts +109 -0
- package/src/index.ts +136 -5
- package/src/key-store-file.ts +10 -81
- package/src/membership-connection-gater.ts +204 -0
- package/src/node-local-snapshot.ts +260 -0
- package/src/peer-authorization.ts +199 -21
- package/src/peer-record.ts +28 -10
- package/src/relay-addrs.ts +111 -0
- package/src/relay-reservation.ts +823 -0
- package/src/seed-bootstrap.ts +397 -197
- package/src/seed-trust-policy.ts +58 -25
- package/src/strand-addr-protocol.ts +84 -53
- package/src/strand-backfill.ts +366 -0
- package/src/strand-formation-manager.ts +145 -19
- package/src/strand-formation-protocol.ts +309 -8
- package/src/strand-instance-manager.ts +111 -14
- package/src/strand-member-registry.ts +18 -13
- package/src/strand-membership-writer.ts +1430 -640
- package/src/strand-solicitation.ts +155 -90
- package/src/strand-transport-key.ts +58 -0
- package/src/strand-wake-protocol.ts +29 -46
- package/src/strand-watcher.ts +66 -3
- package/src/trusted-owner-store-file.ts +41 -0
- package/src/trusted-owner-store.ts +171 -0
- package/src/types.ts +292 -21
package/dist/cadre-node.d.ts
CHANGED
|
@@ -2,12 +2,15 @@ import type { Libp2p, PeerId } from '@libp2p/interface';
|
|
|
2
2
|
import type { Multiaddr } from '@multiformats/multiaddr';
|
|
3
3
|
import type { CadreNodeConfig, StrandInstance, StrandConfig, SAppConfig, CadreNodeEvents, ControlNetworkSeed, ApplySeedResult, AddDroneOptions, AddPhoneOptions, DroneInitResult, InviteResult, CadreInvite, OpenInvitation, FormStrandResult, StrandFormationDisclosure, ResolveOpts, SelfRegistrationOutcome, ServiceWakeResult, PushPlatform, DeviceTokenRecord, ResolveDeviceTokenOpts } from './types.js';
|
|
4
4
|
import { type Ed25519KeyPair } from './ed25519-key.js';
|
|
5
|
+
import { type TrustedOwnerStore, type TrustSource } from './trusted-owner-store.js';
|
|
6
|
+
import { type BootstrapPeerStore } from './bootstrap-peer-store.js';
|
|
5
7
|
import { type SAppIdLookup } from './strand-watcher.js';
|
|
6
8
|
import { EnrollmentService } from './enrollment.js';
|
|
7
9
|
import { ControlDatabase } from './control-database.js';
|
|
8
10
|
import { SeedBootstrapService } from './seed-bootstrap.js';
|
|
9
11
|
import type { SeedTrustPolicy } from './seed-trust-policy.js';
|
|
10
12
|
import { StrandSolicitationService, type StrandSolicitationServiceOptions } from './strand-solicitation.js';
|
|
13
|
+
import { type RelayReservationState, type RelayReservationSupervisorOptions } from './relay-reservation.js';
|
|
11
14
|
import type { WakeAck } from './types.js';
|
|
12
15
|
import { type ConnectionPathSummary } from './diagnostics/connection-path.js';
|
|
13
16
|
type EventHandler<T> = (data: T) => void;
|
|
@@ -31,6 +34,15 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
31
34
|
* launch) reads this resolved field, never `config.privateKey` directly.
|
|
32
35
|
*/
|
|
33
36
|
private identityKey;
|
|
37
|
+
/**
|
|
38
|
+
* Node-local, NON-replicated trusted-owner anchor (see
|
|
39
|
+
* `trusted-owner-store.ts`). Constructed (or adopted from
|
|
40
|
+
* `config.trustedOwners.store`) by {@link initializeTrustedOwnerStore} during
|
|
41
|
+
* {@link start}; deliberately NOT cleared by {@link cleanup}, so an in-memory
|
|
42
|
+
* anchor survives a stop()→start() cycle of the same node instance. Never
|
|
43
|
+
* sourced from replicated control state.
|
|
44
|
+
*/
|
|
45
|
+
private trustedOwnerStore;
|
|
34
46
|
private controlNode;
|
|
35
47
|
private controlDatabase;
|
|
36
48
|
private strandWatcher;
|
|
@@ -70,6 +82,25 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
70
82
|
*/
|
|
71
83
|
private windowWaiters;
|
|
72
84
|
private eventHandlers;
|
|
85
|
+
/**
|
|
86
|
+
* Relay multiaddrs the caller asked {@link reserveRelays} to reserve through.
|
|
87
|
+
* Empty (the default) means nobody asked, so {@link getRelayReservationState}
|
|
88
|
+
* reports `none` — this whole surface is opt-in and dormant for the CLI/host
|
|
89
|
+
* nodes, which use the configured `network.relayAddrs` route instead.
|
|
90
|
+
*/
|
|
91
|
+
private relayReserveAddrs;
|
|
92
|
+
/**
|
|
93
|
+
* The running retry loop for {@link relayReserveAddrs}, or `null` when nobody
|
|
94
|
+
* asked for a reservation / there was no control node to supervise. It owns the
|
|
95
|
+
* in-flight flag, the last failure and the next-attempt time; this class only
|
|
96
|
+
* starts it, stops it and reads it.
|
|
97
|
+
*/
|
|
98
|
+
private relayReserveSupervisor;
|
|
99
|
+
/**
|
|
100
|
+
* Reason {@link reserveRelays} produced no reservation when there is no
|
|
101
|
+
* supervisor to hold one — i.e. the pre-start `control node unavailable` case.
|
|
102
|
+
*/
|
|
103
|
+
private relayReserveError;
|
|
73
104
|
/** Map of strandId -> sAppConfig for sAppId filtering and management */
|
|
74
105
|
private sAppConfigs;
|
|
75
106
|
/**
|
|
@@ -79,6 +110,106 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
79
110
|
* control-network node never needs to dial back to the manager.
|
|
80
111
|
*/
|
|
81
112
|
private latestInviteAddresses;
|
|
113
|
+
/**
|
|
114
|
+
* Epoch ms until which the control-network inbound gate admits
|
|
115
|
+
* not-yet-authorized peers (see {@link admitInboundControlConnection} /
|
|
116
|
+
* {@link openEnrollmentWindow}). 0 = no window open. Opened automatically by
|
|
117
|
+
* {@link createInvite}; deliberately NOT reset on stop() — a stop/start cycle
|
|
118
|
+
* inside an outstanding invite's validity must not strand the invitee.
|
|
119
|
+
*
|
|
120
|
+
* NOTE: in-memory only, so a PROCESS restart mid-invite closes the door until
|
|
121
|
+
* the owner re-mints or the invitee is authorized. Fine while invites are
|
|
122
|
+
* short-lived and redeemed promptly; if long-lived invites or restart-prone
|
|
123
|
+
* hosts become normal, persist the window (or derive it from the issued-invite
|
|
124
|
+
* records `SeedBootstrapService` already keeps).
|
|
125
|
+
*/
|
|
126
|
+
private enrollmentWindowUntil;
|
|
127
|
+
/**
|
|
128
|
+
* Lazily-parsed PeerIds of `controlNetwork.bootstrapNodes` (see
|
|
129
|
+
* {@link getBootstrapPeerIds}) — infrastructure the inbound gate always admits.
|
|
130
|
+
*/
|
|
131
|
+
private bootstrapPeerIds;
|
|
132
|
+
/**
|
|
133
|
+
* Materialized in-memory snapshot of the AUTHORIZED member peer ids (the
|
|
134
|
+
* {@link listAuthorizedMembers} result), consulted by the per-stream
|
|
135
|
+
* control-DB gate ({@link authorizeInboundControlStream}). That predicate
|
|
136
|
+
* runs on EVERY inbound Optimystic control-DB stream and must never await a
|
|
137
|
+
* control-DB read: those reads pull blocks over the very protocols the
|
|
138
|
+
* predicate gates, which closes a circular wait that ends in mutual denial
|
|
139
|
+
* (the upstream gate is fail-closed on timeout, unlike the fail-open
|
|
140
|
+
* connection gater). So the set is refreshed OUT OF BAND instead — after
|
|
141
|
+
* start, on every {@link reconcileControlCohort} pass (15s cadence), and
|
|
142
|
+
* immediately after every LOCAL membership mutation so a just-vouched peer
|
|
143
|
+
* is admitted without waiting for the timer. That last refresh is AUTOMATIC:
|
|
144
|
+
* the control database notifies {@link refreshMembershipGate} after every
|
|
145
|
+
* committed `CadrePeer` write (see `ControlDatabase.mutateCadrePeer`), so no
|
|
146
|
+
* writer has to remember. A change that arrives by REPLICATION is picked up
|
|
147
|
+
* on the next timed refresh — bounded staleness, acceptable because this gate
|
|
148
|
+
* is defense in depth: rows an unadmitted peer manages to write are still
|
|
149
|
+
* disbelieved at read time by the voucher-anchored predicate.
|
|
150
|
+
*/
|
|
151
|
+
private authorizedControlPeers;
|
|
152
|
+
/**
|
|
153
|
+
* Coalescing state for {@link refreshMembershipGate}: a pending "the snapshot
|
|
154
|
+
* is stale" flag, the single in-flight drain that consumes it, and the depth
|
|
155
|
+
* of open {@link deferMembershipGateRefresh} scopes (a burst of writes inside
|
|
156
|
+
* one scope collapses to a single refresh at scope exit).
|
|
157
|
+
*/
|
|
158
|
+
private membershipGateDirty;
|
|
159
|
+
private membershipGateDrain;
|
|
160
|
+
private membershipGateDeferDepth;
|
|
161
|
+
/**
|
|
162
|
+
* In-memory delegate admission grants (see `delegate-admission.ts`): the
|
|
163
|
+
* strand-node transport peerIds this party's members have announced over the
|
|
164
|
+
* strand-addr RPC, admitted at the CONNECTION level so a member's NAT'd
|
|
165
|
+
* strand node can hold a circuit-relay reservation on this node. Consulted
|
|
166
|
+
* ONLY by {@link admitInboundControlConnection}; the fail-closed per-stream
|
|
167
|
+
* gate ({@link authorizeInboundControlStream}) never honors it.
|
|
168
|
+
*/
|
|
169
|
+
private readonly delegateAdmission;
|
|
170
|
+
/**
|
|
171
|
+
* When this node last ANNOUNCED a delegate, keyed `<targetPeerId>\n<strandId>`
|
|
172
|
+
* (the peer announced TO, not the delegate). Throttles
|
|
173
|
+
* {@link refreshDelegateGrants} to once per `DELEGATE_GRANT_TTL_MS / 2` per
|
|
174
|
+
* (relay, strand) so the 15 s reconcile tick never becomes per-tick RPC
|
|
175
|
+
* chatter; the launch/resume announce passes record here too. Keys whose
|
|
176
|
+
* strand is no longer running are pruned on each reconcile pass.
|
|
177
|
+
*/
|
|
178
|
+
private readonly delegateAnnounceAt;
|
|
179
|
+
/**
|
|
180
|
+
* Cold-start bootstrap dial targets: the owner-flagged peers of every seed
|
|
181
|
+
* this node has applied, keyed by peer id, valued by the seed's multiaddr
|
|
182
|
+
* strings (parsed lazily, at dial time). Written by
|
|
183
|
+
* {@link recordSeedBootstrapPeers}; read only by
|
|
184
|
+
* {@link dialColdStartBootstrap} while the control database still has no
|
|
185
|
+
* siblings to dial.
|
|
186
|
+
*
|
|
187
|
+
* A STORE rather than a plain map (see `bootstrap-peer-store.ts`), because the
|
|
188
|
+
* targets must outlive the process: a node seeded into a party it could not
|
|
189
|
+
* reach has nothing else on disk naming that party's addresses (`applySeed`
|
|
190
|
+
* writes no control row, and `CadrePeer` fills in only after a connection
|
|
191
|
+
* succeeds), so an in-memory-only set left it stranded permanently across a
|
|
192
|
+
* restart. Constructed (or adopted from `config.bootstrapPeers.store`) by
|
|
193
|
+
* {@link initializeBootstrapPeerStore} during {@link start}; deliberately NOT
|
|
194
|
+
* cleared by {@link cleanup}, so it survives a stop()→start() cycle of the same
|
|
195
|
+
* node instance — same lifecycle as {@link trustedOwnerStore}. Durability
|
|
196
|
+
* depends on the injected backend: Node gets a file-backed store from the CLI,
|
|
197
|
+
* RN/browser stay ephemeral for now.
|
|
198
|
+
*
|
|
199
|
+
* Deliberately NOT the libp2p peer store, which {@link peerStoreAddrs} already
|
|
200
|
+
* reads for the steady-state path. The peer store is shared with everything
|
|
201
|
+
* libp2p discovers, so "dial every entry" would grow into dialing arbitrary
|
|
202
|
+
* discovered peers as the node lives longer; this store holds exactly the peers
|
|
203
|
+
* an owner-signed, trust-anchored seed nominated as owners. `applySeed` merges
|
|
204
|
+
* the same addresses into the peer store as well, so the two never disagree —
|
|
205
|
+
* this one is just the precisely-scoped subset.
|
|
206
|
+
*
|
|
207
|
+
* A later seed OVERWRITES an entry rather than merging, so a re-seed after an
|
|
208
|
+
* owner's address changes replaces the stale address instead of accumulating
|
|
209
|
+
* dead ones. Entries are never evicted: they are the node's only way back into
|
|
210
|
+
* the party if it is ever stranded again.
|
|
211
|
+
*/
|
|
212
|
+
private bootstrapPeerStore;
|
|
82
213
|
/** Initial self-registration timer (see {@link scheduleSelfRegistration}). */
|
|
83
214
|
private selfRegistrationTimer;
|
|
84
215
|
/** TTL heartbeat that re-publishes the self record before it goes stale. */
|
|
@@ -107,15 +238,38 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
107
238
|
*/
|
|
108
239
|
private registerSelfInFlight;
|
|
109
240
|
/**
|
|
110
|
-
* Re-replication queue for owner
|
|
111
|
-
* node was alone — `controlNode.getConnections().length === 0` at write
|
|
112
|
-
* means the Optimystic commit was local-only (the block's cluster was ≤1)
|
|
113
|
-
* never broadcast. Maps the affected subject peerId →
|
|
114
|
-
* the cohort grows.
|
|
115
|
-
*
|
|
116
|
-
*
|
|
241
|
+
* Re-replication queue for owner `authorizePeer` writes that committed while
|
|
242
|
+
* this node was alone — `controlNode.getConnections().length === 0` at write
|
|
243
|
+
* time means the Optimystic commit was local-only (the block's cluster was ≤1)
|
|
244
|
+
* and never broadcast. Maps the affected subject peerId → 'authorize', to
|
|
245
|
+
* re-issue once the cohort grows. Removals are NOT tracked here — a delete
|
|
246
|
+
* leaves a `Revocation` tombstone, tracked by {@link pendingRevocations}; a
|
|
247
|
+
* removePeer also clears any queued authorize for the same subject (the row is
|
|
248
|
+
* gone, so re-issuing the insert would be wrong). Drained on the 0→≥1
|
|
249
|
+
* control-connection transition by {@link drainPendingControlReplication}.
|
|
117
250
|
*/
|
|
118
251
|
private pendingPeerWrites;
|
|
252
|
+
/**
|
|
253
|
+
* Re-replication queue for `Revocation` tombstones that committed while this
|
|
254
|
+
* node was alone, keyed on the retired `StampId`. Fed by the control DB's
|
|
255
|
+
* committed-delete seam ({@link noteGuardedDelete}), so one queue covers all
|
|
256
|
+
* four guarded tables (`CadrePeer` / `DeviceToken` / `Strand` /
|
|
257
|
+
* `ValidationKey`). Drained (owner-signed `ReissuedAt` bump, which re-broadcasts
|
|
258
|
+
* the tombstone) by {@link drainPendingRevocations}. Cleared on stop alongside
|
|
259
|
+
* {@link pendingPeerWrites} — the tombstone rows are durable, so the next
|
|
260
|
+
* lifetime's first-growth sweep re-covers anything dropped here.
|
|
261
|
+
*/
|
|
262
|
+
private pendingRevocations;
|
|
263
|
+
/**
|
|
264
|
+
* Guards the once-per-lifetime first-growth revocation sweep (re-issue EVERY
|
|
265
|
+
* locally-held tombstone, covering removals from before this node started).
|
|
266
|
+
* Deliberately separate from {@link reconstructedLocalOnlyWrites} and set only
|
|
267
|
+
* after a sweep pass SUCCEEDS (including the nothing-held case) — a throwing
|
|
268
|
+
* sweep retries on the next growth edge instead of being lost for the lifetime.
|
|
269
|
+
* Reset on stop, so a stop()→start() cycle sweeps again (that second start is
|
|
270
|
+
* exactly a "removals from before this lifetime" case).
|
|
271
|
+
*/
|
|
272
|
+
private reissuedHeldRevocations;
|
|
119
273
|
/** This node's own `CadrePeer` self-write committed local-only (re-touched on growth). */
|
|
120
274
|
private pendingSelfPeerWrite;
|
|
121
275
|
/** This node's own `DeviceToken` self-write committed local-only (re-touched on growth). */
|
|
@@ -249,11 +403,11 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
249
403
|
* before any network bring-up. Resolution order:
|
|
250
404
|
*
|
|
251
405
|
* 1. Both `keyStore` and `privateKey` set ⇒ configuration error (throws).
|
|
252
|
-
* 2. `keyStore` set ⇒
|
|
253
|
-
* {@link DEFAULT_IDENTITY_KEY_ID})
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
406
|
+
* 2. `keyStore` set ⇒ {@link loadOrCreateIdentityKey} against `identityKeyId`
|
|
407
|
+
* (default {@link DEFAULT_IDENTITY_KEY_ID}): load when the slot is present,
|
|
408
|
+
* generate + persist when it is empty. A rejected `get` (access denied /
|
|
409
|
+
* backend failure) PROPAGATES — we never generate a new key on a read error,
|
|
410
|
+
* which would silently orphan the real identity.
|
|
257
411
|
* 3. `privateKey` set ⇒ use it directly.
|
|
258
412
|
* 4. Neither ⇒ leave undefined; libp2p generates an ephemeral key.
|
|
259
413
|
*
|
|
@@ -261,7 +415,227 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
261
415
|
* resolved key rather than regenerating or re-persisting.
|
|
262
416
|
*/
|
|
263
417
|
private resolveIdentityKey;
|
|
418
|
+
/**
|
|
419
|
+
* Construct (or adopt) the node-local trusted-owner anchor and seed the
|
|
420
|
+
* out-of-band pinned keys from `config.trustedOwners`. The store is NEVER
|
|
421
|
+
* sourced from the replicated control DB — its entries come only from
|
|
422
|
+
* genesis self-trust ({@link initializeSeedBootstrap}), config pins (here),
|
|
423
|
+
* or runtime enrollment pins ({@link trustOwnerKeys}).
|
|
424
|
+
*
|
|
425
|
+
* Idempotent across stop()→start(): the store instance is kept, and
|
|
426
|
+
* re-seeding config pins is a no-op ({@link TrustedOwnerStore.trust} is
|
|
427
|
+
* idempotent). An injected store scoped to a different party is a
|
|
428
|
+
* configuration error (fail closed before any network bring-up).
|
|
429
|
+
*/
|
|
430
|
+
private initializeTrustedOwnerStore;
|
|
431
|
+
/**
|
|
432
|
+
* Construct (or adopt) the node-local cold-start bootstrap-peer store (see
|
|
433
|
+
* {@link bootstrapPeerStore}). Synchronous: an injected store has already
|
|
434
|
+
* loaded its persisted targets by the time it is handed in (its `open` is the
|
|
435
|
+
* async part), and the in-memory fallback has nothing to load.
|
|
436
|
+
*
|
|
437
|
+
* Idempotent across stop()→start(): the store instance is kept, so retained
|
|
438
|
+
* targets survive a restart of the same node instance even with the ephemeral
|
|
439
|
+
* default. An injected store scoped to a different party is a configuration
|
|
440
|
+
* error (fail closed before any network bring-up) — a foreign party's addresses
|
|
441
|
+
* must never enter this node's dial loop.
|
|
442
|
+
*/
|
|
443
|
+
private initializeBootstrapPeerStore;
|
|
444
|
+
/**
|
|
445
|
+
* The node-local cold-start bootstrap-peer store (null before {@link start}) —
|
|
446
|
+
* the retained owner addresses {@link reconcileControlCohort}'s cold-start
|
|
447
|
+
* branch re-dials. Exposed for diagnostics and for a host that wants to show
|
|
448
|
+
* "what would this node dial if it is stranded?".
|
|
449
|
+
*/
|
|
450
|
+
getBootstrapPeerStore(): BootstrapPeerStore | null;
|
|
451
|
+
/**
|
|
452
|
+
* The node-local trusted-owner anchor (null before {@link start}). This is
|
|
453
|
+
* the set the authorized-membership predicate and seed-trust anchor consult —
|
|
454
|
+
* never the replicated `OwnerKey` table, which any stranger can pollute.
|
|
455
|
+
*/
|
|
456
|
+
getTrustedOwnerStore(): TrustedOwnerStore | null;
|
|
457
|
+
/**
|
|
458
|
+
* Persist out-of-band-established owner keys into the node-local anchor —
|
|
459
|
+
* the runtime enrollment seam: call with `CadreInvite.ownerKeys` when
|
|
460
|
+
* redeeming an invite (BEFORE the first `applySeed`, so the anchor already
|
|
461
|
+
* holds the pins when seed trust consults it), or with an operator-supplied
|
|
462
|
+
* pin. Idempotent. ('genesis' provenance is reserved for the node's own
|
|
463
|
+
* founding key, seeded internally by {@link initializeSeedBootstrap}.)
|
|
464
|
+
*
|
|
465
|
+
* Validates every key's shape before trusting any of them (all-or-nothing):
|
|
466
|
+
* a malformed entry anywhere in `keys` rejects the whole call before a
|
|
467
|
+
* single key is anchored. For the invite route this means a `CadreInvite`
|
|
468
|
+
* carrying one malformed `ownerKeys` entry fails the redemption outright —
|
|
469
|
+
* consistent with the anchor's existing whole-record-or-nothing policy for
|
|
470
|
+
* a corrupt persisted entry (see `trusted-owner-store.ts`'s
|
|
471
|
+
* `unusableEntry: 'discard-all'`) — rather than silently anchoring a subset
|
|
472
|
+
* and leaving the caller to notice a key went missing.
|
|
473
|
+
*/
|
|
474
|
+
trustOwnerKeys(keys: Iterable<string>, source: Exclude<TrustSource, 'genesis'>): Promise<void>;
|
|
264
475
|
private createControlNode;
|
|
476
|
+
/**
|
|
477
|
+
* Map this node's config onto the control network's libp2p node options.
|
|
478
|
+
*
|
|
479
|
+
* Split out of {@link createControlNode} purely so the mapping is assertable without
|
|
480
|
+
* standing up a real libp2p node — `packages/cadre-core/test/cadre-node-control-node-options.spec.ts`
|
|
481
|
+
* calls it on a bare `new CadreNode(config)`. Read nothing else into the split; the
|
|
482
|
+
* only caller in production is `createControlNode`.
|
|
483
|
+
*/
|
|
484
|
+
private buildControlNodeOptions;
|
|
485
|
+
/**
|
|
486
|
+
* Decide whether an inbound CONTROL-network connection from `remotePeerId`
|
|
487
|
+
* should be admitted — the policy behind the connection gater wired in
|
|
488
|
+
* {@link createControlNode}. Deny only on a positive "unauthorized outsider
|
|
489
|
+
* while no stranger path is open" determination; everything ambiguous admits
|
|
490
|
+
* and defers to the fail-closed per-stream gates (see
|
|
491
|
+
* `membership-connection-gater.ts` for the layer's rationale and the
|
|
492
|
+
* stranger-open protocol allowlist).
|
|
493
|
+
*
|
|
494
|
+
* Admits when ANY of:
|
|
495
|
+
* 1. a shared-baseline check admits ({@link admitControlPeerUnconditionally}
|
|
496
|
+
* — not running / DB torn down, absent-or-empty trusted-owner anchor, or
|
|
497
|
+
* configured bootstrap/relay infrastructure);
|
|
498
|
+
* 2. an enrollment window is open ({@link openEnrollmentWindow}, opened by
|
|
499
|
+
* {@link createInvite}) — the invitee dials in before it is authorized;
|
|
500
|
+
* 3. the peer holds a live DELEGATE ADMISSION GRANT — an authorized member
|
|
501
|
+
* announced it (over the strand-addr RPC) as the transport peerId of its
|
|
502
|
+
* own strand node, so a NAT'd member's strand node can hold a
|
|
503
|
+
* circuit-relay reservation here (see `delegate-admission.ts`).
|
|
504
|
+
* Connection only: the per-stream gate below never honors a grant;
|
|
505
|
+
* 4. the authorized-member set is empty — cold start: the rows that would
|
|
506
|
+
* authorize anyone arrive by replication over these very connections;
|
|
507
|
+
* 5. the peer IS an authorized member; or
|
|
508
|
+
* 6. an open invitation is OUTSTANDING — at least one unexpired,
|
|
509
|
+
* not-fully-consumed invitation this node minted or persisted (see
|
|
510
|
+
* `StrandSolicitationService.hasOutstandingInvitation`). A formation
|
|
511
|
+
* initiator is another party's peer by design and its token is only
|
|
512
|
+
* checkable inside the protocol, so the gate asks the coarser question
|
|
513
|
+
* "does this node expect a stranger at all?". Merely REGISTERING the
|
|
514
|
+
* responder ({@link initializeStrandSolicitation}) no longer suspends
|
|
515
|
+
* stranger denial — eager registration (as `reference-app-rn` does at
|
|
516
|
+
* bring-up) and `formStrand`'s lazy initialization both leave the gate
|
|
517
|
+
* armed, because neither mints an invitation.
|
|
518
|
+
*
|
|
519
|
+
* Ordering is semantically free (the checks are OR'd) but decides who pays:
|
|
520
|
+
* checks 1-3 are in-memory, 4/5 share one control-DB read, and only a peer
|
|
521
|
+
* already on the deny path reaches check 6's invitation lookup.
|
|
522
|
+
*
|
|
523
|
+
* Caveats of check 6, both self-healing:
|
|
524
|
+
* - the in-memory mint registry dies with the process, so after a restart
|
|
525
|
+
* only invitations persisted as `FormationInvite` rows still hold the
|
|
526
|
+
* exemption open (re-mint otherwise — same story as the enrollment window);
|
|
527
|
+
* - a peer holding a token whose `FormationInvite` row has not replicated to
|
|
528
|
+
* this node yet is denied even though the formation handler would have
|
|
529
|
+
* accepted it, exactly like the unreplicated-membership-row case below.
|
|
530
|
+
*
|
|
531
|
+
* NOTE: check 4/5 runs a control-DB read per inbound connection
|
|
532
|
+
* (`listAuthorizedMembers`); connections are rare and cadres small, and this
|
|
533
|
+
* layer is fail-open behind `ADMISSION_DECISION_TIMEOUT_MS`, so the live
|
|
534
|
+
* read is safe here — unlike the per-stream gate, which must consult the
|
|
535
|
+
* materialized {@link authorizedControlPeers} snapshot instead.
|
|
536
|
+
* NOTE: a sibling whose membership row has not yet replicated to
|
|
537
|
+
* this node is denied until the row converges (typically via the owner);
|
|
538
|
+
* either side's next outbound reconcile dial (outbound is never gated)
|
|
539
|
+
* re-establishes the link — self-healing, but visible as a transient deny.
|
|
540
|
+
*/
|
|
541
|
+
private admitInboundControlConnection;
|
|
542
|
+
/**
|
|
543
|
+
* Hold the control-network inbound gate open for not-yet-authorized peers
|
|
544
|
+
* until `untilEpochMs` (extends, never shrinks, an already-open window).
|
|
545
|
+
* {@link createInvite} calls this automatically; a host running an
|
|
546
|
+
* out-of-band enrollment flow (e.g. accepting a phone whose invite this node
|
|
547
|
+
* never minted) can open it explicitly before the stranger dials in.
|
|
548
|
+
*/
|
|
549
|
+
openEnrollmentWindow(untilEpochMs: number): void;
|
|
550
|
+
/**
|
|
551
|
+
* Record (or refresh) a delegate admission grant: `delegatePeerId` is the
|
|
552
|
+
* transport peerId of `announcerPeerId`'s strand-`strandId` node, admitted
|
|
553
|
+
* at the CONNECTION level (all a circuit-relay reservation needs) for
|
|
554
|
+
* `DELEGATE_GRANT_TTL_MS`. Called by the strand-addr responder after its
|
|
555
|
+
* authorized-membership gate has passed; public so tests can drive the
|
|
556
|
+
* admission policy without a full strand launch. A re-announce for the same
|
|
557
|
+
* (announcer, strand) REPLACES the previous delegate rather than
|
|
558
|
+
* accumulating. Never honored by {@link authorizeInboundControlStream}.
|
|
559
|
+
*/
|
|
560
|
+
grantDelegateAdmission(announcerPeerId: string, strandId: string, delegatePeerId: string): void;
|
|
561
|
+
/** Is `remotePeerId` covered by a live delegate admission grant? */
|
|
562
|
+
hasDelegateAdmission(remotePeerId: string): boolean;
|
|
563
|
+
/**
|
|
564
|
+
* The admission checks SHARED by the connection gate
|
|
565
|
+
* ({@link admitInboundControlConnection}) and the per-stream control-DB gate
|
|
566
|
+
* ({@link authorizeInboundControlStream}) — factored so the two layers cannot
|
|
567
|
+
* drift. All in-memory and cheap (the stream gate runs this per inbound
|
|
568
|
+
* stream). Returns true when the peer must be admitted BEFORE any membership
|
|
569
|
+
* source is consulted:
|
|
570
|
+
* - the node is not fully up (`start()` in progress / DB torn down) — both
|
|
571
|
+
* gates exist from libp2p bring-up, before the control DB does;
|
|
572
|
+
* - the trusted-owner anchor is absent or empty — an un-enrolled node has
|
|
573
|
+
* no basis to judge anyone and MUST accept its enrollment seed;
|
|
574
|
+
* - the peer is one of the configured control bootstrap/relay nodes —
|
|
575
|
+
* operator-configured infrastructure, not cadre members.
|
|
576
|
+
* False only means "no unconditional admit": the caller then judges the peer
|
|
577
|
+
* against its own membership source (a live DB read for the fail-open
|
|
578
|
+
* connection gate; the materialized snapshot for the fail-closed stream gate).
|
|
579
|
+
*/
|
|
580
|
+
private admitControlPeerUnconditionally;
|
|
581
|
+
/**
|
|
582
|
+
* Per-stream authorization for the four Optimystic control-DB protocols
|
|
583
|
+
* (`/optimystic/control-<party>/{repo,cluster,sync,block-transfer}/…`),
|
|
584
|
+
* wired as `authorizeInboundStream` in {@link createControlNode} — the
|
|
585
|
+
* fail-closed layer behind the fail-open connection gater. Control node
|
|
586
|
+
* ONLY: strand cohort nodes legitimately serve cross-party peers.
|
|
587
|
+
*
|
|
588
|
+
* The STRICT SUBSET of {@link admitInboundControlConnection}: the same
|
|
589
|
+
* "no basis to judge" admissions (shared via
|
|
590
|
+
* {@link admitControlPeerUnconditionally}), minus the stranger carve-outs
|
|
591
|
+
* (enrollment window, outstanding open invitation, delegate admission
|
|
592
|
+
* grant). The first two exist so a stranger can reach `/sereus/seed/1.0.0`
|
|
593
|
+
* and `/sereus/formation/1.0.0` — neither is gated here, and admitting a
|
|
594
|
+
* stranger to `repo` during an enrollment window is exactly the hole this
|
|
595
|
+
* gate closes. A DELEGATE-admitted connection (a member's strand node
|
|
596
|
+
* holding a circuit-relay reservation, see `delegate-admission.ts`) is
|
|
597
|
+
* likewise exactly a case this gate must still refuse: the delegate gets
|
|
598
|
+
* the connection and never the control DB.
|
|
599
|
+
*
|
|
600
|
+
* Admits when ANY of:
|
|
601
|
+
* 1. a shared-baseline check admits (not running / no DB, empty anchor,
|
|
602
|
+
* configured bootstrap infra);
|
|
603
|
+
* 2. the materialized authorized set is empty — cold start, before the
|
|
604
|
+
* rows that would authorize anyone have replicated in;
|
|
605
|
+
* 3. the peer IS in the materialized authorized set.
|
|
606
|
+
*
|
|
607
|
+
* Deliberately SYNCHRONOUS and pure in-memory — see
|
|
608
|
+
* {@link authorizedControlPeers} for why a control-DB read here would
|
|
609
|
+
* deadlock into mutual denial. With a sync predicate the upstream deadline
|
|
610
|
+
* (`authorizeInboundStreamTimeoutMs`) never trips, so it stays at its
|
|
611
|
+
* default.
|
|
612
|
+
*
|
|
613
|
+
* NOTE: the snapshot keys on PEER ID, so a node with no persistent identity
|
|
614
|
+
* key never lands a `CadrePeer` row ({@link registerSelf} skips) and its
|
|
615
|
+
* siblings deny its control-DB streams once their own snapshot is non-empty —
|
|
616
|
+
* owner status is a KEY, not a peer id, so being the owner does not exempt it.
|
|
617
|
+
* Harmless today (a real deployment persists its identity; the ephemeral-owner
|
|
618
|
+
* case appears only in tests, which configure the owner as bootstrap infra —
|
|
619
|
+
* `push-wake-e2e.integration.ts` design note 3). If an ephemeral-identity node
|
|
620
|
+
* ever becomes a supported deployment, this gate needs a key-based admission.
|
|
621
|
+
*/
|
|
622
|
+
private authorizeInboundControlStream;
|
|
623
|
+
/**
|
|
624
|
+
* Refresh {@link authorizedControlPeers} from the control DB, best-effort: a
|
|
625
|
+
* failed read keeps the previous snapshot (never clears it), so a transient
|
|
626
|
+
* DB error can neither flip the stream gate's cold-start carve-out back open
|
|
627
|
+
* nor drop a legitimate member mid-flight. Never rejects.
|
|
628
|
+
*
|
|
629
|
+
* The SOLE caller is {@link drainMembershipGate}, which serializes refreshes —
|
|
630
|
+
* so two reads can no longer settle out of order.
|
|
631
|
+
*/
|
|
632
|
+
private refreshAuthorizedControlPeers;
|
|
633
|
+
/**
|
|
634
|
+
* PeerIds of the configured control bootstrap nodes (relay/bootstrap
|
|
635
|
+
* infrastructure — always admitted inbound). Parsed once, lazily; an
|
|
636
|
+
* address without a `/p2p/<id>` component contributes nothing.
|
|
637
|
+
*/
|
|
638
|
+
private getBootstrapPeerIds;
|
|
265
639
|
private createStrandQueryable;
|
|
266
640
|
/**
|
|
267
641
|
* Schedule the node's initial self-record publish + ongoing refresh shortly
|
|
@@ -280,7 +654,11 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
280
654
|
* PeerId (the resolved node identity from `keyStore`/`config.privateKey`).
|
|
281
655
|
* - If the row already exists: a self-signed UPDATE bumping `UpdatedAt`.
|
|
282
656
|
* - If not, and the node is its own owner: an owner-signed INSERT that
|
|
283
|
-
* also carries the self-signature.
|
|
657
|
+
* also carries the self-signature. That INSERT is idempotent, so if an owner
|
|
658
|
+
* {@link authorizePeer} of this node's own id seated the row inside the
|
|
659
|
+
* read-then-insert window it no-ops — the publish then falls through to the
|
|
660
|
+
* UPDATE path (re-reading and re-signing against the row that landed) rather
|
|
661
|
+
* than leaving the authorize's null `Sig` in place, and reports `refreshed`.
|
|
284
662
|
* - Otherwise: logs and returns (a non-owner node with no row yet must
|
|
285
663
|
* wait for an owner to insert it; it can then self-refresh).
|
|
286
664
|
*
|
|
@@ -294,6 +672,13 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
294
672
|
registerSelf(): Promise<SelfRegistrationOutcome>;
|
|
295
673
|
/** The body of {@link registerSelf}; serialised by its single-flight guard. */
|
|
296
674
|
private publishSelfRecord;
|
|
675
|
+
/**
|
|
676
|
+
* Sign this node's address record for publication, stamped strictly later than
|
|
677
|
+
* `existing` (the row it is about to replace, or null for a first insert) so a
|
|
678
|
+
* same-millisecond re-publish still satisfies the monotonic `UpdatedAt` rule the
|
|
679
|
+
* `CadrePeer.AuthorizedUpdate` self-branch enforces.
|
|
680
|
+
*/
|
|
681
|
+
private signSelfRecord;
|
|
297
682
|
/**
|
|
298
683
|
* The ed25519 keypair (base64url) the node signs its own record with — the key
|
|
299
684
|
* behind its libp2p PeerId. Sourced from the resolved {@link identityKey}
|
|
@@ -399,6 +784,65 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
399
784
|
* parse/lookup failure — never throws.
|
|
400
785
|
*/
|
|
401
786
|
private peerStoreAddrs;
|
|
787
|
+
/**
|
|
788
|
+
* Retain a just-applied seed's owner-flagged peers as cold-start bootstrap
|
|
789
|
+
* dial targets (see {@link bootstrapPeerStore}).
|
|
790
|
+
*
|
|
791
|
+
* Called for every seed this node accepts, on BOTH intake paths — the
|
|
792
|
+
* {@link applySeed} wrapper (which may run on a throwaway service, so the
|
|
793
|
+
* service itself is the wrong place to keep this) and the inbound
|
|
794
|
+
* `/sereus/seed/1.0.0` handler via `onSeedApplied`. Peers with no address are
|
|
795
|
+
* skipped: there is nothing to dial.
|
|
796
|
+
*
|
|
797
|
+
* `isOwner` is the seed's own claim about its peers, exactly as
|
|
798
|
+
* `SeedBootstrapService.applySeed` uses it to choose its dial targets. That is
|
|
799
|
+
* sound here for the same reason it is sound there — the whole seed is
|
|
800
|
+
* signature-checked against a trust-anchored signer before this runs, and the
|
|
801
|
+
* flag only *selects a dial target*; a dial grants no authority.
|
|
802
|
+
*
|
|
803
|
+
* Self is excluded. `createSeed` projects EVERY `CadrePeer` row, so an owner
|
|
804
|
+
* that applies a seed minted after it joined finds itself in the owner list;
|
|
805
|
+
* retaining that would make the cold-start pass dial this node forever (the
|
|
806
|
+
* steady-state pass filters self out of its sibling list for the same reason).
|
|
807
|
+
*/
|
|
808
|
+
private recordSeedBootstrapPeers;
|
|
809
|
+
/**
|
|
810
|
+
* Cold-start branch of the reconcile pass: re-dial the seed's owner peers
|
|
811
|
+
* while the control database still has no siblings to dial.
|
|
812
|
+
*
|
|
813
|
+
* `SeedBootstrapService.applySeed` dials those owners exactly ONCE, best-effort.
|
|
814
|
+
* When that single dial fails — owner momentarily down, relay reservation not
|
|
815
|
+
* up yet, NAT traversal lost the race — the joining node has an empty
|
|
816
|
+
* `CadrePeer` table and no connection, and the steady-state pass above cannot
|
|
817
|
+
* help: it dials only siblings enumerated from that very table. Retrying here
|
|
818
|
+
* is the only way back in.
|
|
819
|
+
*
|
|
820
|
+
* Unbounded, at the reconcile cadence, with no backoff: the branch is already
|
|
821
|
+
* gated by "the control database has no siblings", so it stops the moment the
|
|
822
|
+
* node is actually in the party, and a node that is stranded MUST keep trying —
|
|
823
|
+
* a give-up rule would turn a transient outage into a permanent one. The steady
|
|
824
|
+
* cost is one dial per bootstrap peer per pass (seeds nominate one or a few
|
|
825
|
+
* owners), each failing fast against an unreachable address.
|
|
826
|
+
* NOTE: if seeds ever carry many owner peers, or the reconcile cadence tightens
|
|
827
|
+
* well below its 15 s default, add per-peer backoff here.
|
|
828
|
+
*
|
|
829
|
+
* Best-effort per peer, exactly like {@link dialControlSibling}: one dead
|
|
830
|
+
* address never aborts the pass.
|
|
831
|
+
*/
|
|
832
|
+
private dialColdStartBootstrap;
|
|
833
|
+
/**
|
|
834
|
+
* Bind a bootstrap peer's seed addresses to its peer id, so the dial
|
|
835
|
+
* authenticates the peer it is aiming at rather than trusting whoever answers.
|
|
836
|
+
*
|
|
837
|
+
* An address that already carries `/p2p/<id>` must carry THIS peer's id or it
|
|
838
|
+
* is dropped (a seed that disagrees with itself is not a dial target); one
|
|
839
|
+
* that carries none — an `addressOverrides`-published record, say — gets the
|
|
840
|
+
* id encapsulated. Unencapsulatable addresses are dropped rather than dialed
|
|
841
|
+
* bare.
|
|
842
|
+
*/
|
|
843
|
+
private bootstrapDialAddrs;
|
|
844
|
+
/** Dial one cold-start bootstrap peer, best-effort. Returns whether it connected. */
|
|
845
|
+
private dialBootstrapPeer;
|
|
402
846
|
/**
|
|
403
847
|
* Whether a control write happening now would commit local-only. A sound lower
|
|
404
848
|
* bound is "no connected control peers": 0 connections ⇒ the block's cluster is
|
|
@@ -408,6 +852,9 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
408
852
|
* is caught by the cohort's periodic pull-on-read instead. Re-issuing an already-
|
|
409
853
|
* replicated row is harmless (an idempotent monotonic bump), so the coarse proxy
|
|
410
854
|
* is acceptable as the agreed first cut.
|
|
855
|
+
*
|
|
856
|
+
* Reads {@link getControlConnectionCount} — the public, embedder-facing form of
|
|
857
|
+
* the same sample — so the two cannot drift.
|
|
411
858
|
*/
|
|
412
859
|
private committedAlone;
|
|
413
860
|
/**
|
|
@@ -417,12 +864,23 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
417
864
|
* cohort grows; otherwise the write replicated and any stale queue entry for this
|
|
418
865
|
* subject is dropped.
|
|
419
866
|
*
|
|
420
|
-
* A `remove`
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
867
|
+
* A `remove` never queues here — a delete's re-replicable half is its
|
|
868
|
+
* `Revocation` tombstone, which the committed-delete seam routes into
|
|
869
|
+
* {@link pendingRevocations} ({@link noteGuardedDelete}). The `remove` arm only
|
|
870
|
+
* drops any queued authorize for the subject (the row is gone; re-issuing the
|
|
871
|
+
* insert would resurrect it) and, when the commit was alone, logs the
|
|
872
|
+
* security-relevant window loudly.
|
|
424
873
|
*/
|
|
425
874
|
private noteControlWrite;
|
|
875
|
+
/**
|
|
876
|
+
* Committed-delete seam handler ({@link GuardedDeleteListener}, wired in
|
|
877
|
+
* {@link start}): a guarded-table delete and its `Revocation` tombstone
|
|
878
|
+
* committed. If the node was alone the tombstone is local-only — queue it for
|
|
879
|
+
* an owner-signed re-issue on cohort growth; otherwise it broadcast, so drop
|
|
880
|
+
* any stale queue entry for the stamp. Synchronous by contract — bookkeeping
|
|
881
|
+
* only, never throws into the delete path.
|
|
882
|
+
*/
|
|
883
|
+
private noteGuardedDelete;
|
|
426
884
|
/**
|
|
427
885
|
* Drain the write-while-alone re-replication queue: re-issue the writes that
|
|
428
886
|
* committed local-only now that the cohort can broadcast them. Public so the
|
|
@@ -443,6 +901,38 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
443
901
|
* the drain.
|
|
444
902
|
*/
|
|
445
903
|
private retouchSelfDeviceToken;
|
|
904
|
+
/**
|
|
905
|
+
* Re-issue `Revocation` tombstones so a removal that committed while alone
|
|
906
|
+
* still reaches the cohort. Two regimes, one method:
|
|
907
|
+
*
|
|
908
|
+
* - **First successful pass per process** ({@link reissuedHeldRevocations}
|
|
909
|
+
* false): sweep EVERY locally-held tombstone from `queryRevocations()` — the
|
|
910
|
+
* only cover for a removal made before this process started, since the
|
|
911
|
+
* in-memory queue does not survive a restart. Queued stamps are part of that
|
|
912
|
+
* same batch (exactly once — one transaction, no double bump).
|
|
913
|
+
* - **Later passes**: only the tombstones queued in-session by
|
|
914
|
+
* {@link noteGuardedDelete}.
|
|
915
|
+
*
|
|
916
|
+
* Owner-gated: a node with no owner key cannot sign a re-issue, so stray queue
|
|
917
|
+
* entries are dropped (mirrors {@link drainPendingPeerWrites}). Best-effort — a
|
|
918
|
+
* failure is logged and leaves the queue (and the sweep flag) untouched for the
|
|
919
|
+
* next growth edge. A successful `reissueRevocations` exec is NOT proof of
|
|
920
|
+
* broadcast (the connection that fired the growth edge may not be in the
|
|
921
|
+
* affected block's cluster — see
|
|
922
|
+
* tickets/backlog/control-rereplication-broadcast-confirmation); the drain
|
|
923
|
+
* inherits that known gap, and a full disconnect→reconnect (or the next
|
|
924
|
+
* process's sweep) re-covers it.
|
|
925
|
+
*
|
|
926
|
+
* NOTE: the sweep is O(all tombstones ever) row-updates — plus one owner
|
|
927
|
+
* signature each — in one transaction, once per lifetime. `Revocation` is
|
|
928
|
+
* append-only and unbounded growth is declared acceptable for a cadre-sized
|
|
929
|
+
* party. If the tombstone table ever gets large, bound the sweep (e.g. persist
|
|
930
|
+
* a node-local high-water mark of what has been re-issued while connected)
|
|
931
|
+
* instead of re-touching everything; note the `Math.max(...)` spread below also
|
|
932
|
+
* caps out around 10^5 rows (V8 argument limit) before the size becomes merely
|
|
933
|
+
* a latency problem.
|
|
934
|
+
*/
|
|
935
|
+
private drainPendingRevocations;
|
|
446
936
|
/**
|
|
447
937
|
* One-shot, first-cohort-growth reconstruction of the write-while-alone queue for
|
|
448
938
|
* an OWNER node: re-touch every membership row that may be an unreplicated
|
|
@@ -455,29 +945,36 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
455
945
|
* row is a no-op-equivalent).
|
|
456
946
|
*
|
|
457
947
|
* A node with no owner private key skips this entirely — it cannot re-sign
|
|
458
|
-
* rows for other peers, and rows it merely holds are not its to re-issue.
|
|
459
|
-
*
|
|
460
|
-
*
|
|
948
|
+
* rows for other peers, and rows it merely holds are not its to re-issue.
|
|
949
|
+
* DELETEs are not reconstructed here — the `Revocation` tombstone sweep in
|
|
950
|
+
* {@link drainPendingRevocations} covers them (a removed row leaves no
|
|
951
|
+
* `CadrePeer` trace, but its tombstone is re-issuable).
|
|
461
952
|
*/
|
|
462
953
|
private reconstructAuthoredMembership;
|
|
954
|
+
/**
|
|
955
|
+
* Body of {@link reconstructAuthoredMembership}'s sweep: re-issue each candidate
|
|
956
|
+
* owner-authored row, returning how many were re-touched. A per-row failure is
|
|
957
|
+
* logged and skipped; a teardown mid-sweep stops it.
|
|
958
|
+
*/
|
|
959
|
+
private reissueAuthoredMembershipRows;
|
|
463
960
|
/**
|
|
464
961
|
* Drain the in-session write-while-alone queue: re-issue each pending owner
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
* - `remove` (security-relevant): a removePeer-while-alone physically removed the
|
|
474
|
-
* row locally, so a re-issued DELETE matches nothing and does NOT propagate —
|
|
475
|
-
* full delete durability requires a schema tombstone (deferred:
|
|
476
|
-
* `control-delete-while-alone-tombstone`). We still attempt the re-issue
|
|
477
|
-
* (harmless, and it propagates in the row-still-present case); the commit-alone
|
|
478
|
-
* was already logged loudly in {@link noteControlWrite}.
|
|
962
|
+
* authorize as an idempotent monotonic owner UPDATE
|
|
963
|
+
* ({@link reissuePeerAuthorize}) now that the cohort can broadcast it.
|
|
964
|
+
* Sequential (no fan-out) to avoid a thundering re-touch; an entry is cleared
|
|
965
|
+
* only on success, so a failure (or a still-alone re-commit) leaves it queued
|
|
966
|
+
* for the next growth. An entry is skipped (and cleared) if the row vanished
|
|
967
|
+
* (raced a delete) or now carries a self-`Sig` (the peer self-published and owns
|
|
968
|
+
* its republish). Removals are drained by {@link drainPendingRevocations}, not
|
|
969
|
+
* here.
|
|
479
970
|
*/
|
|
480
971
|
private drainPendingPeerWrites;
|
|
972
|
+
/**
|
|
973
|
+
* Body of {@link drainPendingPeerWrites}' loop. An entry is cleared only on
|
|
974
|
+
* success (or when there is nothing left to re-issue); a failure is logged and
|
|
975
|
+
* the entry stays queued for the next cohort growth.
|
|
976
|
+
*/
|
|
977
|
+
private reissuePendingPeerWrites;
|
|
481
978
|
/**
|
|
482
979
|
* Re-issue an owner membership row as a monotonic owner UPDATE: bump
|
|
483
980
|
* `UpdatedAt` strictly above the stored value (and the wall clock) and re-sign via
|
|
@@ -549,8 +1046,10 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
549
1046
|
* 2. `publicKey <-> peerId` binding — the stored key's libp2p identity is the
|
|
550
1047
|
* requested peerId,
|
|
551
1048
|
* 3. a `DeviceToken` row exists with a known {@link PushPlatform},
|
|
552
|
-
* 4.
|
|
553
|
-
*
|
|
1049
|
+
* 4. the row's `StampId` is NOT retired in `CadreControl.Revocation` — the
|
|
1050
|
+
* read-side half of the clear (see below),
|
|
1051
|
+
* 5. self-signature verifies against the bound `CadrePeer.PublicKey`,
|
|
1052
|
+
* 6. freshness — `updatedAt` is positive and within `opts.maxAgeMs` (default:
|
|
554
1053
|
* no ceiling, since a push token is valid until it rotates).
|
|
555
1054
|
*
|
|
556
1055
|
* A peer that is not a current member, or whose token has no backing `CadrePeer`
|
|
@@ -672,6 +1171,14 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
672
1171
|
/**
|
|
673
1172
|
* Add a strand with its sApp configuration.
|
|
674
1173
|
* The hosting application must provide the sApp schema when creating a strand.
|
|
1174
|
+
*
|
|
1175
|
+
* A rejected call leaves nothing running but DOES leave the sApp config
|
|
1176
|
+
* registered, deliberately: both an explicit retry and the {@link StrandWatcher}'s
|
|
1177
|
+
* automatic relaunch need it. So once the strand's row is visible on the control
|
|
1178
|
+
* network (via {@link publishStrand} or another member), a failed launch keeps
|
|
1179
|
+
* being re-attempted in the background — each failure re-emitting `strand:error`
|
|
1180
|
+
* — until it succeeds. {@link detachStrand} (reached via {@link stopStrand}) is
|
|
1181
|
+
* what abandons a strand for good.
|
|
675
1182
|
*/
|
|
676
1183
|
addStrand(config: StrandConfig): Promise<StrandInstance>;
|
|
677
1184
|
/**
|
|
@@ -689,7 +1196,7 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
689
1196
|
* {@link ed25519KeyPairFromLibp2p} also exposes as the node's owner keypair,
|
|
690
1197
|
* so peer identity and owner key are one and the same. That key must be
|
|
691
1198
|
* enrolled in `OwnerKey` (e.g. via {@link ControlDatabase.ensureOwnerKey}
|
|
692
|
-
* at genesis) or the schema's `Strand.
|
|
1199
|
+
* at genesis) or the schema's `Strand.AuthorizedInsert` constraint rejects the write.
|
|
693
1200
|
* Failing loudly here is intentional: a silently-unpublished strand would run
|
|
694
1201
|
* as a local-only island that no peer could ever discover or join.
|
|
695
1202
|
*
|
|
@@ -697,14 +1204,70 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
697
1204
|
* {@link addStrand}).
|
|
698
1205
|
* @param type - `'o'` for open (default) or `'c'` for closed.
|
|
699
1206
|
* @param memberPrivateKey - Optional membership key for a closed strand.
|
|
700
|
-
* @throws if the node is not started, exposes no owner signing key, or
|
|
701
|
-
* control DB rejects the (unauthorized) insert.
|
|
1207
|
+
* @throws if the node is not started, exposes no owner signing key, the id is blank, or
|
|
1208
|
+
* the control DB rejects the (unauthorized) insert.
|
|
702
1209
|
*/
|
|
703
1210
|
publishStrand(strandId: string, type?: 'o' | 'c', memberPrivateKey?: string): Promise<void>;
|
|
1211
|
+
/**
|
|
1212
|
+
* Remove this party's `Strand` row from the shared control database — the owner-signed
|
|
1213
|
+
* inverse of {@link publishStrand}.
|
|
1214
|
+
*
|
|
1215
|
+
* Party-wide, unlike {@link stopStrand}, which only stops the strand on THIS node: every
|
|
1216
|
+
* cadre node watching the table sees the row vanish on its next poll (default 5 s) and
|
|
1217
|
+
* stops its own instance. This node converges immediately — the method forces a watcher
|
|
1218
|
+
* poll and stops any still-running local instance before resolving. Removing the row
|
|
1219
|
+
* removes OUR party's participation only: other parties in the strand keep their own
|
|
1220
|
+
* rows and the strand network carries on, so no cross-party sign-off is involved — this
|
|
1221
|
+
* is an owner-signed control-plane write like every other, not "destroy the network".
|
|
1222
|
+
*
|
|
1223
|
+
* Irreversible for a closed strand (`Type='c'`): the row carries `MemberPrivateKey`,
|
|
1224
|
+
* this party's membership secret for that network, and it is stored nowhere else.
|
|
1225
|
+
* With it gone the party can never again admit a member to that closed strand, and a
|
|
1226
|
+
* re-published row would carry a DIFFERENT key that does not match the membership
|
|
1227
|
+
* already written into the strand's RBAC layer. The strand id itself is NOT
|
|
1228
|
+
* blacklisted: a fresh owner-signed {@link publishStrand} re-seats it under a new
|
|
1229
|
+
* stamp — only the unsigned consent re-seat is permanently foreclosed (the removal's
|
|
1230
|
+
* `Revocation` tombstone names the id, which the consent branch of
|
|
1231
|
+
* `Strand.AuthorizedInsert` refuses ever after). Outstanding `FormationInvite` rows
|
|
1232
|
+
* bound to the removed strand are unredeemable while the row is absent (the formation
|
|
1233
|
+
* recorder resolves the strand as missing and rejects cleanly).
|
|
1234
|
+
*
|
|
1235
|
+
* Convergence caveats, same class as {@link enrollValidationKey}'s: a sibling that has
|
|
1236
|
+
* not yet synced keeps running the strand until its own watcher observes the missing
|
|
1237
|
+
* row; a sibling whose `strandFilter` never admitted this strand never observes the
|
|
1238
|
+
* removal AT ALL and keeps running its instance indefinitely — opting out of watching a
|
|
1239
|
+
* strand is also opting out of its party-wide removal, so such a node's only stop is its
|
|
1240
|
+
* own local {@link stopStrand}/`unpublishStrand` call; and a removal committed while
|
|
1241
|
+
* ALONE (0 control connections) deletes the row local-only. The accompanying
|
|
1242
|
+
* `Revocation` tombstone IS queued and re-issued on the next cohort-growth edge
|
|
1243
|
+
* ({@link noteGuardedDelete}/{@link drainPendingRevocations}), so the stamp retirement
|
|
1244
|
+
* — and with it the consent re-seat foreclosure — does propagate; but the physical row
|
|
1245
|
+
* deletion cannot be replayed, and `queryStrands` reads raw (no retired-stamp filter),
|
|
1246
|
+
* so siblings that already hold the row keep running the strand until the collection
|
|
1247
|
+
* itself converges (logged loudly; see the delete-while-alone durability note in
|
|
1248
|
+
* docs/architecture.md).
|
|
1249
|
+
*
|
|
1250
|
+
* A no-op (no throw, no tombstone) when the row is already absent — but a
|
|
1251
|
+
* locally-running instance of that id is still stopped.
|
|
1252
|
+
*
|
|
1253
|
+
* NOTE: control-plane only — the strand's local durable storage is retained. Stopping the
|
|
1254
|
+
* instance closes the `StrandDatabase` and its libp2p node but purges no blocks, so a
|
|
1255
|
+
* closed strand's content stays readable on disk to anyone with the data directory. If a
|
|
1256
|
+
* caller ever needs removal to mean "and erase the local copy", that is a separate purge
|
|
1257
|
+
* step, not a widening of this method.
|
|
1258
|
+
*
|
|
1259
|
+
* @param strandId - The `Strand` row id to remove (as passed to {@link publishStrand}).
|
|
1260
|
+
* @throws if the node is not started, exposes no owner signing key, the id is blank, or
|
|
1261
|
+
* the signer is not an enrolled owner (the schema's `Strand.AuthorizedDelete` rejects
|
|
1262
|
+
* the write and the row survives). A rejection does NOT imply the row survived: the
|
|
1263
|
+
* local stop runs after the control-plane delete has already committed, so a failure
|
|
1264
|
+
* there throws over a completed removal.
|
|
1265
|
+
*/
|
|
1266
|
+
unpublishStrand(strandId: string): Promise<void>;
|
|
704
1267
|
/**
|
|
705
1268
|
* Publish an owner-signed `FormationInvite` (open-invitation token) to the
|
|
706
1269
|
* shared control database, so a later {@link formStrand} redemption can be
|
|
707
|
-
* validated against it (the consent branch of `Strand.
|
|
1270
|
+
* validated against it (the consent branch of `Strand.AuthorizedInsert`).
|
|
708
1271
|
*
|
|
709
1272
|
* Counterpart to {@link createOpenInvitation}, which only mints the
|
|
710
1273
|
* out-of-band {@link OpenInvitation} envelope: persisting the matching
|
|
@@ -729,11 +1292,81 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
729
1292
|
validationUrl?: string;
|
|
730
1293
|
strandId?: string;
|
|
731
1294
|
}): Promise<void>;
|
|
1295
|
+
/**
|
|
1296
|
+
* Enroll an approver public key allowed to sign off on `ValidationUrl` redemptions.
|
|
1297
|
+
*
|
|
1298
|
+
* A `FormationInvite` carrying a `ValidationUrl` is only redeemable when the approval
|
|
1299
|
+
* that comes back from that URL is signed by a key present in `CadreControl.ValidationKey`
|
|
1300
|
+
* — this is how a party says which outside approver it trusts. Without an enrollment,
|
|
1301
|
+
* every such invitation is unredeemable.
|
|
1302
|
+
*
|
|
1303
|
+
* Signs with the same self-owner key as {@link publishStrand} (the ed25519 key behind
|
|
1304
|
+
* this node's PeerId, which must be an enrolled `OwnerKey`). Throws loudly if the node
|
|
1305
|
+
* isn't started, exposes no signing key, or the key is blank — a blank key would reach
|
|
1306
|
+
* the database and fail the signature CHECK as an opaque constraint error.
|
|
1307
|
+
*
|
|
1308
|
+
* Enrolling this node's OWN owner key is permitted and harmless: the domain/action tags
|
|
1309
|
+
* baked into each authorization digest mean an owner-key signature can never satisfy the
|
|
1310
|
+
* approval rule and an approval can never satisfy an owner rule. No guard is needed.
|
|
1311
|
+
*
|
|
1312
|
+
* NOTE: enrollment is replicated control state, so a key enrolled here is visible to a
|
|
1313
|
+
* sibling cadre node only once control replication converges. A redemption that arrives
|
|
1314
|
+
* at a node which has not caught up is refused as not-enrolled — the same convergence
|
|
1315
|
+
* gap the schema records on `Strand.StampId`. Enroll before circulating the invitations
|
|
1316
|
+
* that depend on the key.
|
|
1317
|
+
*
|
|
1318
|
+
* @param key - Approver public key to enroll (base64url ed25519 public key).
|
|
1319
|
+
*/
|
|
1320
|
+
enrollValidationKey(key: string): Promise<void>;
|
|
1321
|
+
/**
|
|
1322
|
+
* Remove an approver public key.
|
|
1323
|
+
*
|
|
1324
|
+
* Narrows who may approve FUTURE redemptions ONLY. The schema's approval CHECKs run at
|
|
1325
|
+
* write time, so a join that was already approved by this key stays valid — removal is
|
|
1326
|
+
* not retroactive and does not re-examine committed rows.
|
|
1327
|
+
*
|
|
1328
|
+
* Rotation is therefore add-then-remove, in that order: removing the only enrolled key
|
|
1329
|
+
* while `ValidationUrl` invitations are outstanding makes every one of them unredeemable
|
|
1330
|
+
* until a new key is enrolled.
|
|
1331
|
+
*
|
|
1332
|
+
* Removing a key that is not enrolled is a silent no-op (no throw, no `Revocation`
|
|
1333
|
+
* tombstone) — see {@link ControlDatabase.deleteValidationKey}.
|
|
1334
|
+
*
|
|
1335
|
+
* @param key - Approver public key to remove (base64url ed25519 public key).
|
|
1336
|
+
*/
|
|
1337
|
+
removeValidationKey(key: string): Promise<void>;
|
|
1338
|
+
/**
|
|
1339
|
+
* The approver keys currently enrolled, sorted. Read-only, so unlike
|
|
1340
|
+
* {@link enrollValidationKey} / {@link removeValidationKey} it needs no owner signing
|
|
1341
|
+
* key — only a started node.
|
|
1342
|
+
*/
|
|
1343
|
+
listValidationKeys(): Promise<string[]>;
|
|
1344
|
+
/**
|
|
1345
|
+
* Resolve the owner keypair every owner-signed control write needs — {@link publishStrand},
|
|
1346
|
+
* {@link unpublishStrand}, {@link publishFormationInvite}, {@link enrollValidationKey},
|
|
1347
|
+
* {@link removeValidationKey}
|
|
1348
|
+
* — failing loudly in one two-part shape (not started / no signing key, naming owner
|
|
1349
|
+
* genesis as the fix). Narrows `controlDatabase` for the caller: a non-null return means
|
|
1350
|
+
* `this.controlDatabase` is non-null too.
|
|
1351
|
+
*
|
|
1352
|
+
* @param action - Infinitive phrase naming the attempted write, e.g. `'enroll a
|
|
1353
|
+
* validation key'`; interpolated into both messages.
|
|
1354
|
+
*/
|
|
1355
|
+
private requireOwnerSigningKey;
|
|
732
1356
|
/**
|
|
733
1357
|
* Shared strand launch path for both the explicit (`addStrand`) and the
|
|
734
1358
|
* control-discovered (`handleStrandAdded`) entry points. Resolves the cohort
|
|
735
1359
|
* seed, selects the mode, starts the strand, and registers it with the
|
|
736
1360
|
* hibernation manager before emitting `strand:started`.
|
|
1361
|
+
*
|
|
1362
|
+
* Idempotent no-op when the strand manager already tracks `strand.Id` — the
|
|
1363
|
+
* ordinary founding sequence is `addStrand` (starts it locally) followed by
|
|
1364
|
+
* `publishStrand` (makes it visible), so this node's own `StrandWatcher`
|
|
1365
|
+
* rediscovers a row it already started and calls this again via
|
|
1366
|
+
* `handleStrandAdded`. Without the guard that re-entry would resolve a fresh
|
|
1367
|
+
* (already-connected) cohort seed and re-emit `strand:started` for an
|
|
1368
|
+
* instance that never stopped. The guard runs before the cohort-seed RPC
|
|
1369
|
+
* fan-out, so a rediscovery costs nothing beyond the map lookup.
|
|
737
1370
|
*/
|
|
738
1371
|
private launchStrand;
|
|
739
1372
|
/**
|
|
@@ -749,8 +1382,61 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
749
1382
|
* empty (`[]`) — mode still follows membership, and the empty seed self-heals
|
|
750
1383
|
* on the next resume / check-in pass. Returns an empty seed when the control DB
|
|
751
1384
|
* or node is absent (not yet started / torn down).
|
|
1385
|
+
*
|
|
1386
|
+
* When `delegatePeerId` is given (a strand launch/resume is imminent), the
|
|
1387
|
+
* pass doubles as the delegate ANNOUNCEMENT: our circuit relays are merged
|
|
1388
|
+
* into the RPC targets and every request carries the delegate peerId, so each
|
|
1389
|
+
* receiver records an admission grant BEFORE `libp2p.start()` (re-)dials the
|
|
1390
|
+
* relay reservation — the gate denial there is fatal, not degraded. A
|
|
1391
|
+
* party-member relay answers the RPC (its control node admits us as a
|
|
1392
|
+
* member); a dedicated ops/ relay does not speak the protocol and the
|
|
1393
|
+
* per-peer failure folds to `[]` — harmless, it has no membership gate and
|
|
1394
|
+
* needs no grant. The relay's direct addr rides along as the dial fallback
|
|
1395
|
+
* for a relay we are not yet connected to.
|
|
752
1396
|
*/
|
|
753
1397
|
private resolveCohortSeed;
|
|
1398
|
+
/**
|
|
1399
|
+
* The circuit relays this node's strand nodes would reserve through: the
|
|
1400
|
+
* union of the RESOLVED listen addrs' circuits — a hand-written
|
|
1401
|
+
* `network.listenAddrs` circuit entry or a `network.relayAddrs` entry, which
|
|
1402
|
+
* the strand node inherits verbatim either way — and the control node's own
|
|
1403
|
+
* live `/p2p-circuit` multiaddrs (a reservation this node discovered rather
|
|
1404
|
+
* than configured).
|
|
1405
|
+
*
|
|
1406
|
+
* NOTE: a relay the STRAND node discovers on its own (autorelay — in neither
|
|
1407
|
+
* source) gets no announcement, and a membership-gated one will deny it; fine
|
|
1408
|
+
* now, every realistic topology feeds one of the two sources.
|
|
1409
|
+
*/
|
|
1410
|
+
private circuitRelayTargets;
|
|
1411
|
+
/**
|
|
1412
|
+
* Record the announce timestamps {@link refreshDelegateGrants} throttles on,
|
|
1413
|
+
* for every relay a delegate-carrying announce pass dialed.
|
|
1414
|
+
*
|
|
1415
|
+
* Recorded OPTIMISTICALLY at announce time: `collectStrandAddrs` folds
|
|
1416
|
+
* per-peer failure to `[]` and reports no per-peer success, and threading
|
|
1417
|
+
* success out would change its API for little gain. A failed INITIAL announce
|
|
1418
|
+
* is fatal-at-start anyway (the relay denies the reservation,
|
|
1419
|
+
* `libp2p.start()` throws, and wake/check-in re-resume retries with a fresh
|
|
1420
|
+
* announce); a failed REFRESH retries within `DELEGATE_GRANT_TTL_MS / 2`
|
|
1421
|
+
* (15 min), still inside the 30 min TTL.
|
|
1422
|
+
*/
|
|
1423
|
+
private recordDelegateAnnounces;
|
|
1424
|
+
/**
|
|
1425
|
+
* Re-announce every running strand's delegate peerId to this node's circuit
|
|
1426
|
+
* relays, throttled to once per `DELEGATE_GRANT_TTL_MS / 2` per
|
|
1427
|
+
* (relay, strand). A grant must outlive the reservation: a dropped relay
|
|
1428
|
+
* connection makes the strand's circuit-relay transport re-dial and face the
|
|
1429
|
+
* connection gate again, so the relay must still hold a live grant then.
|
|
1430
|
+
* RELAY targets only — siblings get their announce on every launch/resume
|
|
1431
|
+
* seed pass, and a grant only matters where a reservation can be re-dialed.
|
|
1432
|
+
*
|
|
1433
|
+
* Strands announce CONCURRENTLY: this runs ahead of the reconcile pass's
|
|
1434
|
+
* sibling enumeration, and one unreachable relay costs a dial timeout per
|
|
1435
|
+
* target, which must not stack up per strand.
|
|
1436
|
+
*/
|
|
1437
|
+
private refreshDelegateGrants;
|
|
1438
|
+
/** One strand's share of {@link refreshDelegateGrants}: announce to the relays whose grant is due. */
|
|
1439
|
+
private announceDelegateToDueRelays;
|
|
754
1440
|
/**
|
|
755
1441
|
* The local strand instance's dialable strand-network multiaddrs for the
|
|
756
1442
|
* strand-addr RPC, ordered signaling-first (reusing the control node's
|
|
@@ -762,9 +1448,26 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
762
1448
|
*/
|
|
763
1449
|
private getStrandMultiaddrs;
|
|
764
1450
|
/**
|
|
765
|
-
*
|
|
1451
|
+
* Stop a strand on THIS node only: untrack it from hibernation, drop its sApp config,
|
|
1452
|
+
* stop the local instance, and emit `strand:stopped`. The shared `Strand` row is left
|
|
1453
|
+
* intact, so on the next node restart (or watcher poll) the strand is rediscovered and
|
|
1454
|
+
* surfaces as `strand:discovered` again. Party-wide removal is {@link unpublishStrand}.
|
|
1455
|
+
*/
|
|
1456
|
+
stopStrand(strandId: string): Promise<void>;
|
|
1457
|
+
/**
|
|
1458
|
+
* Local teardown for one strand, shared by the caller-driven {@link stopStrand} and the
|
|
1459
|
+
* watcher-driven `handleStrandRemoved`: untrack hibernation, drop the sApp config, stop
|
|
1460
|
+
* the instance, emit `strand:stopped`. Touches no control-plane row — which side of the
|
|
1461
|
+
* removal the node is on is the caller's concern, not this method's.
|
|
1462
|
+
*
|
|
1463
|
+
* The stop + emit are skipped when the strand manager holds no instance for `strandId` —
|
|
1464
|
+
* e.g. a party owner that published a strand's row but never ran it locally, or an
|
|
1465
|
+
* explicit {@link stopStrand} for an id this node never started. `hibernationManager`
|
|
1466
|
+
* untrack and the `sAppConfigs` delete stay unconditional (both are no-ops when there is
|
|
1467
|
+
* nothing to remove), so a launch that failed before an instance was ever tracked still
|
|
1468
|
+
* gets its stray sApp config cleared.
|
|
766
1469
|
*/
|
|
767
|
-
|
|
1470
|
+
private detachStrand;
|
|
768
1471
|
/**
|
|
769
1472
|
* Record activity on a strand (resets hibernation timer).
|
|
770
1473
|
*
|
|
@@ -841,10 +1544,58 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
841
1544
|
* Get the control network node (for advanced use)
|
|
842
1545
|
*/
|
|
843
1546
|
getControlNode(): Libp2p | null;
|
|
1547
|
+
/**
|
|
1548
|
+
* Start keeping a relay reservation: dial the given relay(s) from the control
|
|
1549
|
+
* node, ask the first one that answers for a reservation slot, wait until the
|
|
1550
|
+
* resulting `/p2p-circuit` address makes this node dialable — and keep a
|
|
1551
|
+
* supervisor running that re-drives whenever the reservation is later lost.
|
|
1552
|
+
*
|
|
1553
|
+
* For nodes that listen on the bare `/p2p-circuit` SEARCH addr (a browser tab)
|
|
1554
|
+
* rather than a configured `network.relayAddrs` circuit listener — see
|
|
1555
|
+
* `relay-reservation.ts` for why the two are alternatives and why configuring
|
|
1556
|
+
* both is a regression. Opt-in: nothing calls this for the CLI/host nodes.
|
|
1557
|
+
*
|
|
1558
|
+
* Resolves once the FIRST attempt has settled, exactly as it did when it drove
|
|
1559
|
+
* once; the retries continue in the background until {@link stop} or the next
|
|
1560
|
+
* `reserveRelays` call. Fail-soft — never throws. An unreachable relay, a
|
|
1561
|
+
* missing control node, or a timeout all resolve to a non-`reserved` status, so
|
|
1562
|
+
* a caller can await this during startup without a dead relay aborting the node.
|
|
1563
|
+
*
|
|
1564
|
+
* Passing an empty list stops the supervisor and resets the posture to `none`
|
|
1565
|
+
* without dialing or waiting.
|
|
1566
|
+
*/
|
|
1567
|
+
reserveRelays(addrs: string[], opts?: RelayReservationSupervisorOptions): Promise<RelayReservationState>;
|
|
1568
|
+
/**
|
|
1569
|
+
* The node's CURRENT relay-reservation posture, recomputed from the control
|
|
1570
|
+
* node's live multiaddrs on every call.
|
|
1571
|
+
*
|
|
1572
|
+
* Deliberately not memoised: a reservation can be lost after
|
|
1573
|
+
* {@link reserveRelays} succeeds (the relay restarts, the connection drops) and
|
|
1574
|
+
* a cached `reserved` would let a caller mint invitations carrying circuit
|
|
1575
|
+
* addresses that no longer route.
|
|
1576
|
+
*
|
|
1577
|
+
* A lost reservation now recovers on its own — the supervisor {@link reserveRelays}
|
|
1578
|
+
* starts re-drives on a backoff, reporting `retrying` meanwhile. `error` here
|
|
1579
|
+
* means nothing is going to try again: no supervisor, or no control node.
|
|
1580
|
+
*/
|
|
1581
|
+
getRelayReservationState(): RelayReservationState;
|
|
844
1582
|
/**
|
|
845
1583
|
* Get the control database (for advanced queries)
|
|
846
1584
|
*/
|
|
847
1585
|
getControlDatabase(): ControlDatabase | null;
|
|
1586
|
+
/**
|
|
1587
|
+
* Open control-network connections right now. A lower-bound proxy for replication
|
|
1588
|
+
* reach: 0 connections ⇒ a control write commits local-only. The private
|
|
1589
|
+
* `committedAlone` write-while-alone seam is defined in terms of this.
|
|
1590
|
+
*
|
|
1591
|
+
* It approximates the precise signal (the block's cluster size), and a caller that
|
|
1592
|
+
* samples it AFTER a write samples a slightly wider window than `committedAlone`
|
|
1593
|
+
* does inside the write itself. It exists so an embedder can warn an owner before a
|
|
1594
|
+
* control write that this machine currently sees none of its siblings, and report
|
|
1595
|
+
* after one that the write may not have travelled — a warning shown unconditionally
|
|
1596
|
+
* is a warning people learn to ignore.
|
|
1597
|
+
*/
|
|
1598
|
+
getControlConnectionCount(): number;
|
|
848
1599
|
/**
|
|
849
1600
|
* Force a poll of the strand watcher (for testing)
|
|
850
1601
|
*/
|
|
@@ -872,7 +1623,10 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
872
1623
|
*/
|
|
873
1624
|
private resolveInviteAddresses;
|
|
874
1625
|
/**
|
|
875
|
-
* Enumerate the cadre's `CadrePeer` membership
|
|
1626
|
+
* Enumerate the cadre's `CadrePeer` membership — the ADDRESSABLE surface (see
|
|
1627
|
+
* {@link isMember}). Rows whose stamp is retired in `CadreControl.Revocation`
|
|
1628
|
+
* never reach here: {@link ControlDatabase.queryCadrePeers} excludes them, so a
|
|
1629
|
+
* revoked peer is not addressable either.
|
|
876
1630
|
*/
|
|
877
1631
|
listMembers(): Promise<Array<{
|
|
878
1632
|
peerId: string;
|
|
@@ -883,30 +1637,72 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
883
1637
|
*
|
|
884
1638
|
* This is the ADDRESSABLE surface ("do I have a dialable address record for this
|
|
885
1639
|
* peer") — it includes this node's own self-published row. Address resolution and
|
|
886
|
-
* push fan-out use this.
|
|
1640
|
+
* push fan-out use this. A peer whose stamp is retired in `CadreControl.Revocation`
|
|
1641
|
+
* is excluded here too (the filter lives in
|
|
1642
|
+
* {@link ControlDatabase.queryCadrePeers}): revocation removes a peer from the
|
|
1643
|
+
* addressable surface, not only the trust-facing one, so a revoked peer is no
|
|
1644
|
+
* longer dialed, RPC'd, or handed out as an address. The trust-facing gate is
|
|
1645
|
+
* {@link isAuthorizedMember}.
|
|
887
1646
|
*/
|
|
888
1647
|
isMember(peerId: string): Promise<boolean>;
|
|
889
1648
|
/**
|
|
890
1649
|
* Enumerate the party's AUTHORIZED members — the trust-facing set, distinct from
|
|
891
|
-
* the addressable set ({@link listMembers}).
|
|
892
|
-
*
|
|
893
|
-
*
|
|
1650
|
+
* the addressable set ({@link listMembers}). A peer is authorized iff ALL hold:
|
|
1651
|
+
*
|
|
1652
|
+
* 1. it is not this node itself — a node publishes its own `CadrePeer` address
|
|
1653
|
+
* row so its dialable address rides in seeds, but "self" is not a peer this
|
|
1654
|
+
* node authorized;
|
|
1655
|
+
* 2. its `CadrePeer` row carries a complete voucher (`StampId`, `VouchOwner`,
|
|
1656
|
+
* `VouchSig` all non-null);
|
|
1657
|
+
* 3. `VouchOwner` is in the NODE-LOCAL trusted-owner anchor
|
|
1658
|
+
* ({@link getTrustedOwnerStore}) — never the replicated `OwnerKey` table,
|
|
1659
|
+
* which any stranger can genesis-pollute; and
|
|
1660
|
+
* 4. `VouchSig` verifies as that owner's signature over the row's voucher
|
|
1661
|
+
* digest ({@link verifyCadrePeerVoucher}), so the anchored owner really
|
|
1662
|
+
* vouched THIS peer id under THIS row's nonce; and
|
|
1663
|
+
* 5. the row's `StampId` is NOT retired in `CadreControl.Revocation` — enforced
|
|
1664
|
+
* upstream in {@link ControlDatabase.queryCadrePeers}, which drops retired rows
|
|
1665
|
+
* before ANY reader (this predicate included) sees them, so a row resurrected
|
|
1666
|
+
* by replaying the captured admission approval on a node that had not yet
|
|
1667
|
+
* converged on the tombstone (the write-time `NotRevoked` CHECK only sees
|
|
1668
|
+
* local rows) is still inert to every reader that has the tombstone.
|
|
1669
|
+
*
|
|
1670
|
+
* Fail-closed at every step: a missing anchor (pre-start), an empty anchor (a
|
|
1671
|
+
* not-yet-enrolled node authorizes no one), a null/partial voucher, an
|
|
1672
|
+
* unanchored `VouchOwner`, or a bad signature all yield "not authorized" —
|
|
1673
|
+
* having an address row is NOT membership. The control-network wake and
|
|
894
1674
|
* strand-address gates consult this set, NOT the addressable one.
|
|
895
1675
|
*
|
|
896
|
-
* NOTE:
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
1676
|
+
* NOTE (rotation): if a party owner rotates keys and only the NEW key is pinned
|
|
1677
|
+
* in the anchor, rows the OLD key vouched fail check 3 until re-vouched — a
|
|
1678
|
+
* legit member goes un-authorized on readers that only pin the new key. Full
|
|
1679
|
+
* rotation handling (re-vouch on rotate) is the
|
|
1680
|
+
* `flip-strand-membership-rotation-known-gap` work, not this predicate's.
|
|
901
1681
|
*/
|
|
902
1682
|
listAuthorizedMembers(): Promise<Array<{
|
|
903
1683
|
peerId: string;
|
|
904
1684
|
multiaddr: string | null;
|
|
905
1685
|
}>>;
|
|
1686
|
+
/**
|
|
1687
|
+
* Checks 2–4 of the authorized-membership predicate (see
|
|
1688
|
+
* {@link listAuthorizedMembers}) for one `CadrePeer` row: complete voucher,
|
|
1689
|
+
* `VouchOwner` in the node-local anchor, signature valid over the row's
|
|
1690
|
+
* (PeerId, StampId) voucher digest.
|
|
1691
|
+
*
|
|
1692
|
+
* NOTE: verifies the ed25519 signature on every call (no memo of already-
|
|
1693
|
+
* verified (peerId, stampId, vouchSig) triples). Cadres are a handful of
|
|
1694
|
+
* devices and the gates run per inbound request, so this is cheap today; if
|
|
1695
|
+
* membership or gate traffic ever grows, cache verified triples keyed on the
|
|
1696
|
+
* row's `StampId` (which rotates with every re-vouch).
|
|
1697
|
+
*/
|
|
1698
|
+
private hasAnchoredVoucher;
|
|
906
1699
|
/**
|
|
907
1700
|
* Probe whether a given peer is an AUTHORIZED party member (see
|
|
908
1701
|
* {@link listAuthorizedMembers}) — the gate the control-network wake and
|
|
909
|
-
* strand-address responders consult, NOT {@link isMember} (the addressable
|
|
1702
|
+
* strand-address responders consult, NOT {@link isMember} (the addressable
|
|
1703
|
+
* surface). Deliberately scans the full membership (one `CadrePeer` query)
|
|
1704
|
+
* rather than adding a single-row read path: cadres are small, and one code
|
|
1705
|
+
* path keeps the predicate impossible to drift from the list.
|
|
910
1706
|
*/
|
|
911
1707
|
isAuthorizedMember(peerId: string): Promise<boolean>;
|
|
912
1708
|
/**
|
|
@@ -935,6 +1731,68 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
935
1731
|
* Get the seed bootstrap service (for advanced use)
|
|
936
1732
|
*/
|
|
937
1733
|
getSeedBootstrapService(): SeedBootstrapService | null;
|
|
1734
|
+
/**
|
|
1735
|
+
* The event callbacks every {@link SeedBootstrapService} this node owns is
|
|
1736
|
+
* wired with — shared by the owner-capable service ({@link initializeSeedBootstrap})
|
|
1737
|
+
* and the listener-only one ({@link enableSeedListener}) so the two cannot drift.
|
|
1738
|
+
*
|
|
1739
|
+
* A seed applied by the INBOUND protocol handler writes no `CadrePeer` row of
|
|
1740
|
+
* its own (it merges the libp2p peer store and dials owners), so the automatic
|
|
1741
|
+
* write-driven refresh never fires for it — yet applying it can ANCHOR a new
|
|
1742
|
+
* owner key, which flips rows already present from unauthorized to authorized.
|
|
1743
|
+
* Hence the explicit refresh here; without it a peer the freshly anchored owner
|
|
1744
|
+
* vouched for stays denied until the next timed cohort reconcile.
|
|
1745
|
+
*/
|
|
1746
|
+
private seedEventCallbacks;
|
|
1747
|
+
/**
|
|
1748
|
+
* Re-materialize the authorized-peer snapshot that the fail-closed per-stream
|
|
1749
|
+
* control-DB gate ({@link authorizeInboundControlStream}) judges against.
|
|
1750
|
+
*
|
|
1751
|
+
* NO `CadrePeer` writer needs to call this: the control database notifies it
|
|
1752
|
+
* after every committed member-row write (`ControlDatabase.mutateCadrePeer`,
|
|
1753
|
+
* wired in {@link start}), which is exactly what makes the refresh automatic
|
|
1754
|
+
* rather than a caller obligation. It stays public for the changes that write
|
|
1755
|
+
* NO row locally and so raise no notification:
|
|
1756
|
+
*
|
|
1757
|
+
* - a membership row that arrived by REPLICATION (otherwise picked up on the
|
|
1758
|
+
* next timed cohort reconcile — bounded staleness by design), and
|
|
1759
|
+
* - a newly anchored trusted owner key ({@link applySeed}), which flips rows
|
|
1760
|
+
* ALREADY present from unauthorized to authorized without touching them.
|
|
1761
|
+
*
|
|
1762
|
+
* Coalescing: marks the snapshot stale and resolves once a refresh that began
|
|
1763
|
+
* after this call has completed, so a caller that awaits it always observes
|
|
1764
|
+
* its own change. Concurrent callers share one read. Idempotent and
|
|
1765
|
+
* best-effort (a failed read keeps the previous snapshot); never rejects.
|
|
1766
|
+
*/
|
|
1767
|
+
refreshMembershipGate(reason?: string): Promise<void>;
|
|
1768
|
+
/**
|
|
1769
|
+
* The single in-flight refresh loop behind {@link refreshMembershipGate}: read
|
|
1770
|
+
* until the stale flag stays clear, then release the slot.
|
|
1771
|
+
*
|
|
1772
|
+
* NOTE: correct only because {@link membershipGateDirty} is guaranteed TRUE at
|
|
1773
|
+
* entry — its sole caller sets it synchronously, with no await in between. Were
|
|
1774
|
+
* this ever invoked with the flag already clear, the body would never suspend,
|
|
1775
|
+
* so the `finally` would null the slot BEFORE the caller's `??=` filled it, and
|
|
1776
|
+
* the slot would be left holding an already-settled drain that every later
|
|
1777
|
+
* refresh reuses — an unbreakable await/re-check spin. If a second caller is
|
|
1778
|
+
* ever added, have it mark the flag first (or assert it here).
|
|
1779
|
+
*/
|
|
1780
|
+
private drainMembershipGate;
|
|
1781
|
+
/**
|
|
1782
|
+
* Collapse a burst of `CadrePeer` writes into ONE gate refresh at scope exit.
|
|
1783
|
+
*
|
|
1784
|
+
* For loops that re-touch many rows (the write-while-alone drains), where a
|
|
1785
|
+
* per-row refresh would mean one full membership read per row for a snapshot
|
|
1786
|
+
* that only has to be correct once the loop settles.
|
|
1787
|
+
*
|
|
1788
|
+
* The depth counter is instance-level, so it also suppresses a genuinely
|
|
1789
|
+
* CONCURRENT external write's refresh for the life of the scope: that writer's
|
|
1790
|
+
* promise resolves before its peer is admitted, and admission lands at scope
|
|
1791
|
+
* exit instead. Acceptable — scope exit is milliseconds away (bounded by the
|
|
1792
|
+
* drain), versus the ~15 s reconcile interval that was the alternative before
|
|
1793
|
+
* any of this existed — but it is the reason these scopes stay short and rare.
|
|
1794
|
+
*/
|
|
1795
|
+
private deferMembershipGateRefresh;
|
|
938
1796
|
/**
|
|
939
1797
|
* Authorize a new peer to join the cadre.
|
|
940
1798
|
* Signs the peer ID with the owner key and inserts into CadrePeer table.
|
|
@@ -967,6 +1825,13 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
967
1825
|
applySeed(seed: ControlNetworkSeed, options?: {
|
|
968
1826
|
trustPolicy?: SeedTrustPolicy;
|
|
969
1827
|
}): Promise<ApplySeedResult>;
|
|
1828
|
+
/**
|
|
1829
|
+
* Post-process an {@link applySeed} outcome: retain the seed's owner peers as
|
|
1830
|
+
* cold-start bootstrap targets, and surface a seed that was accepted but whose
|
|
1831
|
+
* every owner dial failed — the node is now seeded yet unconnected, and the
|
|
1832
|
+
* cold-start reconcile branch is what gets it out of that state.
|
|
1833
|
+
*/
|
|
1834
|
+
private noteAppliedSeed;
|
|
970
1835
|
/**
|
|
971
1836
|
* Deliver a seed directly to a peer via the /sereus/seed/1.0.0 protocol.
|
|
972
1837
|
*/
|