@serfab/cadre-core 0.12.0 → 1.0.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 +23 -8
- package/dist/bootstrap-peer-store-file.js +2 -2
- package/dist/bootstrap-peer-store.d.ts +32 -12
- package/dist/bootstrap-peer-store.js +41 -15
- package/dist/bootstrap-peer-store.js.map +1 -1
- package/dist/cadre-node.d.ts +883 -102
- package/dist/cadre-node.js +1800 -209
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +2 -2
- package/dist/control-authorization.js +1 -0
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +13 -26
- package/dist/control-cohort.js +0 -26
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +318 -44
- package/dist/control-database.js +516 -74
- package/dist/control-database.js.map +1 -1
- package/dist/control-read-retry.d.ts +40 -11
- package/dist/control-read-retry.js +48 -15
- package/dist/control-read-retry.js.map +1 -1
- package/dist/control-retry.d.ts +58 -3
- package/dist/control-retry.js +58 -3
- package/dist/control-retry.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +133 -21
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +80 -11
- package/dist/control-write-retry.js +168 -29
- package/dist/control-write-retry.js.map +1 -1
- package/dist/enrolled-machine-store-file.d.ts +13 -0
- package/dist/enrolled-machine-store-file.js +41 -0
- package/dist/enrolled-machine-store-file.js.map +1 -0
- package/dist/enrolled-machine-store.d.ts +81 -0
- package/dist/enrolled-machine-store.js +225 -0
- package/dist/enrolled-machine-store.js.map +1 -0
- package/dist/file-durable-slot.js +5 -3
- package/dist/file-durable-slot.js.map +1 -1
- package/dist/index.d.ts +17 -8
- package/dist/index.js +54 -9
- package/dist/index.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +15 -1
- package/dist/membership-connection-gater.js +9 -3
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +21 -7
- package/dist/node-local-snapshot.js +30 -9
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/peer-dial.d.ts +143 -0
- package/dist/peer-dial.js +176 -0
- package/dist/peer-dial.js.map +1 -0
- package/dist/{strand-backfill.d.ts → peer-join-backfill.d.ts} +49 -20
- package/dist/{strand-backfill.js → peer-join-backfill.js} +133 -52
- package/dist/peer-join-backfill.js.map +1 -0
- package/dist/relay-addrs.d.ts +160 -49
- package/dist/relay-addrs.js +287 -60
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +71 -3
- package/dist/relay-reservation.js +116 -8
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +30 -1
- package/dist/seed-bootstrap.js +62 -17
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/storage-scope.d.ts +62 -0
- package/dist/storage-scope.js +75 -0
- package/dist/storage-scope.js.map +1 -0
- package/dist/strand-database.d.ts +41 -11
- package/dist/strand-database.js +48 -18
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +152 -0
- package/dist/strand-first-sync-gate.js +208 -0
- package/dist/strand-first-sync-gate.js.map +1 -0
- package/dist/strand-formation-manager.d.ts +99 -5
- package/dist/strand-formation-manager.js +119 -8
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +97 -3
- package/dist/strand-formation-protocol.js +108 -2
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +389 -19
- package/dist/strand-instance-manager.js +678 -65
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-key.d.ts +20 -12
- package/dist/strand-member-key.js +20 -12
- package/dist/strand-member-key.js.map +1 -1
- package/dist/strand-member-registry.js +3 -0
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +361 -0
- package/dist/strand-membership-reconciler.js +548 -0
- package/dist/strand-membership-reconciler.js.map +1 -0
- package/dist/strand-membership-writer.d.ts +143 -11
- package/dist/strand-membership-writer.js +419 -135
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-network-config.d.ts +116 -0
- package/dist/strand-network-config.js +192 -0
- package/dist/strand-network-config.js.map +1 -0
- package/dist/strand-revocation-enforcer.d.ts +392 -0
- package/dist/strand-revocation-enforcer.js +533 -0
- package/dist/strand-revocation-enforcer.js.map +1 -0
- package/dist/strand-solicitation.d.ts +17 -1
- package/dist/strand-solicitation.js +13 -2
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +11 -7
- package/dist/strand-wake-protocol.js +20 -52
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/strand-watcher.js +12 -1
- package/dist/strand-watcher.js.map +1 -1
- package/dist/timed-step.d.ts +12 -0
- package/dist/timed-step.js +28 -0
- package/dist/timed-step.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +19 -0
- package/dist/timeout-scheduler.js +22 -0
- package/dist/timeout-scheduler.js.map +1 -0
- package/dist/types.d.ts +472 -46
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/package.json +16 -8
- package/src/bootstrap-peer-store-file.ts +2 -2
- package/src/bootstrap-peer-store.ts +69 -27
- package/src/cadre-node.ts +1926 -228
- package/src/control-authorization.ts +2 -1
- package/src/control-cohort.ts +122 -130
- package/src/control-database.ts +586 -74
- package/src/control-read-retry.ts +202 -167
- package/src/control-retry.ts +297 -190
- package/src/control-schema.ts +133 -21
- package/src/control-write-retry.ts +525 -383
- package/src/enrolled-machine-store-file.ts +43 -0
- package/src/enrolled-machine-store.ts +280 -0
- package/src/file-durable-slot.ts +5 -3
- package/src/index.ts +134 -15
- package/src/membership-connection-gater.ts +9 -3
- package/src/node-local-snapshot.ts +38 -9
- package/src/peer-addr-book.ts +251 -250
- package/src/peer-dial.ts +215 -0
- package/src/{strand-backfill.ts → peer-join-backfill.ts} +478 -389
- package/src/relay-addrs.ts +458 -218
- package/src/relay-reservation.ts +951 -828
- package/src/seed-bootstrap.ts +76 -18
- package/src/storage-scope.ts +78 -0
- package/src/strand-database.ts +61 -21
- package/src/strand-first-sync-gate.ts +258 -0
- package/src/strand-formation-manager.ts +162 -8
- package/src/strand-formation-protocol.ts +850 -692
- package/src/strand-instance-manager.ts +877 -74
- package/src/strand-member-key.ts +20 -12
- package/src/strand-member-registry.ts +3 -0
- package/src/strand-membership-reconciler.ts +680 -0
- package/src/strand-membership-writer.ts +550 -190
- package/src/strand-network-config.ts +250 -0
- package/src/strand-revocation-enforcer.ts +653 -0
- package/src/strand-solicitation.ts +499 -471
- package/src/strand-wake-protocol.ts +21 -66
- package/src/strand-watcher.ts +325 -314
- package/src/timed-step.ts +28 -0
- package/src/timeout-scheduler.ts +28 -0
- package/src/types.ts +487 -52
- package/dist/strand-backfill.js.map +0 -1
package/dist/cadre-node.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Libp2p, PeerId } from '@libp2p/interface';
|
|
2
2
|
import type { Multiaddr } from '@multiformats/multiaddr';
|
|
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';
|
|
3
|
+
import type { CadreNodeConfig, StrandInstance, StrandRow, StrandConfig, FoundStrandConfig, FoundStrandResult, SAppConfig, CadreNodeEvents, ControlNetworkSeed, ApplySeedResult, AddDroneOptions, AddPhoneOptions, DroneInitResult, InviteResult, CadreInvite, OpenInvitation, FormStrandResult, StrandFormationDisclosure, StrandMembershipInvite, ResolveOpts, SelfRegistrationOutcome, ServiceWakeResult, PushPlatform, DeviceTokenRecord, ResolveDeviceTokenOpts } from './types.js';
|
|
4
4
|
import { type Ed25519KeyPair } from './ed25519-key.js';
|
|
5
5
|
import { type TrustedOwnerStore, type TrustSource } from './trusted-owner-store.js';
|
|
6
6
|
import { type BootstrapPeerStore } from './bootstrap-peer-store.js';
|
|
7
|
+
import { type EnrolledMachineStore } from './enrolled-machine-store.js';
|
|
7
8
|
import { type SAppIdLookup } from './strand-watcher.js';
|
|
9
|
+
import { type ControlCohortReconcileResult } from './control-cohort.js';
|
|
8
10
|
import { EnrollmentService } from './enrollment.js';
|
|
9
11
|
import { ControlDatabase } from './control-database.js';
|
|
10
12
|
import { SeedBootstrapService } from './seed-bootstrap.js';
|
|
@@ -57,6 +59,14 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
57
59
|
private trustedOwnerStore;
|
|
58
60
|
private controlNode;
|
|
59
61
|
private controlDatabase;
|
|
62
|
+
/**
|
|
63
|
+
* The CONTROL network's peer-join block catch-up — same module as the
|
|
64
|
+
* per-strand ones, but membership-gated (see {@link startControlBackfill}).
|
|
65
|
+
* Created in {@link start} after the control node and database are up, stopped
|
|
66
|
+
* and dropped in {@link cleanup} before either is torn down, so a stop()→
|
|
67
|
+
* start() cycle rebuilds it with a fresh caught-up-peer memo.
|
|
68
|
+
*/
|
|
69
|
+
private controlBackfill;
|
|
60
70
|
private strandWatcher;
|
|
61
71
|
private strandManager;
|
|
62
72
|
private hibernationManager;
|
|
@@ -82,10 +92,11 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
82
92
|
private _running;
|
|
83
93
|
/**
|
|
84
94
|
* 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
|
-
*
|
|
87
|
-
* `
|
|
88
|
-
*
|
|
95
|
+
* `start()` and released in {@link cleanup}. Owning it is what keeps the
|
|
96
|
+
* provider call for the control scope — `controlStorageScope(partyId)`, see
|
|
97
|
+
* `storage-scope.ts` — a once-per-runtime call: a `stop()` then `start()` cycle
|
|
98
|
+
* on this object re-resolves against a live cache, and never orphans the
|
|
99
|
+
* previous wrapper's registration in the shared cache pool.
|
|
89
100
|
*/
|
|
90
101
|
private controlStorage;
|
|
91
102
|
/**
|
|
@@ -141,6 +152,14 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
141
152
|
private controlBringUpInFlight;
|
|
142
153
|
/** Map of strandId -> sAppConfig for sAppId filtering and management */
|
|
143
154
|
private sAppConfigs;
|
|
155
|
+
/**
|
|
156
|
+
* Strands the control network advertises that no local sAppConfig claims — the
|
|
157
|
+
* backlog behind `strand:discovered`. Maintained in {@link handleStrandAdded}
|
|
158
|
+
* (added), {@link addStrand} (claimed), {@link detachStrand} (stopped or its
|
|
159
|
+
* control row vanished) and {@link cleanup}. Read through
|
|
160
|
+
* {@link getDiscoveredStrands}.
|
|
161
|
+
*/
|
|
162
|
+
private discoveredStrands;
|
|
144
163
|
/**
|
|
145
164
|
* Most-recently pushed invite addresses (see {@link setInviteAddresses}).
|
|
146
165
|
* When non-null these take priority over `libp2pNode.getMultiaddrs()` when
|
|
@@ -238,39 +257,149 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
238
257
|
*/
|
|
239
258
|
private readonly strandPeerAddrRefreshAt;
|
|
240
259
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
260
|
+
* CROSS-PARTY strand-network addresses learned at formation, keyed by strandId — the
|
|
261
|
+
* responder's live strand addrs, carried back on the formation result
|
|
262
|
+
* (`FormationResultMessage.strandAddrs`) and recorded by {@link formStrand}.
|
|
263
|
+
*
|
|
264
|
+
* This is the ONLY cross-party discovery input there is. The strand-addr RPC that
|
|
265
|
+
* resolves a strand's addresses is membership-gated and answers own-party siblings
|
|
266
|
+
* only, so without this map a joiner's cohort seed for a two-party strand is empty and
|
|
267
|
+
* the mesh never forms.
|
|
268
|
+
*
|
|
269
|
+
* Read by {@link resolveCohortSeed} (launch + hibernation resume) and re-merged by
|
|
270
|
+
* every {@link refreshStrandPeerAddrs} pass — the re-merge is what keeps the entries
|
|
271
|
+
* alive past the peerStore's one-hour address expiry, since nothing can re-resolve
|
|
272
|
+
* them.
|
|
273
|
+
*
|
|
274
|
+
* IN-MEMORY and ONE-SHOT, deliberately (see `docs/strands.md`): the addresses die with
|
|
275
|
+
* the process, and a restarted initiator with no sibling running the strand is back to
|
|
276
|
+
* an empty seed until the durability work lands
|
|
277
|
+
* (`backlog/feat-cross-party-strand-addr-durability`). They are also never refreshed,
|
|
278
|
+
* so a responder whose relay reservation rotates before the initiator dials leaves a
|
|
279
|
+
* dead entry behind; recovery today is a fresh invitation.
|
|
280
|
+
*
|
|
281
|
+
* NOTE: entries are kept for the node's lifetime, including across a
|
|
282
|
+
* {@link stopStrand} — a stopped strand may be rediscovered by the watcher and must
|
|
283
|
+
* still get its seed. Each strand's list is capped at `MAX_STRAND_ADDRS`, but the
|
|
284
|
+
* number of KEYS is bounded only by the strands this node has ever formed as an
|
|
285
|
+
* initiator (not by time); if a node ever forms strands at scale, evict on
|
|
286
|
+
* `unpublishStrand` or cap the map.
|
|
287
|
+
*/
|
|
288
|
+
private readonly crossPartyStrandAddrs;
|
|
289
|
+
/**
|
|
290
|
+
* PENDING strand membership invitations learned at formation, keyed by strandId — the
|
|
291
|
+
* single-use `Strand.Invite` credential a closed-strand formation result carried back
|
|
292
|
+
* ({@link FormStrandResult.membershipInvite}), waiting for this node's strand bring-up
|
|
293
|
+
* to redeem (`consumeInvite` seats the `Strand.Member` row under this party's own key
|
|
294
|
+
* — the `strand-node-binds-member-peer` half of the party-identity chain).
|
|
295
|
+
*
|
|
296
|
+
* IN-MEMORY, deliberately, like {@link crossPartyStrandAddrs}: a restarted joiner
|
|
297
|
+
* re-forms from scratch anyway (durability is
|
|
298
|
+
* `backlog/feat-cross-party-strand-addr-durability`'s problem), and re-forming issues
|
|
299
|
+
* a fresh invitation. A re-formation against the same strand REPLACES the entry — the
|
|
300
|
+
* fresh invitation supersedes one that may have expired. The party key the invitation
|
|
301
|
+
* admits, by contrast, IS persisted (`StrandPartyKey`, seated by
|
|
302
|
+
* {@link adoptFormationMembershipInvite} before the entry lands here), so a lost
|
|
303
|
+
* invitation never orphans an identity.
|
|
304
|
+
*
|
|
305
|
+
* INVALIDATION is owned by the strand's bring-up membership reconciler
|
|
306
|
+
* (`strand-membership-reconciler.ts`, wired via `StartStrandConfig.pendingMembershipInvite`
|
|
307
|
+
* in {@link launchStrand}): it deletes the entry once the invitation is redeemed, burned
|
|
308
|
+
* against an already-seated member, or found dead (expired / cancelled / consumed
|
|
309
|
+
* elsewhere / the strand sealed). Until the strand is actually launched here the entry
|
|
310
|
+
* just waits.
|
|
311
|
+
*
|
|
312
|
+
* NOTE: entries live for the node's lifetime (one small pair per formed closed
|
|
313
|
+
* strand), the same unbounded-keys tripwire {@link crossPartyStrandAddrs} documents —
|
|
314
|
+
* if a node ever forms strands at scale, evict alongside that map.
|
|
315
|
+
*/
|
|
316
|
+
private readonly pendingMembershipInvites;
|
|
317
|
+
/**
|
|
318
|
+
* Founder launches refused because the strand was founded before the per-party
|
|
319
|
+
* identity split (`PreSplitStrandIdentityError`), keyed by strandId. Read by
|
|
320
|
+
* {@link issueStrandMembershipInvite}, which rethrows the recorded error so a bound
|
|
321
|
+
* redemption against the strand is rejected as "must be recreated" — not told to retry
|
|
322
|
+
* because the runtime is not live (a refused fresh launch leaves none) or failed by the
|
|
323
|
+
* `Strand.Invite` gate (a refused in-place founding leaves a joiner instance up whose
|
|
324
|
+
* party key is no manager).
|
|
325
|
+
*
|
|
326
|
+
* Written and cleared by {@link launchStrand}; also cleared by {@link detachStrand},
|
|
327
|
+
* {@link unpublishStrand} and {@link cleanup}, so a recreated id starts clean.
|
|
328
|
+
* In-memory: after a restart the next founder launch of the strand records it again;
|
|
329
|
+
* until then issuance still reads the fingerprint off the live rows whenever a runtime
|
|
330
|
+
* is up — only a strand with no runtime at all answers retryably meanwhile.
|
|
331
|
+
*/
|
|
332
|
+
private readonly strandLaunchRefusals;
|
|
333
|
+
/**
|
|
334
|
+
* Dial targets learned out of band, keyed by peer id, valued by the multiaddr
|
|
335
|
+
* strings this node was handed (parsed lazily, at dial time). Two writers:
|
|
336
|
+
* {@link recordSeedBootstrapPeers} retains the owner-flagged peers of every seed
|
|
337
|
+
* this node applies, and {@link addDrone} retains the addresses of every node
|
|
338
|
+
* this node adds. Two readers: {@link dialColdStartBootstrap} dials every entry
|
|
339
|
+
* while the control database still has no siblings, and
|
|
340
|
+
* {@link resolveControlDialAddrs} falls back to a sibling's entry when neither
|
|
341
|
+
* its signed record nor the address book yields an address.
|
|
247
342
|
*
|
|
248
343
|
* A STORE rather than a plain map (see `bootstrap-peer-store.ts`), because the
|
|
249
344
|
* targets must outlive the process: a node seeded into a party it could not
|
|
250
345
|
* reach has nothing else on disk naming that party's addresses (`applySeed`
|
|
251
346
|
* writes no control row, and `CadrePeer` fills in only after a connection
|
|
252
347
|
* succeeds), so an in-memory-only set left it stranded permanently across a
|
|
253
|
-
* restart.
|
|
348
|
+
* restart. An owner that added a node it cannot be dialed by is in the same
|
|
349
|
+
* position: the added node's row stays unsigned, so unresolvable, until that
|
|
350
|
+
* node self-publishes over a connection only the owner can open. Constructed (or
|
|
351
|
+
* adopted from `config.bootstrapPeers.store`) by
|
|
254
352
|
* {@link initializeBootstrapPeerStore} during {@link start}; deliberately NOT
|
|
255
353
|
* cleared by {@link cleanup}, so it survives a stop()→start() cycle of the same
|
|
256
354
|
* node instance — same lifecycle as {@link trustedOwnerStore}. Durability
|
|
257
|
-
* depends on the injected backend
|
|
258
|
-
* RN/browser stay ephemeral for now.
|
|
355
|
+
* depends on the injected backend; the default is in-memory.
|
|
259
356
|
*
|
|
260
357
|
* Deliberately NOT the libp2p peer store, which {@link peerStoreAddrs} already
|
|
261
358
|
* reads for the steady-state path. The peer store is shared with everything
|
|
262
359
|
* libp2p discovers, so "dial every entry" would grow into dialing arbitrary
|
|
263
360
|
* discovered peers as the node lives longer; this store holds exactly the peers
|
|
264
|
-
* an owner-signed, trust-anchored seed nominated as owners
|
|
265
|
-
*
|
|
266
|
-
*
|
|
361
|
+
* an owner-signed, trust-anchored seed nominated as owners, and the nodes this
|
|
362
|
+
* node chose to add. `applySeed` also merges a seed's addresses into the peer
|
|
363
|
+
* store, where they age out; an added node's addresses are never merged there,
|
|
364
|
+
* because only verified addresses go into the address book (see
|
|
365
|
+
* {@link warmSiblingAddrBook}).
|
|
267
366
|
*
|
|
268
|
-
* A later
|
|
367
|
+
* A later record OVERWRITES an entry rather than merging, so a re-seed after an
|
|
269
368
|
* owner's address changes replaces the stale address instead of accumulating
|
|
270
|
-
* dead ones
|
|
271
|
-
* the
|
|
369
|
+
* dead ones, and {@link warmSiblingAddrBook} replaces an entry with the addresses
|
|
370
|
+
* the sibling's signed record resolves to when they differ. Only this node's own
|
|
371
|
+
* {@link removePeer} evicts an entry: otherwise they are the node's only way back
|
|
372
|
+
* to those peers if it is ever stranded again.
|
|
272
373
|
*/
|
|
273
374
|
private bootstrapPeerStore;
|
|
375
|
+
/**
|
|
376
|
+
* Node-local record of the party's enrolled-machine count (see
|
|
377
|
+
* `enrolled-machine-store.ts`), kept so the CONTROL node can declare a
|
|
378
|
+
* block-repair yardstick at bring-up. Constructed (or adopted from
|
|
379
|
+
* `config.enrolledMachines.store`) by {@link initializeEnrolledMachineStore};
|
|
380
|
+
* written by {@link refreshAuthorizedControlPeers}. Like
|
|
381
|
+
* {@link bootstrapPeerStore} it is deliberately NOT cleared by {@link cleanup},
|
|
382
|
+
* so it survives a stop()→start() cycle of the same node instance.
|
|
383
|
+
*/
|
|
384
|
+
private enrolledMachineStore;
|
|
385
|
+
/**
|
|
386
|
+
* The enrolled-machine count this node's CONTROL libp2p node was (or will be)
|
|
387
|
+
* built with — read out of {@link enrolledMachineStore} during {@link start},
|
|
388
|
+
* before {@link createControlNode}, and consumed by
|
|
389
|
+
* {@link buildControlNodeOptions}.
|
|
390
|
+
*
|
|
391
|
+
* A captured FIELD rather than a live `store.count()` read, deliberately: the
|
|
392
|
+
* store moves as membership changes, but Optimystic froze the policy when the
|
|
393
|
+
* node was built, so this field is the honest answer to "what did this node
|
|
394
|
+
* actually declare?" and does not drift away from the running node. It is
|
|
395
|
+
* re-read on each {@link start}, which is what makes a stop()→start() cycle pick
|
|
396
|
+
* up a count recorded during the previous run.
|
|
397
|
+
*
|
|
398
|
+
* `undefined` — a brand-new node, an unreadable slot, or the ephemeral default
|
|
399
|
+
* store — means "this node does not know", which `controlClusterPolicy` answers
|
|
400
|
+
* with the frozen base policy itself.
|
|
401
|
+
*/
|
|
402
|
+
private declaredEnrolledMachines;
|
|
274
403
|
/** Initial self-registration timer (see {@link scheduleSelfRegistration}). */
|
|
275
404
|
private selfRegistrationTimer;
|
|
276
405
|
/** TTL heartbeat that re-publishes the self record before it goes stale. */
|
|
@@ -298,6 +427,19 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
298
427
|
* both read "no row yet" and race a duplicate INSERT (a `CadrePeer` PK conflict).
|
|
299
428
|
*/
|
|
300
429
|
private registerSelfInFlight;
|
|
430
|
+
/**
|
|
431
|
+
* When this node last PUBLISHED its own `CadrePeer` address record (inserted or
|
|
432
|
+
* refreshed), or null if it never has in this session. Stamped by
|
|
433
|
+
* {@link noteSelfRecordPublished} on every successful publish, wherever it was driven
|
|
434
|
+
* from — the boot pass, the heartbeat, an address change, a drain or an explicit call.
|
|
435
|
+
*
|
|
436
|
+
* Read by {@link escalateIfSelfRecordStale} for the one consequence worth an operator's
|
|
437
|
+
* attention: once this gap exceeds {@link DEFAULT_PEER_RECORD_MAX_AGE_MS}, every other
|
|
438
|
+
* machine in the party is already discarding this node's address as stale.
|
|
439
|
+
*/
|
|
440
|
+
private lastSelfRecordPublishAt;
|
|
441
|
+
/** Say-once latch for {@link escalateIfSelfRecordStale}; re-armed by the next publish. */
|
|
442
|
+
private selfRecordStaleWarned;
|
|
301
443
|
/**
|
|
302
444
|
* Re-replication queue for owner `authorizePeer` writes that committed while
|
|
303
445
|
* this node was alone — `controlNode.getConnections().length === 0` at write
|
|
@@ -331,6 +473,14 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
331
473
|
* exactly a "removals from before this lifetime" case).
|
|
332
474
|
*/
|
|
333
475
|
private reissuedHeldRevocations;
|
|
476
|
+
/**
|
|
477
|
+
* Whether this process has seen the singleton `Revocation` ledger marker filed — its own
|
|
478
|
+
* `'opened'` or an `'already-open'` answer, in {@link openRevocationLedgerIfDue}. A
|
|
479
|
+
* record of work done, not a cached authorization answer: the marker can never be
|
|
480
|
+
* deleted (`NoDelete`), so this can only go stale if the database itself is replaced,
|
|
481
|
+
* which is why stop() clears it with the other per-process replication state.
|
|
482
|
+
*/
|
|
483
|
+
private revocationLedgerOpened;
|
|
334
484
|
/** This node's own `CadrePeer` self-write committed local-only (re-touched on growth). */
|
|
335
485
|
private pendingSelfPeerWrite;
|
|
336
486
|
/** This node's own `DeviceToken` self-write committed local-only (re-touched on growth). */
|
|
@@ -421,6 +571,23 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
421
571
|
* Get all strand instances
|
|
422
572
|
*/
|
|
423
573
|
getStrands(): Map<string, StrandInstance>;
|
|
574
|
+
/**
|
|
575
|
+
* Strands the control network advertises that no local sAppConfig claims —
|
|
576
|
+
* the backlog behind `strand:discovered`.
|
|
577
|
+
*
|
|
578
|
+
* The event fires once per strand, and it can fire before the app has
|
|
579
|
+
* subscribed (the watcher's first poll runs inside `start()`). So an app that
|
|
580
|
+
* auto-joins discovered strands must subscribe FIRST and then drain this map,
|
|
581
|
+
* not rely on the event alone. Entries leave the map when the strand is
|
|
582
|
+
* claimed ({@link addStrand}) or its control row disappears.
|
|
583
|
+
*
|
|
584
|
+
* Bounded by the number of strands this party has that this node does not run
|
|
585
|
+
* — the control database holds only this party's rows — so there is no cap and
|
|
586
|
+
* no eviction policy to reason about.
|
|
587
|
+
*
|
|
588
|
+
* Returns a snapshot; mutating it does not affect the node.
|
|
589
|
+
*/
|
|
590
|
+
getDiscoveredStrands(): Map<string, StrandRow>;
|
|
424
591
|
/**
|
|
425
592
|
* Get a specific strand instance
|
|
426
593
|
*/
|
|
@@ -503,10 +670,36 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
503
670
|
*/
|
|
504
671
|
private initializeBootstrapPeerStore;
|
|
505
672
|
/**
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
673
|
+
* Construct (or adopt) the node-local enrolled-machine store and capture the
|
|
674
|
+
* count this run will declare into {@link declaredEnrolledMachines}.
|
|
675
|
+
*
|
|
676
|
+
* Synchronous for the same reason {@link initializeBootstrapPeerStore} is: an
|
|
677
|
+
* injected store has already loaded its persisted count by the time it is handed
|
|
678
|
+
* in (its `open` is the async part), and the in-memory default has nothing to
|
|
679
|
+
* load. Must run BEFORE {@link createControlNode} — that is the whole point of
|
|
680
|
+
* the record; see `enrolled-machine-store.ts`.
|
|
681
|
+
*
|
|
682
|
+
* The store instance is kept across stop()→start() (so a count recorded during
|
|
683
|
+
* the previous run is not lost with the ephemeral default), but the DECLARED
|
|
684
|
+
* count is re-read every time — which is how a restart applies a number the
|
|
685
|
+
* previous run learned.
|
|
686
|
+
*
|
|
687
|
+
* A store scoped to a different party is a configuration error, fail closed. It
|
|
688
|
+
* is only a repair hint, but a hint sized by a foreign party's membership is a
|
|
689
|
+
* number nobody chose, and the mismatch always means a miswired embedder.
|
|
690
|
+
*/
|
|
691
|
+
private initializeEnrolledMachineStore;
|
|
692
|
+
/**
|
|
693
|
+
* The node-local enrolled-machine store (null before {@link start}) — what this
|
|
694
|
+
* node last knew about its party's size. Exposed for diagnostics and for a host
|
|
695
|
+
* that wants to show which repair yardstick the next launch will declare.
|
|
696
|
+
*/
|
|
697
|
+
getEnrolledMachineStore(): EnrolledMachineStore | null;
|
|
698
|
+
/**
|
|
699
|
+
* The node-local bootstrap-peer store (null before {@link start}) — the dial
|
|
700
|
+
* targets learned out of band that {@link reconcileControlCohort} falls back to.
|
|
701
|
+
* Exposed for diagnostics and for a host that wants to show "what would this
|
|
702
|
+
* node dial if it is stranded?".
|
|
510
703
|
*/
|
|
511
704
|
getBootstrapPeerStore(): BootstrapPeerStore | null;
|
|
512
705
|
/**
|
|
@@ -534,6 +727,44 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
534
727
|
*/
|
|
535
728
|
trustOwnerKeys(keys: Iterable<string>, source: Exclude<TrustSource, 'genesis'>): Promise<void>;
|
|
536
729
|
private createControlNode;
|
|
730
|
+
/**
|
|
731
|
+
* The control libp2p node's Optimystic network name — ONE binding for every
|
|
732
|
+
* derivation from it (`db-p2p` namespaces all of the node's protocol ids as
|
|
733
|
+
* `/optimystic/<networkName>/...`), so the node options and the block-transfer
|
|
734
|
+
* protocol prefix the control backfill dials can never drift apart.
|
|
735
|
+
*/
|
|
736
|
+
private controlNetworkName;
|
|
737
|
+
/**
|
|
738
|
+
* Arm the CONTROL network's peer-join block catch-up: push every block in the
|
|
739
|
+
* control database's own raw store to each newly connected AUTHORIZED member
|
|
740
|
+
* this runtime has not yet caught up. This is what physically replicates
|
|
741
|
+
* control blocks committed while the writer was alone — the named
|
|
742
|
+
* collection-header blocks written once at genesis above all, whose revision
|
|
743
|
+
* never moves again, so no later commit ever carries them to a member that
|
|
744
|
+
* joined after them. Without it, such a member that restarts offline reads
|
|
745
|
+
* the affected control tables as EMPTY, silently (`isMember()` answers false
|
|
746
|
+
* for peers it knew about before the restart).
|
|
747
|
+
*
|
|
748
|
+
* Unlike the per-strand instances (see `strand-instance-manager.ts`), pushes
|
|
749
|
+
* are gated on {@link isAuthorizedMember}, judged at push time: the control
|
|
750
|
+
* network's inbound connection gate deliberately admits non-members in
|
|
751
|
+
* several states (seed delivery to an un-enrolled node, an open enrollment
|
|
752
|
+
* window, an outstanding invitation, configured bootstrap/relay peers), and
|
|
753
|
+
* pushing the whole control store to such a peer would hand a stranger the
|
|
754
|
+
* party's entire membership, addresses and strand list. The receiving side
|
|
755
|
+
* needs no work of its own: `createLibp2pNode` registers the block-transfer
|
|
756
|
+
* handler on every node it builds, control node included, and this node's
|
|
757
|
+
* per-stream gate (`authorizeInboundControlStream`) covers the inbound
|
|
758
|
+
* direction. A denied peer is retried on its next `connection:open`, and —
|
|
759
|
+
* because the production join order is connect-then-authorize — on every
|
|
760
|
+
* committed membership change via {@link refreshAuthorizedControlPeers}'s
|
|
761
|
+
* `scheduleConnectedPeers()` call.
|
|
762
|
+
*
|
|
763
|
+
* No-ops (logged) when the embedder configured no control storage or the
|
|
764
|
+
* node exposes no key network — the backfill would have nothing to read or
|
|
765
|
+
* no way to dial.
|
|
766
|
+
*/
|
|
767
|
+
private startControlBackfill;
|
|
537
768
|
/**
|
|
538
769
|
* The control database's cache-wrapped raw storage, resolved from
|
|
539
770
|
* `config.storage.provider` on first use and then held in {@link controlStorage}
|
|
@@ -564,9 +795,8 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
564
795
|
* operator whose relay slot is decorative may genuinely want only the announced
|
|
565
796
|
* address, so this reports and proceeds.
|
|
566
797
|
*
|
|
567
|
-
* Keyed off
|
|
568
|
-
*
|
|
569
|
-
* longer route — is caught too.
|
|
798
|
+
* Keyed off `relayAddrs` AND `listenAddrs`, so a hand-written `/p2p-circuit` entry
|
|
799
|
+
* in `listenAddrs` — a reservation by the longer route — is caught too.
|
|
570
800
|
*
|
|
571
801
|
* NOTE: the only direct `console.*` in this library — a boot-time operator warning, not a
|
|
572
802
|
* diagnostic trace (those use `debug`, which an operator never sees without `DEBUG=`). If a
|
|
@@ -841,6 +1071,55 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
841
1071
|
registerSelf(): Promise<SelfRegistrationOutcome>;
|
|
842
1072
|
/** The body of {@link registerSelf}; serialised by its single-flight guard. */
|
|
843
1073
|
private publishSelfRecord;
|
|
1074
|
+
/**
|
|
1075
|
+
* Record that this node's own address record just landed, and re-arm the stale-record
|
|
1076
|
+
* warning. Called from BOTH publishing paths of {@link publishSelfRecord} and nowhere
|
|
1077
|
+
* else — a `skipped` publish wrote nothing, so it must not refresh the stamp.
|
|
1078
|
+
*/
|
|
1079
|
+
private noteSelfRecordPublished;
|
|
1080
|
+
/**
|
|
1081
|
+
* Report an abandoned control write to the embedding app.
|
|
1082
|
+
*
|
|
1083
|
+
* Every control write funnels through `ControlDatabase.lockedWithRetry`, which now tells
|
|
1084
|
+
* this node when it gives one up. Foreground writes ALSO reject to their caller; the
|
|
1085
|
+
* background ones ({@link startRecordRefresh}'s republish and the two replication
|
|
1086
|
+
* drains) are fired unawaited with a `debug`-only catch, so this event is the only thing
|
|
1087
|
+
* that reaches an app at all.
|
|
1088
|
+
*
|
|
1089
|
+
* Reporting only — the one operator-visible escalation lives on the republish path
|
|
1090
|
+
* ({@link escalateIfSelfRecordStale}), where the consequence is measurable.
|
|
1091
|
+
*/
|
|
1092
|
+
private noteControlWriteAbandoned;
|
|
1093
|
+
/**
|
|
1094
|
+
* Escalate ONE failed self-address republish to the operator — once — when the node has
|
|
1095
|
+
* not published its own record for longer than a record stays fresh.
|
|
1096
|
+
*
|
|
1097
|
+
* The bar is the CONSEQUENCE, not a failure count: a resolver discards a `CadrePeer`
|
|
1098
|
+
* record older than {@link DEFAULT_PEER_RECORD_MAX_AGE_MS} (15 minutes) and the heartbeat
|
|
1099
|
+
* re-stamps at half that, so by the time this gap opens every other machine in the party
|
|
1100
|
+
* has already stopped accepting this node's address. A count would fire far too early —
|
|
1101
|
+
* one failed heartbeat is already half the budget, and a single miss costs nothing.
|
|
1102
|
+
*
|
|
1103
|
+
* Say-once, re-armed by the next successful publish ({@link noteSelfRecordPublished}), so
|
|
1104
|
+
* a node that stays broken warns once rather than every 7.5 minutes.
|
|
1105
|
+
*
|
|
1106
|
+
* A node that has never published in this session is skipped: it has no record out there
|
|
1107
|
+
* to go stale, and the reason it has none (not a member yet, no signing key, revoked) is
|
|
1108
|
+
* already logged by `registerSelf` itself.
|
|
1109
|
+
*
|
|
1110
|
+
* Only a FAILED republish is checked. A heartbeat whose publish reports `skipped` neither
|
|
1111
|
+
* stamps nor warns, so a node that stops being able to publish without erroring — revoked,
|
|
1112
|
+
* or its row removed — goes quiet here. That is the intended reading: a revoked node has
|
|
1113
|
+
* nothing to publish and its unreachability is the point, not a fault to report.
|
|
1114
|
+
*
|
|
1115
|
+
* NOTE: the second `console.*` in this library, and the reason
|
|
1116
|
+
* {@link warnIfAnnounceAddrsDiscardRelay}'s note says a third should not simply be added:
|
|
1117
|
+
* both are operator warnings about a configuration/health condition an embedder cannot
|
|
1118
|
+
* see otherwise, and both have an event beside them (`control:write-abandoned` carries
|
|
1119
|
+
* the underlying write failure). A third such condition should surface through
|
|
1120
|
+
* {@link CadreNodeEvents} alone unless it likewise degrades the whole party in silence.
|
|
1121
|
+
*/
|
|
1122
|
+
private escalateIfSelfRecordStale;
|
|
844
1123
|
/**
|
|
845
1124
|
* Sign this node's address record for publication, stamped strictly later than
|
|
846
1125
|
* `existing` (the row it is about to replace, or null for a first insert) so a
|
|
@@ -857,6 +1136,24 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
857
1136
|
* producing an unresolvable row.
|
|
858
1137
|
*/
|
|
859
1138
|
private getSelfSigningKey;
|
|
1139
|
+
/**
|
|
1140
|
+
* Does the `Strand` row name THIS machine as its founder — i.e. is its
|
|
1141
|
+
* `FounderOwnerKey` this node's own owner key (the key behind its PeerId)?
|
|
1142
|
+
* A null/absent column (a consent-seated strand, or a hand-built row) and a
|
|
1143
|
+
* node with no owner key both derive `false`: without a positive match this
|
|
1144
|
+
* machine must attach, never bootstrap. Pure key derivation, no I/O — cheap
|
|
1145
|
+
* enough for {@link launchStrand}'s tracked-instance early return.
|
|
1146
|
+
*
|
|
1147
|
+
* NOTE: reads "one owner key per machine" — the reference model, where the owner
|
|
1148
|
+
* key IS the key behind the PeerId. Two machines running the SAME identity key
|
|
1149
|
+
* would both derive `true` and each bootstrap on its own replica, which is the
|
|
1150
|
+
* double-`Header` hazard this derivation exists to avoid. Unreachable today: that
|
|
1151
|
+
* configuration also gives both machines one PeerId, which already breaks control
|
|
1152
|
+
* networking well before any strand launches. Revisit if machines ever share an
|
|
1153
|
+
* owner key while holding distinct transport identities — the derivation would
|
|
1154
|
+
* then need a per-machine discriminator (the `CadrePeer` PeerId) on the row.
|
|
1155
|
+
*/
|
|
1156
|
+
private isSelfFoundedRow;
|
|
860
1157
|
/**
|
|
861
1158
|
* The owner keypair (base64url Ed25519) derived from this node's resolved
|
|
862
1159
|
* identity key. In the single-key reference model the owner signing key is
|
|
@@ -927,7 +1224,7 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
927
1224
|
* Applied by ALL THREE sources of control-dial candidates, so no call site
|
|
928
1225
|
* invents its own rule and none can produce a mixed list: the signed record
|
|
929
1226
|
* ({@link resolvePeerAddrs}), the libp2p address book
|
|
930
|
-
* ({@link peerStoreAddrs}), and a
|
|
1227
|
+
* ({@link peerStoreAddrs}), and a retained out-of-band dial target
|
|
931
1228
|
* ({@link bootstrapDialAddrs}).
|
|
932
1229
|
*
|
|
933
1230
|
* An address naming a DIFFERENT trailing peer id is dropped: it does not reach
|
|
@@ -939,10 +1236,15 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
939
1236
|
* One address bound to `peerId`, or `null` when it cannot be — logged either
|
|
940
1237
|
* way, never thrown, so every caller's list-shaping stays total.
|
|
941
1238
|
*
|
|
1239
|
+
* Used both to normalize addresses this node DIALS (a `CadrePeer` row, a
|
|
1240
|
+
* retained bootstrap addr) and to normalize the ones it ANNOUNCES
|
|
1241
|
+
* ({@link getStrandMultiaddrs}); the rule is the same either way — an address
|
|
1242
|
+
* that does not reach `peerId` is not an address for `peerId`.
|
|
1243
|
+
*
|
|
942
1244
|
* `withTrailingPeerId` encapsulates `/p2p/<peerId>`, which throws on a peer id
|
|
943
1245
|
* that does not parse. Unreachable for a `CadrePeer` row (the binding gate
|
|
944
|
-
* above parsed it already), reachable for a
|
|
945
|
-
* ({@link bootstrapDialAddrs}).
|
|
1246
|
+
* above parsed it already) and for our own node's id, reachable for a
|
|
1247
|
+
* retained one ({@link bootstrapDialAddrs}).
|
|
946
1248
|
*/
|
|
947
1249
|
private bindAddrToPeer;
|
|
948
1250
|
/** Parse multiaddr strings, dropping (and logging) any that fail to parse. */
|
|
@@ -956,13 +1258,32 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
956
1258
|
* `self:peer:update` (all wired in {@link startRecordRefresh}).
|
|
957
1259
|
*
|
|
958
1260
|
* Concurrent triggers collapse into a single in-flight pass
|
|
959
|
-
* (see {@link reconcileControlCohortInFlight}) so two passes never double-dial
|
|
1261
|
+
* (see {@link reconcileControlCohortInFlight}) so two passes never double-dial;
|
|
1262
|
+
* a call that joins an in-flight pass resolves to THAT pass's result.
|
|
960
1263
|
* Best-effort throughout: a failure to resolve/dial any one sibling is logged
|
|
961
1264
|
* and the pass continues; the whole pass is a no-op when the node is alone.
|
|
1265
|
+
*
|
|
1266
|
+
* Resolves to the peers the pass dialled ({@link ControlCohortReconcileResult}),
|
|
1267
|
+
* so a caller can tell a link this pass opened from one something else opened —
|
|
1268
|
+
* the pass skips a peer that is already connected. The timer and event triggers
|
|
1269
|
+
* discard it.
|
|
962
1270
|
*/
|
|
963
|
-
reconcileControlCohort(): Promise<
|
|
1271
|
+
reconcileControlCohort(): Promise<ControlCohortReconcileResult>;
|
|
964
1272
|
/** Body of {@link reconcileControlCohort}; serialised by its single-flight guard. */
|
|
965
1273
|
private runReconcileControlCohort;
|
|
1274
|
+
/**
|
|
1275
|
+
* The owner-only half of the reconcile pass's ledger-marker step: file the singleton
|
|
1276
|
+
* `Revocation` marker ({@link SeedBootstrapService.openRevocationLedger}) unless this
|
|
1277
|
+
* process has already seen it filed. The connectivity gate is the caller's
|
|
1278
|
+
* ({@link runReconcileControlCohort}); why the marker exists is on
|
|
1279
|
+
* {@link ControlDatabase.openRevocationLedger}.
|
|
1280
|
+
*
|
|
1281
|
+
* Sets {@link revocationLedgerOpened} on `'opened'` or `'already-open'`. A node that
|
|
1282
|
+
* cannot sign as an owner does nothing: it picks the owner's marker up the first time it
|
|
1283
|
+
* reads the table, like any other replicated row. Best-effort like every step of the
|
|
1284
|
+
* pass — a failure is logged and leaves the flag clear, so the next connected pass retries.
|
|
1285
|
+
*/
|
|
1286
|
+
private openRevocationLedgerIfDue;
|
|
966
1287
|
/**
|
|
967
1288
|
* Resolve every sibling's signed address record once and merge what resolves
|
|
968
1289
|
* into the control node's libp2p **address book** (peerStore). Returns the
|
|
@@ -982,6 +1303,9 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
982
1303
|
* resolves to nothing (revoked, stale, untrusted) is not written at all, so its
|
|
983
1304
|
* existing entry ages out on its own.
|
|
984
1305
|
*
|
|
1306
|
+
* The same resolution also keeps a sibling's retained out-of-band dial target
|
|
1307
|
+
* current ({@link refreshDialHint}).
|
|
1308
|
+
*
|
|
985
1309
|
* NOTE: this resolves EVERY sibling serially before the dial loop below runs,
|
|
986
1310
|
* so it costs one record query per sibling per reconcile pass (~15s) and each
|
|
987
1311
|
* one delays the pass's first dial. A cadre is a handful of devices, so today
|
|
@@ -989,6 +1313,28 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
989
1313
|
* into one query, merge only on change, or move the warm pass after the dials.
|
|
990
1314
|
*/
|
|
991
1315
|
private warmSiblingAddrBook;
|
|
1316
|
+
/**
|
|
1317
|
+
* Replace a sibling's retained out-of-band dial target (see
|
|
1318
|
+
* {@link bootstrapPeerStore}) with the addresses its signed record just resolved
|
|
1319
|
+
* to, when the two differ — so an address change this node saw while the record
|
|
1320
|
+
* was fresh (a new port, a new LAN address) is what it dials after a relaunch
|
|
1321
|
+
* that outlives the record's freshness window.
|
|
1322
|
+
*
|
|
1323
|
+
* Only a sibling that already HAS an entry (`retained`) is refreshed: the store
|
|
1324
|
+
* holds peers learned out of band and must not grow into a copy of every
|
|
1325
|
+
* sibling's addresses. An empty resolution leaves the entry alone — a stale or
|
|
1326
|
+
* not-yet-signed record is exactly when the entry is needed. The comparison is
|
|
1327
|
+
* order-insensitive and runs on the retained addresses after binding them to the
|
|
1328
|
+
* peer id, so an entry recorded without `/p2p/` suffixes is not rewritten merely
|
|
1329
|
+
* for lacking them.
|
|
1330
|
+
*
|
|
1331
|
+
* NOTE: replaces rather than merges, so the entry becomes exactly what the
|
|
1332
|
+
* sibling announces. If a node's signed record ever lists fewer addresses this
|
|
1333
|
+
* node can reach than it was added with (say an announce override naming only a
|
|
1334
|
+
* public address, for a node added by its LAN address), a relaunch dials the
|
|
1335
|
+
* worse set; merge the two lists here if that shows up.
|
|
1336
|
+
*/
|
|
1337
|
+
private refreshDialHint;
|
|
992
1338
|
/**
|
|
993
1339
|
* {@link resolvePeerAddrs} for one sibling, best-effort: a control-DB read
|
|
994
1340
|
* failure yields `[]` (and the cold-start fallback then gets its turn) rather
|
|
@@ -996,37 +1342,78 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
996
1342
|
*/
|
|
997
1343
|
private resolveSiblingAddrs;
|
|
998
1344
|
/**
|
|
999
|
-
*
|
|
1345
|
+
* Time limits for dialing ONE control-network peer from its candidate
|
|
1346
|
+
* addresses ({@link dialPeerAddrs}): the reconcile pass's steady-state sibling
|
|
1000
1347
|
* ({@link dialControlSibling}) and cold-start bootstrap peer
|
|
1001
|
-
* ({@link dialBootstrapPeer})
|
|
1002
|
-
*
|
|
1348
|
+
* ({@link dialBootstrapPeer}) dials, and — handed to every
|
|
1349
|
+
* {@link SeedBootstrapService} this node builds — `applySeed`'s owner dials and
|
|
1350
|
+
* `dialInvite`.
|
|
1003
1351
|
*
|
|
1004
|
-
* See {@link DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS} for why
|
|
1005
|
-
*
|
|
1352
|
+
* See {@link DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS} for why a peer's dial is
|
|
1353
|
+
* bounded as a whole, and {@link DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS}
|
|
1354
|
+
* for why each address is bounded as well.
|
|
1006
1355
|
*/
|
|
1007
|
-
private
|
|
1356
|
+
private controlDialBudget;
|
|
1008
1357
|
/**
|
|
1009
1358
|
* Dial one sibling from its already-resolved addresses, best-effort. Returns
|
|
1010
|
-
* whether
|
|
1359
|
+
* whether the dial resolved (false when no address resolves or the dial fails).
|
|
1011
1360
|
*
|
|
1012
1361
|
* A per-peer failure (NAT, offline, relay down, connection-gater denial, or
|
|
1013
|
-
* the {@link
|
|
1014
|
-
*
|
|
1362
|
+
* the {@link controlDialBudget} expiring) is logged and swallowed so one
|
|
1363
|
+
* unreachable sibling never aborts the pass — exactly like
|
|
1015
1364
|
* {@link SeedBootstrapService.applySeed}'s owner-dial loop. A failed dial is
|
|
1016
1365
|
* simply retried on the next pass.
|
|
1017
1366
|
*/
|
|
1018
1367
|
private dialControlSibling;
|
|
1019
1368
|
/**
|
|
1020
|
-
* A sibling's control-network dial addresses for the reconcile pass
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
* `resolved` for it by
|
|
1024
|
-
* re-resolved, so the
|
|
1025
|
-
*
|
|
1026
|
-
* the
|
|
1027
|
-
*
|
|
1369
|
+
* A sibling's control-network dial addresses for the reconcile pass, from the
|
|
1370
|
+
* first of three sources that yields any:
|
|
1371
|
+
*
|
|
1372
|
+
* 1. the signed, fresh, trust-gated control addresses `resolved` for it by
|
|
1373
|
+
* {@link warmSiblingAddrBook} — passed in rather than re-resolved, so the
|
|
1374
|
+
* pass makes one record query per sibling;
|
|
1375
|
+
* 2. the libp2p address book ({@link peerStoreAddrs}) — the entries `applySeed`
|
|
1376
|
+
* and identify put there, until they age out;
|
|
1377
|
+
* 3. the sibling's retained out-of-band dial target ({@link retainedDialAddrs}).
|
|
1378
|
+
* For a node that cannot listen, this is the only source for a sibling it
|
|
1379
|
+
* added but has not yet connected to: the added node's row stays unsigned
|
|
1380
|
+
* until it self-publishes, which needs the connection this dial opens. It is
|
|
1381
|
+
* also what a relaunch that outlived the record's freshness window uses.
|
|
1382
|
+
*
|
|
1383
|
+
* Returns `[]` (never throws) when none yields an address — that sibling is
|
|
1384
|
+
* skipped this pass.
|
|
1385
|
+
*
|
|
1386
|
+
* The list may name transports this node cannot dial (a lent node reports TCP
|
|
1387
|
+
* and `/ws` addresses to a phone that dials WebSockets only). That needs no
|
|
1388
|
+
* filtering here: libp2p's dial queue (`calculateMultiaddrs`, libp2p 3.1.3)
|
|
1389
|
+
* rejects an address no transport can dial before touching the network, so
|
|
1390
|
+
* such an address costs a log line.
|
|
1391
|
+
*
|
|
1392
|
+
* The list is NOT handed to one `dial()`. libp2p tries a multi-address dial's
|
|
1393
|
+
* addresses one after another under a single deadline, with no limit per
|
|
1394
|
+
* address, and sorts loopback addresses last — so one or two addresses that
|
|
1395
|
+
* never answer used up the whole deadline before the address that worked was
|
|
1396
|
+
* tried. {@link dialPeerAddrs} dials each address on its own time limit instead.
|
|
1028
1397
|
*/
|
|
1029
1398
|
private resolveControlDialAddrs;
|
|
1399
|
+
/**
|
|
1400
|
+
* A sibling's retained out-of-band dial target (see {@link bootstrapPeerStore}),
|
|
1401
|
+
* bound to its peer id through {@link bootstrapDialAddrs}; `[]` when it has none.
|
|
1402
|
+
*
|
|
1403
|
+
* These addresses are never merged into the libp2p address book: they are
|
|
1404
|
+
* unverified, and the address book takes only verified ones (see
|
|
1405
|
+
* `mergePeerAddrs`). Layers that dial by bare peer id use the connection this
|
|
1406
|
+
* dial opens, and after it drops the next reconcile pass dials again.
|
|
1407
|
+
*
|
|
1408
|
+
* NOTE: an entry is consulted here only for a current sibling, so a peer revoked
|
|
1409
|
+
* by another owner is not dialed from it, even though the entry stays (only this
|
|
1410
|
+
* node's own {@link removePeer} forgets one). The cold-start branch
|
|
1411
|
+
* ({@link dialColdStartBootstrap}) still dials every entry while there are no
|
|
1412
|
+
* siblings at all; if entries for revoked peers ever cause dial churn there,
|
|
1413
|
+
* prune entries whose peer has a retired `CadrePeer` row during the membership
|
|
1414
|
+
* refresh.
|
|
1415
|
+
*/
|
|
1416
|
+
private retainedDialAddrs;
|
|
1030
1417
|
/**
|
|
1031
1418
|
* Cold-start fallback: the libp2p peerStore multiaddrs for `peerId` (seeded by
|
|
1032
1419
|
* {@link SeedBootstrapService.applySeed}). Returns `[]` on a missing entry or any
|
|
@@ -1059,15 +1446,33 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1059
1446
|
* signature-checked against a trust-anchored signer before this runs, and the
|
|
1060
1447
|
* flag only *selects a dial target*; a dial grants no authority.
|
|
1061
1448
|
*
|
|
1062
|
-
*
|
|
1063
|
-
* that applies a seed minted after it joined finds itself in the owner list;
|
|
1064
|
-
* retaining that would make the cold-start pass dial this node forever (the
|
|
1065
|
-
* steady-state pass filters self out of its sibling list for the same reason).
|
|
1449
|
+
* Addressless peers and self are skipped by {@link retainDialTarget}.
|
|
1066
1450
|
*/
|
|
1067
1451
|
private recordSeedBootstrapPeers;
|
|
1068
1452
|
/**
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1453
|
+
* Retain (or replace) one peer's out-of-band dial target in
|
|
1454
|
+
* {@link bootstrapPeerStore}. Shared by every writer: seed intake, {@link addDrone}
|
|
1455
|
+
* and {@link refreshDialHint}.
|
|
1456
|
+
*
|
|
1457
|
+
* Fire-and-log: the entry is visible synchronously by the store's contract, and
|
|
1458
|
+
* the promise tracks durability only. A persist failure costs restart survival,
|
|
1459
|
+
* never this session's dial set — the same trade
|
|
1460
|
+
* `SeedBootstrapService.anchorAcceptedSigner` makes — so it is logged rather than
|
|
1461
|
+
* failing a seed or an add that has already been accepted.
|
|
1462
|
+
*
|
|
1463
|
+
* Two peers are never retained. One with no address: there is nothing to dial.
|
|
1464
|
+
* And self: `createSeed` projects EVERY `CadrePeer` row, so an owner that applies
|
|
1465
|
+
* a seed minted after it joined finds itself in the owner list, and `addDrone`
|
|
1466
|
+
* could be handed this node's own id. Retaining self would make the cold-start
|
|
1467
|
+
* pass dial this node forever (the steady-state pass filters self out of its
|
|
1468
|
+
* sibling list for the same reason).
|
|
1469
|
+
*/
|
|
1470
|
+
private retainDialTarget;
|
|
1471
|
+
/**
|
|
1472
|
+
* Cold-start branch of the reconcile pass: re-dial every retained out-of-band
|
|
1473
|
+
* dial target (see {@link bootstrapPeerStore}) — in practice the owner peers of
|
|
1474
|
+
* the seeds this node applied — while the control database still has no
|
|
1475
|
+
* siblings to dial.
|
|
1071
1476
|
*
|
|
1072
1477
|
* `SeedBootstrapService.applySeed` dials those owners exactly ONCE, best-effort.
|
|
1073
1478
|
* When that single dial fails — owner momentarily down, relay reservation not
|
|
@@ -1090,13 +1495,14 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1090
1495
|
*/
|
|
1091
1496
|
private dialColdStartBootstrap;
|
|
1092
1497
|
/**
|
|
1093
|
-
* Bind a
|
|
1094
|
-
* authenticates the peer it is aiming at rather than
|
|
1498
|
+
* Bind a retained dial target's addresses (see {@link bootstrapPeerStore}) to
|
|
1499
|
+
* its peer id, so the dial authenticates the peer it is aiming at rather than
|
|
1500
|
+
* trusting whoever answers.
|
|
1095
1501
|
*
|
|
1096
1502
|
* The same {@link normalizeDialAddrs} rule the resolved and address-book paths
|
|
1097
1503
|
* use, on the third and last source of control-dial candidates: an address that
|
|
1098
1504
|
* already terminates in `/p2p/<id>` must carry THIS peer's id or it is dropped
|
|
1099
|
-
* (
|
|
1505
|
+
* (an entry that disagrees with itself is not a dial target); one that does not —
|
|
1100
1506
|
* a bare listen addr, or a relay hop with the destination missing — gets the id
|
|
1101
1507
|
* encapsulated; an unencapsulatable one is dropped rather than dialed bare.
|
|
1102
1508
|
*/
|
|
@@ -1354,6 +1760,15 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1354
1760
|
* `control:disconnected` emit, which a never-connected start has no edge for.
|
|
1355
1761
|
*/
|
|
1356
1762
|
private cleanup;
|
|
1763
|
+
/**
|
|
1764
|
+
* A gated joiner's database was just published (`StartStrandConfig.onWritable`): the
|
|
1765
|
+
* instance is `'active'` now, so re-arm its idle timer as any activity would. Nothing
|
|
1766
|
+
* records activity on a gated joiner, so its idle timer may already have fired — the
|
|
1767
|
+
* instance read `'idle'` with a hibernate timer pending — and without this the flip
|
|
1768
|
+
* back to `'active'` would leave that timer to quiesce a strand the app just started
|
|
1769
|
+
* using. Emits `strand:writable` after the timers are settled.
|
|
1770
|
+
*/
|
|
1771
|
+
private handleStrandWritable;
|
|
1357
1772
|
private handleStrandIdle;
|
|
1358
1773
|
/**
|
|
1359
1774
|
* Hibernate a strand: release its strand-network resources via the strand
|
|
@@ -1375,8 +1790,8 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1375
1790
|
/**
|
|
1376
1791
|
* Rebuild a quiesced strand's runtime, re-resolving the volatile cohort input
|
|
1377
1792
|
* first: the discovery seed may have grown since the strand last ran. Shared by
|
|
1378
|
-
* the wake (`handleStrandWake`) and check-in (`handleStrandCheckIn`) paths so
|
|
1379
|
-
*
|
|
1793
|
+
* the wake (`handleStrandWake`) and check-in (`handleStrandCheckIn`) paths so both
|
|
1794
|
+
* apply the same fresh resolution. `resumeStrand` is idempotent
|
|
1380
1795
|
* (returns the live instance unchanged) as a backstop against double-resume.
|
|
1381
1796
|
*/
|
|
1382
1797
|
private resumeStrandRuntime;
|
|
@@ -1437,42 +1852,152 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1437
1852
|
* Add a strand with its sApp configuration.
|
|
1438
1853
|
* The hosting application must provide the sApp schema when creating a strand.
|
|
1439
1854
|
*
|
|
1855
|
+
* This is the ATTACH half only — it starts the local instance and never publishes the
|
|
1856
|
+
* `Strand` row. A joiner (the row arrived over the control network) wants exactly this; a
|
|
1857
|
+
* FOUNDER wants {@link foundStrand}, which publishes and attaches in one resumable call.
|
|
1858
|
+
* With no explicit `founder` flag, founder-ness is derived from the row's
|
|
1859
|
+
* `FounderOwnerKey` (see {@link StrandConfig.founder}) — so attaching a row THIS machine
|
|
1860
|
+
* published (e.g. re-attaching its own orphan after a restart) founds it, and attaching
|
|
1861
|
+
* anyone else's row joins, without the caller needing to know which it is.
|
|
1862
|
+
*
|
|
1440
1863
|
* A rejected call leaves nothing running but DOES leave the sApp config
|
|
1441
1864
|
* registered, deliberately: both an explicit retry and the {@link StrandWatcher}'s
|
|
1442
|
-
* automatic relaunch need it.
|
|
1443
|
-
*
|
|
1444
|
-
*
|
|
1445
|
-
*
|
|
1446
|
-
*
|
|
1865
|
+
* automatic relaunch need it. That background relaunch covers only a strand the
|
|
1866
|
+
* WATCHER launched: it forgets a row whose `onStrandAdded` threw and re-attempts it
|
|
1867
|
+
* on a later poll (each failure re-emitting `strand:error`) until it succeeds. A
|
|
1868
|
+
* strand claimed here after it was DISCOVERED gets no such retry — the watcher
|
|
1869
|
+
* already recorded it in its `knownStrands` when it offered it as
|
|
1870
|
+
* `strand:discovered`, and no later poll re-offers a strand it knows. This call has
|
|
1871
|
+
* also already dropped it from {@link getDiscoveredStrands}, so a failed claim
|
|
1872
|
+
* leaves the strand dormant until the caller retries or the node restarts (see
|
|
1873
|
+
* `backlog/bug-discovered-strand-lost-when-claim-fails`).
|
|
1874
|
+
* {@link detachStrand} (reached via {@link stopStrand}) is what abandons a strand
|
|
1875
|
+
* for good.
|
|
1447
1876
|
*/
|
|
1448
1877
|
addStrand(config: StrandConfig): Promise<StrandInstance>;
|
|
1878
|
+
/**
|
|
1879
|
+
* Resolve once a launched strand is writable — its database published to the app and
|
|
1880
|
+
* its status `'active'` — or reject with the retryable `StrandAwaitingFirstSyncError`
|
|
1881
|
+
* after `timeoutMs` (default `strandFirstSync.timeoutMs`, else
|
|
1882
|
+
* `DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS`). The promise form of the `strand:writable`
|
|
1883
|
+
* event, for a caller that attached without waiting ({@link StrandConfig.awaitFirstSync}
|
|
1884
|
+
* `false`) or that holds a strand the watcher launched. Resolves immediately for a
|
|
1885
|
+
* strand that is already writable; rejects immediately for one this node does not run.
|
|
1886
|
+
* A hibernating strand is not woken — wake it first if that is what is wanted.
|
|
1887
|
+
*/
|
|
1888
|
+
whenStrandWritable(strandId: string, options?: {
|
|
1889
|
+
timeoutMs?: number;
|
|
1890
|
+
}): Promise<StrandInstance>;
|
|
1449
1891
|
/**
|
|
1450
1892
|
* Publish a strand row to the shared control database under this node's own
|
|
1451
1893
|
* owner identity, so other cadre members discover it via control-network
|
|
1452
1894
|
* sync (their {@link StrandWatcher} fires `strand:discovered`).
|
|
1453
1895
|
*
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1458
|
-
* `addStrand` (the row already exists).
|
|
1896
|
+
* **Founding a strand is TWO steps** — this one (publish the row cadre-wide) and
|
|
1897
|
+
* {@link addStrand} (start the local instance, `founder: true` to write the strand's
|
|
1898
|
+
* `Header`/founding membership). `addStrand` deliberately omits the insert: it only
|
|
1899
|
+
* starts the LOCAL instance, whereas publishing makes the strand visible cadre-wide. A
|
|
1900
|
+
* discovering peer only does `addStrand` (the row already exists). Callers founding a
|
|
1901
|
+
* strand should use {@link foundStrand}, which performs both steps and is safe to
|
|
1902
|
+
* re-run — hand-rolling the pair is what left strands half-founded when an app was
|
|
1903
|
+
* killed between them.
|
|
1904
|
+
*
|
|
1905
|
+
* **Idempotent for identical content.** A live row whose `(Type, MemberPrivateKey)` match
|
|
1906
|
+
* the arguments is the state this call would have produced, so the call logs and returns
|
|
1907
|
+
* that row instead of writing — a publish interrupted after it committed can be repeated.
|
|
1908
|
+
* DIFFERENT content on the same id throws, naming the columns that differ, rather than
|
|
1909
|
+
* surfacing the raw `UNIQUE constraint failed: Strand.Id`: silently accepting it would
|
|
1910
|
+
* let a retry reopen a closed strand or swap the key gating its reads. The read and the
|
|
1911
|
+
* insert are not atomic, so a concurrent founder (two machines of one party, same id) is
|
|
1912
|
+
* caught on the insert's uniqueness rejection and resolved the same way — re-read, no-op
|
|
1913
|
+
* on a match, rethrow otherwise. Never an overwrite either way.
|
|
1914
|
+
*
|
|
1915
|
+
* A tombstoned strand is NOT resurrected here: {@link unpublishStrand} deletes the row, so
|
|
1916
|
+
* the idempotent branch is unreachable and the ordinary publish path re-seats it, exactly
|
|
1917
|
+
* as that method documents.
|
|
1918
|
+
*
|
|
1919
|
+
* **Already stuck on `UNIQUE constraint failed: Strand.Id`?** The row is already published
|
|
1920
|
+
* — attach, do NOT republish: `addStrand` (or {@link foundStrand}) with the id and, for a
|
|
1921
|
+
* closed strand, the `MemberPrivateKey` read back from the row. `unpublishStrand` + a
|
|
1922
|
+
* fresh publish also clears it for an OPEN strand, but is destructive for a closed one
|
|
1923
|
+
* (the key exists nowhere else). No app-data wipe is needed for either.
|
|
1459
1924
|
*
|
|
1460
1925
|
* The insert is signed with the ed25519 key behind this node's PeerId — which
|
|
1461
1926
|
* {@link ed25519KeyPairFromLibp2p} also exposes as the node's owner keypair,
|
|
1462
|
-
* so peer identity and owner key are one and the same. That key
|
|
1463
|
-
*
|
|
1927
|
+
* so peer identity and owner key are one and the same. That key is also persisted on the
|
|
1928
|
+
* row as `FounderOwnerKey` (the schema pins the column to the verified signer), which is
|
|
1929
|
+
* what later lets any launch derive "this machine is the founder" from the row alone.
|
|
1930
|
+
* The key must be enrolled in `OwnerKey` (e.g. via {@link ControlDatabase.ensureOwnerKey}
|
|
1464
1931
|
* at genesis) or the schema's `Strand.AuthorizedInsert` constraint rejects the write.
|
|
1465
1932
|
* Failing loudly here is intentional: a silently-unpublished strand would run
|
|
1466
1933
|
* as a local-only island that no peer could ever discover or join.
|
|
1467
1934
|
*
|
|
1935
|
+
* A CLOSED strand's publish also seats this party's own membership identity key —
|
|
1936
|
+
* the `StrandPartyKey` row {@link ensureStrandPartyKey} documents — which is what the
|
|
1937
|
+
* founder bootstrap derives `Member.Key`/`Manager.MemberKey` from. The
|
|
1938
|
+
* `memberPrivateKey` ARGUMENT stays the strand-wide read secret formation hands to
|
|
1939
|
+
* joiners; it derives nobody's identity.
|
|
1940
|
+
*
|
|
1468
1941
|
* @param strandId - Unique strand identifier (typically the same id passed to
|
|
1469
1942
|
* {@link addStrand}).
|
|
1470
1943
|
* @param type - `'o'` for open (default) or `'c'` for closed.
|
|
1471
|
-
* @param memberPrivateKey - Optional membership key for a closed strand.
|
|
1472
|
-
* @
|
|
1473
|
-
*
|
|
1474
|
-
|
|
1475
|
-
|
|
1944
|
+
* @param memberPrivateKey - Optional shared membership (read) key for a closed strand.
|
|
1945
|
+
* @returns The live `Strand` row — the one just inserted, or the matching one already
|
|
1946
|
+
* there. A closed strand's caller should carry THIS row's `MemberPrivateKey` forward:
|
|
1947
|
+
* on a repeat it is the stored key, not the argument.
|
|
1948
|
+
* @throws if the node is not started, exposes no owner signing key, the id is blank, a
|
|
1949
|
+
* row with the same id holds different content, or the control DB rejects the
|
|
1950
|
+
* (unauthorized) insert.
|
|
1951
|
+
*/
|
|
1952
|
+
publishStrand(strandId: string, type?: 'o' | 'c', memberPrivateKey?: string): Promise<StrandRow>;
|
|
1953
|
+
/**
|
|
1954
|
+
* Found a strand — publish its row cadre-wide AND start the local instance as its
|
|
1955
|
+
* founder — in one resumable call. The single entry point for creating a strand; a
|
|
1956
|
+
* caller joining one someone else founded uses {@link addStrand} alone.
|
|
1957
|
+
*
|
|
1958
|
+
* Safe to re-run from any point of interruption, which hand-rolling
|
|
1959
|
+
* {@link publishStrand} + {@link addStrand} is not. Either half can already have
|
|
1960
|
+
* happened:
|
|
1961
|
+
*
|
|
1962
|
+
* - **row already published** → the stored row is adopted rather than re-published, so a
|
|
1963
|
+
* run killed after the insert committed no longer dies on
|
|
1964
|
+
* `UNIQUE constraint failed: Strand.Id`. For a closed strand the row's STORED
|
|
1965
|
+
* `MemberPrivateKey` wins over `config.memberPrivateKey`: a caller that mints a key per
|
|
1966
|
+
* attempt (as the reference apps do) would otherwise present a key that does not match
|
|
1967
|
+
* the membership already seated in the strand.
|
|
1968
|
+
* - **instance already running** → the tracked instance is returned, and — because the
|
|
1969
|
+
* `Strand` row records its publishing machine (`FounderOwnerKey`) — a founder request
|
|
1970
|
+
* against an instance that was first launched as a joiner now runs the (idempotent,
|
|
1971
|
+
* insert-if-absent) founder bootstrap on it rather than silently skipping it
|
|
1972
|
+
* ({@link StrandInstanceManager.foundExistingStrand}). So a strand that something else
|
|
1973
|
+
* attached first — the reference RN app's `strand:discovered` handler after a restart,
|
|
1974
|
+
* or this node's own {@link StrandWatcher} poll winning `launchStrand`'s
|
|
1975
|
+
* `resolveCohortSeed` window — still ends up with its `Strand.Header` written.
|
|
1976
|
+
*
|
|
1977
|
+
* Founder-ness is resolved FROM THE ROW, not assumed: this machine founds iff the
|
|
1978
|
+
* resolved row's `FounderOwnerKey` is its own owner key. A fresh publish records this
|
|
1979
|
+
* machine, so the common path founds; a machine that lost a concurrent founding race to
|
|
1980
|
+
* a sibling adopts the sibling's row and ATTACHES instead — deliberately, since two
|
|
1981
|
+
* machines bootstrapping the same strand on separate replicas is the double-`Header`
|
|
1982
|
+
* hazard. The returned {@link FoundStrandResult.founded} says which happened.
|
|
1983
|
+
*
|
|
1984
|
+
* `Type` is compared, not adopted: a stored row of the other type means the caller and
|
|
1985
|
+
* the control plane disagree about what this strand IS, so it throws.
|
|
1986
|
+
*
|
|
1987
|
+
* @returns The instance AND the row the strand actually runs under — read the membership
|
|
1988
|
+
* key from the returned row, not from a freshly minted one ({@link FoundStrandResult}).
|
|
1989
|
+
* @throws if the node is not started, the id is blank, a published row of the same id has
|
|
1990
|
+
* a different `Type`, or either half rejects.
|
|
1991
|
+
*/
|
|
1992
|
+
foundStrand(config: FoundStrandConfig): Promise<FoundStrandResult>;
|
|
1993
|
+
/**
|
|
1994
|
+
* Resume onto an already-published `Strand` row: keep its stored content, reject a type
|
|
1995
|
+
* disagreement. The stored `MemberPrivateKey` is authoritative — see {@link foundStrand}
|
|
1996
|
+
* for why a caller-minted key must lose here (and why that is NOT the same rule as
|
|
1997
|
+
* {@link publishStrand}'s, which throws on a key mismatch because it is being asked to
|
|
1998
|
+
* WRITE that key, not to resume onto what already exists).
|
|
1999
|
+
*/
|
|
2000
|
+
private adoptPublishedStrand;
|
|
1476
2001
|
/**
|
|
1477
2002
|
* Remove this party's `Strand` row from the shared control database — the owner-signed
|
|
1478
2003
|
* inverse of {@link publishStrand}.
|
|
@@ -1486,10 +2011,12 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1486
2011
|
* is an owner-signed control-plane write like every other, not "destroy the network".
|
|
1487
2012
|
*
|
|
1488
2013
|
* Irreversible for a closed strand (`Type='c'`): the row carries `MemberPrivateKey`,
|
|
1489
|
-
* this party's
|
|
1490
|
-
*
|
|
1491
|
-
*
|
|
1492
|
-
*
|
|
2014
|
+
* this party's read secret for that network, and the strand's `StrandPartyKey` row —
|
|
2015
|
+
* this party's own membership identity, removed (and tombstoned) in the SAME
|
|
2016
|
+
* transaction — is stored nowhere else either. With both gone the party can never
|
|
2017
|
+
* again sign as its seated member/manager there, and a re-published row mints a fresh
|
|
2018
|
+
* identity that does not match the membership already written into the strand's RBAC
|
|
2019
|
+
* layer. The strand id itself is NOT
|
|
1493
2020
|
* blacklisted: a fresh owner-signed {@link publishStrand} re-seats it under a new
|
|
1494
2021
|
* stamp — only the unsigned consent re-seat is permanently foreclosed (the removal's
|
|
1495
2022
|
* `Revocation` tombstone names the id, which the consent branch of
|
|
@@ -1529,6 +2056,55 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1529
2056
|
* there throws over a completed removal.
|
|
1530
2057
|
*/
|
|
1531
2058
|
unpublishStrand(strandId: string): Promise<void>;
|
|
2059
|
+
/**
|
|
2060
|
+
* Seat this party's own strand membership identity key — the `StrandPartyKey` row —
|
|
2061
|
+
* for `strandId`, minting a fresh one when none is given, and adopting the stored one
|
|
2062
|
+
* when a row already exists. Insert-if-absent and stable thereafter: the founding
|
|
2063
|
+
* `Member.Key` must not change across restarts, or the bootstrap's insert-if-absent
|
|
2064
|
+
* guards stop matching.
|
|
2065
|
+
*
|
|
2066
|
+
* This is the identity half of the closed-strand key split: the row's `PrivateKey` —
|
|
2067
|
+
* NOT the strand row's shared `MemberPrivateKey`, which formation hands to every
|
|
2068
|
+
* joining party — is what the founder bootstrap derives `Member.Key`/`Manager.MemberKey`
|
|
2069
|
+
* from. It replicates to every machine this party owns (same plaintext-at-rest stance
|
|
2070
|
+
* as `MemberPrivateKey`; docs/strands.md → "Closed-Strand Member Key Handling") and is
|
|
2071
|
+
* never put on the formation wire.
|
|
2072
|
+
*
|
|
2073
|
+
* Called internally by {@link publishStrand} (closed strands mint at publish) and by a
|
|
2074
|
+
* founder launch that finds no row (a publish interrupted before its mint); public so a
|
|
2075
|
+
* test harness — or a joiner flow that persists a formation-issued identity — can seat
|
|
2076
|
+
* a specific key deliberately.
|
|
2077
|
+
*
|
|
2078
|
+
* @param strandId - The strand the key is this party's identity for.
|
|
2079
|
+
* @param partyMemberPrivateKey - Optional specific key (base64 protobuf, as
|
|
2080
|
+
* `generateStrandMemberKey` mints). When a row already holds a DIFFERENT key this
|
|
2081
|
+
* throws — a party has one identity per strand; rotation is a deliberate
|
|
2082
|
+
* remove-then-insert, not a silent swap.
|
|
2083
|
+
* @returns The live party key: the one just seated, or the stored one.
|
|
2084
|
+
*/
|
|
2085
|
+
ensureStrandPartyKey(strandId: string, partyMemberPrivateKey?: string): Promise<string>;
|
|
2086
|
+
/**
|
|
2087
|
+
* Resolve the party membership key a closed strand's launch threads into the founder
|
|
2088
|
+
* bootstrap: the explicit attach-time key when given, else the party's persisted
|
|
2089
|
+
* `StrandPartyKey` row, else — on the one machine whose owner key the row names as
|
|
2090
|
+
* founder — a freshly minted-and-persisted key, which seats the identity for a publish
|
|
2091
|
+
* that was interrupted before its mint. Everyone else resolves undefined: non-founding
|
|
2092
|
+
* machines never mint (no mint race between a party's machines), and only a founder
|
|
2093
|
+
* bootstrap needs the key at all.
|
|
2094
|
+
*
|
|
2095
|
+
* Minting does NOT repair a strand founded before the key split: its founding
|
|
2096
|
+
* membership was seated under the shared `MemberPrivateKey`, and nothing re-seats it —
|
|
2097
|
+
* the founder bootstrap refuses such a strand (`PreSplitStrandIdentityError`) instead.
|
|
2098
|
+
*
|
|
2099
|
+
* NOTE: "founding machine" here is the ROW's provenance, not the launch's resolved
|
|
2100
|
+
* `founder` flag — so an explicit `founder: false` over a row this machine published
|
|
2101
|
+
* still mints, an owner-signed write a caller that said "I am not founding" did not ask
|
|
2102
|
+
* for. Harmless (the party needs that identity for the strand either way, and the mint
|
|
2103
|
+
* is insert-if-absent) and unreachable today: the one explicit `founder: false` caller
|
|
2104
|
+
* passes a row with a null `FounderOwnerKey`. If a caller ever pairs `founder: false`
|
|
2105
|
+
* with its own published row, gate the mint on the resolved flag too.
|
|
2106
|
+
*/
|
|
2107
|
+
private resolveStrandPartyKey;
|
|
1532
2108
|
/**
|
|
1533
2109
|
* Publish an owner-signed `FormationInvite` (open-invitation token) to the
|
|
1534
2110
|
* shared control database, so a later {@link formStrand} redemption can be
|
|
@@ -1624,16 +2200,36 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1624
2200
|
* seed, starts the strand, and registers it with the hibernation manager
|
|
1625
2201
|
* before emitting `strand:started`.
|
|
1626
2202
|
*
|
|
1627
|
-
*
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
1631
|
-
*
|
|
2203
|
+
* Founder-ness: an explicit `founder` argument wins (the formation/responder flows pass
|
|
2204
|
+
* one deliberately — their consent-seated rows carry a null `FounderOwnerKey`); when
|
|
2205
|
+
* unset it is DERIVED from the row, so the control-discovered path
|
|
2206
|
+
* (`handleStrandAdded`) and a restart's re-attach found this machine's own strands
|
|
2207
|
+
* without the caller having to know. The derivation is pure key comparison — no I/O.
|
|
2208
|
+
*
|
|
2209
|
+
* Idempotent when the strand manager already tracks `strand.Id` — the watcher
|
|
2210
|
+
* rediscovers a row this node already started and calls this again via
|
|
2211
|
+
* `handleStrandAdded`; without the guard that re-entry would resolve a fresh
|
|
1632
2212
|
* (already-connected) cohort seed and re-emit `strand:started` for an
|
|
1633
2213
|
* instance that never stopped. The guard runs before the cohort-seed RPC
|
|
1634
|
-
* fan-out, so a rediscovery costs nothing beyond the map lookup
|
|
2214
|
+
* fan-out, so a rediscovery costs nothing beyond the map lookup plus the (pure)
|
|
2215
|
+
* founder derivation. NOT a silent no-op any more when the launch resolves as a
|
|
2216
|
+
* FOUNDER: the tracked instance may have been launched first as a joiner (an app
|
|
2217
|
+
* attach, or this node's own watcher poll winning the `resolveCohortSeed` window
|
|
2218
|
+
* below), which used to drop the founder request and leave the strand headerless —
|
|
2219
|
+
* now the tracked instance is founded in place
|
|
2220
|
+
* ({@link StrandInstanceManager.foundExistingStrand}), waking it first if quiesced
|
|
2221
|
+
* so the bootstrap actually runs before this resolves.
|
|
2222
|
+
*
|
|
2223
|
+
* A founder launch refused as pre-split (`PreSplitStrandIdentityError`) is recorded in
|
|
2224
|
+
* {@link strandLaunchRefusals} for the formation arm, then rethrown; a founder launch
|
|
2225
|
+
* that succeeds clears the record (it ran the bootstrap's pre-split check and passed).
|
|
1635
2226
|
*/
|
|
1636
2227
|
private launchStrand;
|
|
2228
|
+
/**
|
|
2229
|
+
* The launch itself, for {@link launchStrand} (which documents the behaviour): found a
|
|
2230
|
+
* tracked instance in place, or start a fresh one.
|
|
2231
|
+
*/
|
|
2232
|
+
private startOrFoundStrand;
|
|
1637
2233
|
/**
|
|
1638
2234
|
* Resolve a strand's discovery seed — the dialable strand-network multiaddr
|
|
1639
2235
|
* strings for cohort siblings. Membership comes from the control network's
|
|
@@ -1661,6 +2257,13 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1661
2257
|
* for a relay we are not yet connected to.
|
|
1662
2258
|
*/
|
|
1663
2259
|
private resolveCohortSeed;
|
|
2260
|
+
/**
|
|
2261
|
+
* The own-party half of {@link resolveCohortSeed}: strand-network addresses resolved
|
|
2262
|
+
* from CONNECTED cohort siblings over the control-mesh strand-addr RPC, doubling as
|
|
2263
|
+
* the delegate announcement when `delegatePeerId` is given. Empty when the control DB
|
|
2264
|
+
* or node is absent (not yet started / torn down).
|
|
2265
|
+
*/
|
|
2266
|
+
private resolveSiblingSeed;
|
|
1664
2267
|
/**
|
|
1665
2268
|
* The co-cadre siblings worth sending a strand-addr RPC to right now: cohort
|
|
1666
2269
|
* members (self excluded) we already hold an open control connection to. They
|
|
@@ -1726,10 +2329,10 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1726
2329
|
* Recorded OPTIMISTICALLY at announce time: `collectStrandAddrs` folds
|
|
1727
2330
|
* per-peer failure to `[]` and reports no per-peer success, and threading
|
|
1728
2331
|
* success out would change its API for little gain. A failed INITIAL announce
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
*
|
|
1732
|
-
* (15 min), still inside the 30 min TTL.
|
|
2332
|
+
* costs the strand supervisor its first attempt only (the relay denies the
|
|
2333
|
+
* reservation; every re-drive re-announces first through
|
|
2334
|
+
* {@link announceDelegateToRelay}); a failed REFRESH retries within
|
|
2335
|
+
* `DELEGATE_GRANT_TTL_MS / 2` (15 min), still inside the 30 min TTL.
|
|
1733
2336
|
*/
|
|
1734
2337
|
private recordDelegateAnnounces;
|
|
1735
2338
|
/**
|
|
@@ -1746,6 +2349,37 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1746
2349
|
* target, which must not stack up per strand.
|
|
1747
2350
|
*/
|
|
1748
2351
|
private refreshDelegateGrants;
|
|
2352
|
+
/**
|
|
2353
|
+
* Announce ONE strand's delegate peerId to ONE relay, UNTHROTTLED — the
|
|
2354
|
+
* `beforeRedrive` hook of that strand node's per-relay reservation supervisor
|
|
2355
|
+
* (`strand-instance-manager.ts` → `buildStrandRuntime`), run before every
|
|
2356
|
+
* re-drive after the first attempt.
|
|
2357
|
+
*
|
|
2358
|
+
* Why a re-drive must re-announce first: a party control node running the relay
|
|
2359
|
+
* server admits the strand node's derived peerId on an in-memory delegate grant
|
|
2360
|
+
* (`delegate-admission.ts`), and a relay restart drops every grant it held
|
|
2361
|
+
* without telling the announcer. {@link refreshDelegateGrants} would re-announce
|
|
2362
|
+
* on its own at most every `DELEGATE_GRANT_TTL_MS / 2` (15 min) per (relay,
|
|
2363
|
+
* strand) — far slower than the supervisor's backoff — so without this the first
|
|
2364
|
+
* re-drives after a relay restart would be denied at the relay's connection gate.
|
|
2365
|
+
* The throttle map is updated afterwards, so the periodic pass does not announce
|
|
2366
|
+
* again right away.
|
|
2367
|
+
*
|
|
2368
|
+
* Against a dedicated ops relay (no strand-addr RPC) the request fails per-peer
|
|
2369
|
+
* and `collectStrandAddrs` folds it to `[]`: one wasted protocol negotiation per
|
|
2370
|
+
* re-drive attempt, bounded by the supervisor's backoff. Never throws on that
|
|
2371
|
+
* path; a relay addr that names no peer id is logged and skipped (the hook's
|
|
2372
|
+
* caller runs the drive regardless).
|
|
2373
|
+
*
|
|
2374
|
+
* NOTE: against a relay that is DOWN this hook costs up to two strand-addr
|
|
2375
|
+
* timeouts (10 s each: dial by peer id, then by addr) before the 10 s drive even
|
|
2376
|
+
* starts, so one failed re-drive can hold the supervisor `driving` for ~30 s.
|
|
2377
|
+
* Bounded and harmless while the relay is unreachable anyway; if recovery
|
|
2378
|
+
* latency after a relay comes back ever matters, skip the announce when the
|
|
2379
|
+
* control node holds no connection to the relay (the drive's own dial fails
|
|
2380
|
+
* faster) rather than shortening the strand-addr timeout.
|
|
2381
|
+
*/
|
|
2382
|
+
private announceDelegateToRelay;
|
|
1749
2383
|
/** One strand's share of {@link refreshDelegateGrants}: announce to the relays whose grant is due. */
|
|
1750
2384
|
private announceDelegateToDueRelays;
|
|
1751
2385
|
/**
|
|
@@ -1774,9 +2408,18 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1774
2408
|
*/
|
|
1775
2409
|
private refreshStrandPeerAddrs;
|
|
1776
2410
|
/**
|
|
1777
|
-
* One strand's share of {@link refreshStrandPeerAddrs}: RPC the siblings,
|
|
1778
|
-
*
|
|
1779
|
-
* swallowed so one strand's failure never
|
|
2411
|
+
* One strand's share of {@link refreshStrandPeerAddrs}: RPC the siblings, union their
|
|
2412
|
+
* answers with this strand's cross-party contact addresses, and merge the lot into the
|
|
2413
|
+
* strand's address book. Errors are logged and swallowed so one strand's failure never
|
|
2414
|
+
* costs the others their refresh.
|
|
2415
|
+
*
|
|
2416
|
+
* `targets` may be EMPTY — a solo node, or one whose only control peers are strangers.
|
|
2417
|
+
* The cross-party addresses still have to be re-merged in that case: they were learned
|
|
2418
|
+
* at formation and can never be re-resolved, so this periodic re-merge is the ONLY
|
|
2419
|
+
* thing standing between them and the peerStore's one-hour expiry. A strand with
|
|
2420
|
+
* neither a sibling to ask nor a contact entry does nothing and leaves its throttle
|
|
2421
|
+
* unstamped, so the next reconcile tick retries rather than sitting out the whole
|
|
2422
|
+
* refresh interval having done nothing.
|
|
1780
2423
|
*/
|
|
1781
2424
|
private refreshOneStrandPeerAddrs;
|
|
1782
2425
|
/**
|
|
@@ -1797,6 +2440,15 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1797
2440
|
* handshake, and a bad entry costs one failed dial that ages out at the
|
|
1798
2441
|
* peerStore's one-hour expiry. No new gating here — cross-party strand trust is
|
|
1799
2442
|
* `backlog/strand-network-nat-relay-reachability`.
|
|
2443
|
+
*
|
|
2444
|
+
* NOTE: one input does NOT age out — the cross-party addresses a formation
|
|
2445
|
+
* carried back ({@link crossPartyStrandAddrs}). Nothing can re-resolve them, so
|
|
2446
|
+
* {@link refreshStrandPeerAddrs} re-merges the same stored list every pass, which
|
|
2447
|
+
* also means a junk entry from a responder survives for the node's lifetime rather
|
|
2448
|
+
* than an hour. Bounded to {@link MAX_STRAND_ADDRS} per strand, from a party this
|
|
2449
|
+
* node deliberately formed a strand with, and still authority-free — so the
|
|
2450
|
+
* exposure is a handful of failed dials, not a trust hole. Evict on
|
|
2451
|
+
* `unpublishStrand` if that ever stops being an acceptable price.
|
|
1800
2452
|
*/
|
|
1801
2453
|
private mergeStrandPeerAddrs;
|
|
1802
2454
|
/**
|
|
@@ -1812,22 +2464,32 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1812
2464
|
/**
|
|
1813
2465
|
* Stop a strand on THIS node only: untrack it from hibernation, drop its sApp config,
|
|
1814
2466
|
* stop the local instance, and emit `strand:stopped`. The shared `Strand` row is left
|
|
1815
|
-
* intact, so on the next node
|
|
1816
|
-
*
|
|
2467
|
+
* intact, so on the next node RESTART the strand is rediscovered and surfaces as
|
|
2468
|
+
* `strand:discovered` again — not on the next watcher poll, which never re-offers a
|
|
2469
|
+
* strand it has already seen (`StrandWatcher.knownStrands` retains the id for the life
|
|
2470
|
+
* of the process). The stop also drops the strand from {@link getDiscoveredStrands},
|
|
2471
|
+
* so a drain cannot undo a deliberate stop. Party-wide removal is
|
|
2472
|
+
* {@link unpublishStrand}.
|
|
1817
2473
|
*/
|
|
1818
2474
|
stopStrand(strandId: string): Promise<void>;
|
|
1819
2475
|
/**
|
|
1820
2476
|
* Local teardown for one strand, shared by the caller-driven {@link stopStrand} and the
|
|
1821
|
-
* watcher-driven `handleStrandRemoved`: untrack hibernation, drop the sApp config
|
|
1822
|
-
* the instance, emit `strand:stopped`. Touches no
|
|
1823
|
-
* removal the node is on is the caller's concern,
|
|
2477
|
+
* watcher-driven `handleStrandRemoved`: untrack hibernation, drop the sApp config and any
|
|
2478
|
+
* recorded launch refusal, stop the instance, emit `strand:stopped`. Touches no
|
|
2479
|
+
* control-plane row — which side of the removal the node is on is the caller's concern,
|
|
2480
|
+
* not this method's.
|
|
1824
2481
|
*
|
|
1825
2482
|
* The stop + emit are skipped when the strand manager holds no instance for `strandId` —
|
|
1826
2483
|
* e.g. a party owner that published a strand's row but never ran it locally, or an
|
|
1827
2484
|
* explicit {@link stopStrand} for an id this node never started. `hibernationManager`
|
|
1828
|
-
* untrack and the `sAppConfigs`
|
|
1829
|
-
* nothing to remove), so a launch
|
|
1830
|
-
* gets its stray
|
|
2485
|
+
* untrack and the `sAppConfigs` / {@link strandLaunchRefusals} / {@link discoveredStrands}
|
|
2486
|
+
* deletes stay unconditional (all no-ops when there is nothing to remove), so a launch
|
|
2487
|
+
* that failed before an instance was ever tracked still gets its stray entries cleared.
|
|
2488
|
+
*
|
|
2489
|
+
* Dropping the {@link discoveredStrands} entry here covers both callers, and both
|
|
2490
|
+
* readings are the intended one: `handleStrandRemoved` arrives because the control row
|
|
2491
|
+
* is gone, and an explicit {@link stopStrand} is a deliberate abandonment — neither
|
|
2492
|
+
* strand may be re-offered to a later `getDiscoveredStrands()` drain.
|
|
1831
2493
|
*/
|
|
1832
2494
|
private detachStrand;
|
|
1833
2495
|
/**
|
|
@@ -1934,6 +2596,11 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1934
2596
|
* relay indistinguishable from a hung start, and much shorter would fail nodes
|
|
1935
2597
|
* on links that were merely slow. The retries carry on in the background after
|
|
1936
2598
|
* this resolves, exactly as they do for a {@link reserveRelays} caller.
|
|
2599
|
+
*
|
|
2600
|
+
* `network.requireRelay === false` softens only the outcome below: a first
|
|
2601
|
+
* attempt that lands no `/p2p-circuit` address is logged instead of thrown, and
|
|
2602
|
+
* `start()` carries on with the retry supervisor already running in the
|
|
2603
|
+
* background (the same supervisor {@link reserveRelays} always starts).
|
|
1937
2604
|
*/
|
|
1938
2605
|
private driveControlRelayReservation;
|
|
1939
2606
|
/**
|
|
@@ -2191,6 +2858,23 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
2191
2858
|
* best-effort (a failed read keeps the previous snapshot); never rejects.
|
|
2192
2859
|
*/
|
|
2193
2860
|
refreshMembershipGate(reason?: string): Promise<void>;
|
|
2861
|
+
/**
|
|
2862
|
+
* Re-materialize a CLOSED strand's revoked-peer deny set now, and hang up any
|
|
2863
|
+
* connected peer it newly covers — the strand-side counterpart to
|
|
2864
|
+
* {@link refreshMembershipGate}.
|
|
2865
|
+
*
|
|
2866
|
+
* Unlike that one, NOTHING calls this automatically: strand membership is
|
|
2867
|
+
* written through the strand's own `Database` handle
|
|
2868
|
+
* (`strand-membership-writer.ts`), which raises no notification this runtime
|
|
2869
|
+
* can hook, and a revocation that arrives by REPLICATION raises none either.
|
|
2870
|
+
* The gate therefore polls (default 30 s). An app that has just called
|
|
2871
|
+
* `revokeMember` or `leaveStrand` should follow the write with this call so
|
|
2872
|
+
* the cut is immediate rather than up to one poll interval late.
|
|
2873
|
+
*
|
|
2874
|
+
* Quiet no-op for a strand that is not running, is quiesced, is open, or has
|
|
2875
|
+
* the gate disabled. Never rejects; resolves once the sweep has finished.
|
|
2876
|
+
*/
|
|
2877
|
+
refreshRevocationEnforcement(strandId: string): Promise<void>;
|
|
2194
2878
|
/**
|
|
2195
2879
|
* The single in-flight refresh loop behind {@link refreshMembershipGate}: read
|
|
2196
2880
|
* until the stale flag stays clear, then release the slot.
|
|
@@ -2281,6 +2965,20 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
2281
2965
|
/**
|
|
2282
2966
|
* Add a drone to the cadre (for phone/server adding provider-hosted node).
|
|
2283
2967
|
* Creates authorization and seed for drone initialization.
|
|
2968
|
+
*
|
|
2969
|
+
* Also retains the drone's handed-over addresses as a durable dial target (see
|
|
2970
|
+
* {@link bootstrapPeerStore}). The drone cannot dial an owner that does not
|
|
2971
|
+
* listen (a phone), and its `CadrePeer` row stays unsigned — so unresolvable —
|
|
2972
|
+
* until it self-publishes over a connection; this node therefore has to open
|
|
2973
|
+
* that connection from the addresses it was handed, on this launch or a later
|
|
2974
|
+
* one.
|
|
2975
|
+
*
|
|
2976
|
+
* Nothing is dialed here: the drone has not received the seed yet. After
|
|
2977
|
+
* delivering it, call {@link reconcileControlCohort} to dial straight away;
|
|
2978
|
+
* otherwise the next timed reconcile pass does. A pass already in flight is
|
|
2979
|
+
* joined rather than restarted, and one that listed siblings before this add
|
|
2980
|
+
* does not dial the drone, so a caller waiting for the connection should allow
|
|
2981
|
+
* for one more timed pass.
|
|
2284
2982
|
*/
|
|
2285
2983
|
addDrone(options: AddDroneOptions): Promise<DroneInitResult>;
|
|
2286
2984
|
/**
|
|
@@ -2337,6 +3035,89 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
2337
3035
|
* @returns The member key and strand info if successful
|
|
2338
3036
|
*/
|
|
2339
3037
|
formStrand(invitation: OpenInvitation, disclosure?: StrandFormationDisclosure): Promise<FormStrandResult>;
|
|
3038
|
+
/**
|
|
3039
|
+
* Adopt an approved closed-strand formation's membership half on the JOINER:
|
|
3040
|
+
*
|
|
3041
|
+
* 1. Mint-or-reuse this party's own strand identity — {@link ensureStrandPartyKey}
|
|
3042
|
+
* with no explicit key returns the stored `StrandPartyKey` row when one exists (a
|
|
3043
|
+
* re-formation by an existing member party: lost addresses, an app reinstall with
|
|
3044
|
+
* an intact control DB) and mints + persists a fresh one otherwise. The invitation
|
|
3045
|
+
* will admit THIS key's public half as the `Strand.Member`.
|
|
3046
|
+
* 2. Stage the invitation in {@link pendingMembershipInvites} for the strand
|
|
3047
|
+
* bring-up flow (`strand-node-binds-member-peer`) to redeem via `consumeInvite`.
|
|
3048
|
+
*
|
|
3049
|
+
* Throws — failing the whole {@link formStrand} — when the identity cannot be
|
|
3050
|
+
* persisted: a joiner "joined" without a persistable identity could never become a
|
|
3051
|
+
* member, and failing loudly beats a silent half-member. The formation itself has
|
|
3052
|
+
* already succeeded by then and its one-time token is SPENT, so the error says so:
|
|
3053
|
+
* recovery is fixing the underlying cause (no owner signing key / control DB write
|
|
3054
|
+
* rejected) and redeeming a FRESH invitation.
|
|
3055
|
+
*/
|
|
3056
|
+
private adoptFormationMembershipInvite;
|
|
3057
|
+
/**
|
|
3058
|
+
* The pending single-use membership invitation a closed-strand formation carried back
|
|
3059
|
+
* for `strandId`, or `undefined` when none is staged — the seam the strand bring-up
|
|
3060
|
+
* membership reconciler (`strand-membership-reconciler.ts`) reads to redeem the
|
|
3061
|
+
* joiner's `Strand.Member` seat. `undefined` therefore also means "already redeemed,
|
|
3062
|
+
* burned, or found dead": the reconciler clears the entry as soon as it settles the
|
|
3063
|
+
* invitation, so a caller polling this sees it disappear on its own. In-memory only;
|
|
3064
|
+
* see {@link pendingMembershipInvites} for lifetime and re-formation semantics.
|
|
3065
|
+
*/
|
|
3066
|
+
getPendingMembershipInvite(strandId: string): StrandMembershipInvite | undefined;
|
|
3067
|
+
/**
|
|
3068
|
+
* Responder-side issuer behind the formation manager's `issueMembershipInvite` seam
|
|
3069
|
+
* (see `StrandFormationManagerOptions.issueMembershipInvite` for the contract this
|
|
3070
|
+
* implements): mint a single-use `Strand.Invite` against the LIVE host strand so a
|
|
3071
|
+
* validated joiner can seat its own `Strand.Member` row.
|
|
3072
|
+
*
|
|
3073
|
+
* - Open host strand → `null` (no members, nothing to invite into).
|
|
3074
|
+
* - Closed host strand whose founder launch was refused as pre-split
|
|
3075
|
+
* ({@link strandLaunchRefusals}) → rethrow the recorded `PreSplitStrandIdentityError`;
|
|
3076
|
+
* the manager maps it to the NON-retryable `HOST_STRAND_MUST_BE_RECREATED_REASON`.
|
|
3077
|
+
* - Closed host strand with no `StrandPartyKey` row → throw: this party's identity is
|
|
3078
|
+
* the invite's issuing manager, and without it nothing can sign the issuance. (The
|
|
3079
|
+
* founder's publish/launch paths mint it, so this is a not-yet-converged sibling.)
|
|
3080
|
+
* The manager maps the throw to a clean retryable rejection BEFORE the formation
|
|
3081
|
+
* token is spent.
|
|
3082
|
+
* - Closed host strand with no running local instance/database → throw, same mapping:
|
|
3083
|
+
* a joiner admitted without an invitation would look joined and never become a
|
|
3084
|
+
* member, and a responder not running the strand cannot serve its sync anyway.
|
|
3085
|
+
* - Closed host strand whose LIVE rows carry the pre-split fingerprint
|
|
3086
|
+
* (`assertNotPreSplitStrand`) → throw `PreSplitStrandIdentityError`, same mapping as
|
|
3087
|
+
* the recorded refusal. Covers the responders that never ran a refused founder launch:
|
|
3088
|
+
* a sibling machine of the founding party, or a node restarted since the refusal.
|
|
3089
|
+
*
|
|
3090
|
+
* The recorded refusal is checked first: it is an in-memory read and the only
|
|
3091
|
+
* permanent diagnosis. Identity is checked BEFORE the runtime: it is the cheaper read
|
|
3092
|
+
* and the more actionable diagnosis when both are missing (a missing runtime is
|
|
3093
|
+
* transient, a missing identity is not), and it keeps the branch reachable without
|
|
3094
|
+
* standing a strand runtime up.
|
|
3095
|
+
*
|
|
3096
|
+
* The invitation expires `MEMBERSHIP_INVITE_TTL_MS` from now — see that constant for
|
|
3097
|
+
* the slow-joiner / lost-result tradeoff.
|
|
3098
|
+
*
|
|
3099
|
+
* NOTE: the issuing identity must be a `Strand.Manager` (the schema's `InviteValid`
|
|
3100
|
+
* gate), and only the FOUNDING party's key is seated as one. Today only the founder
|
|
3101
|
+
* party can host a bound formation at all — a joining party never gets the host
|
|
3102
|
+
* strand's control `Strand` row, so `resolveStrand` reports `missing` on it — so this
|
|
3103
|
+
* never bites. If a joined party is ever able to host formations into a strand it
|
|
3104
|
+
* joined (re-invite / multi-hop join), issuance here fails the manager gate and every
|
|
3105
|
+
* such redemption rejects with the retryable-sounding
|
|
3106
|
+
* `MEMBERSHIP_INVITE_UNAVAILABLE_REASON` forever; that flow needs manager delegation,
|
|
3107
|
+
* not a retry.
|
|
3108
|
+
*/
|
|
3109
|
+
private issueStrandMembershipInvite;
|
|
3110
|
+
/**
|
|
3111
|
+
* Remember the responder's strand-network addresses for a strand this node just
|
|
3112
|
+
* formed, so the strand's discovery seed has something to work with when the app
|
|
3113
|
+
* launches it (see {@link crossPartyStrandAddrs}).
|
|
3114
|
+
*
|
|
3115
|
+
* Scoped strictly to `strandId`: these addresses reach ONE strand node of ONE other
|
|
3116
|
+
* party and must never seed another strand's mesh or the control peerStore. An empty
|
|
3117
|
+
* list is recorded as no entry at all rather than an empty one, so a later formation
|
|
3118
|
+
* against the same strand that DOES disclose addresses is not shadowed.
|
|
3119
|
+
*/
|
|
3120
|
+
private recordCrossPartyStrandAddrs;
|
|
2340
3121
|
/**
|
|
2341
3122
|
* Encode an open invitation for out-of-band delivery (QR, link, etc.).
|
|
2342
3123
|
*/
|