@serfab/cadre-core 0.10.0 → 0.12.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.
Files changed (145) hide show
  1. package/README.md +255 -255
  2. package/dist/announce-addrs.d.ts +49 -0
  3. package/dist/announce-addrs.js +78 -0
  4. package/dist/announce-addrs.js.map +1 -0
  5. package/dist/cadre-node.d.ts +484 -58
  6. package/dist/cadre-node.js +953 -182
  7. package/dist/cadre-node.js.map +1 -1
  8. package/dist/control-cohort.d.ts +26 -0
  9. package/dist/control-cohort.js +26 -0
  10. package/dist/control-cohort.js.map +1 -1
  11. package/dist/control-database.d.ts +222 -125
  12. package/dist/control-database.js +356 -266
  13. package/dist/control-database.js.map +1 -1
  14. package/dist/control-formation-recorder.d.ts +8 -7
  15. package/dist/control-formation-recorder.js +8 -7
  16. package/dist/control-formation-recorder.js.map +1 -1
  17. package/dist/control-read-retry.d.ts +103 -0
  18. package/dist/control-read-retry.js +145 -0
  19. package/dist/control-read-retry.js.map +1 -0
  20. package/dist/control-retry.d.ts +101 -0
  21. package/dist/control-retry.js +108 -0
  22. package/dist/control-retry.js.map +1 -0
  23. package/dist/control-schema.d.ts +1 -1
  24. package/dist/control-schema.js +805 -774
  25. package/dist/control-schema.js.map +1 -1
  26. package/dist/control-write-retry.d.ts +65 -31
  27. package/dist/control-write-retry.js +33 -103
  28. package/dist/control-write-retry.js.map +1 -1
  29. package/dist/delegate-admission.d.ts +17 -15
  30. package/dist/delegate-admission.js +17 -15
  31. package/dist/delegate-admission.js.map +1 -1
  32. package/dist/index.d.ts +7 -5
  33. package/dist/index.js +18 -9
  34. package/dist/index.js.map +1 -1
  35. package/dist/membership-connection-gater.d.ts +211 -25
  36. package/dist/membership-connection-gater.js +309 -29
  37. package/dist/membership-connection-gater.js.map +1 -1
  38. package/dist/peer-addr-book.d.ts +92 -0
  39. package/dist/peer-addr-book.js +220 -0
  40. package/dist/peer-addr-book.js.map +1 -0
  41. package/dist/peer-record.d.ts +26 -0
  42. package/dist/peer-record.js +32 -0
  43. package/dist/peer-record.js.map +1 -1
  44. package/dist/relay-addrs.d.ts +76 -26
  45. package/dist/relay-addrs.js +115 -27
  46. package/dist/relay-addrs.js.map +1 -1
  47. package/dist/relay-reservation.d.ts +20 -15
  48. package/dist/relay-reservation.js +18 -14
  49. package/dist/relay-reservation.js.map +1 -1
  50. package/dist/seed-bootstrap.d.ts +7 -0
  51. package/dist/seed-bootstrap.js +15 -13
  52. package/dist/seed-bootstrap.js.map +1 -1
  53. package/dist/strand-addr-protocol.js +16 -0
  54. package/dist/strand-addr-protocol.js.map +1 -1
  55. package/dist/strand-backfill.js +13 -4
  56. package/dist/strand-backfill.js.map +1 -1
  57. package/dist/strand-cohort.d.ts +4 -37
  58. package/dist/strand-cohort.js +4 -15
  59. package/dist/strand-cohort.js.map +1 -1
  60. package/dist/strand-database.d.ts +11 -16
  61. package/dist/strand-database.js +19 -7
  62. package/dist/strand-database.js.map +1 -1
  63. package/dist/strand-formation-manager.d.ts +18 -17
  64. package/dist/strand-formation-manager.js +25 -27
  65. package/dist/strand-formation-manager.js.map +1 -1
  66. package/dist/strand-formation-protocol.d.ts +7 -7
  67. package/dist/strand-formation-protocol.js +8 -8
  68. package/dist/strand-formation-protocol.js.map +1 -1
  69. package/dist/strand-instance-manager.d.ts +53 -21
  70. package/dist/strand-instance-manager.js +128 -37
  71. package/dist/strand-instance-manager.js.map +1 -1
  72. package/dist/strand-membership-writer.d.ts +113 -19
  73. package/dist/strand-membership-writer.js +219 -51
  74. package/dist/strand-membership-writer.js.map +1 -1
  75. package/dist/strand-solicitation.d.ts +3 -3
  76. package/dist/strand-solicitation.js +4 -5
  77. package/dist/strand-solicitation.js.map +1 -1
  78. package/dist/strand-wake-protocol.d.ts +47 -2
  79. package/dist/strand-wake-protocol.js +76 -6
  80. package/dist/strand-wake-protocol.js.map +1 -1
  81. package/dist/types.d.ts +107 -54
  82. package/dist/types.js.map +1 -1
  83. package/package.json +8 -7
  84. package/src/announce-addrs.ts +93 -0
  85. package/src/arachnode-stub.ts +159 -159
  86. package/src/bootstrap-peer-store-file.ts +44 -44
  87. package/src/bootstrap-peer-store.ts +172 -172
  88. package/src/cadre-node.ts +1026 -194
  89. package/src/canonical-json.ts +30 -30
  90. package/src/control-authorization.ts +114 -114
  91. package/src/control-cohort.ts +130 -103
  92. package/src/control-database.ts +376 -298
  93. package/src/control-formation-recorder.ts +305 -304
  94. package/src/control-read-retry.ts +167 -0
  95. package/src/control-retry.ts +190 -0
  96. package/src/control-schema.ts +817 -786
  97. package/src/control-stream.ts +205 -205
  98. package/src/control-write-retry.ts +383 -473
  99. package/src/delegate-admission.ts +304 -302
  100. package/src/device-token.ts +94 -94
  101. package/src/diagnostics/connection-path.ts +277 -277
  102. package/src/diagnostics/webrtc-turn-tracker.ts +222 -222
  103. package/src/ed25519-key.ts +134 -134
  104. package/src/enrollment.ts +199 -199
  105. package/src/file-durable-slot.ts +61 -61
  106. package/src/formation-approval.ts +604 -604
  107. package/src/fs-atomic.ts +107 -107
  108. package/src/hibernation-manager.ts +437 -437
  109. package/src/identity-key.ts +109 -109
  110. package/src/index.ts +435 -406
  111. package/src/key-store-file.ts +132 -132
  112. package/src/key-store.ts +99 -99
  113. package/src/membership-connection-gater.ts +368 -39
  114. package/src/node-local-snapshot.ts +260 -260
  115. package/src/peer-addr-book.ts +250 -0
  116. package/src/peer-authorization.ts +259 -259
  117. package/src/peer-record.ts +197 -164
  118. package/src/push-credentials.ts +84 -84
  119. package/src/push-node.ts +64 -64
  120. package/src/push-notifier-apns.ts +247 -247
  121. package/src/push-notifier-fcm.ts +220 -220
  122. package/src/push-notifier-shared.ts +28 -28
  123. package/src/push-notifier.ts +52 -52
  124. package/src/relay-addrs.ts +134 -27
  125. package/src/relay-reservation.ts +22 -17
  126. package/src/seed-bootstrap.ts +15 -15
  127. package/src/seed-trust-policy.ts +140 -140
  128. package/src/strand-addr-protocol.ts +458 -441
  129. package/src/strand-backfill.ts +28 -5
  130. package/src/strand-cohort.ts +38 -77
  131. package/src/strand-database.ts +202 -204
  132. package/src/strand-formation-manager.ts +26 -28
  133. package/src/strand-formation-protocol.ts +692 -692
  134. package/src/strand-instance-manager.ts +636 -553
  135. package/src/strand-member-key.ts +39 -39
  136. package/src/strand-member-registry.ts +182 -182
  137. package/src/strand-membership-writer.ts +1627 -1430
  138. package/src/strand-solicitation.ts +471 -472
  139. package/src/strand-transport-key.ts +58 -58
  140. package/src/strand-wake-payload.ts +32 -32
  141. package/src/strand-wake-protocol.ts +400 -314
  142. package/src/strand-watcher.ts +314 -314
  143. package/src/trusted-owner-store-file.ts +41 -41
  144. package/src/trusted-owner-store.ts +171 -171
  145. package/src/types.ts +1354 -1303
@@ -13,6 +13,18 @@ import { StrandSolicitationService, type StrandSolicitationServiceOptions } from
13
13
  import { type RelayReservationState, type RelayReservationSupervisorOptions } from './relay-reservation.js';
14
14
  import type { WakeAck } from './types.js';
15
15
  import { type ConnectionPathSummary } from './diagnostics/connection-path.js';
16
+ /**
17
+ * How often a running strand re-resolves its siblings' strand-network addresses
18
+ * and re-merges them into its own libp2p address book
19
+ * ({@link CadreNode.refreshStrandPeerAddrs}). Overridable per node via
20
+ * `network.controlCohort.strandAddrRefreshMs`.
21
+ *
22
+ * Ten minutes sits comfortably inside the peerStore's one-hour address expiry
23
+ * (`MAX_ADDRESS_AGE`, see `peer-addr-book.ts`) with headroom for several missed
24
+ * passes, and far above the 15 s reconcile cadence the refresh rides on, so the
25
+ * strand-addr RPC fan-out stays cheap.
26
+ */
27
+ export declare const STRAND_PEER_ADDR_REFRESH_MS: number;
16
28
  type EventHandler<T> = (data: T) => void;
17
29
  /**
18
30
  * CadreNode is the main entry point for a cadre member.
@@ -68,6 +80,14 @@ export declare class CadreNode implements SAppIdLookup {
68
80
  private pushFanoutService;
69
81
  /** Backing field for the {@link running} / {@link isRunning} getters. */
70
82
  private _running;
83
+ /**
84
+ * The control database's own raw storage (cache-wrapped), resolved once per
85
+ * `start()` and released in {@link cleanup}. Owning it is what keeps
86
+ * `config.storage.provider('control')` a once-per-runtime call: a `stop()` then
87
+ * `start()` cycle on this object re-resolves against a live cache, and never
88
+ * orphans the previous wrapper's registration in the shared cache pool.
89
+ */
90
+ private controlStorage;
71
91
  /**
72
92
  * In-flight {@link serviceWake} operations keyed by strandId. Coalesces
73
93
  * concurrent on-demand wakes for the same strand into one runtime build + one
@@ -83,10 +103,11 @@ export declare class CadreNode implements SAppIdLookup {
83
103
  private windowWaiters;
84
104
  private eventHandlers;
85
105
  /**
86
- * Relay multiaddrs the caller asked {@link reserveRelays} to reserve through.
87
- * Empty (the default) means nobody asked, so {@link getRelayReservationState}
88
- * reports `none` — this whole surface is opt-in and dormant for the CLI/host
89
- * nodes, which use the configured `network.relayAddrs` route instead.
106
+ * Relay multiaddrs the caller asked {@link reserveRelays} to reserve through —
107
+ * or, on a node that names `network.relayAddrs`, the ones
108
+ * {@link driveControlRelayReservation} asked for at the end of {@link start}.
109
+ * Empty (the default) means neither happened, so {@link getRelayReservationState}
110
+ * reports `none`.
90
111
  */
91
112
  private relayReserveAddrs;
92
113
  /**
@@ -101,6 +122,23 @@ export declare class CadreNode implements SAppIdLookup {
101
122
  * supervisor to hold one — i.e. the pre-start `control node unavailable` case.
102
123
  */
103
124
  private relayReserveError;
125
+ /**
126
+ * True from just before the control libp2p node is created until
127
+ * {@link ControlDatabase.initialize} has settled — the connection gate's
128
+ * BRING-UP QUIET PERIOD (`membership-connection-gater.ts`), during which this
129
+ * node refuses every control connection in both directions.
130
+ *
131
+ * The invariant: the control database is built while this node holds zero
132
+ * control connections. Every same-party sibling connected in that window joins
133
+ * the Optimystic cohort the bring-up's block probes consult, and a sibling that
134
+ * has not yet replicated this node's `CadrePeer` row refuses them all — which
135
+ * fails `start()` outright and cannot be retried into convergence (writing the
136
+ * row that would clear the refusal needs the database being built).
137
+ *
138
+ * Cleared on FAILURE as well as success ({@link cleanup} clears it), so
139
+ * teardown is never gated.
140
+ */
141
+ private controlBringUpInFlight;
104
142
  /** Map of strandId -> sAppConfig for sAppId filtering and management */
105
143
  private sAppConfigs;
106
144
  /**
@@ -161,12 +199,25 @@ export declare class CadreNode implements SAppIdLookup {
161
199
  /**
162
200
  * In-memory delegate admission grants (see `delegate-admission.ts`): the
163
201
  * strand-node transport peerIds this party's members have announced over the
164
- * strand-addr RPC, admitted at the CONNECTION level so a member's NAT'd
165
- * strand node can hold a circuit-relay reservation on this node. Consulted
166
- * ONLY by {@link admitInboundControlConnection}; the fail-closed per-stream
167
- * gate ({@link authorizeInboundControlStream}) never honors it.
202
+ * strand-addr RPC, admitted at the CONNECTION and RESERVATION levels so a
203
+ * member's NAT'd strand node can hold a circuit-relay reservation on this
204
+ * node (without spending the unauthorized-reservation budget). Consulted
205
+ * ONLY by {@link admitInboundControlConnection} and
206
+ * {@link admitControlRelayReservation}; the fail-closed per-stream gate
207
+ * ({@link authorizeInboundControlStream}) never honors it.
168
208
  */
169
209
  private readonly delegateAdmission;
210
+ /**
211
+ * Bounded budget of concurrent circuit-relay reservations granted to peers
212
+ * this node cannot (yet) place as members — the boot-ordering window where a
213
+ * genuine sibling reserves before its `CadrePeer` row has replicated here.
214
+ * Consulted only by {@link admitControlRelayReservation} (the gater's
215
+ * `denyInboundRelayReservation` policy); authorized members and announced
216
+ * delegates are admitted before it and never counted. Cap from
217
+ * `network.unauthorizedRelayReservationCap` (default
218
+ * `MAX_UNAUTHORIZED_RELAY_RESERVATIONS`).
219
+ */
220
+ private readonly unauthorizedRelayReservations;
170
221
  /**
171
222
  * When this node last ANNOUNCED a delegate, keyed `<targetPeerId>\n<strandId>`
172
223
  * (the peer announced TO, not the delegate). Throttles
@@ -176,6 +227,16 @@ export declare class CadreNode implements SAppIdLookup {
176
227
  * strand is no longer running are pruned on each reconcile pass.
177
228
  */
178
229
  private readonly delegateAnnounceAt;
230
+ /**
231
+ * When each running strand last had its siblings' strand addresses re-resolved
232
+ * into its own address book, keyed by strandId. Throttles
233
+ * {@link refreshStrandPeerAddrs} to once per {@link STRAND_PEER_ADDR_REFRESH_MS}
234
+ * per strand so the 15 s reconcile tick never becomes per-tick RPC chatter.
235
+ * Entries for strands that are no longer running are pruned on each pass, so a
236
+ * hibernated-then-resumed strand refreshes immediately rather than inheriting a
237
+ * stale stamp.
238
+ */
239
+ private readonly strandPeerAddrRefreshAt;
179
240
  /**
180
241
  * Cold-start bootstrap dial targets: the owner-flagged peers of every seed
181
242
  * this node has applied, keyed by peer id, valued by the seed's multiaddr
@@ -473,6 +534,46 @@ export declare class CadreNode implements SAppIdLookup {
473
534
  */
474
535
  trustOwnerKeys(keys: Iterable<string>, source: Exclude<TrustSource, 'genesis'>): Promise<void>;
475
536
  private createControlNode;
537
+ /**
538
+ * The control database's cache-wrapped raw storage, resolved from
539
+ * `config.storage.provider` on first use and then held in {@link controlStorage}
540
+ * for the rest of this runtime.
541
+ *
542
+ * Resolving once is the contract `RawStorageProvider` (types.ts) states: a provider is
543
+ * called once per scope per runtime lifetime. Re-entering it per call would mint a
544
+ * second store over one backend — a fresh, cold cache, with the previous wrapper's
545
+ * registration orphaned in the process-wide pool.
546
+ *
547
+ * Lazy rather than eager in `start()` so the pure-unit call path
548
+ * (`cadre-node-control-node-options.spec.ts` calls `buildControlNodeOptions` on a
549
+ * bare `new CadreNode`) still resolves — and, on that path too, only once.
550
+ *
551
+ * Wrapped in the write-through raw-storage cache (quereus-plugin-sereus's
552
+ * `cached-storage.ts`) because a control start's cost is its raw-storage
553
+ * operation count.
554
+ */
555
+ private resolveControlStorage;
556
+ /**
557
+ * Warn the operator when `network.announceAddrs` will silently cost this node the
558
+ * relay reachability it also configured.
559
+ *
560
+ * A non-empty announce set REPLACES everything libp2p advertises, so the
561
+ * `/p2p-circuit` address earned by a relay reservation is dropped from the node's
562
+ * advertised addresses even though the reservation itself is still held — peers
563
+ * behind NAT stop being able to reach it through that relay. Not an error: an
564
+ * operator whose relay slot is decorative may genuinely want only the announced
565
+ * address, so this reports and proceeds.
566
+ *
567
+ * Keyed off the RESOLVED listen addrs rather than `relayAddrs` alone, so a
568
+ * hand-written `/p2p-circuit` entry in `listenAddrs` — the same reservation by the
569
+ * longer route — is caught too.
570
+ *
571
+ * NOTE: the only direct `console.*` in this library — a boot-time operator warning, not a
572
+ * diagnostic trace (those use `debug`, which an operator never sees without `DEBUG=`). If a
573
+ * second such warning ever appears here, route both through a `CadreNodeEvents` entry the
574
+ * embedder surfaces instead of growing a console surface inside a library.
575
+ */
576
+ private warnIfAnnounceAddrsDiscardRelay;
476
577
  /**
477
578
  * Map this node's config onto the control network's libp2p node options.
478
579
  *
@@ -482,6 +583,15 @@ export declare class CadreNode implements SAppIdLookup {
482
583
  * only caller in production is `createControlNode`.
483
584
  */
484
585
  private buildControlNodeOptions;
586
+ /**
587
+ * Does this node's CONTROL libp2p run the circuit-relay server? An explicit
588
+ * `network.enableRelay` wins; otherwise storage-profile nodes default to on
589
+ * (better connectivity/uptime). Shared by {@link buildControlNodeOptions}
590
+ * (which configures the server from it) and
591
+ * {@link admitInboundControlConnection} (whose deny/admit-for-relay branch
592
+ * must agree with whether a reservation is even servable here).
593
+ */
594
+ private relayServerEnabled;
485
595
  /**
486
596
  * Decide whether an inbound CONTROL-network connection from `remotePeerId`
487
597
  * should be admitted — the policy behind the connection gater wired in
@@ -491,7 +601,7 @@ export declare class CadreNode implements SAppIdLookup {
491
601
  * `membership-connection-gater.ts` for the layer's rationale and the
492
602
  * stranger-open protocol allowlist).
493
603
  *
494
- * Admits when ANY of:
604
+ * Returns `'admit'` when ANY of:
495
605
  * 1. a shared-baseline check admits ({@link admitControlPeerUnconditionally}
496
606
  * — not running / DB torn down, absent-or-empty trusted-owner anchor, or
497
607
  * configured bootstrap/relay infrastructure);
@@ -528,17 +638,69 @@ export declare class CadreNode implements SAppIdLookup {
528
638
  * this node yet is denied even though the formation handler would have
529
639
  * accepted it, exactly like the unreplicated-membership-row case below.
530
640
  *
641
+ * When every check falls through, the verdict depends on whether this node
642
+ * runs the circuit-relay server ({@link relayServerEnabled}): without one,
643
+ * `'deny'`; with one, `'admit-for-relay'` — a circuit-relay reservation is
644
+ * established by the reserving peer DIALING the relay, so a connection deny
645
+ * here kills the reservation, and that deny is NOT self-healing: an outbound
646
+ * reconcile re-dial re-establishes a data link, but no outbound dial can
647
+ * grant the remote peer a reservation, and a relay-only peer has no address
648
+ * of its own to dial back — the reservation IS its address. The gater admits
649
+ * such a connection, decides the reservation via
650
+ * {@link admitControlRelayReservation}, and drops the connection if no
651
+ * reservation is admitted in time (see `membership-connection-gater.ts` →
652
+ * "The relay-reservation seam").
653
+ *
531
654
  * NOTE: check 4/5 runs a control-DB read per inbound connection
532
655
  * (`listAuthorizedMembers`); connections are rare and cadres small, and this
533
656
  * layer is fail-open behind `ADMISSION_DECISION_TIMEOUT_MS`, so the live
534
657
  * read is safe here — unlike the per-stream gate, which must consult the
535
658
  * materialized {@link authorizedControlPeers} snapshot instead.
536
- * NOTE: a sibling whose membership row has not yet replicated to
537
- * this node is denied until the row converges (typically via the owner);
538
- * either side's next outbound reconcile dial (outbound is never gated)
539
- * re-establishes the link — self-healing, but visible as a transient deny.
659
+ * NOTE: on a relay-DISABLED node, a sibling whose membership row has not yet
660
+ * replicated here is denied until the row converges (typically via the
661
+ * owner); either side's next outbound reconcile dial (outbound is never
662
+ * gated) re-establishes the DATA link — self-healing for data, visible as a
663
+ * transient deny. That self-healing story never covered a reservation, which
664
+ * is exactly why the relay-enabled path above exists.
540
665
  */
541
666
  private admitInboundControlConnection;
667
+ /**
668
+ * Should `remotePeerId` be granted a circuit-relay reservation slot on this
669
+ * node's relay server? The policy behind the gater's
670
+ * `denyInboundRelayReservation` hook (see `membership-connection-gater.ts` →
671
+ * "The relay-reservation seam"); the circuit-relay server consults it per
672
+ * RESERVE request, so it is never called on a node whose relay server is off.
673
+ *
674
+ * Admits when ANY of:
675
+ * 1. a shared-baseline check admits ({@link admitControlPeerUnconditionally}
676
+ * — not running / DB torn down, absent-or-empty trusted-owner anchor, or
677
+ * configured bootstrap/relay infrastructure);
678
+ * 2. the peer holds a live delegate admission grant — a member's strand
679
+ * node reserving here (see `delegate-admission.ts`); never counted
680
+ * against the unauthorized budget;
681
+ * 3. the authorized-member set is empty (cold start) or the peer IS an
682
+ * authorized member — members are never counted against the budget; or
683
+ * 4. the peer takes (or already holds) a slot in the bounded
684
+ * unauthorized-reservation budget — the boot-ordering window where a
685
+ * genuine member reserves before its `CadrePeer` row replicates here.
686
+ * Cap via `network.unauthorizedRelayReservationCap` (0 = refuse every
687
+ * unauthorized reservation).
688
+ *
689
+ * Every uncounted admission also RELEASES the budget slot the peer may hold
690
+ * from an earlier reservation taken while it was still unplaceable — a member
691
+ * that boots, reserves, and only then has its row replicate here would
692
+ * otherwise keep that slot spent for the rest of the entry's TTL.
693
+ *
694
+ * The connection gate's stranger carve-outs (an open enrollment window, an
695
+ * outstanding formation invitation) deliberately do NOT extend here: they
696
+ * exist so a stranger's SEED or FORMATION stream can ride a connection, and
697
+ * neither needs relay capacity. A genuine invitee that does need a relay slot
698
+ * takes one from the budget like any other unplaced peer, so an open window
699
+ * never becomes an unbounded grant of this node's forwarding capacity.
700
+ */
701
+ private admitControlRelayReservation;
702
+ /** Admit a reservation on the peer's own merits, giving back any budget slot it still holds. */
703
+ private admitReservationUncounted;
542
704
  /**
543
705
  * Hold the control-network inbound gate open for not-yet-authorized peers
544
706
  * until `untilEpochMs` (extends, never shrinks, an already-open window).
@@ -550,7 +712,8 @@ export declare class CadreNode implements SAppIdLookup {
550
712
  /**
551
713
  * Record (or refresh) a delegate admission grant: `delegatePeerId` is the
552
714
  * transport peerId of `announcerPeerId`'s strand-`strandId` node, admitted
553
- * at the CONNECTION level (all a circuit-relay reservation needs) for
715
+ * at the CONNECTION and RESERVATION levels (all a circuit-relay reservation
716
+ * needs, without spending the unauthorized-reservation budget) for
554
717
  * `DELEGATE_GRANT_TTL_MS`. Called by the strand-addr responder after its
555
718
  * authorized-membership gate has passed; public so tests can drive the
556
719
  * admission policy without a full strand launch. A re-announce for the same
@@ -628,6 +791,12 @@ export declare class CadreNode implements SAppIdLookup {
628
791
  *
629
792
  * The SOLE caller is {@link drainMembershipGate}, which serializes refreshes —
630
793
  * so two reads can no longer settle out of order.
794
+ *
795
+ * Reads WITHOUT the transient-failure retry, deliberately: `ControlDatabase` drives
796
+ * this listener with its write lock HELD (`notifyMembershipChanged`), so a retrying
797
+ * read here would sleep its backoff holding the lock and stall every other local
798
+ * writer. Nothing is lost — this refresh already keeps the previous snapshot on
799
+ * failure and is re-driven by the next membership write and by the timed reconcile.
631
800
  */
632
801
  private refreshAuthorizedControlPeers;
633
802
  /**
@@ -738,8 +907,44 @@ export declare class CadreNode implements SAppIdLookup {
738
907
  * Survivors are returned signaling (`/p2p-circuit`) first, filtered to
739
908
  * signaling-only when requested, as parsed `Multiaddr`s (unparsable addrs
740
909
  * dropped). Any gate failure yields an empty array rather than throwing.
910
+ *
911
+ * Every returned address is normalized to terminate in `/p2p/<peerId>` — see
912
+ * {@link normalizeDialAddrs} for why that invariant, not each dial site's own
913
+ * handling, is what keeps a mixed list from taking a whole peer offline.
741
914
  */
742
915
  resolvePeerAddrs(peerId: string, opts?: ResolveOpts): Promise<Multiaddr[]>;
916
+ /**
917
+ * Guarantee every address handed to a control-network dial terminates in
918
+ * `/p2p/<peerId>`, with no duplicates — the invariant every dial path
919
+ * downstream relies on.
920
+ *
921
+ * `libp2p.dial(addrs)` requires that the addresses in ONE dial either all name
922
+ * a peer id or none do; a list mixing a circuit address (which ends in
923
+ * `/p2p/<target>`) with a bare direct one (which ends in `/tcp/…`) makes that
924
+ * call throw, and the whole peer is then silently skipped rather than one bad
925
+ * address.
926
+ *
927
+ * Applied by ALL THREE sources of control-dial candidates, so no call site
928
+ * invents its own rule and none can produce a mixed list: the signed record
929
+ * ({@link resolvePeerAddrs}), the libp2p address book
930
+ * ({@link peerStoreAddrs}), and a seed's retained owner addresses
931
+ * ({@link bootstrapDialAddrs}).
932
+ *
933
+ * An address naming a DIFFERENT trailing peer id is dropped: it does not reach
934
+ * `peerId`, so it does not belong in this peer's candidate list (libp2p's own
935
+ * dial queue filters the same shape out one layer lower).
936
+ */
937
+ private normalizeDialAddrs;
938
+ /**
939
+ * One address bound to `peerId`, or `null` when it cannot be — logged either
940
+ * way, never thrown, so every caller's list-shaping stays total.
941
+ *
942
+ * `withTrailingPeerId` encapsulates `/p2p/<peerId>`, which throws on a peer id
943
+ * that does not parse. Unreachable for a `CadrePeer` row (the binding gate
944
+ * above parsed it already), reachable for a seed-supplied one
945
+ * ({@link bootstrapDialAddrs}).
946
+ */
947
+ private bindAddrToPeer;
743
948
  /** Parse multiaddr strings, dropping (and logging) any that fail to parse. */
744
949
  private parseMultiaddrs;
745
950
  /**
@@ -759,29 +964,83 @@ export declare class CadreNode implements SAppIdLookup {
759
964
  /** Body of {@link reconcileControlCohort}; serialised by its single-flight guard. */
760
965
  private runReconcileControlCohort;
761
966
  /**
762
- * Resolve one sibling's control-network dial addresses and dial it, best-effort.
763
- * Returns whether a dial was attempted (false when no address resolves).
967
+ * Resolve every sibling's signed address record once and merge what resolves
968
+ * into the control node's libp2p **address book** (peerStore). Returns the
969
+ * resolved addresses per sibling so the dial loop reuses them rather than
970
+ * re-resolving — one `queryPeerRecord` per sibling per pass, not two.
971
+ *
972
+ * Deliberately every sibling, not the {@link selectControlCohortDials} subset,
973
+ * and deliberately including already-connected ones: everything below
974
+ * cadre-core dials by bare peer id (Optimystic's cluster/repo clients, FRET
975
+ * ping/announce), so the address book has to be warm BEFORE a live connection
976
+ * drops, and a sibling the out-degree cap declines to dial is still one those
977
+ * layers may need to reach.
978
+ *
979
+ * Only `resolvePeerAddrs` output is merged — never the cold-start
980
+ * `peerStoreAddrs` fallback, which came out of the address book to begin with
981
+ * and would restamp unverified seed addresses indefinitely. A sibling that
982
+ * resolves to nothing (revoked, stale, untrusted) is not written at all, so its
983
+ * existing entry ages out on its own.
984
+ *
985
+ * NOTE: this resolves EVERY sibling serially before the dial loop below runs,
986
+ * so it costs one record query per sibling per reconcile pass (~15s) and each
987
+ * one delays the pass's first dial. A cadre is a handful of devices, so today
988
+ * that is a few extra local reads; if cadres ever grow large, batch the records
989
+ * into one query, merge only on change, or move the warm pass after the dials.
990
+ */
991
+ private warmSiblingAddrBook;
992
+ /**
993
+ * {@link resolvePeerAddrs} for one sibling, best-effort: a control-DB read
994
+ * failure yields `[]` (and the cold-start fallback then gets its turn) rather
995
+ * than aborting the whole pass, like every other step here.
996
+ */
997
+ private resolveSiblingAddrs;
998
+ /**
999
+ * Budget for ONE proactive dial of the reconcile pass — steady-state sibling
1000
+ * ({@link dialControlSibling}) and cold-start bootstrap peer
1001
+ * ({@link dialBootstrapPeer}) alike, since both feed a whole address list to
1002
+ * one `dial()` and both run in a sequential loop that the next entry waits on.
1003
+ *
1004
+ * See {@link DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS} for why the pass bounds
1005
+ * this itself rather than inheriting each address's libp2p attempt timeout.
1006
+ */
1007
+ private controlDialTimeoutMs;
1008
+ /**
1009
+ * Dial one sibling from its already-resolved addresses, best-effort. Returns
1010
+ * whether a dial was attempted (false when no address resolves).
764
1011
  *
765
- * A per-peer failure (NAT, offline, relay down, connection-gater denial) is
766
- * logged and swallowed so one unreachable sibling never aborts the pass —
767
- * exactly like {@link SeedBootstrapService.applySeed}'s owner-dial loop. A
768
- * failed dial is simply retried on the next pass.
1012
+ * A per-peer failure (NAT, offline, relay down, connection-gater denial, or
1013
+ * the {@link controlDialTimeoutMs} budget expiring) is logged and swallowed so
1014
+ * one unreachable sibling never aborts the pass — exactly like
1015
+ * {@link SeedBootstrapService.applySeed}'s owner-dial loop. A failed dial is
1016
+ * simply retried on the next pass.
769
1017
  */
770
1018
  private dialControlSibling;
771
1019
  /**
772
- * Resolve a sibling's control-network dial addresses for the reconcile pass.
1020
+ * A sibling's control-network dial addresses for the reconcile pass.
773
1021
  *
774
- * Primary (steady state): the signed, fresh, trust-gated control addresses from
775
- * the converged `CadrePeer` record via {@link resolvePeerAddrs}. Fallback (cold
776
- * start): the libp2p peerStore entries `applySeed` populated, used only while the
777
- * record is not yet resolvable. Returns `[]` (never throws) when neither yields
778
- * an address — that sibling is skipped this pass.
1022
+ * Primary (steady state): the signed, fresh, trust-gated control addresses
1023
+ * `resolved` for it by {@link warmSiblingAddrBook} — passed in rather than
1024
+ * re-resolved, so the pass makes one record query per sibling. Fallback (cold
1025
+ * start): the libp2p peerStore entries `applySeed` populated, used only while
1026
+ * the record is not yet resolvable. Returns `[]` (never throws) when neither
1027
+ * yields an address — that sibling is skipped this pass.
779
1028
  */
780
1029
  private resolveControlDialAddrs;
781
1030
  /**
782
1031
  * Cold-start fallback: the libp2p peerStore multiaddrs for `peerId` (seeded by
783
1032
  * {@link SeedBootstrapService.applySeed}). Returns `[]` on a missing entry or any
784
1033
  * parse/lookup failure — never throws.
1034
+ *
1035
+ * Normalized through {@link normalizeDialAddrs} for the same reason
1036
+ * {@link resolvePeerAddrs} is, and with more cause: the address book hands back
1037
+ * a list that is inherently MIXED. `@libp2p/peer-store`'s
1038
+ * `dedupeFilterAndSortAddresses` strips a trailing `/p2p/<peerId>` only when
1039
+ * that id is the address's FIRST `/p2p/` component, so a direct address
1040
+ * round-trips bare while a relayed one — whose first `/p2p/` names the relay —
1041
+ * keeps its suffix (the same asymmetry `peer-addr-book.ts`'s `addrKey` exists
1042
+ * to absorb). Feeding both to one `dial()` is exactly the
1043
+ * `InvalidParametersError` that skipped the whole peer.
785
1044
  */
786
1045
  private peerStoreAddrs;
787
1046
  /**
@@ -834,11 +1093,12 @@ export declare class CadreNode implements SAppIdLookup {
834
1093
  * Bind a bootstrap peer's seed addresses to its peer id, so the dial
835
1094
  * authenticates the peer it is aiming at rather than trusting whoever answers.
836
1095
  *
837
- * An address that already carries `/p2p/<id>` must carry THIS peer's id or it
838
- * is dropped (a seed that disagrees with itself is not a dial target); one
839
- * that carries none — an `addressOverrides`-published record, say — gets the
840
- * id encapsulated. Unencapsulatable addresses are dropped rather than dialed
841
- * bare.
1096
+ * The same {@link normalizeDialAddrs} rule the resolved and address-book paths
1097
+ * use, on the third and last source of control-dial candidates: an address that
1098
+ * already terminates in `/p2p/<id>` must carry THIS peer's id or it is dropped
1099
+ * (a seed that disagrees with itself is not a dial target); one that does not —
1100
+ * a bare listen addr, or a relay hop with the destination missing — gets the id
1101
+ * encapsulated; an unencapsulatable one is dropped rather than dialed bare.
842
1102
  */
843
1103
  private bootstrapDialAddrs;
844
1104
  /** Dial one cold-start bootstrap peer, best-effort. Returns whether it connected. */
@@ -1087,6 +1347,12 @@ export declare class CadreNode implements SAppIdLookup {
1087
1347
  expireDeviceToken(peerId: string): Promise<void>;
1088
1348
  private handleStrandAdded;
1089
1349
  private handleStrandRemoved;
1350
+ /**
1351
+ * Return this node to its pre-{@link start} state. The ONE teardown — `stop()` and
1352
+ * a failed `start()` both come here, so anything a stopped node must not still be
1353
+ * doing belongs in this method and not in `stop()`. `stop()` adds only the
1354
+ * `control:disconnected` emit, which a never-connected start has no edge for.
1355
+ */
1090
1356
  private cleanup;
1091
1357
  private handleStrandIdle;
1092
1358
  /**
@@ -1099,7 +1365,7 @@ export declare class CadreNode implements SAppIdLookup {
1099
1365
  private handleStrandHibernate;
1100
1366
  /**
1101
1367
  * Wake a strand. If it was hibernating (quiesced — no libp2p node), re-resolve
1102
- * the cohort discovery seed and mode exactly as `launchStrand` does and rebuild
1368
+ * the cohort discovery seed exactly as `launchStrand` does and rebuild
1103
1369
  * its runtime via the strand manager. If it is still live (e.g. waking an idle
1104
1370
  * strand, which retains its resources), just flip the status. Overlapping wake
1105
1371
  * triggers are coalesced upstream by `HibernationManager`, so this runs once
@@ -1107,11 +1373,10 @@ export declare class CadreNode implements SAppIdLookup {
1107
1373
  */
1108
1374
  private handleStrandWake;
1109
1375
  /**
1110
- * Rebuild a quiesced strand's runtime, re-resolving the volatile cohort inputs
1111
- * first: the discovery seed may have grown and cohort membership may have
1112
- * shifted the mode `bootstrap → networked` since the strand last ran. Shared by
1376
+ * Rebuild a quiesced strand's runtime, re-resolving the volatile cohort input
1377
+ * first: the discovery seed may have grown since the strand last ran. Shared by
1113
1378
  * the wake (`handleStrandWake`) and check-in (`handleStrandCheckIn`) paths so
1114
- * both apply the same fresh seed/mode resolution. `resumeStrand` is idempotent
1379
+ * both apply the same fresh seed resolution. `resumeStrand` is idempotent
1115
1380
  * (returns the live instance unchanged) as a backstop against double-resume.
1116
1381
  */
1117
1382
  private resumeStrandRuntime;
@@ -1124,7 +1389,7 @@ export declare class CadreNode implements SAppIdLookup {
1124
1389
  * realized as a resume → bounded window → re-hibernate-if-idle cycle that
1125
1390
  * reuses the existing quiesce/resume primitives rather than a bespoke probe:
1126
1391
  *
1127
- * 1. Resume the strand (rebuild node + db, re-resolve cohort seed/mode) so
1392
+ * 1. Resume the strand (rebuild node + db, re-resolve the cohort seed) so
1128
1393
  * its strand network can reach cohort peers — exactly as a wake does.
1129
1394
  * 2. Hold it resumed for a bounded window, during which the app may drive
1130
1395
  * reads (pull-on-read) and record activity.
@@ -1356,8 +1621,8 @@ export declare class CadreNode implements SAppIdLookup {
1356
1621
  /**
1357
1622
  * Shared strand launch path for both the explicit (`addStrand`) and the
1358
1623
  * control-discovered (`handleStrandAdded`) entry points. Resolves the cohort
1359
- * seed, selects the mode, starts the strand, and registers it with the
1360
- * hibernation manager before emitting `strand:started`.
1624
+ * seed, starts the strand, and registers it with the hibernation manager
1625
+ * before emitting `strand:started`.
1361
1626
  *
1362
1627
  * Idempotent no-op when the strand manager already tracks `strand.Id` — the
1363
1628
  * ordinary founding sequence is `addStrand` (starts it locally) followed by
@@ -1370,18 +1635,19 @@ export declare class CadreNode implements SAppIdLookup {
1370
1635
  */
1371
1636
  private launchStrand;
1372
1637
  /**
1373
- * Resolve a strand's discovery seed. Membership (and thus mode) comes from the
1374
- * control network's CadrePeer rows; the **strand-network** bootstrap addresses
1375
- * are resolved on demand over the control mesh via the strand-addr RPC —
1376
- * deliberately NOT from `CadrePeer.Multiaddr`, which carries *control*
1377
- * addresses that must not seed the strand mesh.
1638
+ * Resolve a strand's discovery seed — the dialable strand-network multiaddr
1639
+ * strings for cohort siblings. Membership comes from the control network's
1640
+ * CadrePeer rows; the **strand-network** bootstrap addresses are resolved on
1641
+ * demand over the control mesh via the strand-addr RPC — deliberately NOT
1642
+ * from `CadrePeer.Multiaddr`, which carries *control* addresses that must not
1643
+ * seed the strand mesh.
1378
1644
  *
1379
1645
  * Only siblings we already hold an open control connection to are RPC'd: they
1380
1646
  * are the ones that can answer right now, and dialing them by peerId reuses the
1381
1647
  * live connection. When no connected sibling yet runs the strand the seed is
1382
- * empty (`[]`) — mode still follows membership, and the empty seed self-heals
1383
- * on the next resume / check-in pass. Returns an empty seed when the control DB
1384
- * or node is absent (not yet started / torn down).
1648
+ * empty (`[]`) — the empty seed self-heals on the next resume / check-in pass.
1649
+ * Returns an empty seed when the control DB or node is absent (not yet
1650
+ * started / torn down).
1385
1651
  *
1386
1652
  * When `delegatePeerId` is given (a strand launch/resume is imminent), the
1387
1653
  * pass doubles as the delegate ANNOUNCEMENT: our circuit relays are merged
@@ -1395,13 +1661,58 @@ export declare class CadreNode implements SAppIdLookup {
1395
1661
  * for a relay we are not yet connected to.
1396
1662
  */
1397
1663
  private resolveCohortSeed;
1664
+ /**
1665
+ * The co-cadre siblings worth sending a strand-addr RPC to right now: cohort
1666
+ * members (self excluded) we already hold an open control connection to. They
1667
+ * are the ones that can answer immediately, and dialing them by peerId reuses
1668
+ * the live connection instead of opening a second one.
1669
+ *
1670
+ * Shared by {@link resolveCohortSeed} (launch/resume) and
1671
+ * {@link refreshStrandPeerAddrs} (the periodic pass) so both ask the same set.
1672
+ * Empty when the control DB or node is absent (not yet started / torn down).
1673
+ *
1674
+ * The read below is issued even when the node holds no control connection and
1675
+ * the answer is therefore empty by construction. That is deliberate on the
1676
+ * launch path: `control-database-solo-warm-start.spec.ts` exists to prove this
1677
+ * exact read does not stall as an embedder's FIRST awaited control operation,
1678
+ * on a warm cohort no one can reach. A caller that re-enters often enough for
1679
+ * the read to matter should skip the call itself, as
1680
+ * {@link refreshStrandPeerAddrs} does.
1681
+ *
1682
+ * NOTE: this read is UNBOUNDED and sits on the critical path an embedding app
1683
+ * awaits during startup — `addStrand` cannot resolve until it does, and it is
1684
+ * the first control operation an embedder's boot order issues (before genesis,
1685
+ * before seed bootstrap). Measured fine today: the warm-start-alone shape a
1686
+ * report pointed at — a `CadrePeer` list naming peers that are all gone, read
1687
+ * off real files after a restart — completes in milliseconds, and
1688
+ * `control-database-solo-warm-start.spec.ts` covers it under a deadline. If a
1689
+ * control read ever CAN stall (a transactor change that consults the network
1690
+ * for a local row, a storage backend with blocking I/O), give this one a
1691
+ * timeout — and decide then whether the breach fails `addStrand` or degrades
1692
+ * to an empty seed, because those promise callers different things.
1693
+ */
1694
+ private connectedSiblingTargets;
1398
1695
  /**
1399
1696
  * The circuit relays this node's strand nodes would reserve through: the
1400
- * union of the RESOLVED listen addrs' circuits — a hand-written
1401
- * `network.listenAddrs` circuit entry or a `network.relayAddrs` entry, which
1402
- * the strand node inherits verbatim either way — and the control node's own
1403
- * live `/p2p-circuit` multiaddrs (a reservation this node discovered rather
1404
- * than configured).
1697
+ * union of CONFIGURED relays — `network.relayAddrs`, plus any hand-written
1698
+ * `network.listenAddrs` circuit entry, both of which a strand node inherits
1699
+ * verbatim — and the control node's own live `/p2p-circuit` multiaddrs (a
1700
+ * reservation this node discovered rather than configured).
1701
+ *
1702
+ * The CONFIGURED half reads `network.relayAddrs` and `network.listenAddrs`
1703
+ * directly rather than going through {@link resolveListenAddrs}: the control
1704
+ * node's resolution takes the `'search'` route, whose bare `/p2p-circuit` entry
1705
+ * names no relay, so a configured relay would be unannounceable here until this
1706
+ * node's own reservation landed — and the announce must be in place BEFORE a
1707
+ * strand node dials its reservation, since the relay's membership gate does not
1708
+ * know the strand's derived peerId (see delegate-admission.ts). Configuration is
1709
+ * authoritative; the live multiaddrs only ADD relays nobody configured.
1710
+ *
1711
+ * The `listenAddrs` half is now unreachable on a control node — `relay-addrs.ts`
1712
+ * rejects a hand-written `<relay>/p2p-circuit` listen entry outright, because the
1713
+ * bring-up quiet period denies the dial that listener makes from inside
1714
+ * `libp2p.start()`. Kept because it costs one spread and this is the one place
1715
+ * that would silently stop announcing if that rejection is ever relaxed.
1405
1716
  *
1406
1717
  * NOTE: a relay the STRAND node discovers on its own (autorelay — in neither
1407
1718
  * source) gets no announcement, and a membership-gated one will deny it; fine
@@ -1437,6 +1748,57 @@ export declare class CadreNode implements SAppIdLookup {
1437
1748
  private refreshDelegateGrants;
1438
1749
  /** One strand's share of {@link refreshDelegateGrants}: announce to the relays whose grant is due. */
1439
1750
  private announceDelegateToDueRelays;
1751
+ /**
1752
+ * Re-resolve each running strand's SIBLING addresses over the control mesh and
1753
+ * merge them into that strand's own libp2p address book, throttled to once per
1754
+ * {@link STRAND_PEER_ADDR_REFRESH_MS} per strand.
1755
+ *
1756
+ * Without this, a strand's address book is written exactly once — the
1757
+ * launch/resume seed — and everything below cadre-core that dials a strand peer
1758
+ * by bare peer id (Optimystic's cluster and repo clients, FRET ping/announce)
1759
+ * loses its address for any sibling it is not currently connected to: a sibling
1760
+ * that restarted its strand node or rotated its relay reservation is never
1761
+ * re-resolved, and even the original seed addresses fall off at the peerStore's
1762
+ * one-hour expiry (see `peer-addr-book.ts`).
1763
+ *
1764
+ * Distinct from {@link refreshDelegateGrants}, deliberately: that pass covers
1765
+ * RELAYS on a `DELEGATE_GRANT_TTL_MS / 2` throttle to keep circuit-relay
1766
+ * admission grants alive, this one covers SIBLINGS on an address-expiry
1767
+ * throttle to keep the address book warm. They overlap only in that both carry
1768
+ * `delegatePeerId`, so a sibling that also runs a relay gets its grant
1769
+ * refreshed here as a side effect. Merging them would tie an admission-grant
1770
+ * TTL to an address-expiry window that has nothing to do with it.
1771
+ *
1772
+ * Strands refresh CONCURRENTLY, like `refreshDelegateGrants`, so one
1773
+ * unreachable sibling's dial timeout does not stack up per strand.
1774
+ */
1775
+ private refreshStrandPeerAddrs;
1776
+ /**
1777
+ * One strand's share of {@link refreshStrandPeerAddrs}: RPC the siblings, then
1778
+ * merge their answers into this strand's address book. Errors are logged and
1779
+ * swallowed so one strand's failure never costs the others their refresh.
1780
+ */
1781
+ private refreshOneStrandPeerAddrs;
1782
+ /**
1783
+ * Merge strand-network addresses into ONE strand node's libp2p address book,
1784
+ * attributed per peer. Best-effort throughout: an address-book write must never
1785
+ * fail a launch, a resume, or a reconcile pass.
1786
+ *
1787
+ * `addrs` is the peer-agnostic union the strand-addr RPC returns, so
1788
+ * {@link groupAddrsByPeerId} attributes each entry to the **strand transport**
1789
+ * peerId in its final `/p2p/` component — never the sibling's control peerId,
1790
+ * which names a different libp2p node entirely. Entries that name no peer are
1791
+ * dropped there and counted here, once per pass.
1792
+ *
1793
+ * NOTE: a member could answer with arbitrary multiaddrs bound to arbitrary peer
1794
+ * ids and poison this address book. That is the same exposure the launch-time
1795
+ * `bootstrapNodes` seeding already accepts, and the cost is bounded: an address
1796
+ * grants no authority, the dialed peer authenticates by peer id at the
1797
+ * handshake, and a bad entry costs one failed dial that ages out at the
1798
+ * peerStore's one-hour expiry. No new gating here — cross-party strand trust is
1799
+ * `backlog/strand-network-nat-relay-reachability`.
1800
+ */
1801
+ private mergeStrandPeerAddrs;
1440
1802
  /**
1441
1803
  * The local strand instance's dialable strand-network multiaddrs for the
1442
1804
  * strand-addr RPC, ordered signaling-first (reusing the control node's
@@ -1544,16 +1906,48 @@ export declare class CadreNode implements SAppIdLookup {
1544
1906
  * Get the control network node (for advanced use)
1545
1907
  */
1546
1908
  getControlNode(): Libp2p | null;
1909
+ /**
1910
+ * The boot-path half of {@link reserveRelays}: reserve through every
1911
+ * `network.relayAddrs` entry, or fail `start()`.
1912
+ *
1913
+ * WHY IT RUNS HERE, AT THE END. `network.relayAddrs` used to resolve to a
1914
+ * CONFIGURED circuit listener, which libp2p dials from inside `libp2p.start()`
1915
+ * — so the relay was already a connected peer, and therefore already in this
1916
+ * node's Optimystic cohort, before `ControlDatabase.initialize()` ran. Building
1917
+ * that database is a long chain of cohort-consulting block probes (catalog
1918
+ * hydration, then one per table, then the indexes), and a sibling that has not
1919
+ * yet replicated this node's `CadrePeer` row correctly refuses every one of
1920
+ * them, so bring-up died on `BlockUnavailableError` — deterministically, on
1921
+ * every boot of a relay-only node. Retrying could not converge either: the
1922
+ * condition that clears the refusal is this node's own row reaching the
1923
+ * sibling, and writing that row needs the database the retry is building.
1924
+ *
1925
+ * So the invariant is ordering, not retrying: the control database is built
1926
+ * while this node holds ZERO control connections (a cohort of one, entirely
1927
+ * local), and only then does it reach out. Everything above in {@link start}
1928
+ * has completed by the time this runs.
1929
+ *
1930
+ * BUDGET: the supervisor's first attempt — `relay-reservation.ts`'s
1931
+ * `DEFAULT_RELAY_RESERVE_TIMEOUT_MS`, 10 s — is what `start()` waits on — deliberately the module default rather
1932
+ * than a boot-specific one. A healthy dial-plus-reserve is sub-second even over
1933
+ * a WAN, so 10 s is slack for a slow link; going much longer would make a dead
1934
+ * relay indistinguishable from a hung start, and much shorter would fail nodes
1935
+ * on links that were merely slow. The retries carry on in the background after
1936
+ * this resolves, exactly as they do for a {@link reserveRelays} caller.
1937
+ */
1938
+ private driveControlRelayReservation;
1547
1939
  /**
1548
1940
  * Start keeping a relay reservation: dial the given relay(s) from the control
1549
1941
  * node, ask the first one that answers for a reservation slot, wait until the
1550
1942
  * resulting `/p2p-circuit` address makes this node dialable — and keep a
1551
1943
  * supervisor running that re-drives whenever the reservation is later lost.
1552
1944
  *
1553
- * For nodes that listen on the bare `/p2p-circuit` SEARCH addr (a browser tab)
1554
- * rather than a configured `network.relayAddrs` circuit listener — see
1555
- * `relay-reservation.ts` for why the two are alternatives and why configuring
1556
- * both is a regression. Opt-in: nothing calls this for the CLI/host nodes.
1945
+ * The FAIL-SOFT entry point, for a caller that discovers its relay at runtime
1946
+ * (a browser tab, a host UI). A node that names `network.relayAddrs` gets the
1947
+ * same drive from {@link driveControlRelayReservation} at the end of
1948
+ * {@link start}, which is fail-fast instead. Both fill the pending reservation
1949
+ * the bare `/p2p-circuit` search listener registered; calling this afterwards
1950
+ * simply replaces the supervisor with one over the new list.
1557
1951
  *
1558
1952
  * Resolves once the FIRST attempt has settled, exactly as it did when it drove
1559
1953
  * once; the retries continue in the background until {@link stop} or the next
@@ -1615,13 +2009,40 @@ export declare class CadreNode implements SAppIdLookup {
1615
2009
  * Push the multiaddrs that future invites should advertise. Pass `null` to
1616
2010
  * revert to the libp2p-reported addresses (the default). The host calls this
1617
2011
  * at spawn and on every NAT change.
2012
+ *
2013
+ * Entries need NOT carry a `/p2p/<peerId>` suffix — {@link resolveInviteAddresses}
2014
+ * appends this node's own before anything publishes or dials them (see
2015
+ * {@link normalizeSelfAddrs}). Passing them suffixed is equally fine.
1618
2016
  */
1619
2017
  setInviteAddresses(addresses: string[] | null): void;
1620
2018
  /**
1621
2019
  * Resolve the addresses to embed in invites. Prefers pushed addresses, then
1622
- * any config-supplied resolver, then the libp2p-observed multiaddrs.
2020
+ * any config-supplied resolver, then the libp2p-observed multiaddrs. The two
2021
+ * app-supplied sources are normalized onto `/p2p/<self>` (see
2022
+ * {@link normalizeSelfAddrs}) so neither hook has to remember the suffix;
2023
+ * libp2p's own addresses already carry it.
1623
2024
  */
1624
2025
  private resolveInviteAddresses;
2026
+ /**
2027
+ * Normalize addresses an app handed us for THIS node onto `/p2p/<self>`, the
2028
+ * same invariant {@link normalizeDialAddrs} enforces on the way out.
2029
+ *
2030
+ * `setInviteAddresses` (the admin API `PUT /admin/invite-addresses`) and
2031
+ * `network.inviteAddressResolver` both take arbitrary strings, and whatever
2032
+ * they return ends up in this node's published `CadrePeer` record — so ONE
2033
+ * unsuffixed entry from either hook is enough to give every sibling in the
2034
+ * party a mixed candidate list for this peer. Establishing the suffix here,
2035
+ * where those strings enter the system, means neither hook has to know the
2036
+ * rule; `cadre-host`'s `buildInviteAddresses` already appends it, and
2037
+ * re-normalizing an already-suffixed address is a no-op.
2038
+ *
2039
+ * Unparsable and other-peer-addressed entries are passed through untouched
2040
+ * rather than dropped: publication is not the place to police an address's
2041
+ * validity, and {@link resolvePeerAddrs} already drops both on the read side.
2042
+ * Before `start()` resolves an identity there is no peer id to append, so the
2043
+ * list is returned as given.
2044
+ */
2045
+ private normalizeSelfAddrs;
1625
2046
  /**
1626
2047
  * Enumerate the cadre's `CadrePeer` membership — the ADDRESSABLE surface (see
1627
2048
  * {@link isMember}). Rows whose stamp is retired in `CadreControl.Revocation`
@@ -1678,8 +2099,13 @@ export declare class CadreNode implements SAppIdLookup {
1678
2099
  * legit member goes un-authorized on readers that only pin the new key. Full
1679
2100
  * rotation handling (re-vouch on rotate) is the
1680
2101
  * `flip-strand-membership-rotation-known-gap` work, not this predicate's.
2102
+ *
2103
+ * @param retry - Whether the underlying membership read may retry a transient cluster
2104
+ * failure. Only {@link refreshAuthorizedControlPeers} passes `false`, because it runs
2105
+ * as the control database's membership listener with that database's write lock held;
2106
+ * see its comment.
1681
2107
  */
1682
- listAuthorizedMembers(): Promise<Array<{
2108
+ listAuthorizedMembers(retry?: boolean): Promise<Array<{
1683
2109
  peerId: string;
1684
2110
  multiaddr: string | null;
1685
2111
  }>>;