@serfab/cadre-core 0.9.0 → 0.11.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 +255 -253
- 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 +1121 -81
- package/dist/cadre-node.js +2276 -262
- 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-cohort.d.ts +26 -0
- package/dist/control-cohort.js +26 -0
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +959 -54
- package/dist/control-database.js +1421 -148
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +90 -10
- package/dist/control-formation-recorder.js +130 -14
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +651 -72
- 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 +161 -0
- package/dist/control-write-retry.js +412 -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 +17 -7
- package/dist/index.js +40 -6
- 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-addr-book.d.ts +92 -0
- package/dist/peer-addr-book.js +220 -0
- package/dist/peer-addr-book.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 +228 -56
- package/dist/seed-bootstrap.js +340 -198
- 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-cohort.d.ts +4 -37
- package/dist/strand-cohort.js +4 -15
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +11 -16
- package/dist/strand-database.js +19 -7
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-formation-manager.d.ts +36 -7
- package/dist/strand-formation-manager.js +115 -15
- 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 +88 -19
- package/dist/strand-instance-manager.js +178 -34
- 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 +484 -88
- package/dist/strand-membership-writer.js +751 -128
- 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 +307 -44
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -1
- package/package.json +20 -8
- package/src/arachnode-stub.ts +159 -159
- package/src/bootstrap-peer-store-file.ts +44 -0
- package/src/bootstrap-peer-store.ts +172 -0
- package/src/cadre-node.ts +5158 -2981
- package/src/canonical-json.ts +30 -30
- package/src/control-authorization.ts +114 -0
- package/src/control-cohort.ts +130 -103
- package/src/control-database.ts +2406 -948
- package/src/control-formation-recorder.ts +305 -151
- package/src/control-schema.ts +651 -72
- package/src/control-stream.ts +205 -128
- package/src/control-write-retry.ts +483 -0
- package/src/delegate-admission.ts +302 -0
- package/src/device-token.ts +94 -89
- package/src/diagnostics/connection-path.ts +277 -277
- package/src/diagnostics/webrtc-turn-tracker.ts +222 -222
- package/src/ed25519-key.ts +66 -1
- package/src/enrollment.ts +199 -199
- package/src/file-durable-slot.ts +61 -0
- package/src/formation-approval.ts +604 -0
- package/src/fs-atomic.ts +107 -0
- package/src/hibernation-manager.ts +437 -437
- package/src/identity-key.ts +109 -0
- package/src/index.ts +137 -5
- package/src/key-store-file.ts +132 -203
- package/src/key-store.ts +99 -99
- package/src/membership-connection-gater.ts +204 -0
- package/src/node-local-snapshot.ts +260 -0
- package/src/peer-addr-book.ts +250 -0
- package/src/peer-authorization.ts +259 -81
- package/src/peer-record.ts +164 -146
- package/src/push-credentials.ts +84 -84
- package/src/push-node.ts +64 -64
- package/src/push-notifier-apns.ts +247 -247
- package/src/push-notifier-fcm.ts +220 -220
- package/src/push-notifier-shared.ts +28 -28
- package/src/push-notifier.ts +52 -52
- package/src/relay-addrs.ts +111 -0
- package/src/relay-reservation.ts +823 -0
- package/src/seed-bootstrap.ts +409 -209
- package/src/seed-trust-policy.ts +140 -107
- package/src/strand-addr-protocol.ts +441 -410
- package/src/strand-backfill.ts +366 -0
- package/src/strand-cohort.ts +38 -77
- package/src/strand-database.ts +202 -204
- package/src/strand-formation-manager.ts +491 -368
- package/src/strand-formation-protocol.ts +692 -391
- package/src/strand-instance-manager.ts +201 -46
- package/src/strand-member-key.ts +39 -39
- package/src/strand-member-registry.ts +182 -177
- package/src/strand-membership-writer.ts +938 -146
- package/src/strand-solicitation.ts +472 -407
- package/src/strand-transport-key.ts +58 -0
- package/src/strand-wake-payload.ts +32 -32
- package/src/strand-wake-protocol.ts +314 -331
- package/src/strand-watcher.ts +314 -251
- package/src/trusted-owner-store-file.ts +41 -0
- package/src/trusted-owner-store.ts +171 -0
- package/src/types.ts +1309 -1032
package/dist/cadre-node.d.ts
CHANGED
|
@@ -2,14 +2,29 @@ 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';
|
|
16
|
+
/**
|
|
17
|
+
* How often a running strand re-resolves its siblings' strand-network addresses
|
|
18
|
+
* and re-merges them into its own libp2p address book
|
|
19
|
+
* ({@link CadreNode.refreshStrandPeerAddrs}). Overridable per node via
|
|
20
|
+
* `network.controlCohort.strandAddrRefreshMs`.
|
|
21
|
+
*
|
|
22
|
+
* Ten minutes sits comfortably inside the peerStore's one-hour address expiry
|
|
23
|
+
* (`MAX_ADDRESS_AGE`, see `peer-addr-book.ts`) with headroom for several missed
|
|
24
|
+
* passes, and far above the 15 s reconcile cadence the refresh rides on, so the
|
|
25
|
+
* strand-addr RPC fan-out stays cheap.
|
|
26
|
+
*/
|
|
27
|
+
export declare const STRAND_PEER_ADDR_REFRESH_MS: number;
|
|
13
28
|
type EventHandler<T> = (data: T) => void;
|
|
14
29
|
/**
|
|
15
30
|
* CadreNode is the main entry point for a cadre member.
|
|
@@ -31,6 +46,15 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
31
46
|
* launch) reads this resolved field, never `config.privateKey` directly.
|
|
32
47
|
*/
|
|
33
48
|
private identityKey;
|
|
49
|
+
/**
|
|
50
|
+
* Node-local, NON-replicated trusted-owner anchor (see
|
|
51
|
+
* `trusted-owner-store.ts`). Constructed (or adopted from
|
|
52
|
+
* `config.trustedOwners.store`) by {@link initializeTrustedOwnerStore} during
|
|
53
|
+
* {@link start}; deliberately NOT cleared by {@link cleanup}, so an in-memory
|
|
54
|
+
* anchor survives a stop()→start() cycle of the same node instance. Never
|
|
55
|
+
* sourced from replicated control state.
|
|
56
|
+
*/
|
|
57
|
+
private trustedOwnerStore;
|
|
34
58
|
private controlNode;
|
|
35
59
|
private controlDatabase;
|
|
36
60
|
private strandWatcher;
|
|
@@ -56,6 +80,14 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
56
80
|
private pushFanoutService;
|
|
57
81
|
/** Backing field for the {@link running} / {@link isRunning} getters. */
|
|
58
82
|
private _running;
|
|
83
|
+
/**
|
|
84
|
+
* The control database's own raw storage (cache-wrapped), resolved once per
|
|
85
|
+
* `start()` and released in {@link cleanup}. Owning it is what keeps
|
|
86
|
+
* `config.storage.provider('control')` a once-per-runtime call: a `stop()` then
|
|
87
|
+
* `start()` cycle on this object re-resolves against a live cache, and never
|
|
88
|
+
* orphans the previous wrapper's registration in the shared cache pool.
|
|
89
|
+
*/
|
|
90
|
+
private controlStorage;
|
|
59
91
|
/**
|
|
60
92
|
* In-flight {@link serviceWake} operations keyed by strandId. Coalesces
|
|
61
93
|
* concurrent on-demand wakes for the same strand into one runtime build + one
|
|
@@ -70,6 +102,25 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
70
102
|
*/
|
|
71
103
|
private windowWaiters;
|
|
72
104
|
private eventHandlers;
|
|
105
|
+
/**
|
|
106
|
+
* Relay multiaddrs the caller asked {@link reserveRelays} to reserve through.
|
|
107
|
+
* Empty (the default) means nobody asked, so {@link getRelayReservationState}
|
|
108
|
+
* reports `none` — this whole surface is opt-in and dormant for the CLI/host
|
|
109
|
+
* nodes, which use the configured `network.relayAddrs` route instead.
|
|
110
|
+
*/
|
|
111
|
+
private relayReserveAddrs;
|
|
112
|
+
/**
|
|
113
|
+
* The running retry loop for {@link relayReserveAddrs}, or `null` when nobody
|
|
114
|
+
* asked for a reservation / there was no control node to supervise. It owns the
|
|
115
|
+
* in-flight flag, the last failure and the next-attempt time; this class only
|
|
116
|
+
* starts it, stops it and reads it.
|
|
117
|
+
*/
|
|
118
|
+
private relayReserveSupervisor;
|
|
119
|
+
/**
|
|
120
|
+
* Reason {@link reserveRelays} produced no reservation when there is no
|
|
121
|
+
* supervisor to hold one — i.e. the pre-start `control node unavailable` case.
|
|
122
|
+
*/
|
|
123
|
+
private relayReserveError;
|
|
73
124
|
/** Map of strandId -> sAppConfig for sAppId filtering and management */
|
|
74
125
|
private sAppConfigs;
|
|
75
126
|
/**
|
|
@@ -79,6 +130,116 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
79
130
|
* control-network node never needs to dial back to the manager.
|
|
80
131
|
*/
|
|
81
132
|
private latestInviteAddresses;
|
|
133
|
+
/**
|
|
134
|
+
* Epoch ms until which the control-network inbound gate admits
|
|
135
|
+
* not-yet-authorized peers (see {@link admitInboundControlConnection} /
|
|
136
|
+
* {@link openEnrollmentWindow}). 0 = no window open. Opened automatically by
|
|
137
|
+
* {@link createInvite}; deliberately NOT reset on stop() — a stop/start cycle
|
|
138
|
+
* inside an outstanding invite's validity must not strand the invitee.
|
|
139
|
+
*
|
|
140
|
+
* NOTE: in-memory only, so a PROCESS restart mid-invite closes the door until
|
|
141
|
+
* the owner re-mints or the invitee is authorized. Fine while invites are
|
|
142
|
+
* short-lived and redeemed promptly; if long-lived invites or restart-prone
|
|
143
|
+
* hosts become normal, persist the window (or derive it from the issued-invite
|
|
144
|
+
* records `SeedBootstrapService` already keeps).
|
|
145
|
+
*/
|
|
146
|
+
private enrollmentWindowUntil;
|
|
147
|
+
/**
|
|
148
|
+
* Lazily-parsed PeerIds of `controlNetwork.bootstrapNodes` (see
|
|
149
|
+
* {@link getBootstrapPeerIds}) — infrastructure the inbound gate always admits.
|
|
150
|
+
*/
|
|
151
|
+
private bootstrapPeerIds;
|
|
152
|
+
/**
|
|
153
|
+
* Materialized in-memory snapshot of the AUTHORIZED member peer ids (the
|
|
154
|
+
* {@link listAuthorizedMembers} result), consulted by the per-stream
|
|
155
|
+
* control-DB gate ({@link authorizeInboundControlStream}). That predicate
|
|
156
|
+
* runs on EVERY inbound Optimystic control-DB stream and must never await a
|
|
157
|
+
* control-DB read: those reads pull blocks over the very protocols the
|
|
158
|
+
* predicate gates, which closes a circular wait that ends in mutual denial
|
|
159
|
+
* (the upstream gate is fail-closed on timeout, unlike the fail-open
|
|
160
|
+
* connection gater). So the set is refreshed OUT OF BAND instead — after
|
|
161
|
+
* start, on every {@link reconcileControlCohort} pass (15s cadence), and
|
|
162
|
+
* immediately after every LOCAL membership mutation so a just-vouched peer
|
|
163
|
+
* is admitted without waiting for the timer. That last refresh is AUTOMATIC:
|
|
164
|
+
* the control database notifies {@link refreshMembershipGate} after every
|
|
165
|
+
* committed `CadrePeer` write (see `ControlDatabase.mutateCadrePeer`), so no
|
|
166
|
+
* writer has to remember. A change that arrives by REPLICATION is picked up
|
|
167
|
+
* on the next timed refresh — bounded staleness, acceptable because this gate
|
|
168
|
+
* is defense in depth: rows an unadmitted peer manages to write are still
|
|
169
|
+
* disbelieved at read time by the voucher-anchored predicate.
|
|
170
|
+
*/
|
|
171
|
+
private authorizedControlPeers;
|
|
172
|
+
/**
|
|
173
|
+
* Coalescing state for {@link refreshMembershipGate}: a pending "the snapshot
|
|
174
|
+
* is stale" flag, the single in-flight drain that consumes it, and the depth
|
|
175
|
+
* of open {@link deferMembershipGateRefresh} scopes (a burst of writes inside
|
|
176
|
+
* one scope collapses to a single refresh at scope exit).
|
|
177
|
+
*/
|
|
178
|
+
private membershipGateDirty;
|
|
179
|
+
private membershipGateDrain;
|
|
180
|
+
private membershipGateDeferDepth;
|
|
181
|
+
/**
|
|
182
|
+
* In-memory delegate admission grants (see `delegate-admission.ts`): the
|
|
183
|
+
* strand-node transport peerIds this party's members have announced over the
|
|
184
|
+
* strand-addr RPC, admitted at the CONNECTION level so a member's NAT'd
|
|
185
|
+
* strand node can hold a circuit-relay reservation on this node. Consulted
|
|
186
|
+
* ONLY by {@link admitInboundControlConnection}; the fail-closed per-stream
|
|
187
|
+
* gate ({@link authorizeInboundControlStream}) never honors it.
|
|
188
|
+
*/
|
|
189
|
+
private readonly delegateAdmission;
|
|
190
|
+
/**
|
|
191
|
+
* When this node last ANNOUNCED a delegate, keyed `<targetPeerId>\n<strandId>`
|
|
192
|
+
* (the peer announced TO, not the delegate). Throttles
|
|
193
|
+
* {@link refreshDelegateGrants} to once per `DELEGATE_GRANT_TTL_MS / 2` per
|
|
194
|
+
* (relay, strand) so the 15 s reconcile tick never becomes per-tick RPC
|
|
195
|
+
* chatter; the launch/resume announce passes record here too. Keys whose
|
|
196
|
+
* strand is no longer running are pruned on each reconcile pass.
|
|
197
|
+
*/
|
|
198
|
+
private readonly delegateAnnounceAt;
|
|
199
|
+
/**
|
|
200
|
+
* When each running strand last had its siblings' strand addresses re-resolved
|
|
201
|
+
* into its own address book, keyed by strandId. Throttles
|
|
202
|
+
* {@link refreshStrandPeerAddrs} to once per {@link STRAND_PEER_ADDR_REFRESH_MS}
|
|
203
|
+
* per strand so the 15 s reconcile tick never becomes per-tick RPC chatter.
|
|
204
|
+
* Entries for strands that are no longer running are pruned on each pass, so a
|
|
205
|
+
* hibernated-then-resumed strand refreshes immediately rather than inheriting a
|
|
206
|
+
* stale stamp.
|
|
207
|
+
*/
|
|
208
|
+
private readonly strandPeerAddrRefreshAt;
|
|
209
|
+
/**
|
|
210
|
+
* Cold-start bootstrap dial targets: the owner-flagged peers of every seed
|
|
211
|
+
* this node has applied, keyed by peer id, valued by the seed's multiaddr
|
|
212
|
+
* strings (parsed lazily, at dial time). Written by
|
|
213
|
+
* {@link recordSeedBootstrapPeers}; read only by
|
|
214
|
+
* {@link dialColdStartBootstrap} while the control database still has no
|
|
215
|
+
* siblings to dial.
|
|
216
|
+
*
|
|
217
|
+
* A STORE rather than a plain map (see `bootstrap-peer-store.ts`), because the
|
|
218
|
+
* targets must outlive the process: a node seeded into a party it could not
|
|
219
|
+
* reach has nothing else on disk naming that party's addresses (`applySeed`
|
|
220
|
+
* writes no control row, and `CadrePeer` fills in only after a connection
|
|
221
|
+
* succeeds), so an in-memory-only set left it stranded permanently across a
|
|
222
|
+
* restart. Constructed (or adopted from `config.bootstrapPeers.store`) by
|
|
223
|
+
* {@link initializeBootstrapPeerStore} during {@link start}; deliberately NOT
|
|
224
|
+
* cleared by {@link cleanup}, so it survives a stop()→start() cycle of the same
|
|
225
|
+
* node instance — same lifecycle as {@link trustedOwnerStore}. Durability
|
|
226
|
+
* depends on the injected backend: Node gets a file-backed store from the CLI,
|
|
227
|
+
* RN/browser stay ephemeral for now.
|
|
228
|
+
*
|
|
229
|
+
* Deliberately NOT the libp2p peer store, which {@link peerStoreAddrs} already
|
|
230
|
+
* reads for the steady-state path. The peer store is shared with everything
|
|
231
|
+
* libp2p discovers, so "dial every entry" would grow into dialing arbitrary
|
|
232
|
+
* discovered peers as the node lives longer; this store holds exactly the peers
|
|
233
|
+
* an owner-signed, trust-anchored seed nominated as owners. `applySeed` merges
|
|
234
|
+
* the same addresses into the peer store as well, so the two never disagree —
|
|
235
|
+
* this one is just the precisely-scoped subset.
|
|
236
|
+
*
|
|
237
|
+
* A later seed OVERWRITES an entry rather than merging, so a re-seed after an
|
|
238
|
+
* owner's address changes replaces the stale address instead of accumulating
|
|
239
|
+
* dead ones. Entries are never evicted: they are the node's only way back into
|
|
240
|
+
* the party if it is ever stranded again.
|
|
241
|
+
*/
|
|
242
|
+
private bootstrapPeerStore;
|
|
82
243
|
/** Initial self-registration timer (see {@link scheduleSelfRegistration}). */
|
|
83
244
|
private selfRegistrationTimer;
|
|
84
245
|
/** TTL heartbeat that re-publishes the self record before it goes stale. */
|
|
@@ -107,15 +268,38 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
107
268
|
*/
|
|
108
269
|
private registerSelfInFlight;
|
|
109
270
|
/**
|
|
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
|
-
*
|
|
271
|
+
* Re-replication queue for owner `authorizePeer` writes that committed while
|
|
272
|
+
* this node was alone — `controlNode.getConnections().length === 0` at write
|
|
273
|
+
* time means the Optimystic commit was local-only (the block's cluster was ≤1)
|
|
274
|
+
* and never broadcast. Maps the affected subject peerId → 'authorize', to
|
|
275
|
+
* re-issue once the cohort grows. Removals are NOT tracked here — a delete
|
|
276
|
+
* leaves a `Revocation` tombstone, tracked by {@link pendingRevocations}; a
|
|
277
|
+
* removePeer also clears any queued authorize for the same subject (the row is
|
|
278
|
+
* gone, so re-issuing the insert would be wrong). Drained on the 0→≥1
|
|
279
|
+
* control-connection transition by {@link drainPendingControlReplication}.
|
|
117
280
|
*/
|
|
118
281
|
private pendingPeerWrites;
|
|
282
|
+
/**
|
|
283
|
+
* Re-replication queue for `Revocation` tombstones that committed while this
|
|
284
|
+
* node was alone, keyed on the retired `StampId`. Fed by the control DB's
|
|
285
|
+
* committed-delete seam ({@link noteGuardedDelete}), so one queue covers all
|
|
286
|
+
* four guarded tables (`CadrePeer` / `DeviceToken` / `Strand` /
|
|
287
|
+
* `ValidationKey`). Drained (owner-signed `ReissuedAt` bump, which re-broadcasts
|
|
288
|
+
* the tombstone) by {@link drainPendingRevocations}. Cleared on stop alongside
|
|
289
|
+
* {@link pendingPeerWrites} — the tombstone rows are durable, so the next
|
|
290
|
+
* lifetime's first-growth sweep re-covers anything dropped here.
|
|
291
|
+
*/
|
|
292
|
+
private pendingRevocations;
|
|
293
|
+
/**
|
|
294
|
+
* Guards the once-per-lifetime first-growth revocation sweep (re-issue EVERY
|
|
295
|
+
* locally-held tombstone, covering removals from before this node started).
|
|
296
|
+
* Deliberately separate from {@link reconstructedLocalOnlyWrites} and set only
|
|
297
|
+
* after a sweep pass SUCCEEDS (including the nothing-held case) — a throwing
|
|
298
|
+
* sweep retries on the next growth edge instead of being lost for the lifetime.
|
|
299
|
+
* Reset on stop, so a stop()→start() cycle sweeps again (that second start is
|
|
300
|
+
* exactly a "removals from before this lifetime" case).
|
|
301
|
+
*/
|
|
302
|
+
private reissuedHeldRevocations;
|
|
119
303
|
/** This node's own `CadrePeer` self-write committed local-only (re-touched on growth). */
|
|
120
304
|
private pendingSelfPeerWrite;
|
|
121
305
|
/** This node's own `DeviceToken` self-write committed local-only (re-touched on growth). */
|
|
@@ -249,11 +433,11 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
249
433
|
* before any network bring-up. Resolution order:
|
|
250
434
|
*
|
|
251
435
|
* 1. Both `keyStore` and `privateKey` set ⇒ configuration error (throws).
|
|
252
|
-
* 2. `keyStore` set ⇒
|
|
253
|
-
* {@link DEFAULT_IDENTITY_KEY_ID})
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
436
|
+
* 2. `keyStore` set ⇒ {@link loadOrCreateIdentityKey} against `identityKeyId`
|
|
437
|
+
* (default {@link DEFAULT_IDENTITY_KEY_ID}): load when the slot is present,
|
|
438
|
+
* generate + persist when it is empty. A rejected `get` (access denied /
|
|
439
|
+
* backend failure) PROPAGATES — we never generate a new key on a read error,
|
|
440
|
+
* which would silently orphan the real identity.
|
|
257
441
|
* 3. `privateKey` set ⇒ use it directly.
|
|
258
442
|
* 4. Neither ⇒ leave undefined; libp2p generates an ephemeral key.
|
|
259
443
|
*
|
|
@@ -261,7 +445,246 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
261
445
|
* resolved key rather than regenerating or re-persisting.
|
|
262
446
|
*/
|
|
263
447
|
private resolveIdentityKey;
|
|
448
|
+
/**
|
|
449
|
+
* Construct (or adopt) the node-local trusted-owner anchor and seed the
|
|
450
|
+
* out-of-band pinned keys from `config.trustedOwners`. The store is NEVER
|
|
451
|
+
* sourced from the replicated control DB — its entries come only from
|
|
452
|
+
* genesis self-trust ({@link initializeSeedBootstrap}), config pins (here),
|
|
453
|
+
* or runtime enrollment pins ({@link trustOwnerKeys}).
|
|
454
|
+
*
|
|
455
|
+
* Idempotent across stop()→start(): the store instance is kept, and
|
|
456
|
+
* re-seeding config pins is a no-op ({@link TrustedOwnerStore.trust} is
|
|
457
|
+
* idempotent). An injected store scoped to a different party is a
|
|
458
|
+
* configuration error (fail closed before any network bring-up).
|
|
459
|
+
*/
|
|
460
|
+
private initializeTrustedOwnerStore;
|
|
461
|
+
/**
|
|
462
|
+
* Construct (or adopt) the node-local cold-start bootstrap-peer store (see
|
|
463
|
+
* {@link bootstrapPeerStore}). Synchronous: an injected store has already
|
|
464
|
+
* loaded its persisted targets by the time it is handed in (its `open` is the
|
|
465
|
+
* async part), and the in-memory fallback has nothing to load.
|
|
466
|
+
*
|
|
467
|
+
* Idempotent across stop()→start(): the store instance is kept, so retained
|
|
468
|
+
* targets survive a restart of the same node instance even with the ephemeral
|
|
469
|
+
* default. An injected store scoped to a different party is a configuration
|
|
470
|
+
* error (fail closed before any network bring-up) — a foreign party's addresses
|
|
471
|
+
* must never enter this node's dial loop.
|
|
472
|
+
*/
|
|
473
|
+
private initializeBootstrapPeerStore;
|
|
474
|
+
/**
|
|
475
|
+
* The node-local cold-start bootstrap-peer store (null before {@link start}) —
|
|
476
|
+
* the retained owner addresses {@link reconcileControlCohort}'s cold-start
|
|
477
|
+
* branch re-dials. Exposed for diagnostics and for a host that wants to show
|
|
478
|
+
* "what would this node dial if it is stranded?".
|
|
479
|
+
*/
|
|
480
|
+
getBootstrapPeerStore(): BootstrapPeerStore | null;
|
|
481
|
+
/**
|
|
482
|
+
* The node-local trusted-owner anchor (null before {@link start}). This is
|
|
483
|
+
* the set the authorized-membership predicate and seed-trust anchor consult —
|
|
484
|
+
* never the replicated `OwnerKey` table, which any stranger can pollute.
|
|
485
|
+
*/
|
|
486
|
+
getTrustedOwnerStore(): TrustedOwnerStore | null;
|
|
487
|
+
/**
|
|
488
|
+
* Persist out-of-band-established owner keys into the node-local anchor —
|
|
489
|
+
* the runtime enrollment seam: call with `CadreInvite.ownerKeys` when
|
|
490
|
+
* redeeming an invite (BEFORE the first `applySeed`, so the anchor already
|
|
491
|
+
* holds the pins when seed trust consults it), or with an operator-supplied
|
|
492
|
+
* pin. Idempotent. ('genesis' provenance is reserved for the node's own
|
|
493
|
+
* founding key, seeded internally by {@link initializeSeedBootstrap}.)
|
|
494
|
+
*
|
|
495
|
+
* Validates every key's shape before trusting any of them (all-or-nothing):
|
|
496
|
+
* a malformed entry anywhere in `keys` rejects the whole call before a
|
|
497
|
+
* single key is anchored. For the invite route this means a `CadreInvite`
|
|
498
|
+
* carrying one malformed `ownerKeys` entry fails the redemption outright —
|
|
499
|
+
* consistent with the anchor's existing whole-record-or-nothing policy for
|
|
500
|
+
* a corrupt persisted entry (see `trusted-owner-store.ts`'s
|
|
501
|
+
* `unusableEntry: 'discard-all'`) — rather than silently anchoring a subset
|
|
502
|
+
* and leaving the caller to notice a key went missing.
|
|
503
|
+
*/
|
|
504
|
+
trustOwnerKeys(keys: Iterable<string>, source: Exclude<TrustSource, 'genesis'>): Promise<void>;
|
|
264
505
|
private createControlNode;
|
|
506
|
+
/**
|
|
507
|
+
* The control database's cache-wrapped raw storage, resolved from
|
|
508
|
+
* `config.storage.provider` on first use and then held in {@link controlStorage}
|
|
509
|
+
* for the rest of this runtime.
|
|
510
|
+
*
|
|
511
|
+
* Resolving once is the contract `RawStorageProvider` (types.ts) states: a provider is
|
|
512
|
+
* called once per scope per runtime lifetime. Re-entering it per call would mint a
|
|
513
|
+
* second store over one backend — a fresh, cold cache, with the previous wrapper's
|
|
514
|
+
* registration orphaned in the process-wide pool.
|
|
515
|
+
*
|
|
516
|
+
* Lazy rather than eager in `start()` so the pure-unit call path
|
|
517
|
+
* (`cadre-node-control-node-options.spec.ts` calls `buildControlNodeOptions` on a
|
|
518
|
+
* bare `new CadreNode`) still resolves — and, on that path too, only once.
|
|
519
|
+
*
|
|
520
|
+
* Wrapped in the write-through raw-storage cache (quereus-plugin-sereus's
|
|
521
|
+
* `cached-storage.ts`) because a control start's cost is its raw-storage
|
|
522
|
+
* operation count.
|
|
523
|
+
*/
|
|
524
|
+
private resolveControlStorage;
|
|
525
|
+
/**
|
|
526
|
+
* Map this node's config onto the control network's libp2p node options.
|
|
527
|
+
*
|
|
528
|
+
* Split out of {@link createControlNode} purely so the mapping is assertable without
|
|
529
|
+
* standing up a real libp2p node — `packages/cadre-core/test/cadre-node-control-node-options.spec.ts`
|
|
530
|
+
* calls it on a bare `new CadreNode(config)`. Read nothing else into the split; the
|
|
531
|
+
* only caller in production is `createControlNode`.
|
|
532
|
+
*/
|
|
533
|
+
private buildControlNodeOptions;
|
|
534
|
+
/**
|
|
535
|
+
* Decide whether an inbound CONTROL-network connection from `remotePeerId`
|
|
536
|
+
* should be admitted — the policy behind the connection gater wired in
|
|
537
|
+
* {@link createControlNode}. Deny only on a positive "unauthorized outsider
|
|
538
|
+
* while no stranger path is open" determination; everything ambiguous admits
|
|
539
|
+
* and defers to the fail-closed per-stream gates (see
|
|
540
|
+
* `membership-connection-gater.ts` for the layer's rationale and the
|
|
541
|
+
* stranger-open protocol allowlist).
|
|
542
|
+
*
|
|
543
|
+
* Admits when ANY of:
|
|
544
|
+
* 1. a shared-baseline check admits ({@link admitControlPeerUnconditionally}
|
|
545
|
+
* — not running / DB torn down, absent-or-empty trusted-owner anchor, or
|
|
546
|
+
* configured bootstrap/relay infrastructure);
|
|
547
|
+
* 2. an enrollment window is open ({@link openEnrollmentWindow}, opened by
|
|
548
|
+
* {@link createInvite}) — the invitee dials in before it is authorized;
|
|
549
|
+
* 3. the peer holds a live DELEGATE ADMISSION GRANT — an authorized member
|
|
550
|
+
* announced it (over the strand-addr RPC) as the transport peerId of its
|
|
551
|
+
* own strand node, so a NAT'd member's strand node can hold a
|
|
552
|
+
* circuit-relay reservation here (see `delegate-admission.ts`).
|
|
553
|
+
* Connection only: the per-stream gate below never honors a grant;
|
|
554
|
+
* 4. the authorized-member set is empty — cold start: the rows that would
|
|
555
|
+
* authorize anyone arrive by replication over these very connections;
|
|
556
|
+
* 5. the peer IS an authorized member; or
|
|
557
|
+
* 6. an open invitation is OUTSTANDING — at least one unexpired,
|
|
558
|
+
* not-fully-consumed invitation this node minted or persisted (see
|
|
559
|
+
* `StrandSolicitationService.hasOutstandingInvitation`). A formation
|
|
560
|
+
* initiator is another party's peer by design and its token is only
|
|
561
|
+
* checkable inside the protocol, so the gate asks the coarser question
|
|
562
|
+
* "does this node expect a stranger at all?". Merely REGISTERING the
|
|
563
|
+
* responder ({@link initializeStrandSolicitation}) no longer suspends
|
|
564
|
+
* stranger denial — eager registration (as `reference-app-rn` does at
|
|
565
|
+
* bring-up) and `formStrand`'s lazy initialization both leave the gate
|
|
566
|
+
* armed, because neither mints an invitation.
|
|
567
|
+
*
|
|
568
|
+
* Ordering is semantically free (the checks are OR'd) but decides who pays:
|
|
569
|
+
* checks 1-3 are in-memory, 4/5 share one control-DB read, and only a peer
|
|
570
|
+
* already on the deny path reaches check 6's invitation lookup.
|
|
571
|
+
*
|
|
572
|
+
* Caveats of check 6, both self-healing:
|
|
573
|
+
* - the in-memory mint registry dies with the process, so after a restart
|
|
574
|
+
* only invitations persisted as `FormationInvite` rows still hold the
|
|
575
|
+
* exemption open (re-mint otherwise — same story as the enrollment window);
|
|
576
|
+
* - a peer holding a token whose `FormationInvite` row has not replicated to
|
|
577
|
+
* this node yet is denied even though the formation handler would have
|
|
578
|
+
* accepted it, exactly like the unreplicated-membership-row case below.
|
|
579
|
+
*
|
|
580
|
+
* NOTE: check 4/5 runs a control-DB read per inbound connection
|
|
581
|
+
* (`listAuthorizedMembers`); connections are rare and cadres small, and this
|
|
582
|
+
* layer is fail-open behind `ADMISSION_DECISION_TIMEOUT_MS`, so the live
|
|
583
|
+
* read is safe here — unlike the per-stream gate, which must consult the
|
|
584
|
+
* materialized {@link authorizedControlPeers} snapshot instead.
|
|
585
|
+
* NOTE: a sibling whose membership row has not yet replicated to
|
|
586
|
+
* this node is denied until the row converges (typically via the owner);
|
|
587
|
+
* either side's next outbound reconcile dial (outbound is never gated)
|
|
588
|
+
* re-establishes the link — self-healing, but visible as a transient deny.
|
|
589
|
+
*/
|
|
590
|
+
private admitInboundControlConnection;
|
|
591
|
+
/**
|
|
592
|
+
* Hold the control-network inbound gate open for not-yet-authorized peers
|
|
593
|
+
* until `untilEpochMs` (extends, never shrinks, an already-open window).
|
|
594
|
+
* {@link createInvite} calls this automatically; a host running an
|
|
595
|
+
* out-of-band enrollment flow (e.g. accepting a phone whose invite this node
|
|
596
|
+
* never minted) can open it explicitly before the stranger dials in.
|
|
597
|
+
*/
|
|
598
|
+
openEnrollmentWindow(untilEpochMs: number): void;
|
|
599
|
+
/**
|
|
600
|
+
* Record (or refresh) a delegate admission grant: `delegatePeerId` is the
|
|
601
|
+
* transport peerId of `announcerPeerId`'s strand-`strandId` node, admitted
|
|
602
|
+
* at the CONNECTION level (all a circuit-relay reservation needs) for
|
|
603
|
+
* `DELEGATE_GRANT_TTL_MS`. Called by the strand-addr responder after its
|
|
604
|
+
* authorized-membership gate has passed; public so tests can drive the
|
|
605
|
+
* admission policy without a full strand launch. A re-announce for the same
|
|
606
|
+
* (announcer, strand) REPLACES the previous delegate rather than
|
|
607
|
+
* accumulating. Never honored by {@link authorizeInboundControlStream}.
|
|
608
|
+
*/
|
|
609
|
+
grantDelegateAdmission(announcerPeerId: string, strandId: string, delegatePeerId: string): void;
|
|
610
|
+
/** Is `remotePeerId` covered by a live delegate admission grant? */
|
|
611
|
+
hasDelegateAdmission(remotePeerId: string): boolean;
|
|
612
|
+
/**
|
|
613
|
+
* The admission checks SHARED by the connection gate
|
|
614
|
+
* ({@link admitInboundControlConnection}) and the per-stream control-DB gate
|
|
615
|
+
* ({@link authorizeInboundControlStream}) — factored so the two layers cannot
|
|
616
|
+
* drift. All in-memory and cheap (the stream gate runs this per inbound
|
|
617
|
+
* stream). Returns true when the peer must be admitted BEFORE any membership
|
|
618
|
+
* source is consulted:
|
|
619
|
+
* - the node is not fully up (`start()` in progress / DB torn down) — both
|
|
620
|
+
* gates exist from libp2p bring-up, before the control DB does;
|
|
621
|
+
* - the trusted-owner anchor is absent or empty — an un-enrolled node has
|
|
622
|
+
* no basis to judge anyone and MUST accept its enrollment seed;
|
|
623
|
+
* - the peer is one of the configured control bootstrap/relay nodes —
|
|
624
|
+
* operator-configured infrastructure, not cadre members.
|
|
625
|
+
* False only means "no unconditional admit": the caller then judges the peer
|
|
626
|
+
* against its own membership source (a live DB read for the fail-open
|
|
627
|
+
* connection gate; the materialized snapshot for the fail-closed stream gate).
|
|
628
|
+
*/
|
|
629
|
+
private admitControlPeerUnconditionally;
|
|
630
|
+
/**
|
|
631
|
+
* Per-stream authorization for the four Optimystic control-DB protocols
|
|
632
|
+
* (`/optimystic/control-<party>/{repo,cluster,sync,block-transfer}/…`),
|
|
633
|
+
* wired as `authorizeInboundStream` in {@link createControlNode} — the
|
|
634
|
+
* fail-closed layer behind the fail-open connection gater. Control node
|
|
635
|
+
* ONLY: strand cohort nodes legitimately serve cross-party peers.
|
|
636
|
+
*
|
|
637
|
+
* The STRICT SUBSET of {@link admitInboundControlConnection}: the same
|
|
638
|
+
* "no basis to judge" admissions (shared via
|
|
639
|
+
* {@link admitControlPeerUnconditionally}), minus the stranger carve-outs
|
|
640
|
+
* (enrollment window, outstanding open invitation, delegate admission
|
|
641
|
+
* grant). The first two exist so a stranger can reach `/sereus/seed/1.0.0`
|
|
642
|
+
* and `/sereus/formation/1.0.0` — neither is gated here, and admitting a
|
|
643
|
+
* stranger to `repo` during an enrollment window is exactly the hole this
|
|
644
|
+
* gate closes. A DELEGATE-admitted connection (a member's strand node
|
|
645
|
+
* holding a circuit-relay reservation, see `delegate-admission.ts`) is
|
|
646
|
+
* likewise exactly a case this gate must still refuse: the delegate gets
|
|
647
|
+
* the connection and never the control DB.
|
|
648
|
+
*
|
|
649
|
+
* Admits when ANY of:
|
|
650
|
+
* 1. a shared-baseline check admits (not running / no DB, empty anchor,
|
|
651
|
+
* configured bootstrap infra);
|
|
652
|
+
* 2. the materialized authorized set is empty — cold start, before the
|
|
653
|
+
* rows that would authorize anyone have replicated in;
|
|
654
|
+
* 3. the peer IS in the materialized authorized set.
|
|
655
|
+
*
|
|
656
|
+
* Deliberately SYNCHRONOUS and pure in-memory — see
|
|
657
|
+
* {@link authorizedControlPeers} for why a control-DB read here would
|
|
658
|
+
* deadlock into mutual denial. With a sync predicate the upstream deadline
|
|
659
|
+
* (`authorizeInboundStreamTimeoutMs`) never trips, so it stays at its
|
|
660
|
+
* default.
|
|
661
|
+
*
|
|
662
|
+
* NOTE: the snapshot keys on PEER ID, so a node with no persistent identity
|
|
663
|
+
* key never lands a `CadrePeer` row ({@link registerSelf} skips) and its
|
|
664
|
+
* siblings deny its control-DB streams once their own snapshot is non-empty —
|
|
665
|
+
* owner status is a KEY, not a peer id, so being the owner does not exempt it.
|
|
666
|
+
* Harmless today (a real deployment persists its identity; the ephemeral-owner
|
|
667
|
+
* case appears only in tests, which configure the owner as bootstrap infra —
|
|
668
|
+
* `push-wake-e2e.integration.ts` design note 3). If an ephemeral-identity node
|
|
669
|
+
* ever becomes a supported deployment, this gate needs a key-based admission.
|
|
670
|
+
*/
|
|
671
|
+
private authorizeInboundControlStream;
|
|
672
|
+
/**
|
|
673
|
+
* Refresh {@link authorizedControlPeers} from the control DB, best-effort: a
|
|
674
|
+
* failed read keeps the previous snapshot (never clears it), so a transient
|
|
675
|
+
* DB error can neither flip the stream gate's cold-start carve-out back open
|
|
676
|
+
* nor drop a legitimate member mid-flight. Never rejects.
|
|
677
|
+
*
|
|
678
|
+
* The SOLE caller is {@link drainMembershipGate}, which serializes refreshes —
|
|
679
|
+
* so two reads can no longer settle out of order.
|
|
680
|
+
*/
|
|
681
|
+
private refreshAuthorizedControlPeers;
|
|
682
|
+
/**
|
|
683
|
+
* PeerIds of the configured control bootstrap nodes (relay/bootstrap
|
|
684
|
+
* infrastructure — always admitted inbound). Parsed once, lazily; an
|
|
685
|
+
* address without a `/p2p/<id>` component contributes nothing.
|
|
686
|
+
*/
|
|
687
|
+
private getBootstrapPeerIds;
|
|
265
688
|
private createStrandQueryable;
|
|
266
689
|
/**
|
|
267
690
|
* Schedule the node's initial self-record publish + ongoing refresh shortly
|
|
@@ -280,7 +703,11 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
280
703
|
* PeerId (the resolved node identity from `keyStore`/`config.privateKey`).
|
|
281
704
|
* - If the row already exists: a self-signed UPDATE bumping `UpdatedAt`.
|
|
282
705
|
* - If not, and the node is its own owner: an owner-signed INSERT that
|
|
283
|
-
* also carries the self-signature.
|
|
706
|
+
* also carries the self-signature. That INSERT is idempotent, so if an owner
|
|
707
|
+
* {@link authorizePeer} of this node's own id seated the row inside the
|
|
708
|
+
* read-then-insert window it no-ops — the publish then falls through to the
|
|
709
|
+
* UPDATE path (re-reading and re-signing against the row that landed) rather
|
|
710
|
+
* than leaving the authorize's null `Sig` in place, and reports `refreshed`.
|
|
284
711
|
* - Otherwise: logs and returns (a non-owner node with no row yet must
|
|
285
712
|
* wait for an owner to insert it; it can then self-refresh).
|
|
286
713
|
*
|
|
@@ -294,6 +721,13 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
294
721
|
registerSelf(): Promise<SelfRegistrationOutcome>;
|
|
295
722
|
/** The body of {@link registerSelf}; serialised by its single-flight guard. */
|
|
296
723
|
private publishSelfRecord;
|
|
724
|
+
/**
|
|
725
|
+
* Sign this node's address record for publication, stamped strictly later than
|
|
726
|
+
* `existing` (the row it is about to replace, or null for a first insert) so a
|
|
727
|
+
* same-millisecond re-publish still satisfies the monotonic `UpdatedAt` rule the
|
|
728
|
+
* `CadrePeer.AuthorizedUpdate` self-branch enforces.
|
|
729
|
+
*/
|
|
730
|
+
private signSelfRecord;
|
|
297
731
|
/**
|
|
298
732
|
* The ed25519 keypair (base64url) the node signs its own record with — the key
|
|
299
733
|
* behind its libp2p PeerId. Sourced from the resolved {@link identityKey}
|
|
@@ -374,23 +808,67 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
374
808
|
/** Body of {@link reconcileControlCohort}; serialised by its single-flight guard. */
|
|
375
809
|
private runReconcileControlCohort;
|
|
376
810
|
/**
|
|
377
|
-
* Resolve
|
|
378
|
-
*
|
|
811
|
+
* Resolve every sibling's signed address record once and merge what resolves
|
|
812
|
+
* into the control node's libp2p **address book** (peerStore). Returns the
|
|
813
|
+
* resolved addresses per sibling so the dial loop reuses them rather than
|
|
814
|
+
* re-resolving — one `queryPeerRecord` per sibling per pass, not two.
|
|
815
|
+
*
|
|
816
|
+
* Deliberately every sibling, not the {@link selectControlCohortDials} subset,
|
|
817
|
+
* and deliberately including already-connected ones: everything below
|
|
818
|
+
* cadre-core dials by bare peer id (Optimystic's cluster/repo clients, FRET
|
|
819
|
+
* ping/announce), so the address book has to be warm BEFORE a live connection
|
|
820
|
+
* drops, and a sibling the out-degree cap declines to dial is still one those
|
|
821
|
+
* layers may need to reach.
|
|
822
|
+
*
|
|
823
|
+
* Only `resolvePeerAddrs` output is merged — never the cold-start
|
|
824
|
+
* `peerStoreAddrs` fallback, which came out of the address book to begin with
|
|
825
|
+
* and would restamp unverified seed addresses indefinitely. A sibling that
|
|
826
|
+
* resolves to nothing (revoked, stale, untrusted) is not written at all, so its
|
|
827
|
+
* existing entry ages out on its own.
|
|
828
|
+
*
|
|
829
|
+
* NOTE: this resolves EVERY sibling serially before the dial loop below runs,
|
|
830
|
+
* so it costs one record query per sibling per reconcile pass (~15s) and each
|
|
831
|
+
* one delays the pass's first dial. A cadre is a handful of devices, so today
|
|
832
|
+
* that is a few extra local reads; if cadres ever grow large, batch the records
|
|
833
|
+
* into one query, merge only on change, or move the warm pass after the dials.
|
|
834
|
+
*/
|
|
835
|
+
private warmSiblingAddrBook;
|
|
836
|
+
/**
|
|
837
|
+
* {@link resolvePeerAddrs} for one sibling, best-effort: a control-DB read
|
|
838
|
+
* failure yields `[]` (and the cold-start fallback then gets its turn) rather
|
|
839
|
+
* than aborting the whole pass, like every other step here.
|
|
840
|
+
*/
|
|
841
|
+
private resolveSiblingAddrs;
|
|
842
|
+
/**
|
|
843
|
+
* Budget for ONE proactive dial of the reconcile pass — steady-state sibling
|
|
844
|
+
* ({@link dialControlSibling}) and cold-start bootstrap peer
|
|
845
|
+
* ({@link dialBootstrapPeer}) alike, since both feed a whole address list to
|
|
846
|
+
* one `dial()` and both run in a sequential loop that the next entry waits on.
|
|
379
847
|
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
|
|
383
|
-
|
|
848
|
+
* See {@link DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS} for why the pass bounds
|
|
849
|
+
* this itself rather than inheriting each address's libp2p attempt timeout.
|
|
850
|
+
*/
|
|
851
|
+
private controlDialTimeoutMs;
|
|
852
|
+
/**
|
|
853
|
+
* Dial one sibling from its already-resolved addresses, best-effort. Returns
|
|
854
|
+
* whether a dial was attempted (false when no address resolves).
|
|
855
|
+
*
|
|
856
|
+
* A per-peer failure (NAT, offline, relay down, connection-gater denial, or
|
|
857
|
+
* the {@link controlDialTimeoutMs} budget expiring) is logged and swallowed so
|
|
858
|
+
* one unreachable sibling never aborts the pass — exactly like
|
|
859
|
+
* {@link SeedBootstrapService.applySeed}'s owner-dial loop. A failed dial is
|
|
860
|
+
* simply retried on the next pass.
|
|
384
861
|
*/
|
|
385
862
|
private dialControlSibling;
|
|
386
863
|
/**
|
|
387
|
-
*
|
|
864
|
+
* A sibling's control-network dial addresses for the reconcile pass.
|
|
388
865
|
*
|
|
389
|
-
* Primary (steady state): the signed, fresh, trust-gated control addresses
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
866
|
+
* Primary (steady state): the signed, fresh, trust-gated control addresses
|
|
867
|
+
* `resolved` for it by {@link warmSiblingAddrBook} — passed in rather than
|
|
868
|
+
* re-resolved, so the pass makes one record query per sibling. Fallback (cold
|
|
869
|
+
* start): the libp2p peerStore entries `applySeed` populated, used only while
|
|
870
|
+
* the record is not yet resolvable. Returns `[]` (never throws) when neither
|
|
871
|
+
* yields an address — that sibling is skipped this pass.
|
|
394
872
|
*/
|
|
395
873
|
private resolveControlDialAddrs;
|
|
396
874
|
/**
|
|
@@ -399,6 +877,65 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
399
877
|
* parse/lookup failure — never throws.
|
|
400
878
|
*/
|
|
401
879
|
private peerStoreAddrs;
|
|
880
|
+
/**
|
|
881
|
+
* Retain a just-applied seed's owner-flagged peers as cold-start bootstrap
|
|
882
|
+
* dial targets (see {@link bootstrapPeerStore}).
|
|
883
|
+
*
|
|
884
|
+
* Called for every seed this node accepts, on BOTH intake paths — the
|
|
885
|
+
* {@link applySeed} wrapper (which may run on a throwaway service, so the
|
|
886
|
+
* service itself is the wrong place to keep this) and the inbound
|
|
887
|
+
* `/sereus/seed/1.0.0` handler via `onSeedApplied`. Peers with no address are
|
|
888
|
+
* skipped: there is nothing to dial.
|
|
889
|
+
*
|
|
890
|
+
* `isOwner` is the seed's own claim about its peers, exactly as
|
|
891
|
+
* `SeedBootstrapService.applySeed` uses it to choose its dial targets. That is
|
|
892
|
+
* sound here for the same reason it is sound there — the whole seed is
|
|
893
|
+
* signature-checked against a trust-anchored signer before this runs, and the
|
|
894
|
+
* flag only *selects a dial target*; a dial grants no authority.
|
|
895
|
+
*
|
|
896
|
+
* Self is excluded. `createSeed` projects EVERY `CadrePeer` row, so an owner
|
|
897
|
+
* that applies a seed minted after it joined finds itself in the owner list;
|
|
898
|
+
* retaining that would make the cold-start pass dial this node forever (the
|
|
899
|
+
* steady-state pass filters self out of its sibling list for the same reason).
|
|
900
|
+
*/
|
|
901
|
+
private recordSeedBootstrapPeers;
|
|
902
|
+
/**
|
|
903
|
+
* Cold-start branch of the reconcile pass: re-dial the seed's owner peers
|
|
904
|
+
* while the control database still has no siblings to dial.
|
|
905
|
+
*
|
|
906
|
+
* `SeedBootstrapService.applySeed` dials those owners exactly ONCE, best-effort.
|
|
907
|
+
* When that single dial fails — owner momentarily down, relay reservation not
|
|
908
|
+
* up yet, NAT traversal lost the race — the joining node has an empty
|
|
909
|
+
* `CadrePeer` table and no connection, and the steady-state pass above cannot
|
|
910
|
+
* help: it dials only siblings enumerated from that very table. Retrying here
|
|
911
|
+
* is the only way back in.
|
|
912
|
+
*
|
|
913
|
+
* Unbounded, at the reconcile cadence, with no backoff: the branch is already
|
|
914
|
+
* gated by "the control database has no siblings", so it stops the moment the
|
|
915
|
+
* node is actually in the party, and a node that is stranded MUST keep trying —
|
|
916
|
+
* a give-up rule would turn a transient outage into a permanent one. The steady
|
|
917
|
+
* cost is one dial per bootstrap peer per pass (seeds nominate one or a few
|
|
918
|
+
* owners), each failing fast against an unreachable address.
|
|
919
|
+
* NOTE: if seeds ever carry many owner peers, or the reconcile cadence tightens
|
|
920
|
+
* well below its 15 s default, add per-peer backoff here.
|
|
921
|
+
*
|
|
922
|
+
* Best-effort per peer, exactly like {@link dialControlSibling}: one dead
|
|
923
|
+
* address never aborts the pass.
|
|
924
|
+
*/
|
|
925
|
+
private dialColdStartBootstrap;
|
|
926
|
+
/**
|
|
927
|
+
* Bind a bootstrap peer's seed addresses to its peer id, so the dial
|
|
928
|
+
* authenticates the peer it is aiming at rather than trusting whoever answers.
|
|
929
|
+
*
|
|
930
|
+
* An address that already carries `/p2p/<id>` must carry THIS peer's id or it
|
|
931
|
+
* is dropped (a seed that disagrees with itself is not a dial target); one
|
|
932
|
+
* that carries none — an `addressOverrides`-published record, say — gets the
|
|
933
|
+
* id encapsulated. Unencapsulatable addresses are dropped rather than dialed
|
|
934
|
+
* bare.
|
|
935
|
+
*/
|
|
936
|
+
private bootstrapDialAddrs;
|
|
937
|
+
/** Dial one cold-start bootstrap peer, best-effort. Returns whether it connected. */
|
|
938
|
+
private dialBootstrapPeer;
|
|
402
939
|
/**
|
|
403
940
|
* Whether a control write happening now would commit local-only. A sound lower
|
|
404
941
|
* bound is "no connected control peers": 0 connections ⇒ the block's cluster is
|
|
@@ -408,6 +945,9 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
408
945
|
* is caught by the cohort's periodic pull-on-read instead. Re-issuing an already-
|
|
409
946
|
* replicated row is harmless (an idempotent monotonic bump), so the coarse proxy
|
|
410
947
|
* is acceptable as the agreed first cut.
|
|
948
|
+
*
|
|
949
|
+
* Reads {@link getControlConnectionCount} — the public, embedder-facing form of
|
|
950
|
+
* the same sample — so the two cannot drift.
|
|
411
951
|
*/
|
|
412
952
|
private committedAlone;
|
|
413
953
|
/**
|
|
@@ -417,12 +957,23 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
417
957
|
* cohort grows; otherwise the write replicated and any stale queue entry for this
|
|
418
958
|
* subject is dropped.
|
|
419
959
|
*
|
|
420
|
-
* A `remove`
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
960
|
+
* A `remove` never queues here — a delete's re-replicable half is its
|
|
961
|
+
* `Revocation` tombstone, which the committed-delete seam routes into
|
|
962
|
+
* {@link pendingRevocations} ({@link noteGuardedDelete}). The `remove` arm only
|
|
963
|
+
* drops any queued authorize for the subject (the row is gone; re-issuing the
|
|
964
|
+
* insert would resurrect it) and, when the commit was alone, logs the
|
|
965
|
+
* security-relevant window loudly.
|
|
424
966
|
*/
|
|
425
967
|
private noteControlWrite;
|
|
968
|
+
/**
|
|
969
|
+
* Committed-delete seam handler ({@link GuardedDeleteListener}, wired in
|
|
970
|
+
* {@link start}): a guarded-table delete and its `Revocation` tombstone
|
|
971
|
+
* committed. If the node was alone the tombstone is local-only — queue it for
|
|
972
|
+
* an owner-signed re-issue on cohort growth; otherwise it broadcast, so drop
|
|
973
|
+
* any stale queue entry for the stamp. Synchronous by contract — bookkeeping
|
|
974
|
+
* only, never throws into the delete path.
|
|
975
|
+
*/
|
|
976
|
+
private noteGuardedDelete;
|
|
426
977
|
/**
|
|
427
978
|
* Drain the write-while-alone re-replication queue: re-issue the writes that
|
|
428
979
|
* committed local-only now that the cohort can broadcast them. Public so the
|
|
@@ -443,6 +994,38 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
443
994
|
* the drain.
|
|
444
995
|
*/
|
|
445
996
|
private retouchSelfDeviceToken;
|
|
997
|
+
/**
|
|
998
|
+
* Re-issue `Revocation` tombstones so a removal that committed while alone
|
|
999
|
+
* still reaches the cohort. Two regimes, one method:
|
|
1000
|
+
*
|
|
1001
|
+
* - **First successful pass per process** ({@link reissuedHeldRevocations}
|
|
1002
|
+
* false): sweep EVERY locally-held tombstone from `queryRevocations()` — the
|
|
1003
|
+
* only cover for a removal made before this process started, since the
|
|
1004
|
+
* in-memory queue does not survive a restart. Queued stamps are part of that
|
|
1005
|
+
* same batch (exactly once — one transaction, no double bump).
|
|
1006
|
+
* - **Later passes**: only the tombstones queued in-session by
|
|
1007
|
+
* {@link noteGuardedDelete}.
|
|
1008
|
+
*
|
|
1009
|
+
* Owner-gated: a node with no owner key cannot sign a re-issue, so stray queue
|
|
1010
|
+
* entries are dropped (mirrors {@link drainPendingPeerWrites}). Best-effort — a
|
|
1011
|
+
* failure is logged and leaves the queue (and the sweep flag) untouched for the
|
|
1012
|
+
* next growth edge. A successful `reissueRevocations` exec is NOT proof of
|
|
1013
|
+
* broadcast (the connection that fired the growth edge may not be in the
|
|
1014
|
+
* affected block's cluster — see
|
|
1015
|
+
* tickets/backlog/control-rereplication-broadcast-confirmation); the drain
|
|
1016
|
+
* inherits that known gap, and a full disconnect→reconnect (or the next
|
|
1017
|
+
* process's sweep) re-covers it.
|
|
1018
|
+
*
|
|
1019
|
+
* NOTE: the sweep is O(all tombstones ever) row-updates — plus one owner
|
|
1020
|
+
* signature each — in one transaction, once per lifetime. `Revocation` is
|
|
1021
|
+
* append-only and unbounded growth is declared acceptable for a cadre-sized
|
|
1022
|
+
* party. If the tombstone table ever gets large, bound the sweep (e.g. persist
|
|
1023
|
+
* a node-local high-water mark of what has been re-issued while connected)
|
|
1024
|
+
* instead of re-touching everything; note the `Math.max(...)` spread below also
|
|
1025
|
+
* caps out around 10^5 rows (V8 argument limit) before the size becomes merely
|
|
1026
|
+
* a latency problem.
|
|
1027
|
+
*/
|
|
1028
|
+
private drainPendingRevocations;
|
|
446
1029
|
/**
|
|
447
1030
|
* One-shot, first-cohort-growth reconstruction of the write-while-alone queue for
|
|
448
1031
|
* an OWNER node: re-touch every membership row that may be an unreplicated
|
|
@@ -455,29 +1038,36 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
455
1038
|
* row is a no-op-equivalent).
|
|
456
1039
|
*
|
|
457
1040
|
* 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
|
-
*
|
|
1041
|
+
* rows for other peers, and rows it merely holds are not its to re-issue.
|
|
1042
|
+
* DELETEs are not reconstructed here — the `Revocation` tombstone sweep in
|
|
1043
|
+
* {@link drainPendingRevocations} covers them (a removed row leaves no
|
|
1044
|
+
* `CadrePeer` trace, but its tombstone is re-issuable).
|
|
461
1045
|
*/
|
|
462
1046
|
private reconstructAuthoredMembership;
|
|
1047
|
+
/**
|
|
1048
|
+
* Body of {@link reconstructAuthoredMembership}'s sweep: re-issue each candidate
|
|
1049
|
+
* owner-authored row, returning how many were re-touched. A per-row failure is
|
|
1050
|
+
* logged and skipped; a teardown mid-sweep stops it.
|
|
1051
|
+
*/
|
|
1052
|
+
private reissueAuthoredMembershipRows;
|
|
463
1053
|
/**
|
|
464
1054
|
* 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}.
|
|
1055
|
+
* authorize as an idempotent monotonic owner UPDATE
|
|
1056
|
+
* ({@link reissuePeerAuthorize}) now that the cohort can broadcast it.
|
|
1057
|
+
* Sequential (no fan-out) to avoid a thundering re-touch; an entry is cleared
|
|
1058
|
+
* only on success, so a failure (or a still-alone re-commit) leaves it queued
|
|
1059
|
+
* for the next growth. An entry is skipped (and cleared) if the row vanished
|
|
1060
|
+
* (raced a delete) or now carries a self-`Sig` (the peer self-published and owns
|
|
1061
|
+
* its republish). Removals are drained by {@link drainPendingRevocations}, not
|
|
1062
|
+
* here.
|
|
479
1063
|
*/
|
|
480
1064
|
private drainPendingPeerWrites;
|
|
1065
|
+
/**
|
|
1066
|
+
* Body of {@link drainPendingPeerWrites}' loop. An entry is cleared only on
|
|
1067
|
+
* success (or when there is nothing left to re-issue); a failure is logged and
|
|
1068
|
+
* the entry stays queued for the next cohort growth.
|
|
1069
|
+
*/
|
|
1070
|
+
private reissuePendingPeerWrites;
|
|
481
1071
|
/**
|
|
482
1072
|
* Re-issue an owner membership row as a monotonic owner UPDATE: bump
|
|
483
1073
|
* `UpdatedAt` strictly above the stored value (and the wall clock) and re-sign via
|
|
@@ -549,8 +1139,10 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
549
1139
|
* 2. `publicKey <-> peerId` binding — the stored key's libp2p identity is the
|
|
550
1140
|
* requested peerId,
|
|
551
1141
|
* 3. a `DeviceToken` row exists with a known {@link PushPlatform},
|
|
552
|
-
* 4.
|
|
553
|
-
*
|
|
1142
|
+
* 4. the row's `StampId` is NOT retired in `CadreControl.Revocation` — the
|
|
1143
|
+
* read-side half of the clear (see below),
|
|
1144
|
+
* 5. self-signature verifies against the bound `CadrePeer.PublicKey`,
|
|
1145
|
+
* 6. freshness — `updatedAt` is positive and within `opts.maxAgeMs` (default:
|
|
554
1146
|
* no ceiling, since a push token is valid until it rotates).
|
|
555
1147
|
*
|
|
556
1148
|
* A peer that is not a current member, or whose token has no backing `CadrePeer`
|
|
@@ -600,7 +1192,7 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
600
1192
|
private handleStrandHibernate;
|
|
601
1193
|
/**
|
|
602
1194
|
* Wake a strand. If it was hibernating (quiesced — no libp2p node), re-resolve
|
|
603
|
-
* the cohort discovery seed
|
|
1195
|
+
* the cohort discovery seed exactly as `launchStrand` does and rebuild
|
|
604
1196
|
* its runtime via the strand manager. If it is still live (e.g. waking an idle
|
|
605
1197
|
* strand, which retains its resources), just flip the status. Overlapping wake
|
|
606
1198
|
* triggers are coalesced upstream by `HibernationManager`, so this runs once
|
|
@@ -608,11 +1200,10 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
608
1200
|
*/
|
|
609
1201
|
private handleStrandWake;
|
|
610
1202
|
/**
|
|
611
|
-
* Rebuild a quiesced strand's runtime, re-resolving the volatile cohort
|
|
612
|
-
* first: the discovery seed may have grown
|
|
613
|
-
* shifted the mode `bootstrap → networked` since the strand last ran. Shared by
|
|
1203
|
+
* Rebuild a quiesced strand's runtime, re-resolving the volatile cohort input
|
|
1204
|
+
* first: the discovery seed may have grown since the strand last ran. Shared by
|
|
614
1205
|
* the wake (`handleStrandWake`) and check-in (`handleStrandCheckIn`) paths so
|
|
615
|
-
* both apply the same fresh seed
|
|
1206
|
+
* both apply the same fresh seed resolution. `resumeStrand` is idempotent
|
|
616
1207
|
* (returns the live instance unchanged) as a backstop against double-resume.
|
|
617
1208
|
*/
|
|
618
1209
|
private resumeStrandRuntime;
|
|
@@ -625,7 +1216,7 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
625
1216
|
* realized as a resume → bounded window → re-hibernate-if-idle cycle that
|
|
626
1217
|
* reuses the existing quiesce/resume primitives rather than a bespoke probe:
|
|
627
1218
|
*
|
|
628
|
-
* 1. Resume the strand (rebuild node + db, re-resolve cohort seed
|
|
1219
|
+
* 1. Resume the strand (rebuild node + db, re-resolve the cohort seed) so
|
|
629
1220
|
* its strand network can reach cohort peers — exactly as a wake does.
|
|
630
1221
|
* 2. Hold it resumed for a bounded window, during which the app may drive
|
|
631
1222
|
* reads (pull-on-read) and record activity.
|
|
@@ -672,6 +1263,14 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
672
1263
|
/**
|
|
673
1264
|
* Add a strand with its sApp configuration.
|
|
674
1265
|
* The hosting application must provide the sApp schema when creating a strand.
|
|
1266
|
+
*
|
|
1267
|
+
* A rejected call leaves nothing running but DOES leave the sApp config
|
|
1268
|
+
* registered, deliberately: both an explicit retry and the {@link StrandWatcher}'s
|
|
1269
|
+
* automatic relaunch need it. So once the strand's row is visible on the control
|
|
1270
|
+
* network (via {@link publishStrand} or another member), a failed launch keeps
|
|
1271
|
+
* being re-attempted in the background — each failure re-emitting `strand:error`
|
|
1272
|
+
* — until it succeeds. {@link detachStrand} (reached via {@link stopStrand}) is
|
|
1273
|
+
* what abandons a strand for good.
|
|
675
1274
|
*/
|
|
676
1275
|
addStrand(config: StrandConfig): Promise<StrandInstance>;
|
|
677
1276
|
/**
|
|
@@ -689,7 +1288,7 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
689
1288
|
* {@link ed25519KeyPairFromLibp2p} also exposes as the node's owner keypair,
|
|
690
1289
|
* so peer identity and owner key are one and the same. That key must be
|
|
691
1290
|
* enrolled in `OwnerKey` (e.g. via {@link ControlDatabase.ensureOwnerKey}
|
|
692
|
-
* at genesis) or the schema's `Strand.
|
|
1291
|
+
* at genesis) or the schema's `Strand.AuthorizedInsert` constraint rejects the write.
|
|
693
1292
|
* Failing loudly here is intentional: a silently-unpublished strand would run
|
|
694
1293
|
* as a local-only island that no peer could ever discover or join.
|
|
695
1294
|
*
|
|
@@ -697,14 +1296,70 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
697
1296
|
* {@link addStrand}).
|
|
698
1297
|
* @param type - `'o'` for open (default) or `'c'` for closed.
|
|
699
1298
|
* @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.
|
|
1299
|
+
* @throws if the node is not started, exposes no owner signing key, the id is blank, or
|
|
1300
|
+
* the control DB rejects the (unauthorized) insert.
|
|
702
1301
|
*/
|
|
703
1302
|
publishStrand(strandId: string, type?: 'o' | 'c', memberPrivateKey?: string): Promise<void>;
|
|
1303
|
+
/**
|
|
1304
|
+
* Remove this party's `Strand` row from the shared control database — the owner-signed
|
|
1305
|
+
* inverse of {@link publishStrand}.
|
|
1306
|
+
*
|
|
1307
|
+
* Party-wide, unlike {@link stopStrand}, which only stops the strand on THIS node: every
|
|
1308
|
+
* cadre node watching the table sees the row vanish on its next poll (default 5 s) and
|
|
1309
|
+
* stops its own instance. This node converges immediately — the method forces a watcher
|
|
1310
|
+
* poll and stops any still-running local instance before resolving. Removing the row
|
|
1311
|
+
* removes OUR party's participation only: other parties in the strand keep their own
|
|
1312
|
+
* rows and the strand network carries on, so no cross-party sign-off is involved — this
|
|
1313
|
+
* is an owner-signed control-plane write like every other, not "destroy the network".
|
|
1314
|
+
*
|
|
1315
|
+
* Irreversible for a closed strand (`Type='c'`): the row carries `MemberPrivateKey`,
|
|
1316
|
+
* this party's membership secret for that network, and it is stored nowhere else.
|
|
1317
|
+
* With it gone the party can never again admit a member to that closed strand, and a
|
|
1318
|
+
* re-published row would carry a DIFFERENT key that does not match the membership
|
|
1319
|
+
* already written into the strand's RBAC layer. The strand id itself is NOT
|
|
1320
|
+
* blacklisted: a fresh owner-signed {@link publishStrand} re-seats it under a new
|
|
1321
|
+
* stamp — only the unsigned consent re-seat is permanently foreclosed (the removal's
|
|
1322
|
+
* `Revocation` tombstone names the id, which the consent branch of
|
|
1323
|
+
* `Strand.AuthorizedInsert` refuses ever after). Outstanding `FormationInvite` rows
|
|
1324
|
+
* bound to the removed strand are unredeemable while the row is absent (the formation
|
|
1325
|
+
* recorder resolves the strand as missing and rejects cleanly).
|
|
1326
|
+
*
|
|
1327
|
+
* Convergence caveats, same class as {@link enrollValidationKey}'s: a sibling that has
|
|
1328
|
+
* not yet synced keeps running the strand until its own watcher observes the missing
|
|
1329
|
+
* row; a sibling whose `strandFilter` never admitted this strand never observes the
|
|
1330
|
+
* removal AT ALL and keeps running its instance indefinitely — opting out of watching a
|
|
1331
|
+
* strand is also opting out of its party-wide removal, so such a node's only stop is its
|
|
1332
|
+
* own local {@link stopStrand}/`unpublishStrand` call; and a removal committed while
|
|
1333
|
+
* ALONE (0 control connections) deletes the row local-only. The accompanying
|
|
1334
|
+
* `Revocation` tombstone IS queued and re-issued on the next cohort-growth edge
|
|
1335
|
+
* ({@link noteGuardedDelete}/{@link drainPendingRevocations}), so the stamp retirement
|
|
1336
|
+
* — and with it the consent re-seat foreclosure — does propagate; but the physical row
|
|
1337
|
+
* deletion cannot be replayed, and `queryStrands` reads raw (no retired-stamp filter),
|
|
1338
|
+
* so siblings that already hold the row keep running the strand until the collection
|
|
1339
|
+
* itself converges (logged loudly; see the delete-while-alone durability note in
|
|
1340
|
+
* docs/architecture.md).
|
|
1341
|
+
*
|
|
1342
|
+
* A no-op (no throw, no tombstone) when the row is already absent — but a
|
|
1343
|
+
* locally-running instance of that id is still stopped.
|
|
1344
|
+
*
|
|
1345
|
+
* NOTE: control-plane only — the strand's local durable storage is retained. Stopping the
|
|
1346
|
+
* instance closes the `StrandDatabase` and its libp2p node but purges no blocks, so a
|
|
1347
|
+
* closed strand's content stays readable on disk to anyone with the data directory. If a
|
|
1348
|
+
* caller ever needs removal to mean "and erase the local copy", that is a separate purge
|
|
1349
|
+
* step, not a widening of this method.
|
|
1350
|
+
*
|
|
1351
|
+
* @param strandId - The `Strand` row id to remove (as passed to {@link publishStrand}).
|
|
1352
|
+
* @throws if the node is not started, exposes no owner signing key, the id is blank, or
|
|
1353
|
+
* the signer is not an enrolled owner (the schema's `Strand.AuthorizedDelete` rejects
|
|
1354
|
+
* the write and the row survives). A rejection does NOT imply the row survived: the
|
|
1355
|
+
* local stop runs after the control-plane delete has already committed, so a failure
|
|
1356
|
+
* there throws over a completed removal.
|
|
1357
|
+
*/
|
|
1358
|
+
unpublishStrand(strandId: string): Promise<void>;
|
|
704
1359
|
/**
|
|
705
1360
|
* Publish an owner-signed `FormationInvite` (open-invitation token) to the
|
|
706
1361
|
* shared control database, so a later {@link formStrand} redemption can be
|
|
707
|
-
* validated against it (the consent branch of `Strand.
|
|
1362
|
+
* validated against it (the consent branch of `Strand.AuthorizedInsert`).
|
|
708
1363
|
*
|
|
709
1364
|
* Counterpart to {@link createOpenInvitation}, which only mints the
|
|
710
1365
|
* out-of-band {@link OpenInvitation} envelope: persisting the matching
|
|
@@ -729,28 +1384,234 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
729
1384
|
validationUrl?: string;
|
|
730
1385
|
strandId?: string;
|
|
731
1386
|
}): Promise<void>;
|
|
1387
|
+
/**
|
|
1388
|
+
* Enroll an approver public key allowed to sign off on `ValidationUrl` redemptions.
|
|
1389
|
+
*
|
|
1390
|
+
* A `FormationInvite` carrying a `ValidationUrl` is only redeemable when the approval
|
|
1391
|
+
* that comes back from that URL is signed by a key present in `CadreControl.ValidationKey`
|
|
1392
|
+
* — this is how a party says which outside approver it trusts. Without an enrollment,
|
|
1393
|
+
* every such invitation is unredeemable.
|
|
1394
|
+
*
|
|
1395
|
+
* Signs with the same self-owner key as {@link publishStrand} (the ed25519 key behind
|
|
1396
|
+
* this node's PeerId, which must be an enrolled `OwnerKey`). Throws loudly if the node
|
|
1397
|
+
* isn't started, exposes no signing key, or the key is blank — a blank key would reach
|
|
1398
|
+
* the database and fail the signature CHECK as an opaque constraint error.
|
|
1399
|
+
*
|
|
1400
|
+
* Enrolling this node's OWN owner key is permitted and harmless: the domain/action tags
|
|
1401
|
+
* baked into each authorization digest mean an owner-key signature can never satisfy the
|
|
1402
|
+
* approval rule and an approval can never satisfy an owner rule. No guard is needed.
|
|
1403
|
+
*
|
|
1404
|
+
* NOTE: enrollment is replicated control state, so a key enrolled here is visible to a
|
|
1405
|
+
* sibling cadre node only once control replication converges. A redemption that arrives
|
|
1406
|
+
* at a node which has not caught up is refused as not-enrolled — the same convergence
|
|
1407
|
+
* gap the schema records on `Strand.StampId`. Enroll before circulating the invitations
|
|
1408
|
+
* that depend on the key.
|
|
1409
|
+
*
|
|
1410
|
+
* @param key - Approver public key to enroll (base64url ed25519 public key).
|
|
1411
|
+
*/
|
|
1412
|
+
enrollValidationKey(key: string): Promise<void>;
|
|
1413
|
+
/**
|
|
1414
|
+
* Remove an approver public key.
|
|
1415
|
+
*
|
|
1416
|
+
* Narrows who may approve FUTURE redemptions ONLY. The schema's approval CHECKs run at
|
|
1417
|
+
* write time, so a join that was already approved by this key stays valid — removal is
|
|
1418
|
+
* not retroactive and does not re-examine committed rows.
|
|
1419
|
+
*
|
|
1420
|
+
* Rotation is therefore add-then-remove, in that order: removing the only enrolled key
|
|
1421
|
+
* while `ValidationUrl` invitations are outstanding makes every one of them unredeemable
|
|
1422
|
+
* until a new key is enrolled.
|
|
1423
|
+
*
|
|
1424
|
+
* Removing a key that is not enrolled is a silent no-op (no throw, no `Revocation`
|
|
1425
|
+
* tombstone) — see {@link ControlDatabase.deleteValidationKey}.
|
|
1426
|
+
*
|
|
1427
|
+
* @param key - Approver public key to remove (base64url ed25519 public key).
|
|
1428
|
+
*/
|
|
1429
|
+
removeValidationKey(key: string): Promise<void>;
|
|
1430
|
+
/**
|
|
1431
|
+
* The approver keys currently enrolled, sorted. Read-only, so unlike
|
|
1432
|
+
* {@link enrollValidationKey} / {@link removeValidationKey} it needs no owner signing
|
|
1433
|
+
* key — only a started node.
|
|
1434
|
+
*/
|
|
1435
|
+
listValidationKeys(): Promise<string[]>;
|
|
1436
|
+
/**
|
|
1437
|
+
* Resolve the owner keypair every owner-signed control write needs — {@link publishStrand},
|
|
1438
|
+
* {@link unpublishStrand}, {@link publishFormationInvite}, {@link enrollValidationKey},
|
|
1439
|
+
* {@link removeValidationKey}
|
|
1440
|
+
* — failing loudly in one two-part shape (not started / no signing key, naming owner
|
|
1441
|
+
* genesis as the fix). Narrows `controlDatabase` for the caller: a non-null return means
|
|
1442
|
+
* `this.controlDatabase` is non-null too.
|
|
1443
|
+
*
|
|
1444
|
+
* @param action - Infinitive phrase naming the attempted write, e.g. `'enroll a
|
|
1445
|
+
* validation key'`; interpolated into both messages.
|
|
1446
|
+
*/
|
|
1447
|
+
private requireOwnerSigningKey;
|
|
732
1448
|
/**
|
|
733
1449
|
* Shared strand launch path for both the explicit (`addStrand`) and the
|
|
734
1450
|
* control-discovered (`handleStrandAdded`) entry points. Resolves the cohort
|
|
735
|
-
* seed,
|
|
736
|
-
*
|
|
1451
|
+
* seed, starts the strand, and registers it with the hibernation manager
|
|
1452
|
+
* before emitting `strand:started`.
|
|
1453
|
+
*
|
|
1454
|
+
* Idempotent no-op when the strand manager already tracks `strand.Id` — the
|
|
1455
|
+
* ordinary founding sequence is `addStrand` (starts it locally) followed by
|
|
1456
|
+
* `publishStrand` (makes it visible), so this node's own `StrandWatcher`
|
|
1457
|
+
* rediscovers a row it already started and calls this again via
|
|
1458
|
+
* `handleStrandAdded`. Without the guard that re-entry would resolve a fresh
|
|
1459
|
+
* (already-connected) cohort seed and re-emit `strand:started` for an
|
|
1460
|
+
* instance that never stopped. The guard runs before the cohort-seed RPC
|
|
1461
|
+
* fan-out, so a rediscovery costs nothing beyond the map lookup.
|
|
737
1462
|
*/
|
|
738
1463
|
private launchStrand;
|
|
739
1464
|
/**
|
|
740
|
-
* Resolve a strand's discovery seed
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
* addresses that must not
|
|
1465
|
+
* Resolve a strand's discovery seed — the dialable strand-network multiaddr
|
|
1466
|
+
* strings for cohort siblings. Membership comes from the control network's
|
|
1467
|
+
* CadrePeer rows; the **strand-network** bootstrap addresses are resolved on
|
|
1468
|
+
* demand over the control mesh via the strand-addr RPC — deliberately NOT
|
|
1469
|
+
* from `CadrePeer.Multiaddr`, which carries *control* addresses that must not
|
|
1470
|
+
* seed the strand mesh.
|
|
745
1471
|
*
|
|
746
1472
|
* Only siblings we already hold an open control connection to are RPC'd: they
|
|
747
1473
|
* are the ones that can answer right now, and dialing them by peerId reuses the
|
|
748
1474
|
* live connection. When no connected sibling yet runs the strand the seed is
|
|
749
|
-
* empty (`[]`) —
|
|
750
|
-
*
|
|
751
|
-
*
|
|
1475
|
+
* empty (`[]`) — the empty seed self-heals on the next resume / check-in pass.
|
|
1476
|
+
* Returns an empty seed when the control DB or node is absent (not yet
|
|
1477
|
+
* started / torn down).
|
|
1478
|
+
*
|
|
1479
|
+
* When `delegatePeerId` is given (a strand launch/resume is imminent), the
|
|
1480
|
+
* pass doubles as the delegate ANNOUNCEMENT: our circuit relays are merged
|
|
1481
|
+
* into the RPC targets and every request carries the delegate peerId, so each
|
|
1482
|
+
* receiver records an admission grant BEFORE `libp2p.start()` (re-)dials the
|
|
1483
|
+
* relay reservation — the gate denial there is fatal, not degraded. A
|
|
1484
|
+
* party-member relay answers the RPC (its control node admits us as a
|
|
1485
|
+
* member); a dedicated ops/ relay does not speak the protocol and the
|
|
1486
|
+
* per-peer failure folds to `[]` — harmless, it has no membership gate and
|
|
1487
|
+
* needs no grant. The relay's direct addr rides along as the dial fallback
|
|
1488
|
+
* for a relay we are not yet connected to.
|
|
752
1489
|
*/
|
|
753
1490
|
private resolveCohortSeed;
|
|
1491
|
+
/**
|
|
1492
|
+
* The co-cadre siblings worth sending a strand-addr RPC to right now: cohort
|
|
1493
|
+
* members (self excluded) we already hold an open control connection to. They
|
|
1494
|
+
* are the ones that can answer immediately, and dialing them by peerId reuses
|
|
1495
|
+
* the live connection instead of opening a second one.
|
|
1496
|
+
*
|
|
1497
|
+
* Shared by {@link resolveCohortSeed} (launch/resume) and
|
|
1498
|
+
* {@link refreshStrandPeerAddrs} (the periodic pass) so both ask the same set.
|
|
1499
|
+
* Empty when the control DB or node is absent (not yet started / torn down).
|
|
1500
|
+
*
|
|
1501
|
+
* The read below is issued even when the node holds no control connection and
|
|
1502
|
+
* the answer is therefore empty by construction. That is deliberate on the
|
|
1503
|
+
* launch path: `control-database-solo-warm-start.spec.ts` exists to prove this
|
|
1504
|
+
* exact read does not stall as an embedder's FIRST awaited control operation,
|
|
1505
|
+
* on a warm cohort no one can reach. A caller that re-enters often enough for
|
|
1506
|
+
* the read to matter should skip the call itself, as
|
|
1507
|
+
* {@link refreshStrandPeerAddrs} does.
|
|
1508
|
+
*
|
|
1509
|
+
* NOTE: this read is UNBOUNDED and sits on the critical path an embedding app
|
|
1510
|
+
* awaits during startup — `addStrand` cannot resolve until it does, and it is
|
|
1511
|
+
* the first control operation an embedder's boot order issues (before genesis,
|
|
1512
|
+
* before seed bootstrap). Measured fine today: the warm-start-alone shape a
|
|
1513
|
+
* report pointed at — a `CadrePeer` list naming peers that are all gone, read
|
|
1514
|
+
* off real files after a restart — completes in milliseconds, and
|
|
1515
|
+
* `control-database-solo-warm-start.spec.ts` covers it under a deadline. If a
|
|
1516
|
+
* control read ever CAN stall (a transactor change that consults the network
|
|
1517
|
+
* for a local row, a storage backend with blocking I/O), give this one a
|
|
1518
|
+
* timeout — and decide then whether the breach fails `addStrand` or degrades
|
|
1519
|
+
* to an empty seed, because those promise callers different things.
|
|
1520
|
+
*/
|
|
1521
|
+
private connectedSiblingTargets;
|
|
1522
|
+
/**
|
|
1523
|
+
* The circuit relays this node's strand nodes would reserve through: the
|
|
1524
|
+
* union of the RESOLVED listen addrs' circuits — a hand-written
|
|
1525
|
+
* `network.listenAddrs` circuit entry or a `network.relayAddrs` entry, which
|
|
1526
|
+
* the strand node inherits verbatim either way — and the control node's own
|
|
1527
|
+
* live `/p2p-circuit` multiaddrs (a reservation this node discovered rather
|
|
1528
|
+
* than configured).
|
|
1529
|
+
*
|
|
1530
|
+
* NOTE: a relay the STRAND node discovers on its own (autorelay — in neither
|
|
1531
|
+
* source) gets no announcement, and a membership-gated one will deny it; fine
|
|
1532
|
+
* now, every realistic topology feeds one of the two sources.
|
|
1533
|
+
*/
|
|
1534
|
+
private circuitRelayTargets;
|
|
1535
|
+
/**
|
|
1536
|
+
* Record the announce timestamps {@link refreshDelegateGrants} throttles on,
|
|
1537
|
+
* for every relay a delegate-carrying announce pass dialed.
|
|
1538
|
+
*
|
|
1539
|
+
* Recorded OPTIMISTICALLY at announce time: `collectStrandAddrs` folds
|
|
1540
|
+
* per-peer failure to `[]` and reports no per-peer success, and threading
|
|
1541
|
+
* success out would change its API for little gain. A failed INITIAL announce
|
|
1542
|
+
* is fatal-at-start anyway (the relay denies the reservation,
|
|
1543
|
+
* `libp2p.start()` throws, and wake/check-in re-resume retries with a fresh
|
|
1544
|
+
* announce); a failed REFRESH retries within `DELEGATE_GRANT_TTL_MS / 2`
|
|
1545
|
+
* (15 min), still inside the 30 min TTL.
|
|
1546
|
+
*/
|
|
1547
|
+
private recordDelegateAnnounces;
|
|
1548
|
+
/**
|
|
1549
|
+
* Re-announce every running strand's delegate peerId to this node's circuit
|
|
1550
|
+
* relays, throttled to once per `DELEGATE_GRANT_TTL_MS / 2` per
|
|
1551
|
+
* (relay, strand). A grant must outlive the reservation: a dropped relay
|
|
1552
|
+
* connection makes the strand's circuit-relay transport re-dial and face the
|
|
1553
|
+
* connection gate again, so the relay must still hold a live grant then.
|
|
1554
|
+
* RELAY targets only — siblings get their announce on every launch/resume
|
|
1555
|
+
* seed pass, and a grant only matters where a reservation can be re-dialed.
|
|
1556
|
+
*
|
|
1557
|
+
* Strands announce CONCURRENTLY: this runs ahead of the reconcile pass's
|
|
1558
|
+
* sibling enumeration, and one unreachable relay costs a dial timeout per
|
|
1559
|
+
* target, which must not stack up per strand.
|
|
1560
|
+
*/
|
|
1561
|
+
private refreshDelegateGrants;
|
|
1562
|
+
/** One strand's share of {@link refreshDelegateGrants}: announce to the relays whose grant is due. */
|
|
1563
|
+
private announceDelegateToDueRelays;
|
|
1564
|
+
/**
|
|
1565
|
+
* Re-resolve each running strand's SIBLING addresses over the control mesh and
|
|
1566
|
+
* merge them into that strand's own libp2p address book, throttled to once per
|
|
1567
|
+
* {@link STRAND_PEER_ADDR_REFRESH_MS} per strand.
|
|
1568
|
+
*
|
|
1569
|
+
* Without this, a strand's address book is written exactly once — the
|
|
1570
|
+
* launch/resume seed — and everything below cadre-core that dials a strand peer
|
|
1571
|
+
* by bare peer id (Optimystic's cluster and repo clients, FRET ping/announce)
|
|
1572
|
+
* loses its address for any sibling it is not currently connected to: a sibling
|
|
1573
|
+
* that restarted its strand node or rotated its relay reservation is never
|
|
1574
|
+
* re-resolved, and even the original seed addresses fall off at the peerStore's
|
|
1575
|
+
* one-hour expiry (see `peer-addr-book.ts`).
|
|
1576
|
+
*
|
|
1577
|
+
* Distinct from {@link refreshDelegateGrants}, deliberately: that pass covers
|
|
1578
|
+
* RELAYS on a `DELEGATE_GRANT_TTL_MS / 2` throttle to keep circuit-relay
|
|
1579
|
+
* admission grants alive, this one covers SIBLINGS on an address-expiry
|
|
1580
|
+
* throttle to keep the address book warm. They overlap only in that both carry
|
|
1581
|
+
* `delegatePeerId`, so a sibling that also runs a relay gets its grant
|
|
1582
|
+
* refreshed here as a side effect. Merging them would tie an admission-grant
|
|
1583
|
+
* TTL to an address-expiry window that has nothing to do with it.
|
|
1584
|
+
*
|
|
1585
|
+
* Strands refresh CONCURRENTLY, like `refreshDelegateGrants`, so one
|
|
1586
|
+
* unreachable sibling's dial timeout does not stack up per strand.
|
|
1587
|
+
*/
|
|
1588
|
+
private refreshStrandPeerAddrs;
|
|
1589
|
+
/**
|
|
1590
|
+
* One strand's share of {@link refreshStrandPeerAddrs}: RPC the siblings, then
|
|
1591
|
+
* merge their answers into this strand's address book. Errors are logged and
|
|
1592
|
+
* swallowed so one strand's failure never costs the others their refresh.
|
|
1593
|
+
*/
|
|
1594
|
+
private refreshOneStrandPeerAddrs;
|
|
1595
|
+
/**
|
|
1596
|
+
* Merge strand-network addresses into ONE strand node's libp2p address book,
|
|
1597
|
+
* attributed per peer. Best-effort throughout: an address-book write must never
|
|
1598
|
+
* fail a launch, a resume, or a reconcile pass.
|
|
1599
|
+
*
|
|
1600
|
+
* `addrs` is the peer-agnostic union the strand-addr RPC returns, so
|
|
1601
|
+
* {@link groupAddrsByPeerId} attributes each entry to the **strand transport**
|
|
1602
|
+
* peerId in its final `/p2p/` component — never the sibling's control peerId,
|
|
1603
|
+
* which names a different libp2p node entirely. Entries that name no peer are
|
|
1604
|
+
* dropped there and counted here, once per pass.
|
|
1605
|
+
*
|
|
1606
|
+
* NOTE: a member could answer with arbitrary multiaddrs bound to arbitrary peer
|
|
1607
|
+
* ids and poison this address book. That is the same exposure the launch-time
|
|
1608
|
+
* `bootstrapNodes` seeding already accepts, and the cost is bounded: an address
|
|
1609
|
+
* grants no authority, the dialed peer authenticates by peer id at the
|
|
1610
|
+
* handshake, and a bad entry costs one failed dial that ages out at the
|
|
1611
|
+
* peerStore's one-hour expiry. No new gating here — cross-party strand trust is
|
|
1612
|
+
* `backlog/strand-network-nat-relay-reachability`.
|
|
1613
|
+
*/
|
|
1614
|
+
private mergeStrandPeerAddrs;
|
|
754
1615
|
/**
|
|
755
1616
|
* The local strand instance's dialable strand-network multiaddrs for the
|
|
756
1617
|
* strand-addr RPC, ordered signaling-first (reusing the control node's
|
|
@@ -762,9 +1623,26 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
762
1623
|
*/
|
|
763
1624
|
private getStrandMultiaddrs;
|
|
764
1625
|
/**
|
|
765
|
-
*
|
|
1626
|
+
* Stop a strand on THIS node only: untrack it from hibernation, drop its sApp config,
|
|
1627
|
+
* stop the local instance, and emit `strand:stopped`. The shared `Strand` row is left
|
|
1628
|
+
* intact, so on the next node restart (or watcher poll) the strand is rediscovered and
|
|
1629
|
+
* surfaces as `strand:discovered` again. Party-wide removal is {@link unpublishStrand}.
|
|
766
1630
|
*/
|
|
767
|
-
|
|
1631
|
+
stopStrand(strandId: string): Promise<void>;
|
|
1632
|
+
/**
|
|
1633
|
+
* Local teardown for one strand, shared by the caller-driven {@link stopStrand} and the
|
|
1634
|
+
* watcher-driven `handleStrandRemoved`: untrack hibernation, drop the sApp config, stop
|
|
1635
|
+
* the instance, emit `strand:stopped`. Touches no control-plane row — which side of the
|
|
1636
|
+
* removal the node is on is the caller's concern, not this method's.
|
|
1637
|
+
*
|
|
1638
|
+
* The stop + emit are skipped when the strand manager holds no instance for `strandId` —
|
|
1639
|
+
* e.g. a party owner that published a strand's row but never ran it locally, or an
|
|
1640
|
+
* explicit {@link stopStrand} for an id this node never started. `hibernationManager`
|
|
1641
|
+
* untrack and the `sAppConfigs` delete stay unconditional (both are no-ops when there is
|
|
1642
|
+
* nothing to remove), so a launch that failed before an instance was ever tracked still
|
|
1643
|
+
* gets its stray sApp config cleared.
|
|
1644
|
+
*/
|
|
1645
|
+
private detachStrand;
|
|
768
1646
|
/**
|
|
769
1647
|
* Record activity on a strand (resets hibernation timer).
|
|
770
1648
|
*
|
|
@@ -841,10 +1719,58 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
841
1719
|
* Get the control network node (for advanced use)
|
|
842
1720
|
*/
|
|
843
1721
|
getControlNode(): Libp2p | null;
|
|
1722
|
+
/**
|
|
1723
|
+
* Start keeping a relay reservation: dial the given relay(s) from the control
|
|
1724
|
+
* node, ask the first one that answers for a reservation slot, wait until the
|
|
1725
|
+
* resulting `/p2p-circuit` address makes this node dialable — and keep a
|
|
1726
|
+
* supervisor running that re-drives whenever the reservation is later lost.
|
|
1727
|
+
*
|
|
1728
|
+
* For nodes that listen on the bare `/p2p-circuit` SEARCH addr (a browser tab)
|
|
1729
|
+
* rather than a configured `network.relayAddrs` circuit listener — see
|
|
1730
|
+
* `relay-reservation.ts` for why the two are alternatives and why configuring
|
|
1731
|
+
* both is a regression. Opt-in: nothing calls this for the CLI/host nodes.
|
|
1732
|
+
*
|
|
1733
|
+
* Resolves once the FIRST attempt has settled, exactly as it did when it drove
|
|
1734
|
+
* once; the retries continue in the background until {@link stop} or the next
|
|
1735
|
+
* `reserveRelays` call. Fail-soft — never throws. An unreachable relay, a
|
|
1736
|
+
* missing control node, or a timeout all resolve to a non-`reserved` status, so
|
|
1737
|
+
* a caller can await this during startup without a dead relay aborting the node.
|
|
1738
|
+
*
|
|
1739
|
+
* Passing an empty list stops the supervisor and resets the posture to `none`
|
|
1740
|
+
* without dialing or waiting.
|
|
1741
|
+
*/
|
|
1742
|
+
reserveRelays(addrs: string[], opts?: RelayReservationSupervisorOptions): Promise<RelayReservationState>;
|
|
1743
|
+
/**
|
|
1744
|
+
* The node's CURRENT relay-reservation posture, recomputed from the control
|
|
1745
|
+
* node's live multiaddrs on every call.
|
|
1746
|
+
*
|
|
1747
|
+
* Deliberately not memoised: a reservation can be lost after
|
|
1748
|
+
* {@link reserveRelays} succeeds (the relay restarts, the connection drops) and
|
|
1749
|
+
* a cached `reserved` would let a caller mint invitations carrying circuit
|
|
1750
|
+
* addresses that no longer route.
|
|
1751
|
+
*
|
|
1752
|
+
* A lost reservation now recovers on its own — the supervisor {@link reserveRelays}
|
|
1753
|
+
* starts re-drives on a backoff, reporting `retrying` meanwhile. `error` here
|
|
1754
|
+
* means nothing is going to try again: no supervisor, or no control node.
|
|
1755
|
+
*/
|
|
1756
|
+
getRelayReservationState(): RelayReservationState;
|
|
844
1757
|
/**
|
|
845
1758
|
* Get the control database (for advanced queries)
|
|
846
1759
|
*/
|
|
847
1760
|
getControlDatabase(): ControlDatabase | null;
|
|
1761
|
+
/**
|
|
1762
|
+
* Open control-network connections right now. A lower-bound proxy for replication
|
|
1763
|
+
* reach: 0 connections ⇒ a control write commits local-only. The private
|
|
1764
|
+
* `committedAlone` write-while-alone seam is defined in terms of this.
|
|
1765
|
+
*
|
|
1766
|
+
* It approximates the precise signal (the block's cluster size), and a caller that
|
|
1767
|
+
* samples it AFTER a write samples a slightly wider window than `committedAlone`
|
|
1768
|
+
* does inside the write itself. It exists so an embedder can warn an owner before a
|
|
1769
|
+
* control write that this machine currently sees none of its siblings, and report
|
|
1770
|
+
* after one that the write may not have travelled — a warning shown unconditionally
|
|
1771
|
+
* is a warning people learn to ignore.
|
|
1772
|
+
*/
|
|
1773
|
+
getControlConnectionCount(): number;
|
|
848
1774
|
/**
|
|
849
1775
|
* Force a poll of the strand watcher (for testing)
|
|
850
1776
|
*/
|
|
@@ -872,7 +1798,10 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
872
1798
|
*/
|
|
873
1799
|
private resolveInviteAddresses;
|
|
874
1800
|
/**
|
|
875
|
-
* Enumerate the cadre's `CadrePeer` membership
|
|
1801
|
+
* Enumerate the cadre's `CadrePeer` membership — the ADDRESSABLE surface (see
|
|
1802
|
+
* {@link isMember}). Rows whose stamp is retired in `CadreControl.Revocation`
|
|
1803
|
+
* never reach here: {@link ControlDatabase.queryCadrePeers} excludes them, so a
|
|
1804
|
+
* revoked peer is not addressable either.
|
|
876
1805
|
*/
|
|
877
1806
|
listMembers(): Promise<Array<{
|
|
878
1807
|
peerId: string;
|
|
@@ -883,30 +1812,72 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
883
1812
|
*
|
|
884
1813
|
* This is the ADDRESSABLE surface ("do I have a dialable address record for this
|
|
885
1814
|
* peer") — it includes this node's own self-published row. Address resolution and
|
|
886
|
-
* push fan-out use this.
|
|
1815
|
+
* push fan-out use this. A peer whose stamp is retired in `CadreControl.Revocation`
|
|
1816
|
+
* is excluded here too (the filter lives in
|
|
1817
|
+
* {@link ControlDatabase.queryCadrePeers}): revocation removes a peer from the
|
|
1818
|
+
* addressable surface, not only the trust-facing one, so a revoked peer is no
|
|
1819
|
+
* longer dialed, RPC'd, or handed out as an address. The trust-facing gate is
|
|
1820
|
+
* {@link isAuthorizedMember}.
|
|
887
1821
|
*/
|
|
888
1822
|
isMember(peerId: string): Promise<boolean>;
|
|
889
1823
|
/**
|
|
890
1824
|
* Enumerate the party's AUTHORIZED members — the trust-facing set, distinct from
|
|
891
|
-
* the addressable set ({@link listMembers}).
|
|
892
|
-
*
|
|
893
|
-
*
|
|
1825
|
+
* the addressable set ({@link listMembers}). A peer is authorized iff ALL hold:
|
|
1826
|
+
*
|
|
1827
|
+
* 1. it is not this node itself — a node publishes its own `CadrePeer` address
|
|
1828
|
+
* row so its dialable address rides in seeds, but "self" is not a peer this
|
|
1829
|
+
* node authorized;
|
|
1830
|
+
* 2. its `CadrePeer` row carries a complete voucher (`StampId`, `VouchOwner`,
|
|
1831
|
+
* `VouchSig` all non-null);
|
|
1832
|
+
* 3. `VouchOwner` is in the NODE-LOCAL trusted-owner anchor
|
|
1833
|
+
* ({@link getTrustedOwnerStore}) — never the replicated `OwnerKey` table,
|
|
1834
|
+
* which any stranger can genesis-pollute; and
|
|
1835
|
+
* 4. `VouchSig` verifies as that owner's signature over the row's voucher
|
|
1836
|
+
* digest ({@link verifyCadrePeerVoucher}), so the anchored owner really
|
|
1837
|
+
* vouched THIS peer id under THIS row's nonce; and
|
|
1838
|
+
* 5. the row's `StampId` is NOT retired in `CadreControl.Revocation` — enforced
|
|
1839
|
+
* upstream in {@link ControlDatabase.queryCadrePeers}, which drops retired rows
|
|
1840
|
+
* before ANY reader (this predicate included) sees them, so a row resurrected
|
|
1841
|
+
* by replaying the captured admission approval on a node that had not yet
|
|
1842
|
+
* converged on the tombstone (the write-time `NotRevoked` CHECK only sees
|
|
1843
|
+
* local rows) is still inert to every reader that has the tombstone.
|
|
1844
|
+
*
|
|
1845
|
+
* Fail-closed at every step: a missing anchor (pre-start), an empty anchor (a
|
|
1846
|
+
* not-yet-enrolled node authorizes no one), a null/partial voucher, an
|
|
1847
|
+
* unanchored `VouchOwner`, or a bad signature all yield "not authorized" —
|
|
1848
|
+
* having an address row is NOT membership. The control-network wake and
|
|
894
1849
|
* strand-address gates consult this set, NOT the addressable one.
|
|
895
1850
|
*
|
|
896
|
-
* NOTE:
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
1851
|
+
* NOTE (rotation): if a party owner rotates keys and only the NEW key is pinned
|
|
1852
|
+
* in the anchor, rows the OLD key vouched fail check 3 until re-vouched — a
|
|
1853
|
+
* legit member goes un-authorized on readers that only pin the new key. Full
|
|
1854
|
+
* rotation handling (re-vouch on rotate) is the
|
|
1855
|
+
* `flip-strand-membership-rotation-known-gap` work, not this predicate's.
|
|
901
1856
|
*/
|
|
902
1857
|
listAuthorizedMembers(): Promise<Array<{
|
|
903
1858
|
peerId: string;
|
|
904
1859
|
multiaddr: string | null;
|
|
905
1860
|
}>>;
|
|
1861
|
+
/**
|
|
1862
|
+
* Checks 2–4 of the authorized-membership predicate (see
|
|
1863
|
+
* {@link listAuthorizedMembers}) for one `CadrePeer` row: complete voucher,
|
|
1864
|
+
* `VouchOwner` in the node-local anchor, signature valid over the row's
|
|
1865
|
+
* (PeerId, StampId) voucher digest.
|
|
1866
|
+
*
|
|
1867
|
+
* NOTE: verifies the ed25519 signature on every call (no memo of already-
|
|
1868
|
+
* verified (peerId, stampId, vouchSig) triples). Cadres are a handful of
|
|
1869
|
+
* devices and the gates run per inbound request, so this is cheap today; if
|
|
1870
|
+
* membership or gate traffic ever grows, cache verified triples keyed on the
|
|
1871
|
+
* row's `StampId` (which rotates with every re-vouch).
|
|
1872
|
+
*/
|
|
1873
|
+
private hasAnchoredVoucher;
|
|
906
1874
|
/**
|
|
907
1875
|
* Probe whether a given peer is an AUTHORIZED party member (see
|
|
908
1876
|
* {@link listAuthorizedMembers}) — the gate the control-network wake and
|
|
909
|
-
* strand-address responders consult, NOT {@link isMember} (the addressable
|
|
1877
|
+
* strand-address responders consult, NOT {@link isMember} (the addressable
|
|
1878
|
+
* surface). Deliberately scans the full membership (one `CadrePeer` query)
|
|
1879
|
+
* rather than adding a single-row read path: cadres are small, and one code
|
|
1880
|
+
* path keeps the predicate impossible to drift from the list.
|
|
910
1881
|
*/
|
|
911
1882
|
isAuthorizedMember(peerId: string): Promise<boolean>;
|
|
912
1883
|
/**
|
|
@@ -935,6 +1906,68 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
935
1906
|
* Get the seed bootstrap service (for advanced use)
|
|
936
1907
|
*/
|
|
937
1908
|
getSeedBootstrapService(): SeedBootstrapService | null;
|
|
1909
|
+
/**
|
|
1910
|
+
* The event callbacks every {@link SeedBootstrapService} this node owns is
|
|
1911
|
+
* wired with — shared by the owner-capable service ({@link initializeSeedBootstrap})
|
|
1912
|
+
* and the listener-only one ({@link enableSeedListener}) so the two cannot drift.
|
|
1913
|
+
*
|
|
1914
|
+
* A seed applied by the INBOUND protocol handler writes no `CadrePeer` row of
|
|
1915
|
+
* its own (it merges the libp2p peer store and dials owners), so the automatic
|
|
1916
|
+
* write-driven refresh never fires for it — yet applying it can ANCHOR a new
|
|
1917
|
+
* owner key, which flips rows already present from unauthorized to authorized.
|
|
1918
|
+
* Hence the explicit refresh here; without it a peer the freshly anchored owner
|
|
1919
|
+
* vouched for stays denied until the next timed cohort reconcile.
|
|
1920
|
+
*/
|
|
1921
|
+
private seedEventCallbacks;
|
|
1922
|
+
/**
|
|
1923
|
+
* Re-materialize the authorized-peer snapshot that the fail-closed per-stream
|
|
1924
|
+
* control-DB gate ({@link authorizeInboundControlStream}) judges against.
|
|
1925
|
+
*
|
|
1926
|
+
* NO `CadrePeer` writer needs to call this: the control database notifies it
|
|
1927
|
+
* after every committed member-row write (`ControlDatabase.mutateCadrePeer`,
|
|
1928
|
+
* wired in {@link start}), which is exactly what makes the refresh automatic
|
|
1929
|
+
* rather than a caller obligation. It stays public for the changes that write
|
|
1930
|
+
* NO row locally and so raise no notification:
|
|
1931
|
+
*
|
|
1932
|
+
* - a membership row that arrived by REPLICATION (otherwise picked up on the
|
|
1933
|
+
* next timed cohort reconcile — bounded staleness by design), and
|
|
1934
|
+
* - a newly anchored trusted owner key ({@link applySeed}), which flips rows
|
|
1935
|
+
* ALREADY present from unauthorized to authorized without touching them.
|
|
1936
|
+
*
|
|
1937
|
+
* Coalescing: marks the snapshot stale and resolves once a refresh that began
|
|
1938
|
+
* after this call has completed, so a caller that awaits it always observes
|
|
1939
|
+
* its own change. Concurrent callers share one read. Idempotent and
|
|
1940
|
+
* best-effort (a failed read keeps the previous snapshot); never rejects.
|
|
1941
|
+
*/
|
|
1942
|
+
refreshMembershipGate(reason?: string): Promise<void>;
|
|
1943
|
+
/**
|
|
1944
|
+
* The single in-flight refresh loop behind {@link refreshMembershipGate}: read
|
|
1945
|
+
* until the stale flag stays clear, then release the slot.
|
|
1946
|
+
*
|
|
1947
|
+
* NOTE: correct only because {@link membershipGateDirty} is guaranteed TRUE at
|
|
1948
|
+
* entry — its sole caller sets it synchronously, with no await in between. Were
|
|
1949
|
+
* this ever invoked with the flag already clear, the body would never suspend,
|
|
1950
|
+
* so the `finally` would null the slot BEFORE the caller's `??=` filled it, and
|
|
1951
|
+
* the slot would be left holding an already-settled drain that every later
|
|
1952
|
+
* refresh reuses — an unbreakable await/re-check spin. If a second caller is
|
|
1953
|
+
* ever added, have it mark the flag first (or assert it here).
|
|
1954
|
+
*/
|
|
1955
|
+
private drainMembershipGate;
|
|
1956
|
+
/**
|
|
1957
|
+
* Collapse a burst of `CadrePeer` writes into ONE gate refresh at scope exit.
|
|
1958
|
+
*
|
|
1959
|
+
* For loops that re-touch many rows (the write-while-alone drains), where a
|
|
1960
|
+
* per-row refresh would mean one full membership read per row for a snapshot
|
|
1961
|
+
* that only has to be correct once the loop settles.
|
|
1962
|
+
*
|
|
1963
|
+
* The depth counter is instance-level, so it also suppresses a genuinely
|
|
1964
|
+
* CONCURRENT external write's refresh for the life of the scope: that writer's
|
|
1965
|
+
* promise resolves before its peer is admitted, and admission lands at scope
|
|
1966
|
+
* exit instead. Acceptable — scope exit is milliseconds away (bounded by the
|
|
1967
|
+
* drain), versus the ~15 s reconcile interval that was the alternative before
|
|
1968
|
+
* any of this existed — but it is the reason these scopes stay short and rare.
|
|
1969
|
+
*/
|
|
1970
|
+
private deferMembershipGateRefresh;
|
|
938
1971
|
/**
|
|
939
1972
|
* Authorize a new peer to join the cadre.
|
|
940
1973
|
* Signs the peer ID with the owner key and inserts into CadrePeer table.
|
|
@@ -967,6 +2000,13 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
967
2000
|
applySeed(seed: ControlNetworkSeed, options?: {
|
|
968
2001
|
trustPolicy?: SeedTrustPolicy;
|
|
969
2002
|
}): Promise<ApplySeedResult>;
|
|
2003
|
+
/**
|
|
2004
|
+
* Post-process an {@link applySeed} outcome: retain the seed's owner peers as
|
|
2005
|
+
* cold-start bootstrap targets, and surface a seed that was accepted but whose
|
|
2006
|
+
* every owner dial failed — the node is now seeded yet unconnected, and the
|
|
2007
|
+
* cold-start reconcile branch is what gets it out of that state.
|
|
2008
|
+
*/
|
|
2009
|
+
private noteAppliedSeed;
|
|
970
2010
|
/**
|
|
971
2011
|
* Deliver a seed directly to a peer via the /sereus/seed/1.0.0 protocol.
|
|
972
2012
|
*/
|