@serfab/cadre-core 0.13.0 → 1.1.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 +20 -6
- 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 +558 -80
- package/dist/cadre-node.js +1060 -170
- 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 +299 -44
- package/dist/control-database.js +469 -69
- 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 +103 -19
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +80 -11
- package/dist/control-write-retry.js +176 -29
- package/dist/control-write-retry.js.map +1 -1
- package/dist/enrolled-machine-store.js +2 -2
- package/dist/index.d.ts +14 -7
- package/dist/index.js +33 -6
- package/dist/index.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +15 -7
- package/dist/node-local-snapshot.js +23 -8
- 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/relay-addrs.d.ts +90 -52
- package/dist/relay-addrs.js +91 -59
- 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 +34 -16
- package/dist/strand-database.js +38 -24
- 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 +87 -5
- package/dist/strand-formation-manager.js +109 -5
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +21 -1
- package/dist/strand-formation-protocol.js +30 -0
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +243 -6
- package/dist/strand-instance-manager.js +470 -37
- 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 +131 -11
- package/dist/strand-membership-writer.js +396 -134
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-network-config.d.ts +46 -14
- package/dist/strand-network-config.js +79 -23
- package/dist/strand-network-config.js.map +1 -1
- package/dist/strand-solicitation.d.ts +10 -1
- package/dist/strand-solicitation.js +5 -0
- 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 +6 -0
- 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 +262 -30
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/bootstrap-peer-store-file.ts +2 -2
- package/src/bootstrap-peer-store.ts +69 -27
- package/src/cadre-node.ts +7301 -6353
- package/src/control-authorization.ts +2 -1
- package/src/control-cohort.ts +122 -130
- package/src/control-database.ts +537 -69
- package/src/control-read-retry.ts +202 -167
- package/src/control-retry.ts +297 -190
- package/src/control-schema.ts +103 -19
- package/src/control-write-retry.ts +533 -383
- package/src/enrolled-machine-store.ts +2 -2
- package/src/index.ts +83 -6
- package/src/node-local-snapshot.ts +25 -8
- package/src/peer-dial.ts +215 -0
- package/src/relay-addrs.ts +458 -436
- 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 +50 -27
- package/src/strand-first-sync-gate.ts +258 -0
- package/src/strand-formation-manager.ts +140 -5
- package/src/strand-formation-protocol.ts +43 -1
- package/src/strand-instance-manager.ts +577 -37
- 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 +527 -189
- package/src/strand-network-config.ts +102 -24
- package/src/strand-solicitation.ts +16 -1
- package/src/strand-wake-protocol.ts +21 -66
- package/src/strand-watcher.ts +6 -0
- package/src/timed-step.ts +28 -0
- package/src/timeout-scheduler.ts +28 -0
- package/src/types.ts +1789 -1554
package/dist/cadre-node.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Libp2p, PeerId } from '@libp2p/interface';
|
|
2
2
|
import type { Multiaddr } from '@multiformats/multiaddr';
|
|
3
|
-
import type { CadreNodeConfig, StrandInstance, StrandRow, StrandConfig, FoundStrandConfig, FoundStrandResult, 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
7
|
import { type EnrolledMachineStore } from './enrolled-machine-store.js';
|
|
8
8
|
import { type SAppIdLookup } from './strand-watcher.js';
|
|
9
|
+
import { type ControlCohortReconcileResult } from './control-cohort.js';
|
|
9
10
|
import { EnrollmentService } from './enrollment.js';
|
|
10
11
|
import { ControlDatabase } from './control-database.js';
|
|
11
12
|
import { SeedBootstrapService } from './seed-bootstrap.js';
|
|
@@ -91,10 +92,11 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
91
92
|
private _running;
|
|
92
93
|
/**
|
|
93
94
|
* The control database's own raw storage (cache-wrapped), resolved once per
|
|
94
|
-
* `start()` and released in {@link cleanup}. Owning it is what keeps
|
|
95
|
-
*
|
|
96
|
-
* `
|
|
97
|
-
*
|
|
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.
|
|
98
100
|
*/
|
|
99
101
|
private controlStorage;
|
|
100
102
|
/**
|
|
@@ -150,6 +152,14 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
150
152
|
private controlBringUpInFlight;
|
|
151
153
|
/** Map of strandId -> sAppConfig for sAppId filtering and management */
|
|
152
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;
|
|
153
163
|
/**
|
|
154
164
|
* Most-recently pushed invite addresses (see {@link setInviteAddresses}).
|
|
155
165
|
* When non-null these take priority over `libp2pNode.getMultiaddrs()` when
|
|
@@ -277,37 +287,89 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
277
287
|
*/
|
|
278
288
|
private readonly crossPartyStrandAddrs;
|
|
279
289
|
/**
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
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.
|
|
286
342
|
*
|
|
287
343
|
* A STORE rather than a plain map (see `bootstrap-peer-store.ts`), because the
|
|
288
344
|
* targets must outlive the process: a node seeded into a party it could not
|
|
289
345
|
* reach has nothing else on disk naming that party's addresses (`applySeed`
|
|
290
346
|
* writes no control row, and `CadrePeer` fills in only after a connection
|
|
291
347
|
* succeeds), so an in-memory-only set left it stranded permanently across a
|
|
292
|
-
* 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
|
|
293
352
|
* {@link initializeBootstrapPeerStore} during {@link start}; deliberately NOT
|
|
294
353
|
* cleared by {@link cleanup}, so it survives a stop()→start() cycle of the same
|
|
295
354
|
* node instance — same lifecycle as {@link trustedOwnerStore}. Durability
|
|
296
|
-
* depends on the injected backend
|
|
297
|
-
* RN/browser stay ephemeral for now.
|
|
355
|
+
* depends on the injected backend; the default is in-memory.
|
|
298
356
|
*
|
|
299
357
|
* Deliberately NOT the libp2p peer store, which {@link peerStoreAddrs} already
|
|
300
358
|
* reads for the steady-state path. The peer store is shared with everything
|
|
301
359
|
* libp2p discovers, so "dial every entry" would grow into dialing arbitrary
|
|
302
360
|
* discovered peers as the node lives longer; this store holds exactly the peers
|
|
303
|
-
* an owner-signed, trust-anchored seed nominated as owners
|
|
304
|
-
*
|
|
305
|
-
*
|
|
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}).
|
|
306
366
|
*
|
|
307
|
-
* A later
|
|
367
|
+
* A later record OVERWRITES an entry rather than merging, so a re-seed after an
|
|
308
368
|
* owner's address changes replaces the stale address instead of accumulating
|
|
309
|
-
* dead ones
|
|
310
|
-
* 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.
|
|
311
373
|
*/
|
|
312
374
|
private bootstrapPeerStore;
|
|
313
375
|
/**
|
|
@@ -365,6 +427,19 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
365
427
|
* both read "no row yet" and race a duplicate INSERT (a `CadrePeer` PK conflict).
|
|
366
428
|
*/
|
|
367
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;
|
|
368
443
|
/**
|
|
369
444
|
* Re-replication queue for owner `authorizePeer` writes that committed while
|
|
370
445
|
* this node was alone — `controlNode.getConnections().length === 0` at write
|
|
@@ -398,6 +473,14 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
398
473
|
* exactly a "removals from before this lifetime" case).
|
|
399
474
|
*/
|
|
400
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;
|
|
401
484
|
/** This node's own `CadrePeer` self-write committed local-only (re-touched on growth). */
|
|
402
485
|
private pendingSelfPeerWrite;
|
|
403
486
|
/** This node's own `DeviceToken` self-write committed local-only (re-touched on growth). */
|
|
@@ -488,6 +571,23 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
488
571
|
* Get all strand instances
|
|
489
572
|
*/
|
|
490
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>;
|
|
491
591
|
/**
|
|
492
592
|
* Get a specific strand instance
|
|
493
593
|
*/
|
|
@@ -596,10 +696,10 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
596
696
|
*/
|
|
597
697
|
getEnrolledMachineStore(): EnrolledMachineStore | null;
|
|
598
698
|
/**
|
|
599
|
-
* The node-local
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
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?".
|
|
603
703
|
*/
|
|
604
704
|
getBootstrapPeerStore(): BootstrapPeerStore | null;
|
|
605
705
|
/**
|
|
@@ -695,9 +795,8 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
695
795
|
* operator whose relay slot is decorative may genuinely want only the announced
|
|
696
796
|
* address, so this reports and proceeds.
|
|
697
797
|
*
|
|
698
|
-
* Keyed off
|
|
699
|
-
*
|
|
700
|
-
* 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.
|
|
701
800
|
*
|
|
702
801
|
* NOTE: the only direct `console.*` in this library — a boot-time operator warning, not a
|
|
703
802
|
* diagnostic trace (those use `debug`, which an operator never sees without `DEBUG=`). If a
|
|
@@ -972,6 +1071,55 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
972
1071
|
registerSelf(): Promise<SelfRegistrationOutcome>;
|
|
973
1072
|
/** The body of {@link registerSelf}; serialised by its single-flight guard. */
|
|
974
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;
|
|
975
1123
|
/**
|
|
976
1124
|
* Sign this node's address record for publication, stamped strictly later than
|
|
977
1125
|
* `existing` (the row it is about to replace, or null for a first insert) so a
|
|
@@ -1076,7 +1224,7 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1076
1224
|
* Applied by ALL THREE sources of control-dial candidates, so no call site
|
|
1077
1225
|
* invents its own rule and none can produce a mixed list: the signed record
|
|
1078
1226
|
* ({@link resolvePeerAddrs}), the libp2p address book
|
|
1079
|
-
* ({@link peerStoreAddrs}), and a
|
|
1227
|
+
* ({@link peerStoreAddrs}), and a retained out-of-band dial target
|
|
1080
1228
|
* ({@link bootstrapDialAddrs}).
|
|
1081
1229
|
*
|
|
1082
1230
|
* An address naming a DIFFERENT trailing peer id is dropped: it does not reach
|
|
@@ -1089,14 +1237,14 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1089
1237
|
* way, never thrown, so every caller's list-shaping stays total.
|
|
1090
1238
|
*
|
|
1091
1239
|
* Used both to normalize addresses this node DIALS (a `CadrePeer` row, a
|
|
1092
|
-
*
|
|
1240
|
+
* retained bootstrap addr) and to normalize the ones it ANNOUNCES
|
|
1093
1241
|
* ({@link getStrandMultiaddrs}); the rule is the same either way — an address
|
|
1094
1242
|
* that does not reach `peerId` is not an address for `peerId`.
|
|
1095
1243
|
*
|
|
1096
1244
|
* `withTrailingPeerId` encapsulates `/p2p/<peerId>`, which throws on a peer id
|
|
1097
1245
|
* that does not parse. Unreachable for a `CadrePeer` row (the binding gate
|
|
1098
1246
|
* above parsed it already) and for our own node's id, reachable for a
|
|
1099
|
-
*
|
|
1247
|
+
* retained one ({@link bootstrapDialAddrs}).
|
|
1100
1248
|
*/
|
|
1101
1249
|
private bindAddrToPeer;
|
|
1102
1250
|
/** Parse multiaddr strings, dropping (and logging) any that fail to parse. */
|
|
@@ -1110,13 +1258,32 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1110
1258
|
* `self:peer:update` (all wired in {@link startRecordRefresh}).
|
|
1111
1259
|
*
|
|
1112
1260
|
* Concurrent triggers collapse into a single in-flight pass
|
|
1113
|
-
* (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.
|
|
1114
1263
|
* Best-effort throughout: a failure to resolve/dial any one sibling is logged
|
|
1115
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.
|
|
1116
1270
|
*/
|
|
1117
|
-
reconcileControlCohort(): Promise<
|
|
1271
|
+
reconcileControlCohort(): Promise<ControlCohortReconcileResult>;
|
|
1118
1272
|
/** Body of {@link reconcileControlCohort}; serialised by its single-flight guard. */
|
|
1119
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;
|
|
1120
1287
|
/**
|
|
1121
1288
|
* Resolve every sibling's signed address record once and merge what resolves
|
|
1122
1289
|
* into the control node's libp2p **address book** (peerStore). Returns the
|
|
@@ -1136,6 +1303,9 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1136
1303
|
* resolves to nothing (revoked, stale, untrusted) is not written at all, so its
|
|
1137
1304
|
* existing entry ages out on its own.
|
|
1138
1305
|
*
|
|
1306
|
+
* The same resolution also keeps a sibling's retained out-of-band dial target
|
|
1307
|
+
* current ({@link refreshDialHint}).
|
|
1308
|
+
*
|
|
1139
1309
|
* NOTE: this resolves EVERY sibling serially before the dial loop below runs,
|
|
1140
1310
|
* so it costs one record query per sibling per reconcile pass (~15s) and each
|
|
1141
1311
|
* one delays the pass's first dial. A cadre is a handful of devices, so today
|
|
@@ -1143,6 +1313,28 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1143
1313
|
* into one query, merge only on change, or move the warm pass after the dials.
|
|
1144
1314
|
*/
|
|
1145
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;
|
|
1146
1338
|
/**
|
|
1147
1339
|
* {@link resolvePeerAddrs} for one sibling, best-effort: a control-DB read
|
|
1148
1340
|
* failure yields `[]` (and the cold-start fallback then gets its turn) rather
|
|
@@ -1150,37 +1342,78 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1150
1342
|
*/
|
|
1151
1343
|
private resolveSiblingAddrs;
|
|
1152
1344
|
/**
|
|
1153
|
-
*
|
|
1345
|
+
* Time limits for dialing ONE control-network peer from its candidate
|
|
1346
|
+
* addresses ({@link dialPeerAddrs}): the reconcile pass's steady-state sibling
|
|
1154
1347
|
* ({@link dialControlSibling}) and cold-start bootstrap peer
|
|
1155
|
-
* ({@link dialBootstrapPeer})
|
|
1156
|
-
*
|
|
1348
|
+
* ({@link dialBootstrapPeer}) dials, and — handed to every
|
|
1349
|
+
* {@link SeedBootstrapService} this node builds — `applySeed`'s owner dials and
|
|
1350
|
+
* `dialInvite`.
|
|
1157
1351
|
*
|
|
1158
|
-
* See {@link DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS} for why
|
|
1159
|
-
*
|
|
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.
|
|
1160
1355
|
*/
|
|
1161
|
-
private
|
|
1356
|
+
private controlDialBudget;
|
|
1162
1357
|
/**
|
|
1163
1358
|
* Dial one sibling from its already-resolved addresses, best-effort. Returns
|
|
1164
|
-
* whether
|
|
1359
|
+
* whether the dial resolved (false when no address resolves or the dial fails).
|
|
1165
1360
|
*
|
|
1166
1361
|
* A per-peer failure (NAT, offline, relay down, connection-gater denial, or
|
|
1167
|
-
* the {@link
|
|
1168
|
-
*
|
|
1362
|
+
* the {@link controlDialBudget} expiring) is logged and swallowed so one
|
|
1363
|
+
* unreachable sibling never aborts the pass — exactly like
|
|
1169
1364
|
* {@link SeedBootstrapService.applySeed}'s owner-dial loop. A failed dial is
|
|
1170
1365
|
* simply retried on the next pass.
|
|
1171
1366
|
*/
|
|
1172
1367
|
private dialControlSibling;
|
|
1173
1368
|
/**
|
|
1174
|
-
* A sibling's control-network dial addresses for the reconcile pass
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
* `resolved` for it by
|
|
1178
|
-
* re-resolved, so the
|
|
1179
|
-
*
|
|
1180
|
-
* the
|
|
1181
|
-
*
|
|
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.
|
|
1182
1397
|
*/
|
|
1183
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;
|
|
1184
1417
|
/**
|
|
1185
1418
|
* Cold-start fallback: the libp2p peerStore multiaddrs for `peerId` (seeded by
|
|
1186
1419
|
* {@link SeedBootstrapService.applySeed}). Returns `[]` on a missing entry or any
|
|
@@ -1213,15 +1446,33 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1213
1446
|
* signature-checked against a trust-anchored signer before this runs, and the
|
|
1214
1447
|
* flag only *selects a dial target*; a dial grants no authority.
|
|
1215
1448
|
*
|
|
1216
|
-
*
|
|
1217
|
-
* that applies a seed minted after it joined finds itself in the owner list;
|
|
1218
|
-
* retaining that would make the cold-start pass dial this node forever (the
|
|
1219
|
-
* steady-state pass filters self out of its sibling list for the same reason).
|
|
1449
|
+
* Addressless peers and self are skipped by {@link retainDialTarget}.
|
|
1220
1450
|
*/
|
|
1221
1451
|
private recordSeedBootstrapPeers;
|
|
1222
1452
|
/**
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
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.
|
|
1225
1476
|
*
|
|
1226
1477
|
* `SeedBootstrapService.applySeed` dials those owners exactly ONCE, best-effort.
|
|
1227
1478
|
* When that single dial fails — owner momentarily down, relay reservation not
|
|
@@ -1244,13 +1495,14 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1244
1495
|
*/
|
|
1245
1496
|
private dialColdStartBootstrap;
|
|
1246
1497
|
/**
|
|
1247
|
-
* Bind a
|
|
1248
|
-
* 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.
|
|
1249
1501
|
*
|
|
1250
1502
|
* The same {@link normalizeDialAddrs} rule the resolved and address-book paths
|
|
1251
1503
|
* use, on the third and last source of control-dial candidates: an address that
|
|
1252
1504
|
* already terminates in `/p2p/<id>` must carry THIS peer's id or it is dropped
|
|
1253
|
-
* (
|
|
1505
|
+
* (an entry that disagrees with itself is not a dial target); one that does not —
|
|
1254
1506
|
* a bare listen addr, or a relay hop with the destination missing — gets the id
|
|
1255
1507
|
* encapsulated; an unencapsulatable one is dropped rather than dialed bare.
|
|
1256
1508
|
*/
|
|
@@ -1508,6 +1760,15 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1508
1760
|
* `control:disconnected` emit, which a never-connected start has no edge for.
|
|
1509
1761
|
*/
|
|
1510
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;
|
|
1511
1772
|
private handleStrandIdle;
|
|
1512
1773
|
/**
|
|
1513
1774
|
* Hibernate a strand: release its strand-network resources via the strand
|
|
@@ -1601,13 +1862,32 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1601
1862
|
*
|
|
1602
1863
|
* A rejected call leaves nothing running but DOES leave the sApp config
|
|
1603
1864
|
* registered, deliberately: both an explicit retry and the {@link StrandWatcher}'s
|
|
1604
|
-
* automatic relaunch need it.
|
|
1605
|
-
*
|
|
1606
|
-
*
|
|
1607
|
-
*
|
|
1608
|
-
*
|
|
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.
|
|
1609
1876
|
*/
|
|
1610
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>;
|
|
1611
1891
|
/**
|
|
1612
1892
|
* Publish a strand row to the shared control database under this node's own
|
|
1613
1893
|
* owner identity, so other cadre members discover it via control-network
|
|
@@ -1652,10 +1932,16 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1652
1932
|
* Failing loudly here is intentional: a silently-unpublished strand would run
|
|
1653
1933
|
* as a local-only island that no peer could ever discover or join.
|
|
1654
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
|
+
*
|
|
1655
1941
|
* @param strandId - Unique strand identifier (typically the same id passed to
|
|
1656
1942
|
* {@link addStrand}).
|
|
1657
1943
|
* @param type - `'o'` for open (default) or `'c'` for closed.
|
|
1658
|
-
* @param memberPrivateKey - Optional membership key for a closed strand.
|
|
1944
|
+
* @param memberPrivateKey - Optional shared membership (read) key for a closed strand.
|
|
1659
1945
|
* @returns The live `Strand` row — the one just inserted, or the matching one already
|
|
1660
1946
|
* there. A closed strand's caller should carry THIS row's `MemberPrivateKey` forward:
|
|
1661
1947
|
* on a repeat it is the stored key, not the argument.
|
|
@@ -1725,10 +2011,12 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1725
2011
|
* is an owner-signed control-plane write like every other, not "destroy the network".
|
|
1726
2012
|
*
|
|
1727
2013
|
* Irreversible for a closed strand (`Type='c'`): the row carries `MemberPrivateKey`,
|
|
1728
|
-
* this party's
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
*
|
|
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
|
|
1732
2020
|
* blacklisted: a fresh owner-signed {@link publishStrand} re-seats it under a new
|
|
1733
2021
|
* stamp — only the unsigned consent re-seat is permanently foreclosed (the removal's
|
|
1734
2022
|
* `Revocation` tombstone names the id, which the consent branch of
|
|
@@ -1768,6 +2056,55 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1768
2056
|
* there throws over a completed removal.
|
|
1769
2057
|
*/
|
|
1770
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;
|
|
1771
2108
|
/**
|
|
1772
2109
|
* Publish an owner-signed `FormationInvite` (open-invitation token) to the
|
|
1773
2110
|
* shared control database, so a later {@link formStrand} redemption can be
|
|
@@ -1882,8 +2219,17 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1882
2219
|
* now the tracked instance is founded in place
|
|
1883
2220
|
* ({@link StrandInstanceManager.foundExistingStrand}), waking it first if quiesced
|
|
1884
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).
|
|
1885
2226
|
*/
|
|
1886
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;
|
|
1887
2233
|
/**
|
|
1888
2234
|
* Resolve a strand's discovery seed — the dialable strand-network multiaddr
|
|
1889
2235
|
* strings for cohort siblings. Membership comes from the control network's
|
|
@@ -1983,10 +2329,10 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1983
2329
|
* Recorded OPTIMISTICALLY at announce time: `collectStrandAddrs` folds
|
|
1984
2330
|
* per-peer failure to `[]` and reports no per-peer success, and threading
|
|
1985
2331
|
* success out would change its API for little gain. A failed INITIAL announce
|
|
1986
|
-
*
|
|
1987
|
-
*
|
|
1988
|
-
*
|
|
1989
|
-
* (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.
|
|
1990
2336
|
*/
|
|
1991
2337
|
private recordDelegateAnnounces;
|
|
1992
2338
|
/**
|
|
@@ -2003,6 +2349,37 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
2003
2349
|
* target, which must not stack up per strand.
|
|
2004
2350
|
*/
|
|
2005
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;
|
|
2006
2383
|
/** One strand's share of {@link refreshDelegateGrants}: announce to the relays whose grant is due. */
|
|
2007
2384
|
private announceDelegateToDueRelays;
|
|
2008
2385
|
/**
|
|
@@ -2087,22 +2464,32 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
2087
2464
|
/**
|
|
2088
2465
|
* Stop a strand on THIS node only: untrack it from hibernation, drop its sApp config,
|
|
2089
2466
|
* stop the local instance, and emit `strand:stopped`. The shared `Strand` row is left
|
|
2090
|
-
* intact, so on the next node
|
|
2091
|
-
*
|
|
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}.
|
|
2092
2473
|
*/
|
|
2093
2474
|
stopStrand(strandId: string): Promise<void>;
|
|
2094
2475
|
/**
|
|
2095
2476
|
* Local teardown for one strand, shared by the caller-driven {@link stopStrand} and the
|
|
2096
|
-
* watcher-driven `handleStrandRemoved`: untrack hibernation, drop the sApp config
|
|
2097
|
-
* the instance, emit `strand:stopped`. Touches no
|
|
2098
|
-
* 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.
|
|
2099
2481
|
*
|
|
2100
2482
|
* The stop + emit are skipped when the strand manager holds no instance for `strandId` —
|
|
2101
2483
|
* e.g. a party owner that published a strand's row but never ran it locally, or an
|
|
2102
2484
|
* explicit {@link stopStrand} for an id this node never started. `hibernationManager`
|
|
2103
|
-
* untrack and the `sAppConfigs`
|
|
2104
|
-
* nothing to remove), so a launch
|
|
2105
|
-
* 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.
|
|
2106
2493
|
*/
|
|
2107
2494
|
private detachStrand;
|
|
2108
2495
|
/**
|
|
@@ -2209,6 +2596,11 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
2209
2596
|
* relay indistinguishable from a hung start, and much shorter would fail nodes
|
|
2210
2597
|
* on links that were merely slow. The retries carry on in the background after
|
|
2211
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).
|
|
2212
2604
|
*/
|
|
2213
2605
|
private driveControlRelayReservation;
|
|
2214
2606
|
/**
|
|
@@ -2573,6 +2965,20 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
2573
2965
|
/**
|
|
2574
2966
|
* Add a drone to the cadre (for phone/server adding provider-hosted node).
|
|
2575
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.
|
|
2576
2982
|
*/
|
|
2577
2983
|
addDrone(options: AddDroneOptions): Promise<DroneInitResult>;
|
|
2578
2984
|
/**
|
|
@@ -2629,6 +3035,78 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
2629
3035
|
* @returns The member key and strand info if successful
|
|
2630
3036
|
*/
|
|
2631
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;
|
|
2632
3110
|
/**
|
|
2633
3111
|
* Remember the responder's strand-network addresses for a strand this node just
|
|
2634
3112
|
* formed, so the strand's discovery seed has something to work with when the app
|