@serfab/cadre-core 0.11.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 (86) hide show
  1. package/dist/announce-addrs.d.ts +49 -0
  2. package/dist/announce-addrs.js +78 -0
  3. package/dist/announce-addrs.js.map +1 -0
  4. package/dist/cadre-node.d.ts +281 -30
  5. package/dist/cadre-node.js +513 -103
  6. package/dist/cadre-node.js.map +1 -1
  7. package/dist/control-database.d.ts +155 -23
  8. package/dist/control-database.js +272 -104
  9. package/dist/control-database.js.map +1 -1
  10. package/dist/control-read-retry.d.ts +103 -0
  11. package/dist/control-read-retry.js +145 -0
  12. package/dist/control-read-retry.js.map +1 -0
  13. package/dist/control-retry.d.ts +101 -0
  14. package/dist/control-retry.js +108 -0
  15. package/dist/control-retry.js.map +1 -0
  16. package/dist/control-schema.d.ts +1 -1
  17. package/dist/control-schema.js +20 -4
  18. package/dist/control-schema.js.map +1 -1
  19. package/dist/control-write-retry.d.ts +59 -35
  20. package/dist/control-write-retry.js +21 -92
  21. package/dist/control-write-retry.js.map +1 -1
  22. package/dist/delegate-admission.d.ts +17 -15
  23. package/dist/delegate-admission.js +17 -15
  24. package/dist/delegate-admission.js.map +1 -1
  25. package/dist/index.d.ts +6 -4
  26. package/dist/index.js +17 -8
  27. package/dist/index.js.map +1 -1
  28. package/dist/membership-connection-gater.d.ts +211 -25
  29. package/dist/membership-connection-gater.js +309 -29
  30. package/dist/membership-connection-gater.js.map +1 -1
  31. package/dist/peer-record.d.ts +26 -0
  32. package/dist/peer-record.js +32 -0
  33. package/dist/peer-record.js.map +1 -1
  34. package/dist/relay-addrs.d.ts +76 -26
  35. package/dist/relay-addrs.js +115 -27
  36. package/dist/relay-addrs.js.map +1 -1
  37. package/dist/relay-reservation.d.ts +20 -15
  38. package/dist/relay-reservation.js +18 -14
  39. package/dist/relay-reservation.js.map +1 -1
  40. package/dist/strand-addr-protocol.js +16 -0
  41. package/dist/strand-addr-protocol.js.map +1 -1
  42. package/dist/strand-backfill.js +13 -4
  43. package/dist/strand-backfill.js.map +1 -1
  44. package/dist/strand-formation-manager.d.ts +10 -9
  45. package/dist/strand-formation-manager.js +10 -9
  46. package/dist/strand-formation-manager.js.map +1 -1
  47. package/dist/strand-formation-protocol.d.ts +7 -7
  48. package/dist/strand-formation-protocol.js +8 -8
  49. package/dist/strand-formation-protocol.js.map +1 -1
  50. package/dist/strand-instance-manager.d.ts +4 -3
  51. package/dist/strand-instance-manager.js +31 -6
  52. package/dist/strand-instance-manager.js.map +1 -1
  53. package/dist/strand-membership-writer.d.ts +106 -14
  54. package/dist/strand-membership-writer.js +180 -14
  55. package/dist/strand-membership-writer.js.map +1 -1
  56. package/dist/strand-solicitation.d.ts +3 -3
  57. package/dist/strand-solicitation.js +4 -5
  58. package/dist/strand-solicitation.js.map +1 -1
  59. package/dist/strand-wake-protocol.d.ts +47 -2
  60. package/dist/strand-wake-protocol.js +76 -6
  61. package/dist/strand-wake-protocol.js.map +1 -1
  62. package/dist/types.d.ts +74 -29
  63. package/dist/types.js.map +1 -1
  64. package/package.json +8 -8
  65. package/src/announce-addrs.ts +93 -0
  66. package/src/cadre-node.ts +5603 -5158
  67. package/src/control-database.ts +2584 -2406
  68. package/src/control-read-retry.ts +167 -0
  69. package/src/control-retry.ts +190 -0
  70. package/src/control-schema.ts +20 -4
  71. package/src/control-write-retry.ts +30 -130
  72. package/src/delegate-admission.ts +17 -15
  73. package/src/index.ts +435 -407
  74. package/src/membership-connection-gater.ts +533 -204
  75. package/src/peer-record.ts +33 -0
  76. package/src/relay-addrs.ts +218 -111
  77. package/src/relay-reservation.ts +828 -823
  78. package/src/strand-addr-protocol.ts +17 -0
  79. package/src/strand-backfill.ts +389 -366
  80. package/src/strand-formation-manager.ts +492 -491
  81. package/src/strand-formation-protocol.ts +8 -8
  82. package/src/strand-instance-manager.ts +636 -611
  83. package/src/strand-membership-writer.ts +209 -14
  84. package/src/strand-solicitation.ts +6 -7
  85. package/src/strand-wake-protocol.ts +94 -8
  86. package/src/types.ts +74 -29
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Translate `network.announceAddrs` / `network.appendAnnounceAddrs` — the operator-facing
3
+ * "advertise these addresses" settings — into the two `@optimystic/db-p2p` `NodeOptions`
4
+ * fields of the same names, which reach libp2p's `addresses.announce` /
5
+ * `addresses.appendAnnounce`.
6
+ *
7
+ * Two jobs, both of which the control node and every strand node need identically, hence
8
+ * one module rather than the same conditional spread written twice:
9
+ *
10
+ * - **Empty means unset.** libp2p treats `announce: []` as "no override", and forwarding
11
+ * `[]` would still be forwarding a field. An empty config array is dropped so it cannot
12
+ * land as an explicit empty announce set.
13
+ * - **Validate the multiaddrs here, at start.** libp2p does NOT validate announce addrs
14
+ * at construction: `AddressManager` stores them as raw strings and only parses them on
15
+ * the first `getAnnounceAddrs()`. A typo therefore produces a node that starts
16
+ * cleanly, then throws `InvalidMultiaddrError` out of every `getMultiaddrs()` call —
17
+ * including an UNHANDLED one from the debounced peer-store update libp2p runs on
18
+ * `transport:listening`. Verified against libp2p's `address-manager` (its constructor
19
+ * does `announce.map(ma => ma.toString())`; `getAnnounceAddrs()` does `multiaddr(a)`).
20
+ * Parsing each entry up front converts that into a loud failure at node start, which is
21
+ * what {@link relayCircuitAddrs} already does for `network.relayAddrs`.
22
+ *
23
+ * Parsing alone is not enough, though: `''` and `'/'` both parse into a component-less
24
+ * multiaddr, and a set holding only those is non-empty by the rule above — so it would
25
+ * replace every advertised address with nothing at all. An entry naming no address is
26
+ * therefore rejected alongside one that fails to parse.
27
+ */
28
+ import type { NetworkConfig } from './types.js';
29
+ /**
30
+ * The announce fields to spread into `createLibp2pNode` options. A field is present
31
+ * only when the operator configured a non-empty list for it, so a node that configures
32
+ * neither passes neither.
33
+ */
34
+ export interface ResolvedAnnounceAddrs {
35
+ announceAddrs?: string[];
36
+ appendAnnounceAddrs?: string[];
37
+ }
38
+ /**
39
+ * Resolve both announce fields off a `NetworkConfig`. Throws — naming the offending
40
+ * config field and entry — when any entry is not a parsable multiaddr.
41
+ */
42
+ export declare function resolveAnnounceAddrs(network: NetworkConfig | undefined): ResolvedAnnounceAddrs;
43
+ /**
44
+ * Whether this node's advertised set will be REPLACED rather than extended — i.e.
45
+ * whether {@link ResolvedAnnounceAddrs.announceAddrs} is in play at all. The single
46
+ * predicate behind `CadreNode.start()`'s operator warning, kept here so the warning
47
+ * and the forwarding agree on what "set" means (non-empty, not merely present).
48
+ */
49
+ export declare function replacesAdvertisedAddrs(network: NetworkConfig | undefined): boolean;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Translate `network.announceAddrs` / `network.appendAnnounceAddrs` — the operator-facing
3
+ * "advertise these addresses" settings — into the two `@optimystic/db-p2p` `NodeOptions`
4
+ * fields of the same names, which reach libp2p's `addresses.announce` /
5
+ * `addresses.appendAnnounce`.
6
+ *
7
+ * Two jobs, both of which the control node and every strand node need identically, hence
8
+ * one module rather than the same conditional spread written twice:
9
+ *
10
+ * - **Empty means unset.** libp2p treats `announce: []` as "no override", and forwarding
11
+ * `[]` would still be forwarding a field. An empty config array is dropped so it cannot
12
+ * land as an explicit empty announce set.
13
+ * - **Validate the multiaddrs here, at start.** libp2p does NOT validate announce addrs
14
+ * at construction: `AddressManager` stores them as raw strings and only parses them on
15
+ * the first `getAnnounceAddrs()`. A typo therefore produces a node that starts
16
+ * cleanly, then throws `InvalidMultiaddrError` out of every `getMultiaddrs()` call —
17
+ * including an UNHANDLED one from the debounced peer-store update libp2p runs on
18
+ * `transport:listening`. Verified against libp2p's `address-manager` (its constructor
19
+ * does `announce.map(ma => ma.toString())`; `getAnnounceAddrs()` does `multiaddr(a)`).
20
+ * Parsing each entry up front converts that into a loud failure at node start, which is
21
+ * what {@link relayCircuitAddrs} already does for `network.relayAddrs`.
22
+ *
23
+ * Parsing alone is not enough, though: `''` and `'/'` both parse into a component-less
24
+ * multiaddr, and a set holding only those is non-empty by the rule above — so it would
25
+ * replace every advertised address with nothing at all. An entry naming no address is
26
+ * therefore rejected alongside one that fails to parse.
27
+ */
28
+ import { multiaddr } from '@multiformats/multiaddr';
29
+ /**
30
+ * Resolve both announce fields off a `NetworkConfig`. Throws — naming the offending
31
+ * config field and entry — when any entry is not a parsable multiaddr.
32
+ */
33
+ export function resolveAnnounceAddrs(network) {
34
+ const announceAddrs = configuredAddrs('network.announceAddrs', network?.announceAddrs);
35
+ const appendAnnounceAddrs = configuredAddrs('network.appendAnnounceAddrs', network?.appendAnnounceAddrs);
36
+ return {
37
+ ...(announceAddrs && { announceAddrs }),
38
+ ...(appendAnnounceAddrs && { appendAnnounceAddrs })
39
+ };
40
+ }
41
+ /**
42
+ * Whether this node's advertised set will be REPLACED rather than extended — i.e.
43
+ * whether {@link ResolvedAnnounceAddrs.announceAddrs} is in play at all. The single
44
+ * predicate behind `CadreNode.start()`'s operator warning, kept here so the warning
45
+ * and the forwarding agree on what "set" means (non-empty, not merely present).
46
+ */
47
+ export function replacesAdvertisedAddrs(network) {
48
+ return (network?.announceAddrs?.length ?? 0) > 0;
49
+ }
50
+ /** The entries to forward for one announce field, or `undefined` when it is unset or empty. */
51
+ function configuredAddrs(field, addrs) {
52
+ if (!addrs || addrs.length === 0) {
53
+ return undefined;
54
+ }
55
+ return addrs.map((addr) => validated(field, addr));
56
+ }
57
+ /** `addr` unchanged, having proved it names a real address — config field named on failure. */
58
+ function validated(field, addr) {
59
+ if (parsed(field, addr).getComponents().length === 0) {
60
+ // `''` and `'/'` PARSE: both yield a component-less multiaddr that stringifies to
61
+ // `/`. Forwarded, they make an announce set that is non-empty but names nothing —
62
+ // which by the replacement rule above costs the node every address it would
63
+ // otherwise advertise, silently. A templated `cadre.yaml` whose address variable went
64
+ // unsubstituted is the realistic way in, so it is rejected rather than forwarded.
65
+ throw new Error(`${field} entry names no address: ${JSON.stringify(addr)}`);
66
+ }
67
+ return addr;
68
+ }
69
+ /** `addr` as a multiaddr, with the config field named on a parse failure. */
70
+ function parsed(field, addr) {
71
+ try {
72
+ return multiaddr(addr);
73
+ }
74
+ catch (err) {
75
+ throw new Error(`${field} entry is not a valid multiaddr: ${addr}`, { cause: err });
76
+ }
77
+ }
78
+ //# sourceMappingURL=announce-addrs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"announce-addrs.js","sourceRoot":"","sources":["../src/announce-addrs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,SAAS,EAAkB,MAAM,yBAAyB,CAAC;AAapE;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAkC;IACrE,MAAM,aAAa,GAAG,eAAe,CAAC,uBAAuB,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;IACvF,MAAM,mBAAmB,GAAG,eAAe,CAAC,6BAA6B,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC;IACzG,OAAO;QACL,GAAG,CAAC,aAAa,IAAI,EAAE,aAAa,EAAE,CAAC;QACvC,GAAG,CAAC,mBAAmB,IAAI,EAAE,mBAAmB,EAAE,CAAC;KACpD,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAkC;IACxE,OAAO,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;AACnD,CAAC;AAED,+FAA+F;AAC/F,SAAS,eAAe,CAAC,KAAa,EAAE,KAAoC;IAC1E,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;AACrD,CAAC;AAED,+FAA+F;AAC/F,SAAS,SAAS,CAAC,KAAa,EAAE,IAAY;IAC5C,IAAI,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrD,kFAAkF;QAClF,kFAAkF;QAClF,4EAA4E;QAC5E,sFAAsF;QACtF,kFAAkF;QAClF,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,4BAA4B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC9E,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,6EAA6E;AAC7E,SAAS,MAAM,CAAC,KAAa,EAAE,IAAY;IACzC,IAAI,CAAC;QACH,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,oCAAoC,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IACtF,CAAC;AACH,CAAC"}
@@ -103,10 +103,11 @@ export declare class CadreNode implements SAppIdLookup {
103
103
  private windowWaiters;
104
104
  private eventHandlers;
105
105
  /**
106
- * Relay multiaddrs the caller asked {@link reserveRelays} to reserve through.
107
- * Empty (the default) means nobody asked, so {@link getRelayReservationState}
108
- * reports `none` — this whole surface is opt-in and dormant for the CLI/host
109
- * nodes, which use the configured `network.relayAddrs` route instead.
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`.
110
111
  */
111
112
  private relayReserveAddrs;
112
113
  /**
@@ -121,6 +122,23 @@ export declare class CadreNode implements SAppIdLookup {
121
122
  * supervisor to hold one — i.e. the pre-start `control node unavailable` case.
122
123
  */
123
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;
124
142
  /** Map of strandId -> sAppConfig for sAppId filtering and management */
125
143
  private sAppConfigs;
126
144
  /**
@@ -181,12 +199,25 @@ export declare class CadreNode implements SAppIdLookup {
181
199
  /**
182
200
  * In-memory delegate admission grants (see `delegate-admission.ts`): the
183
201
  * strand-node transport peerIds this party's members have announced over the
184
- * strand-addr RPC, admitted at the CONNECTION level so a member's NAT'd
185
- * strand node can hold a circuit-relay reservation on this node. Consulted
186
- * ONLY by {@link admitInboundControlConnection}; the fail-closed per-stream
187
- * gate ({@link authorizeInboundControlStream}) never honors it.
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.
188
208
  */
189
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;
190
221
  /**
191
222
  * When this node last ANNOUNCED a delegate, keyed `<targetPeerId>\n<strandId>`
192
223
  * (the peer announced TO, not the delegate). Throttles
@@ -522,6 +553,27 @@ export declare class CadreNode implements SAppIdLookup {
522
553
  * operation count.
523
554
  */
524
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;
525
577
  /**
526
578
  * Map this node's config onto the control network's libp2p node options.
527
579
  *
@@ -531,6 +583,15 @@ export declare class CadreNode implements SAppIdLookup {
531
583
  * only caller in production is `createControlNode`.
532
584
  */
533
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;
534
595
  /**
535
596
  * Decide whether an inbound CONTROL-network connection from `remotePeerId`
536
597
  * should be admitted — the policy behind the connection gater wired in
@@ -540,7 +601,7 @@ export declare class CadreNode implements SAppIdLookup {
540
601
  * `membership-connection-gater.ts` for the layer's rationale and the
541
602
  * stranger-open protocol allowlist).
542
603
  *
543
- * Admits when ANY of:
604
+ * Returns `'admit'` when ANY of:
544
605
  * 1. a shared-baseline check admits ({@link admitControlPeerUnconditionally}
545
606
  * — not running / DB torn down, absent-or-empty trusted-owner anchor, or
546
607
  * configured bootstrap/relay infrastructure);
@@ -577,17 +638,69 @@ export declare class CadreNode implements SAppIdLookup {
577
638
  * this node yet is denied even though the formation handler would have
578
639
  * accepted it, exactly like the unreplicated-membership-row case below.
579
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
+ *
580
654
  * NOTE: check 4/5 runs a control-DB read per inbound connection
581
655
  * (`listAuthorizedMembers`); connections are rare and cadres small, and this
582
656
  * layer is fail-open behind `ADMISSION_DECISION_TIMEOUT_MS`, so the live
583
657
  * read is safe here — unlike the per-stream gate, which must consult the
584
658
  * materialized {@link authorizedControlPeers} snapshot instead.
585
- * NOTE: a sibling whose membership row has not yet replicated to
586
- * this node is denied until the row converges (typically via the owner);
587
- * either side's next outbound reconcile dial (outbound is never gated)
588
- * re-establishes the link — self-healing, but visible as a transient deny.
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.
589
665
  */
590
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;
591
704
  /**
592
705
  * Hold the control-network inbound gate open for not-yet-authorized peers
593
706
  * until `untilEpochMs` (extends, never shrinks, an already-open window).
@@ -599,7 +712,8 @@ export declare class CadreNode implements SAppIdLookup {
599
712
  /**
600
713
  * Record (or refresh) a delegate admission grant: `delegatePeerId` is the
601
714
  * transport peerId of `announcerPeerId`'s strand-`strandId` node, admitted
602
- * 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
603
717
  * `DELEGATE_GRANT_TTL_MS`. Called by the strand-addr responder after its
604
718
  * authorized-membership gate has passed; public so tests can drive the
605
719
  * admission policy without a full strand launch. A re-announce for the same
@@ -677,6 +791,12 @@ export declare class CadreNode implements SAppIdLookup {
677
791
  *
678
792
  * The SOLE caller is {@link drainMembershipGate}, which serializes refreshes —
679
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.
680
800
  */
681
801
  private refreshAuthorizedControlPeers;
682
802
  /**
@@ -787,8 +907,44 @@ export declare class CadreNode implements SAppIdLookup {
787
907
  * Survivors are returned signaling (`/p2p-circuit`) first, filtered to
788
908
  * signaling-only when requested, as parsed `Multiaddr`s (unparsable addrs
789
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.
790
914
  */
791
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;
792
948
  /** Parse multiaddr strings, dropping (and logging) any that fail to parse. */
793
949
  private parseMultiaddrs;
794
950
  /**
@@ -875,6 +1031,16 @@ export declare class CadreNode implements SAppIdLookup {
875
1031
  * Cold-start fallback: the libp2p peerStore multiaddrs for `peerId` (seeded by
876
1032
  * {@link SeedBootstrapService.applySeed}). Returns `[]` on a missing entry or any
877
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.
878
1044
  */
879
1045
  private peerStoreAddrs;
880
1046
  /**
@@ -927,11 +1093,12 @@ export declare class CadreNode implements SAppIdLookup {
927
1093
  * Bind a bootstrap peer's seed addresses to its peer id, so the dial
928
1094
  * authenticates the peer it is aiming at rather than trusting whoever answers.
929
1095
  *
930
- * An address that already carries `/p2p/<id>` must carry THIS peer's id or it
931
- * is dropped (a seed that disagrees with itself is not a dial target); one
932
- * that carries none — an `addressOverrides`-published record, say — gets the
933
- * id encapsulated. Unencapsulatable addresses are dropped rather than dialed
934
- * bare.
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.
935
1102
  */
936
1103
  private bootstrapDialAddrs;
937
1104
  /** Dial one cold-start bootstrap peer, best-effort. Returns whether it connected. */
@@ -1180,6 +1347,12 @@ export declare class CadreNode implements SAppIdLookup {
1180
1347
  expireDeviceToken(peerId: string): Promise<void>;
1181
1348
  private handleStrandAdded;
1182
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
+ */
1183
1356
  private cleanup;
1184
1357
  private handleStrandIdle;
1185
1358
  /**
@@ -1521,11 +1694,25 @@ export declare class CadreNode implements SAppIdLookup {
1521
1694
  private connectedSiblingTargets;
1522
1695
  /**
1523
1696
  * The circuit relays this node's strand nodes would reserve through: the
1524
- * union of the RESOLVED listen addrs' circuits — a hand-written
1525
- * `network.listenAddrs` circuit entry or a `network.relayAddrs` entry, which
1526
- * the strand node inherits verbatim either way — and the control node's own
1527
- * live `/p2p-circuit` multiaddrs (a reservation this node discovered rather
1528
- * than configured).
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.
1529
1716
  *
1530
1717
  * NOTE: a relay the STRAND node discovers on its own (autorelay — in neither
1531
1718
  * source) gets no announcement, and a membership-gated one will deny it; fine
@@ -1719,16 +1906,48 @@ export declare class CadreNode implements SAppIdLookup {
1719
1906
  * Get the control network node (for advanced use)
1720
1907
  */
1721
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;
1722
1939
  /**
1723
1940
  * Start keeping a relay reservation: dial the given relay(s) from the control
1724
1941
  * node, ask the first one that answers for a reservation slot, wait until the
1725
1942
  * resulting `/p2p-circuit` address makes this node dialable — and keep a
1726
1943
  * supervisor running that re-drives whenever the reservation is later lost.
1727
1944
  *
1728
- * For nodes that listen on the bare `/p2p-circuit` SEARCH addr (a browser tab)
1729
- * rather than a configured `network.relayAddrs` circuit listener — see
1730
- * `relay-reservation.ts` for why the two are alternatives and why configuring
1731
- * both is a regression. Opt-in: nothing calls this for the CLI/host nodes.
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.
1732
1951
  *
1733
1952
  * Resolves once the FIRST attempt has settled, exactly as it did when it drove
1734
1953
  * once; the retries continue in the background until {@link stop} or the next
@@ -1790,13 +2009,40 @@ export declare class CadreNode implements SAppIdLookup {
1790
2009
  * Push the multiaddrs that future invites should advertise. Pass `null` to
1791
2010
  * revert to the libp2p-reported addresses (the default). The host calls this
1792
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.
1793
2016
  */
1794
2017
  setInviteAddresses(addresses: string[] | null): void;
1795
2018
  /**
1796
2019
  * Resolve the addresses to embed in invites. Prefers pushed addresses, then
1797
- * 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.
1798
2024
  */
1799
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;
1800
2046
  /**
1801
2047
  * Enumerate the cadre's `CadrePeer` membership — the ADDRESSABLE surface (see
1802
2048
  * {@link isMember}). Rows whose stamp is retired in `CadreControl.Revocation`
@@ -1853,8 +2099,13 @@ export declare class CadreNode implements SAppIdLookup {
1853
2099
  * legit member goes un-authorized on readers that only pin the new key. Full
1854
2100
  * rotation handling (re-vouch on rotate) is the
1855
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.
1856
2107
  */
1857
- listAuthorizedMembers(): Promise<Array<{
2108
+ listAuthorizedMembers(retry?: boolean): Promise<Array<{
1858
2109
  peerId: string;
1859
2110
  multiaddr: string | null;
1860
2111
  }>>;