@serfab/cadre-core 0.12.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +23 -8
  2. package/dist/bootstrap-peer-store-file.js +2 -2
  3. package/dist/bootstrap-peer-store.d.ts +32 -12
  4. package/dist/bootstrap-peer-store.js +41 -15
  5. package/dist/bootstrap-peer-store.js.map +1 -1
  6. package/dist/cadre-node.d.ts +883 -102
  7. package/dist/cadre-node.js +1800 -209
  8. package/dist/cadre-node.js.map +1 -1
  9. package/dist/control-authorization.d.ts +2 -2
  10. package/dist/control-authorization.js +1 -0
  11. package/dist/control-authorization.js.map +1 -1
  12. package/dist/control-cohort.d.ts +13 -26
  13. package/dist/control-cohort.js +0 -26
  14. package/dist/control-cohort.js.map +1 -1
  15. package/dist/control-database.d.ts +318 -44
  16. package/dist/control-database.js +516 -74
  17. package/dist/control-database.js.map +1 -1
  18. package/dist/control-read-retry.d.ts +40 -11
  19. package/dist/control-read-retry.js +48 -15
  20. package/dist/control-read-retry.js.map +1 -1
  21. package/dist/control-retry.d.ts +58 -3
  22. package/dist/control-retry.js +58 -3
  23. package/dist/control-retry.js.map +1 -1
  24. package/dist/control-schema.d.ts +1 -1
  25. package/dist/control-schema.js +133 -21
  26. package/dist/control-schema.js.map +1 -1
  27. package/dist/control-write-retry.d.ts +80 -11
  28. package/dist/control-write-retry.js +168 -29
  29. package/dist/control-write-retry.js.map +1 -1
  30. package/dist/enrolled-machine-store-file.d.ts +13 -0
  31. package/dist/enrolled-machine-store-file.js +41 -0
  32. package/dist/enrolled-machine-store-file.js.map +1 -0
  33. package/dist/enrolled-machine-store.d.ts +81 -0
  34. package/dist/enrolled-machine-store.js +225 -0
  35. package/dist/enrolled-machine-store.js.map +1 -0
  36. package/dist/file-durable-slot.js +5 -3
  37. package/dist/file-durable-slot.js.map +1 -1
  38. package/dist/index.d.ts +17 -8
  39. package/dist/index.js +54 -9
  40. package/dist/index.js.map +1 -1
  41. package/dist/membership-connection-gater.d.ts +15 -1
  42. package/dist/membership-connection-gater.js +9 -3
  43. package/dist/membership-connection-gater.js.map +1 -1
  44. package/dist/node-local-snapshot.d.ts +21 -7
  45. package/dist/node-local-snapshot.js +30 -9
  46. package/dist/node-local-snapshot.js.map +1 -1
  47. package/dist/peer-dial.d.ts +143 -0
  48. package/dist/peer-dial.js +176 -0
  49. package/dist/peer-dial.js.map +1 -0
  50. package/dist/{strand-backfill.d.ts → peer-join-backfill.d.ts} +49 -20
  51. package/dist/{strand-backfill.js → peer-join-backfill.js} +133 -52
  52. package/dist/peer-join-backfill.js.map +1 -0
  53. package/dist/relay-addrs.d.ts +160 -49
  54. package/dist/relay-addrs.js +287 -60
  55. package/dist/relay-addrs.js.map +1 -1
  56. package/dist/relay-reservation.d.ts +71 -3
  57. package/dist/relay-reservation.js +116 -8
  58. package/dist/relay-reservation.js.map +1 -1
  59. package/dist/seed-bootstrap.d.ts +30 -1
  60. package/dist/seed-bootstrap.js +62 -17
  61. package/dist/seed-bootstrap.js.map +1 -1
  62. package/dist/storage-scope.d.ts +62 -0
  63. package/dist/storage-scope.js +75 -0
  64. package/dist/storage-scope.js.map +1 -0
  65. package/dist/strand-database.d.ts +41 -11
  66. package/dist/strand-database.js +48 -18
  67. package/dist/strand-database.js.map +1 -1
  68. package/dist/strand-first-sync-gate.d.ts +152 -0
  69. package/dist/strand-first-sync-gate.js +208 -0
  70. package/dist/strand-first-sync-gate.js.map +1 -0
  71. package/dist/strand-formation-manager.d.ts +99 -5
  72. package/dist/strand-formation-manager.js +119 -8
  73. package/dist/strand-formation-manager.js.map +1 -1
  74. package/dist/strand-formation-protocol.d.ts +97 -3
  75. package/dist/strand-formation-protocol.js +108 -2
  76. package/dist/strand-formation-protocol.js.map +1 -1
  77. package/dist/strand-instance-manager.d.ts +389 -19
  78. package/dist/strand-instance-manager.js +678 -65
  79. package/dist/strand-instance-manager.js.map +1 -1
  80. package/dist/strand-member-key.d.ts +20 -12
  81. package/dist/strand-member-key.js +20 -12
  82. package/dist/strand-member-key.js.map +1 -1
  83. package/dist/strand-member-registry.js +3 -0
  84. package/dist/strand-member-registry.js.map +1 -1
  85. package/dist/strand-membership-reconciler.d.ts +361 -0
  86. package/dist/strand-membership-reconciler.js +548 -0
  87. package/dist/strand-membership-reconciler.js.map +1 -0
  88. package/dist/strand-membership-writer.d.ts +143 -11
  89. package/dist/strand-membership-writer.js +419 -135
  90. package/dist/strand-membership-writer.js.map +1 -1
  91. package/dist/strand-network-config.d.ts +116 -0
  92. package/dist/strand-network-config.js +192 -0
  93. package/dist/strand-network-config.js.map +1 -0
  94. package/dist/strand-revocation-enforcer.d.ts +392 -0
  95. package/dist/strand-revocation-enforcer.js +533 -0
  96. package/dist/strand-revocation-enforcer.js.map +1 -0
  97. package/dist/strand-solicitation.d.ts +17 -1
  98. package/dist/strand-solicitation.js +13 -2
  99. package/dist/strand-solicitation.js.map +1 -1
  100. package/dist/strand-wake-protocol.d.ts +11 -7
  101. package/dist/strand-wake-protocol.js +20 -52
  102. package/dist/strand-wake-protocol.js.map +1 -1
  103. package/dist/strand-watcher.js +12 -1
  104. package/dist/strand-watcher.js.map +1 -1
  105. package/dist/timed-step.d.ts +12 -0
  106. package/dist/timed-step.js +28 -0
  107. package/dist/timed-step.js.map +1 -0
  108. package/dist/timeout-scheduler.d.ts +19 -0
  109. package/dist/timeout-scheduler.js +22 -0
  110. package/dist/timeout-scheduler.js.map +1 -0
  111. package/dist/types.d.ts +472 -46
  112. package/dist/types.js +7 -1
  113. package/dist/types.js.map +1 -1
  114. package/package.json +16 -8
  115. package/src/bootstrap-peer-store-file.ts +2 -2
  116. package/src/bootstrap-peer-store.ts +69 -27
  117. package/src/cadre-node.ts +1926 -228
  118. package/src/control-authorization.ts +2 -1
  119. package/src/control-cohort.ts +122 -130
  120. package/src/control-database.ts +586 -74
  121. package/src/control-read-retry.ts +202 -167
  122. package/src/control-retry.ts +297 -190
  123. package/src/control-schema.ts +133 -21
  124. package/src/control-write-retry.ts +525 -383
  125. package/src/enrolled-machine-store-file.ts +43 -0
  126. package/src/enrolled-machine-store.ts +280 -0
  127. package/src/file-durable-slot.ts +5 -3
  128. package/src/index.ts +134 -15
  129. package/src/membership-connection-gater.ts +9 -3
  130. package/src/node-local-snapshot.ts +38 -9
  131. package/src/peer-addr-book.ts +251 -250
  132. package/src/peer-dial.ts +215 -0
  133. package/src/{strand-backfill.ts → peer-join-backfill.ts} +478 -389
  134. package/src/relay-addrs.ts +458 -218
  135. package/src/relay-reservation.ts +951 -828
  136. package/src/seed-bootstrap.ts +76 -18
  137. package/src/storage-scope.ts +78 -0
  138. package/src/strand-database.ts +61 -21
  139. package/src/strand-first-sync-gate.ts +258 -0
  140. package/src/strand-formation-manager.ts +162 -8
  141. package/src/strand-formation-protocol.ts +850 -692
  142. package/src/strand-instance-manager.ts +877 -74
  143. package/src/strand-member-key.ts +20 -12
  144. package/src/strand-member-registry.ts +3 -0
  145. package/src/strand-membership-reconciler.ts +680 -0
  146. package/src/strand-membership-writer.ts +550 -190
  147. package/src/strand-network-config.ts +250 -0
  148. package/src/strand-revocation-enforcer.ts +653 -0
  149. package/src/strand-solicitation.ts +499 -471
  150. package/src/strand-wake-protocol.ts +21 -66
  151. package/src/strand-watcher.ts +325 -314
  152. package/src/timed-step.ts +28 -0
  153. package/src/timeout-scheduler.ts +28 -0
  154. package/src/types.ts +487 -52
  155. package/dist/strand-backfill.js.map +0 -1
@@ -3,34 +3,47 @@
3
3
  * setting — into the listen entry a node needs before it can hold a circuit-relay
4
4
  * slot, and validate the operator's entries while doing it.
5
5
  *
6
- * `@libp2p/circuit-relay-v2`'s listener branches on the SHAPE of the listen addr,
7
- * and the same `relayAddrs` list resolves to either shape depending on WHO is
8
- * listening ({@link RelayListenRoute}):
9
- *
10
- * | route | listen entry | libp2p behaviour |
11
- * | -------------- | ------------------------------------------- | --------------------------------------------------------- |
12
- * | `'configured'` | `<relayAddr>/p2p-circuit`, one per relay | dial that relay from inside `listen()` and reserve, or throw |
13
- * | `'search'` | a single bare `/p2p-circuit` | register a pending reservation; open no connection |
14
- *
15
- * The CONTROL node takes the `'search'` route, and `CadreNode.start()` drives the
16
- * reservation explicitly (`relay-reservation.ts`) once the control database is up.
17
- * That ordering is the point: a configured circuit listener dials the relay from
18
- * inside `libp2p.start()`, so the control database was being built while a sibling
19
- * relay was already in this node's cohort — and a sibling that has not yet
20
- * replicated this node's `CadrePeer` row correctly refuses its control-DB streams,
21
- * which killed `start()` outright. See `cadre-node.ts` → `start()`.
22
- *
23
- * STRAND nodes keep the `'configured'` route (`strand-instance-manager.ts`): they
24
- * inherit the control node's `NetworkConfig`, nothing drives an explicit
25
- * reservation for them, and their relay connection cannot disturb a control-DB
26
- * bring-up (a strand node's protocol ids are namespaced `/optimystic/strand-<id>/…`,
27
- * so the relay is never in its cohort).
28
- *
29
- * `network.relayAddrs` is FAIL-FAST for the operator on both routes, just from
30
- * different places: a malformed entry throws here at config resolution, and on the
31
- * control node a first reservation attempt that does not land throws out of
32
- * `start()` (`RelayReservationFailedError`). Naming a relay that is down still
33
- * means the node does not come up.
6
+ * `@libp2p/circuit-relay-v2`'s listener branches on the SHAPE of the listen addr:
7
+ *
8
+ * | listen entry | libp2p behaviour |
9
+ * | ----------------------------------- | --------------------------------------------------------------- |
10
+ * | `<relayAddr>/p2p-circuit` | CONFIGURED: dial that relay from inside `listen()` and reserve, or throw |
11
+ * | bare `/p2p-circuit` | SEARCH: register a pending reservation; open no connection |
12
+ *
13
+ * Every cadre-built node takes the SEARCH shape, and something explicit drives the
14
+ * reservation afterwards (`relay-reservation.ts`):
15
+ *
16
+ * - The CONTROL node resolves `relayAddrs` here to ONE bare entry (whichever relay
17
+ * answers first fills it) and `CadreNode.start()` drives the reservation once the
18
+ * control database is up. That ordering is the point: a configured circuit
19
+ * listener dials the relay from inside `libp2p.start()`, so the control database
20
+ * was being built while a sibling relay was already in this node's cohort — and a
21
+ * sibling that has not yet replicated this node's `CadrePeer` row correctly refuses
22
+ * its control-DB streams, which killed `start()` outright. See `cadre-node.ts` →
23
+ * `start()`.
24
+ * - Each STRAND node resolves the same `relayAddrs` to one bare entry PER relay and
25
+ * runs one reservation supervisor per relay (`strand-network-config.ts`,
26
+ * `strand-instance-manager.ts`).
27
+ *
28
+ * The CONFIGURED shape is deliberately not producible from a `NetworkConfig` any
29
+ * more: it loses its address and never recovers when the relay restarts, when either
30
+ * side hangs up, and — with no network event at all — on libp2p's own reservation
31
+ * refresh, which removes the reservation before re-creating it while a configured
32
+ * listener republishes only from inside its own `listen()`. A hand-written
33
+ * `<relay>/p2p-circuit` entry in `network.listenAddrs` is rejected on the control
34
+ * node ({@link resolveListenAddrs}) and folded into the supervised relay set on a
35
+ * strand node.
36
+ *
37
+ * `network.relayAddrs` is FAIL-FAST for the operator by default: a malformed entry
38
+ * throws here at config resolution regardless of posture, and on the control node a
39
+ * first reservation attempt that does not land throws out of `start()`
40
+ * (`RelayReservationFailedError`). Naming a relay that is down still means the control
41
+ * node does not come up. `network.requireRelay: false` softens the second half only —
42
+ * for a node that must still boot with no network (a phone, a browser tab) — logging
43
+ * the failed attempt instead of throwing and leaving the retry supervisor running in
44
+ * the background; the malformed-entry check is unaffected. A strand node is
45
+ * fail-SOFT regardless of `requireRelay` (its supervisor keeps trying while the
46
+ * strand serves).
34
47
  *
35
48
  * The bare `/p2p-circuit` search listener cannot open a connection on its own:
36
49
  * libp2p fills a pending reservation from `RelayDiscovery`, which nominates a peer
@@ -44,19 +57,21 @@ import { circuitRelayTargetOrThrow } from './delegate-admission.js';
44
57
  /**
45
58
  * The direct listener a node keeps when it configures `relayAddrs` but no
46
59
  * `listenAddrs`. Without this, naming a relay would silently REPLACE the node's
47
- * direct TCP listener with a circuit-only one.
60
+ * direct TCP listener with a circuit-only one. Shared with the strand-node
61
+ * derivation (`strand-network-config.ts`), which applies the same rule.
48
62
  *
49
63
  * NOTE: mirrors `createLibp2pNode`'s own default listen addr in
50
64
  * `../optimystic/packages/db-p2p/src/libp2p-node.ts` (`/ip4/0.0.0.0/tcp/<port>`,
51
65
  * and `CadreNode` passes `port: 0`) — that file is the source of truth. If it
52
66
  * ever changes its default, this constant has to follow.
53
67
  */
54
- const DEFAULT_DIRECT_LISTEN_ADDR = '/ip4/0.0.0.0/tcp/0';
68
+ export const DEFAULT_DIRECT_LISTEN_ADDR = '/ip4/0.0.0.0/tcp/0';
55
69
  /**
56
70
  * libp2p's relay SEARCH listener: registers one pending reservation and dials
57
- * nothing. One entry covers every configured relay — the pending reservation is
58
- * per-listener, not per-relay, and `relay-reservation.ts` fills it with whichever
59
- * relay answers first.
71
+ * nothing. The pending reservation is per-LISTENER, not per-relay: the control
72
+ * node binds one entry for its whole relay list (`relay-reservation.ts` fills it
73
+ * with whichever relay answers first), a strand node binds one entry per relay so
74
+ * each relay's supervisor has a slot of its own to fill.
60
75
  */
61
76
  export const RELAY_SEARCH_LISTEN_ADDR = '/p2p-circuit';
62
77
  /**
@@ -66,6 +81,10 @@ export const RELAY_SEARCH_LISTEN_ADDR = '/p2p-circuit';
66
81
  * operator configuration, so a typo must fail loudly at start rather than
67
82
  * silently costing the node its reachability.
68
83
  *
84
+ * No node LISTENS on these any more (see the module doc); they are the validated
85
+ * form of `relayAddrs`, read by `CadreNode.circuitRelayTargets` for the delegate
86
+ * announce and by `strand-network-config.ts` for the per-relay supervisor list.
87
+ *
69
88
  * Deliberately unlike `extractCircuitRelayTargets` (`delegate-admission.ts`), which
70
89
  * SKIPS a bad entry: that one reads addresses discovered at runtime from peers, where
71
90
  * one bad entry must not be fatal.
@@ -74,24 +93,26 @@ export function relayCircuitAddrs(relayAddrs) {
74
93
  return dedupe(relayAddrs.map(circuitListenAddr));
75
94
  }
76
95
  /**
77
- * The listen multiaddrs a node actually binds: configured `listenAddrs` plus the
78
- * relay listen entry `route` calls for, deduplicated, order-stable (configured
79
- * entries first). Returns `undefined` when neither field is set, so callers keep
80
- * omitting `listenAddrs` and inherit db-p2p's default.
96
+ * The listen multiaddrs the CONTROL node binds: configured `listenAddrs` plus ONE
97
+ * bare `/p2p-circuit` search entry when `relayAddrs` names any relay, deduplicated,
98
+ * order-stable (configured entries first). Returns `undefined` when neither field
99
+ * is set, so callers keep omitting `listenAddrs` and inherit db-p2p's default.
81
100
  *
82
- * Every `relayAddrs` entry is validated on BOTH routes — the `'search'` route
83
- * discards the resolved circuit addrs, but an operator typo must fail at config
84
- * resolution either way.
101
+ * Every `relayAddrs` entry is validated even though the search entry discards the
102
+ * resolved circuit addrs — an operator typo must fail at config resolution. A
103
+ * hand-written `<relay>/p2p-circuit` entry in `listenAddrs` is rejected outright
104
+ * (see {@link rejectConfiguredCircuitListenAddrs}).
105
+ *
106
+ * Strand nodes derive their own listen set from the same config in
107
+ * `strand-network-config.ts` (`strandNodeAddrs`) — one search entry per relay,
108
+ * fixed direct ports made ephemeral.
85
109
  */
86
- export function resolveListenAddrs(network, route = 'configured') {
87
- const configured = relayCircuitAddrs(network?.relayAddrs ?? []);
88
- const relayEntries = route === 'search'
89
- ? (configured.length > 0 ? [RELAY_SEARCH_LISTEN_ADDR] : [])
90
- : configured;
110
+ export function resolveListenAddrs(network) {
111
+ const relayEntries = relayCircuitAddrs(network?.relayAddrs ?? []).length > 0
112
+ ? [RELAY_SEARCH_LISTEN_ADDR]
113
+ : [];
91
114
  const listenAddrs = network?.listenAddrs;
92
- if (route === 'search') {
93
- rejectConfiguredCircuitListenAddrs(listenAddrs ?? []);
94
- }
115
+ rejectConfiguredCircuitListenAddrs(listenAddrs ?? []);
95
116
  if (!listenAddrs && relayEntries.length === 0) {
96
117
  return undefined;
97
118
  }
@@ -100,12 +121,210 @@ export function resolveListenAddrs(network, route = 'configured') {
100
121
  // acquiring a slot is the whole point of configuring a relay.
101
122
  return dedupe([...(listenAddrs ?? [DEFAULT_DIRECT_LISTEN_ADDR]), ...relayEntries]);
102
123
  }
124
+ /**
125
+ * The `wsPort` value passed purely to turn `@optimystic/db-p2p`'s WebSocket transport
126
+ * ON. It is never bound, so it is deliberately `0` rather than a port scraped from a
127
+ * listen entry.
128
+ *
129
+ * WHY it is a switch and not a port: `createLibp2pNode`
130
+ * (`../optimystic/packages/db-p2p/src/libp2p-node.ts`) feeds `wsPort` into BOTH its
131
+ * default transports and its default listen addrs, and `createLibp2pNodeBase`
132
+ * (`libp2p-node-base.ts:490-491`) falls back to those two defaults INDEPENDENTLY:
133
+ *
134
+ * ```ts
135
+ * const listenAddrs = options.listenAddrs ?? defaults.listenAddrs;
136
+ * const transports = options.transports ?? defaults.transports;
137
+ * ```
138
+ *
139
+ * `cadre-core` always supplies explicit `listenAddrs` alongside it, so the synthesized
140
+ * `/ip4/<wsHost>/tcp/<wsPort>/ws` default ADDRESS is discarded while the `webSockets()`
141
+ * transport it added survives. A scraped port would also be a lie whenever a config
142
+ * names several WebSocket addresses on different ports, and the strand path rewrites
143
+ * fixed ports to `0` regardless (`strand-network-config.ts`).
144
+ *
145
+ * NOTE: db-p2p's own doc comment on `wsPort` (`libp2p-node-base.ts:170-172`) says it is
146
+ * "Ignored when `transports`/`listenAddrs` are explicitly provided" — true of the
147
+ * address half, NOT of the transport half, which is the half this depends on. Re-verify
148
+ * empirically rather than trusting that comment: `test/listen-transport-options.spec.ts`
149
+ * boots a real node and asserts a `/ws` multiaddr appears. If db-p2p ever makes the
150
+ * comment true of both halves, it needs an explicit `enableWebSockets` switch and this
151
+ * constant becomes unusable.
152
+ */
153
+ const WS_TRANSPORT_SWITCH_PORT = 0;
154
+ /**
155
+ * The transport-derived libp2p options `listenAddrs` implies, and the gate that stops a
156
+ * listen address the node has no transport for from being SILENTLY dropped.
157
+ *
158
+ * libp2p's transport manager sorts configured listen addresses by which transport claims
159
+ * them, discards the unclaimed ones, and raises `UnsupportedListenAddressesError` only
160
+ * when EVERY address was discarded. Pair a TCP address with a WebSocket one — which is
161
+ * exactly what the shipped configs do — and the TCP address carries the start while the
162
+ * missing WebSocket listener is never reported. Two halves of `NetworkConfig` that have
163
+ * to agree (`listenAddrs`, and the transports the node will actually have) were never
164
+ * checked against each other; this is that check, and it covers both node kinds because
165
+ * both derive their listen set from the same config here ({@link resolveListenAddrs}) or
166
+ * in `strand-network-config.ts`, and both call this on the result.
167
+ *
168
+ * The two transport classes are handled differently, on purpose:
169
+ *
170
+ * - **WebSocket is DERIVED.** It is the one non-TCP transport the shipped configs need
171
+ * (the React Native reference app's companion drone; `cadre start --ws-port`), and
172
+ * `@optimystic/db-p2p` already knows how to add it. `cadre-core` grows no dependency
173
+ * and decides no transport policy — it flips db-p2p's own switch
174
+ * ({@link WS_TRANSPORT_SWITCH_PORT}).
175
+ * - **Everything else is REFUSED.** Deriving `/quic-v1`, `/webrtc` or `/webtransport`
176
+ * would mean importing transport packages into every `cadre-core` consumer including
177
+ * the React Native and browser bundles (against the cross-platform rule in
178
+ * `AGENTS.md`) and duplicating policy `libp2p-node.ts` owns. So name the address, name
179
+ * the transport it needs, and refuse to start — matching how `network.relayAddrs` and
180
+ * `network.announceAddrs` already treat an operator typo.
181
+ *
182
+ * Returns `{}` unconditionally when `network.transports` is set: a programmatic embedder
183
+ * supplying transport factories owns the policy, and the factories are opaque — nothing
184
+ * can be inferred from them. That is what keeps the RN phone, the web app, and the
185
+ * integration-test harness unaffected.
186
+ *
187
+ * NOTE: pairing this with {@link resolveListenAddrs} is a CONVENTION, not a structure —
188
+ * a third libp2p-node build site could resolve listen addrs and forget to call this,
189
+ * putting the original bug back on that path. Both existing sites are covered
190
+ * (`cadre-node.ts` → `buildControlNodeOptions`, `strand-network-config.ts` →
191
+ * `strandNodeAddrs`), and they are the only two `createLibp2pNode` callers in this repo
192
+ * that derive their listen set from a `NetworkConfig` — the other callers
193
+ * (`integration-tests/src/harness/test-party.ts`, `quereus-plugin-sereus`'s `connect.ts`
194
+ * and `connect-browser.ts`) pass no `listenAddrs` at all, or pass their own transports
195
+ * with it. If a third CONFIG-derived site appears, fold the two functions into one that
196
+ * returns listen addrs and transport options together, so forgetting becomes impossible
197
+ * rather than merely unlikely.
198
+ *
199
+ * @param listenAddrs the ALREADY-resolved listen set — the output of
200
+ * {@link resolveListenAddrs}, or the strand derivation, after any per-node rewriting —
201
+ * so the check reads what this node will actually bind.
202
+ * @throws {UnbindableListenAddressError} when a listen entry names a transport outside
203
+ * {tcp, ws/wss, p2p-circuit}.
204
+ */
205
+ export function resolveTransportOptions(network, listenAddrs) {
206
+ if (network?.transports) {
207
+ return {};
208
+ }
209
+ const kinds = (listenAddrs ?? []).map((addr) => [addr, listenTransportKind(addr)]);
210
+ const unsupported = kinds.filter(([, kind]) => kind === 'unsupported').map(([addr]) => addr);
211
+ if (unsupported.length > 0) {
212
+ throw new UnbindableListenAddressError(unsupported);
213
+ }
214
+ return kinds.some(([, kind]) => kind === 'websockets') ? { wsPort: WS_TRANSPORT_SWITCH_PORT } : {};
215
+ }
216
+ /**
217
+ * Thrown at config resolution when `network.listenAddrs` names an address none of the
218
+ * default transports can bind. Names each offending address alongside the libp2p
219
+ * transport package it would need — what the operator has without this is a node that
220
+ * starts and silently never listens there.
221
+ */
222
+ export class UnbindableListenAddressError extends Error {
223
+ constructor(listenAddrs) {
224
+ super('network.listenAddrs names an address this node has no transport for: ' +
225
+ `${listenAddrs.map((a) => `${a} — ${unbindableReason(a)}`).join('; ')}. ` +
226
+ 'Default transports bind TCP (including /unix/<path>), WebSocket (/ws, /wss) and circuit-relay addresses only. ' +
227
+ 'Either drop the address, or supply the transport programmatically via network.transports.');
228
+ this.listenAddrs = listenAddrs;
229
+ this.name = 'UnbindableListenAddressError';
230
+ }
231
+ }
232
+ /**
233
+ * Which default transport claims `listenAddr`, or `'unsupported'` when none does.
234
+ *
235
+ * Classification is a multiaddr COMPONENT question, not a string question, so this
236
+ * parses and reads the transport stack — the component names left once the addressing
237
+ * and security layers are dropped. That stack is outermost-LAST
238
+ * (`/ip4/…/tcp/443/tls/ws` → `['tcp', 'ws']`), which is why its terminal entry decides.
239
+ *
240
+ * An unparsable entry is `'ignored'` and passes through untouched, following
241
+ * `isConfiguredCircuitListenAddr` below and `ephemeralPortListenAddr`
242
+ * (`strand-network-config.ts`): libp2p reports a bad listen addr itself, and this check
243
+ * must only ever ADD a denial.
244
+ */
245
+ function listenTransportKind(listenAddr) {
246
+ let stack;
247
+ try {
248
+ stack = transportStack(listenAddr);
249
+ }
250
+ catch {
251
+ return 'ignored';
252
+ }
253
+ const outermost = stack.at(-1);
254
+ if (outermost === undefined) {
255
+ // Nothing but addressing components — a bare `/ip4/1.2.3.4`. No transport claims
256
+ // it, so it is dropped by exactly the same silent path this check exists to close.
257
+ return 'unsupported';
258
+ }
259
+ if (outermost === 'p2p-circuit') {
260
+ return 'circuit';
261
+ }
262
+ if (outermost === 'ws' || outermost === 'wss') {
263
+ return 'websockets';
264
+ }
265
+ // Only a BARE tcp or unix stack is TCP. `@libp2p/tcp` binds both — its `listenFilter`
266
+ // accepts an exact TCP match OR a `/unix/<path>` address (a named pipe on Windows) —
267
+ // and nothing layered on top of either, which is why `/tcp/<port>/<anything else>` is
268
+ // NOT waved through merely because a `tcp` component happens to appear.
269
+ return stack.length === 1 && (outermost === 'tcp' || outermost === 'unix') ? 'tcp' : 'unsupported';
270
+ }
271
+ /**
272
+ * Component names of `listenAddr` with the addressing and security layers removed, so
273
+ * what remains names transports. Host components (`ip4`, `dns4`, …) and the layers that
274
+ * ride inside a transport without being one (`tls`, `sni`, `certhash`, `p2p`) say
275
+ * nothing about which transport has to be configured.
276
+ */
277
+ function transportStack(listenAddr) {
278
+ return multiaddr(listenAddr).getComponents()
279
+ .map((component) => component.name)
280
+ .filter((name) => !NON_TRANSPORT_COMPONENTS.has(name));
281
+ }
282
+ /** Components that address or secure a connection rather than name its transport. */
283
+ const NON_TRANSPORT_COMPONENTS = new Set([
284
+ 'ip4', 'ip6', 'ip6zone', 'dns', 'dns4', 'dns6', 'dnsaddr',
285
+ 'tls', 'sni', 'certhash', 'p2p', 'noise'
286
+ ]);
287
+ /**
288
+ * Why `listenAddr` cannot be bound, phrased for the operator: the libp2p package that
289
+ * would make it bindable where one is known, and otherwise what is wrong with it.
290
+ *
291
+ * Read OUTERMOST-first (`/udp/…/quic-v1/webtransport` needs `@libp2p/webtransport`,
292
+ * not the `@libp2p/quic` it rides on), matching how `listenTransportKind` decides.
293
+ */
294
+ function unbindableReason(listenAddr) {
295
+ let stack;
296
+ try {
297
+ stack = transportStack(listenAddr);
298
+ }
299
+ catch {
300
+ return 'not a parsable multiaddr';
301
+ }
302
+ if (stack.length === 0) {
303
+ return 'it names no transport component';
304
+ }
305
+ const pkg = [...stack].reverse().map((name) => TRANSPORT_PACKAGES[name]).find((p) => p !== undefined);
306
+ return pkg === undefined ? 'needs an unrecognized transport' : `needs ${pkg}`;
307
+ }
308
+ /**
309
+ * The libp2p package each unbindable transport component comes from. Naming the package
310
+ * is the actionable half of the refusal — the operator either drops the address or wires
311
+ * that package in through `network.transports`.
312
+ */
313
+ const TRANSPORT_PACKAGES = {
314
+ 'quic': '@libp2p/quic',
315
+ 'quic-v1': '@libp2p/quic',
316
+ 'webtransport': '@libp2p/webtransport',
317
+ 'webrtc': '@libp2p/webrtc',
318
+ 'webrtc-direct': '@libp2p/webrtc'
319
+ };
103
320
  /**
104
321
  * Thrown out of `CadreNode.start()` when the boot-path reservation drive for
105
- * `network.relayAddrs` produces no `/p2p-circuit` address on its FIRST attempt.
322
+ * `network.relayAddrs` produces no `/p2p-circuit` address on its FIRST attempt,
323
+ * UNLESS `network.requireRelay` is explicitly `false` (see `driveControlRelayReservation`,
324
+ * which logs and returns instead on that posture).
106
325
  *
107
- * This is what keeps `network.relayAddrs` fail-fast now that the control node
108
- * takes the `'search'` route: libp2p's own `UnsupportedListenAddressesError` used
326
+ * This is what keeps `network.relayAddrs` fail-fast by default now that the control node
327
+ * listens on the bare search entry: libp2p's own `UnsupportedListenAddressesError` used
109
328
  * to abort start from inside `listen()`, and an operator who names a relay is
110
329
  * telling the node it has no other reachability — coming up undialable is worse
111
330
  * than not coming up. Names the relays and the reservation's own reason, both of
@@ -122,7 +341,7 @@ export class RelayReservationFailedError extends Error {
122
341
  }
123
342
  /**
124
343
  * A hand-written `<relay>/p2p-circuit` entry in `network.listenAddrs` is the
125
- * CONFIGURED listener shape, and it is unusable on the `'search'` route: libp2p
344
+ * CONFIGURED listener shape, and it is unusable on the control node: libp2p
126
345
  * dials that relay from inside `listen()`, the bring-up quiet period
127
346
  * (`membership-connection-gater.ts`) denies exactly that dial, `listen()` fails, and
128
347
  * the transport manager's default `FATAL_ALL` aborts `libp2p.start()`. The operator
@@ -130,7 +349,10 @@ export class RelayReservationFailedError extends Error {
130
349
  * naming the cause, so name it here instead.
131
350
  *
132
351
  * `network.relayAddrs` is the exact replacement — same relay, and the reservation is
133
- * driven after bring-up, which is the whole point of the route.
352
+ * driven after bring-up, which is the whole point of the search shape. (A strand node
353
+ * built directly from such a config folds the entry into its supervised relay set
354
+ * instead — `strand-network-config.ts` — but the control node's resolution runs first
355
+ * on every machine, so in production the entry never reaches a strand node.)
134
356
  */
135
357
  function rejectConfiguredCircuitListenAddrs(listenAddrs) {
136
358
  const configured = listenAddrs.filter(isConfiguredCircuitListenAddr);
@@ -141,8 +363,12 @@ function rejectConfiguredCircuitListenAddrs(listenAddrs) {
141
363
  'connections. Move the relay to network.relayAddrs, which reserves after bring-up.');
142
364
  }
143
365
  }
144
- /** `<something>/p2p-circuit` — the configured shape, as opposed to the bare search addr. */
145
- function isConfiguredCircuitListenAddr(listenAddr) {
366
+ /**
367
+ * `<something>/p2p-circuit` — the configured shape, as opposed to the bare search
368
+ * addr. An unparsable entry is `false`: libp2p reports a bad listen addr itself,
369
+ * and this helper only ever ADDS a denial (or, on the strand path, a supervisor).
370
+ */
371
+ export function isConfiguredCircuitListenAddr(listenAddr) {
146
372
  if (listenAddr === RELAY_SEARCH_LISTEN_ADDR) {
147
373
  return false;
148
374
  }
@@ -156,13 +382,14 @@ function isConfiguredCircuitListenAddr(listenAddr) {
156
382
  }
157
383
  }
158
384
  /**
159
- * One entry of {@link relayCircuitAddrs}: the addr the node listens on for this relay.
385
+ * One entry of {@link relayCircuitAddrs}: this relay's addr in the configured shape.
160
386
  *
161
387
  * NOTE: an entry carrying components AFTER `/p2p-circuit` — a full relayed DIAL addr,
162
- * `…/p2p/<relay>/p2p-circuit/p2p/<someone else>` — is passed through as written and
163
- * would be listened on verbatim. It validates (the relay it names is real), so only an
164
- * operator who pastes a peer's relayed address into `relayAddrs` hits it; if that turns
165
- * out to be a common paste, truncate at the `/p2p-circuit` component here.
388
+ * `…/p2p/<relay>/p2p-circuit/p2p/<someone else>` — is passed through as written. It
389
+ * validates (the relay it names is real), and every consumer reads only the relay
390
+ * peer id and the dial prefix before `/p2p-circuit`, so the tail is ignored rather
391
+ * than harmful; if that paste turns out to be common, truncate at the `/p2p-circuit`
392
+ * component here so the resolved list is canonical.
166
393
  */
167
394
  function circuitListenAddr(relayAddr) {
168
395
  const alreadyCircuit = parseComponents(relayAddr).some((c) => c.name === 'p2p-circuit');
@@ -1 +1 @@
1
- {"version":3,"file":"relay-addrs.js","sourceRoot":"","sources":["../src/relay-addrs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"relay-addrs.js","sourceRoot":"","sources":["../src/relay-addrs.ts"],"names":[],"mappings":"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"}
@@ -16,7 +16,7 @@
16
16
  * module. It is **fail-soft by construction**: nothing here throws.
17
17
  *
18
18
  * EVERY cadre CONTROL node now takes that route — `network.relayAddrs` resolves
19
- * to the bare search entry (`relay-addrs.ts` → `RelayListenRoute`) and
19
+ * to the bare search entry (`relay-addrs.ts` → `resolveListenAddrs`) and
20
20
  * `CadreNode.start()` drives this module once the control database is up. The
21
21
  * configured shape would dial the relay from inside `libp2p.start()`, putting a
22
22
  * sibling in this node's cohort before its own control database existed, and a
@@ -28,8 +28,30 @@
28
28
  * a `RelayReservationFailedError` out of `start()` when the first attempt lands
29
29
  * nothing (fail-fast, the operator asked for this relay), while a node that calls
30
30
  * `CadreNode.reserveRelays()` itself gets a non-`reserved` status and stays up
31
- * (fail-soft, the browser-tab posture). Configuring both is now redundant, not
32
- * fatal. STRAND nodes still take the configured shape — see `relay-addrs.ts`.
31
+ * (fail-soft, the browser-tab posture). `network.requireRelay: false` gives a
32
+ * CONFIGURED relay the fail-soft posture too, for a node that must boot with no
33
+ * network at all — same drive, same supervisor, only the throw is dropped.
34
+ * Configuring both is now redundant, not fatal.
35
+ *
36
+ * STRAND nodes take the same route, ONE supervisor PER RELAY
37
+ * (`strand-instance-manager.ts` → `buildStrandRuntime`): each configured relay
38
+ * gets its own bare `/p2p-circuit` listener (`strand-network-config.ts`) and its
39
+ * own {@link superviseRelayReservation} over just that relay's addr, fail-soft
40
+ * like the browser tab. The configured shape they used to take
41
+ * (`<relay>/p2p-circuit`, reserved from inside `libp2p.start()`) is gone from
42
+ * cadre-built nodes entirely, because it loses its address and never recovers in
43
+ * three situations — the relay restarts, either side hangs up the relay
44
+ * connection, and libp2p's OWN reservation refresh (which removes the reservation
45
+ * before re-creating it, and a configured listener republishes only from inside
46
+ * its own `listen()`). The last one needs no network event at all: at the relay
47
+ * default TTL of two hours every configured-shape node went undialable ~1 h 55 min
48
+ * after reserving. A search listener republishes on `relay:created-reservation`
49
+ * for its own pending id, which the refresh re-queues, and the supervisor covers
50
+ * the other two.
51
+ *
52
+ * Because a strand node runs one supervisor per relay, "is the reservation held"
53
+ * is asked PER RELAY ({@link circuitMultiaddrsVia}): relay X's supervisor must not
54
+ * be satisfied by relay Y's circuit addr, or it would never re-drive X.
33
55
  *
34
56
  * ⚠️ WHY THE RESERVATION IS REQUESTED EXPLICITLY RATHER THAN LEFT TO DISCOVERY.
35
57
  * libp2p fills a search listener's pending reservation from `RelayDiscovery`,
@@ -60,6 +82,19 @@
60
82
  * so {@link driveRelayReservation} stays a single-shot primitive and
61
83
  * {@link superviseRelayReservation} owns the retry cadence on top of it. That
62
84
  * loop is what makes a lost reservation recover without a page reload.
85
+ *
86
+ * One measured refinement to "out of reach": libp2p 3.1.3's `connection.newStream`
87
+ * records every protocol OUR OWN outbound stream negotiated into the peer store, so
88
+ * the explicit reservation request writes the relay-hop protocol id against the
89
+ * relay itself. From then on, discovery CAN refill a freed slot on its own — but
90
+ * only when the relay answers at that moment (a hangup while the relay stays up;
91
+ * measured at ~25 ms). A relay that is down when the slot frees fails that one
92
+ * discovery attempt, which poisons the store's `relayFilter` against it, and
93
+ * nothing in libp2p tries again: a relay RESTART still needs the supervisor, which
94
+ * un-poisons the filter before every attempt ({@link clearRelayFilterEntry}). Two
95
+ * mechanisms in production, one guarantee; the specs disable discovery's half
96
+ * (`forgetRelayProtocol` in `relay-reservation.spec.ts`) so they prove the
97
+ * supervisor's.
63
98
  */
64
99
  import type { Libp2p, PeerId } from '@libp2p/interface';
65
100
  /**
@@ -133,6 +168,20 @@ export interface CircuitRelayTransportLike {
133
168
  * live rather than a stale snapshot.
134
169
  */
135
170
  export declare function circuitMultiaddrs(node: Libp2p): string[];
171
+ /**
172
+ * The node's `/p2p-circuit` multiaddrs THROUGH the relays `addrs` name — the ones
173
+ * whose `/p2p/<relayPeerId>/p2p-circuit` component names one of them. This is the
174
+ * "held" question a supervisor over a SUBSET of the node's relays has to ask: a
175
+ * strand node runs one supervisor per relay, and relay X's supervisor must not be
176
+ * satisfied by relay Y's addr, or a lost X is never re-driven.
177
+ *
178
+ * Falls back to EVERY circuit addr when any entry names no relay peer id (no
179
+ * `/p2p/` component, or unparsable): such an addr can still be dialed and reserved
180
+ * through, but the addr it earns cannot be attributed to it, so "any circuit addr"
181
+ * is the only answer that does not mark a live reservation as lost. The control
182
+ * node passes its whole relay list, so its answer is the same either way.
183
+ */
184
+ export declare function circuitMultiaddrsVia(node: Libp2p, addrs: readonly string[]): string[];
136
185
  /**
137
186
  * The running node's circuit-relay transport, or `null` when it has none.
138
187
  *
@@ -174,6 +223,19 @@ export interface RelayReservationSupervisorOptions extends RelayReserveOptions {
174
223
  minBackoffMs?: number;
175
224
  /** Backoff ceiling. Default 60_000. */
176
225
  maxBackoffMs?: number;
226
+ /**
227
+ * Awaited before every drive AFTER the first — never before the first attempt,
228
+ * which the caller has typically prepared for already. A hook that throws or
229
+ * rejects is logged and the drive still runs: the hook is preparation, and a
230
+ * failed preparation must not cost the attempt itself.
231
+ *
232
+ * What a strand node uses it for: re-announcing its delegate peer id to the relay
233
+ * about to be re-dialed. A party control node running the relay server admits a
234
+ * strand node on an in-memory delegate grant, and a relay restart drops every
235
+ * grant it held — so a re-drive that did not re-announce first would be denied
236
+ * at the relay's connection gate.
237
+ */
238
+ beforeRedrive?: () => Promise<void> | void;
177
239
  }
178
240
  /**
179
241
  * A running retry loop for one node + relay list. Obtained from
@@ -217,6 +279,12 @@ export interface RelayReservationSupervisor {
217
279
  * If that lands a reservation, rejoin the healthy path above; if not, sleep
218
280
  * the current backoff and double it up to `maxBackoffMs`.
219
281
  *
282
+ * "Held" is judged PER RELAY — a circuit addr through one of `addrs`' relays
283
+ * ({@link circuitMultiaddrsVia}) — so several loops over one node, one relay each
284
+ * (a strand node), each re-drive exactly the relay they own. Every drive after the
285
+ * first is preceded by the optional `beforeRedrive` hook
286
+ * ({@link RelayReservationSupervisorOptions}).
287
+ *
220
288
  * Starts immediately; the first tick runs before this returns.
221
289
  */
222
290
  export declare function superviseRelayReservation(node: Libp2p, addrs: readonly string[], opts?: RelayReservationSupervisorOptions): RelayReservationSupervisor;