@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
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Merging verified cadre addresses into a libp2p node's **address book** (its
3
+ * peerStore), so that every layer below cadre-core which dials by bare peer id
4
+ * — Optimystic's cluster client, repo client, FRET ping/announce, all funnelling
5
+ * into `libp2p.dialProtocol(peerId, …)` — has an address to use for a sibling
6
+ * whose connection has dropped, instead of failing with `NoValidAddressesError`
7
+ * until the next reconcile pass happens to re-dial it.
8
+ *
9
+ * Shared by the control-cohort reconcile pass and the strand one (both in
10
+ * `CadreNode`): each resolves a peer's addresses its own way and then needs
11
+ * exactly this write, with exactly this workaround.
12
+ *
13
+ * Two exports, in the order a caller uses them: {@link groupAddrsByPeerId} turns
14
+ * a flat address list into per-peer groups (the strand path starts from the
15
+ * peer-agnostic union the strand-addr RPC returns), and {@link mergePeerAddrs}
16
+ * writes one peer's group.
17
+ */
18
+ import debug from 'debug';
19
+ import { peerIdFromString } from '@libp2p/peer-id';
20
+ import { multiaddr, CODE_P2P, CODE_P2P_CIRCUIT } from '@multiformats/multiaddr';
21
+ const log = debug('sereus:cadre:peer-addr-book');
22
+ /**
23
+ * Write `addrs` into `host`'s address book for `peerId`, best-effort. `peerId`
24
+ * may be the string form callers hold (a `CadrePeer.PeerId` column, a strand-addr
25
+ * RPC result); parsing it is part of the best-effort contract below, so no caller
26
+ * has to wrap this in a second try/catch of its own.
27
+ *
28
+ * - **Empty `addrs` writes nothing** and reports `'skipped'`. That is how a
29
+ * revoked / stale / untrusted peer's existing entry is allowed to age out on
30
+ * its own: the caller resolves `[]` for it, and nothing here refreshes it.
31
+ * - Otherwise the addresses are merged, and — see the timestamp trap below —
32
+ * re-saved when the merge alone leaves them invisible (`'restamped'`).
33
+ * - Any throw (unparsable peer id, datastore failure, a peerStore double without
34
+ * `merge`) is logged and folded to `'failed'`. Callers are best-effort loops:
35
+ * an address-book failure must never abort a reconcile pass.
36
+ *
37
+ * NOTE: `peerStore.merge` cannot refresh an address's `observed` timestamp in
38
+ * `@libp2p/peer-store` 12.0.10 — `to-peer-pb.js` shadows its own loop variable
39
+ * (`…addresses?.find(addr => uint8ArrayEquals(addr.multiaddr, addr.multiaddr))`,
40
+ * always true, so it returns the FIRST stored address's timestamp), and every
41
+ * read filters out addresses older than `MAX_ADDRESS_AGE` (1 hour). So an entry
42
+ * silently dies at the one-hour mark and no amount of re-merging revives it —
43
+ * not even merging a brand-new address. The `save` below is the workaround:
44
+ * `save` is the one write path that omits `existingPeer` and therefore stamps
45
+ * `Date.now()`. Drop it once upstream stamps `Date.now()` on merge.
46
+ *
47
+ * NOTE: raising the store's own `maxAddressAge` (libp2p forwards `init.peerStore`
48
+ * straight into `persistentPeerStore`) would sidestep the bug in one line, and is
49
+ * deliberately NOT done: expiry is load-bearing here. Only *verified* addresses
50
+ * come through this helper, and the design relies on everything else — the
51
+ * cold-start seed entries, identify-learned addresses — still ageing out on the
52
+ * stock schedule. A global age bump would keep those alive too.
53
+ *
54
+ * NOTE: if the store ever REJECTS an address we hand it, the restamp repeats
55
+ * every pass forever — `allVisible` stays false, and `save` re-submits the same
56
+ * rejected address. libp2p wires the store's `addressFilter` to
57
+ * `connectionGater.filterMultiaddrForPeer`, which nothing in this repo implements
58
+ * today (`createMembershipConnectionGater` gates dials, not addresses), so the
59
+ * loop is unreachable. If an app ever supplies that gater hook via
60
+ * `NetworkConfig.connectionGater`, bound the retry — the cost is one redundant
61
+ * datastore write per gated sibling per reconcile pass.
62
+ */
63
+ export async function mergePeerAddrs(host, peerId, addrs) {
64
+ if (addrs.length === 0) {
65
+ return 'skipped';
66
+ }
67
+ try {
68
+ const id = typeof peerId === 'string' ? peerIdFromString(peerId) : peerId;
69
+ const peer = await host.peerStore.merge(id, { multiaddrs: addrs });
70
+ // The returned Peer is already expiry-filtered, so it says whether the merge
71
+ // is actually VISIBLE — which is the only question that matters downstream.
72
+ if (allVisible(peer.addresses, addrs, id)) {
73
+ return 'merged';
74
+ }
75
+ await host.peerStore.save(id, restampData(peer, addrs, id));
76
+ return 'restamped';
77
+ }
78
+ catch (error) {
79
+ log('mergePeerAddrs: address-book write for %s failed: %o', peerId.toString(), error);
80
+ return 'failed';
81
+ }
82
+ }
83
+ /**
84
+ * Group multiaddr strings by the peer id in their final `/p2p/` component — the
85
+ * attribution step between a flat, peer-agnostic address list and the per-peer
86
+ * writes {@link mergePeerAddrs} takes.
87
+ *
88
+ * The **last** `/p2p/` component is the addressed peer, not the first: a relayed
89
+ * address (`…/p2p/<relay>/p2p-circuit/p2p/<dst>`) names the relay first and the
90
+ * destination last, while a direct address (`…/tcp/…/p2p/<dst>`) names only the
91
+ * destination. Taking the last therefore attributes both shapes to the peer the
92
+ * address actually reaches.
93
+ *
94
+ * Three shapes are dropped rather than attributed, because none of them names a
95
+ * peer the address reaches: one that does not parse, one carrying no `/p2p/`
96
+ * component at all (a peer advertising a bare listen addr), and one whose
97
+ * `p2p-circuit` comes AFTER its last `/p2p/` (`…/p2p/<relay>/p2p-circuit`) — that
98
+ * last one is a relay hop with the destination missing, so its trailing peer id
99
+ * is the RELAY's and filing it under the relay would claim the relay is reachable
100
+ * at a circuit leading nowhere. Duplicates collapse within a group. Insertion
101
+ * order is preserved, both between groups and inside one.
102
+ */
103
+ export function groupAddrsByPeerId(addrs) {
104
+ const groups = new Map();
105
+ const seen = new Set();
106
+ for (const addr of addrs) {
107
+ const attributed = attributeAddr(addr);
108
+ if (!attributed) {
109
+ continue;
110
+ }
111
+ const key = `${attributed.peerId}\n${addr}`;
112
+ if (seen.has(key)) {
113
+ continue;
114
+ }
115
+ seen.add(key);
116
+ const group = groups.get(attributed.peerId);
117
+ if (group) {
118
+ group.push(attributed.multiaddr);
119
+ }
120
+ else {
121
+ groups.set(attributed.peerId, [attributed.multiaddr]);
122
+ }
123
+ }
124
+ return groups;
125
+ }
126
+ /**
127
+ * Parse one address and pull out the peer id it addresses (its last `/p2p/`
128
+ * value); null when the address is unparsable or names no destination. Every case
129
+ * is logged at the same level — an unattributable address is a sibling telling us
130
+ * something we cannot use, which is worth seeing but never worth throwing over.
131
+ */
132
+ function attributeAddr(addr) {
133
+ let parsed;
134
+ try {
135
+ parsed = multiaddr(addr);
136
+ }
137
+ catch (error) {
138
+ log('groupAddrsByPeerId: skipping unparsable addr %s: %o', addr, error);
139
+ return null;
140
+ }
141
+ const peerId = lastAddressedPeerId(parsed);
142
+ if (peerId === null) {
143
+ log('groupAddrsByPeerId: skipping addr naming no destination peer: %s', addr);
144
+ return null;
145
+ }
146
+ return { peerId, multiaddr: parsed };
147
+ }
148
+ /**
149
+ * The peer id a parsed address reaches: its last `/p2p/` value, or null when it
150
+ * has none — or when a `p2p-circuit` sits after it, which makes that value the
151
+ * relay's rather than a destination's (see {@link groupAddrsByPeerId}). Walking
152
+ * backwards, meeting the circuit first is exactly that case.
153
+ */
154
+ function lastAddressedPeerId(parsed) {
155
+ const components = parsed.getComponents();
156
+ for (let i = components.length - 1; i >= 0; i--) {
157
+ const component = components[i];
158
+ if (component.code === CODE_P2P_CIRCUIT) {
159
+ return null;
160
+ }
161
+ if (component.code === CODE_P2P && component.value) {
162
+ return component.value;
163
+ }
164
+ }
165
+ return null;
166
+ }
167
+ /**
168
+ * The peerStore strips a trailing `/p2p/<peerId>` from an address it stores when
169
+ * that id is the address's FIRST `/p2p/` component (`dedupe-addresses.js`), so a
170
+ * plain address round-trips without its suffix while a relayed one — whose first
171
+ * `/p2p/` names the relay — keeps it. Comparing and de-duplicating on this
172
+ * suffix-stripped form matches either shape.
173
+ */
174
+ function addrKey(addr, peerId) {
175
+ const suffix = `/p2p/${peerId.toString()}`;
176
+ return addr.endsWith(suffix) ? addr.slice(0, -suffix.length) : addr;
177
+ }
178
+ /** Is every address in `addrs` readable back from the stored, expiry-filtered set? */
179
+ function allVisible(stored, addrs, peerId) {
180
+ const keys = new Set(stored.map((a) => addrKey(a.multiaddr.toString(), peerId)));
181
+ return addrs.every((addr) => keys.has(addrKey(addr.toString(), peerId)));
182
+ }
183
+ /**
184
+ * The full `save` payload for a restamp: `save` re-uses the `patch` strategy with
185
+ * no existing peer, so every field it is not handed is DROPPED — and Optimystic
186
+ * classifies peers by their peerStore protocol list (`libp2p-key-network.ts`
187
+ * `membershipOf`), so losing protocols here would break cluster/repo routing.
188
+ * Everything therefore carries forward from the just-merged `peer`.
189
+ *
190
+ * The read-modify-write is NOT atomic against a concurrent `identify` write to
191
+ * the same entry. The loss window is milliseconds and costs at most one identify
192
+ * update, recovered by the next identify push — not worth a lock.
193
+ *
194
+ * A tag's remaining TTL is not carried: the public `Tag` type exposes only
195
+ * `value`, so the stored expiry is unreadable from here and the tag comes back
196
+ * non-expiring. `publicKey` is likewise not passed, which is lossless for the
197
+ * Ed25519 peer ids a cadre uses (the key is embedded in the id).
198
+ */
199
+ function restampData(peer, addrs, peerId) {
200
+ const addresses = new Map();
201
+ for (const address of peer.addresses) {
202
+ addresses.set(addrKey(address.multiaddr.toString(), peerId), address);
203
+ }
204
+ for (const addr of addrs) {
205
+ const key = addrKey(addr.toString(), peerId);
206
+ if (!addresses.has(key)) {
207
+ // Uncertified, matching what `merge`'s own `multiaddrs` path would have
208
+ // stored; an address already present keeps whatever flag it carried.
209
+ addresses.set(key, { multiaddr: addr, isCertified: false });
210
+ }
211
+ }
212
+ return {
213
+ addresses: [...addresses.values()],
214
+ protocols: peer.protocols,
215
+ metadata: new Map(peer.metadata),
216
+ tags: new Map([...peer.tags].map(([name, tag]) => [name, { value: tag.value }])),
217
+ peerRecordEnvelope: peer.peerRecordEnvelope
218
+ };
219
+ }
220
+ //# sourceMappingURL=peer-addr-book.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer-addr-book.js","sourceRoot":"","sources":["../src/peer-addr-book.ts"],"names":[],"mappings":"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"}
@@ -84,6 +84,32 @@ export declare function isSignalingAddr(addr: string): boolean;
84
84
  * cannot express that distinction.
85
85
  */
86
86
  export declare function trailingPeerId(addr: Multiaddr): string | null;
87
+ /**
88
+ * `addr` guaranteed to terminate in `/p2p/<peerId>` — the shape every dial path
89
+ * needs — or `null` when it cannot, because the address already terminates in a
90
+ * DIFFERENT peer id and therefore does not reach `peerId` at all.
91
+ *
92
+ * Three input shapes, matching what a `CadrePeer` record can legitimately carry:
93
+ *
94
+ * - no `/p2p/` component (`/ip4/1.2.3.4/tcp/4001`) — the suffix is appended;
95
+ * - a relay hop with the destination missing (`…/p2p/<relay>/p2p-circuit`) — the
96
+ * last component is the circuit, not a peer id, so the suffix is appended and
97
+ * the address becomes the full `…/p2p/<relay>/p2p-circuit/p2p/<peerId>`;
98
+ * - already terminating in `/p2p/<X>` — returned untouched when `X` is `peerId`,
99
+ * dropped otherwise.
100
+ *
101
+ * This is deliberately the same rule libp2p applies internally in
102
+ * `calculateMultiaddrs` (append when the LAST component is not `p2p`) followed by
103
+ * its wrong-peer-id filter, so a normalized list matches what libp2p would have
104
+ * built anyway. Normalizing before the dial is what keeps a list from mixing
105
+ * suffixed and unsuffixed entries, which `getPeerAddress` rejects outright
106
+ * ("Multiaddrs must all have the same peer id or have no peer id") — taking the
107
+ * whole peer down with it, not just the odd address.
108
+ *
109
+ * Never use before signature verification: verification runs against the
110
+ * original on-record strings.
111
+ */
112
+ export declare function withTrailingPeerId(addr: Multiaddr, peerId: string): Multiaddr | null;
87
113
  /**
88
114
  * Return `addrs` with signaling (`/p2p-circuit`) addrs first, otherwise stable.
89
115
  * Used to present the WebRTC dial input ahead of direct addrs. Does NOT mutate
@@ -104,6 +104,38 @@ export function trailingPeerId(addr) {
104
104
  const p2p = addr.getComponents().filter((c) => c.name === 'p2p');
105
105
  return p2p.length > 0 ? (p2p[p2p.length - 1].value ?? null) : null;
106
106
  }
107
+ /**
108
+ * `addr` guaranteed to terminate in `/p2p/<peerId>` — the shape every dial path
109
+ * needs — or `null` when it cannot, because the address already terminates in a
110
+ * DIFFERENT peer id and therefore does not reach `peerId` at all.
111
+ *
112
+ * Three input shapes, matching what a `CadrePeer` record can legitimately carry:
113
+ *
114
+ * - no `/p2p/` component (`/ip4/1.2.3.4/tcp/4001`) — the suffix is appended;
115
+ * - a relay hop with the destination missing (`…/p2p/<relay>/p2p-circuit`) — the
116
+ * last component is the circuit, not a peer id, so the suffix is appended and
117
+ * the address becomes the full `…/p2p/<relay>/p2p-circuit/p2p/<peerId>`;
118
+ * - already terminating in `/p2p/<X>` — returned untouched when `X` is `peerId`,
119
+ * dropped otherwise.
120
+ *
121
+ * This is deliberately the same rule libp2p applies internally in
122
+ * `calculateMultiaddrs` (append when the LAST component is not `p2p`) followed by
123
+ * its wrong-peer-id filter, so a normalized list matches what libp2p would have
124
+ * built anyway. Normalizing before the dial is what keeps a list from mixing
125
+ * suffixed and unsuffixed entries, which `getPeerAddress` rejects outright
126
+ * ("Multiaddrs must all have the same peer id or have no peer id") — taking the
127
+ * whole peer down with it, not just the odd address.
128
+ *
129
+ * Never use before signature verification: verification runs against the
130
+ * original on-record strings.
131
+ */
132
+ export function withTrailingPeerId(addr, peerId) {
133
+ const components = addr.getComponents();
134
+ if (components[components.length - 1]?.name !== 'p2p') {
135
+ return addr.encapsulate(`/p2p/${peerId}`);
136
+ }
137
+ return trailingPeerId(addr) === peerId ? addr : null;
138
+ }
107
139
  /**
108
140
  * Return `addrs` with signaling (`/p2p-circuit`) addrs first, otherwise stable.
109
141
  * Used to present the WebRTC dial input ahead of direct addrs. Does NOT mutate
@@ -1 +1 @@
1
- {"version":3,"file":"peer-record.js","sourceRoot":"","sources":["../src/peer-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAGxE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE7D;;;GAGG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,8BAA8B,GAAG,CAAC,CAAC;AAEnF,+EAA+E;AAC/E,MAAM,gBAAgB,GAAG,cAAc,CAAC;AAExC;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAc,EAAE,SAAiB,EAAE,SAAiB;IAC1F,MAAM,MAAM,GAAG,0BAA0B,CAAC,wBAAwB,EAAE,SAAS,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IACvH,OAAO,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,WAAW,CAAW,CAAC;AACzD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAiF,EACjF,aAAqB;IAErB,MAAM,aAAa,GAAG,uBAAuB,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IACvG,MAAM,GAAG,GAAG,IAAI,CACd,aAAa,EACb,aAAa,EACb,SAAS,EACT,WAAW,EACX,WAAW,EACX,WAAW,CACF,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC;AAC5H,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,MAAyB;IACjE,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QACrC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,aAAa,GAAG,uBAAuB,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IACvG,OAAO,MAAM,CACX,aAAa,EACb,MAAM,CAAC,GAAG,EACV,MAAM,CAAC,SAAS,EAChB,SAAS,EACT,WAAW,EACX,WAAW,EACX,WAAW,CACZ,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAiB,EAAE,QAAgB,EAAE,GAAW;IAChF,IAAI,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,EAAE,CAAC;QACrB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,GAAG,GAAG,SAAS,IAAI,QAAQ,CAAC;AACrC,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,IAAe;IAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC;IACjE,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACrE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAe;IACjD,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,OAAO,CAAC,GAAG,SAAS,EAAE,GAAG,IAAI,CAAC,CAAC;AACjC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO;QACL,QAAQ;YACN,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"peer-record.js","sourceRoot":"","sources":["../src/peer-record.ts"],"names":[],"mappings":"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"}
@@ -1,29 +1,59 @@
1
1
  /**
2
2
  * Translate `network.relayAddrs` — the operator-facing "use these relay servers"
3
- * setting — into the mechanism this repo already has for reserving a circuit-relay
4
- * slot: a `…/p2p/<relayPeerId>/p2p-circuit` entry in the node's listen addrs.
5
- *
6
- * `@libp2p/circuit-relay-v2`'s transport is always present on the Node path
7
- * (`../optimystic/packages/db-p2p/src/libp2p-node.ts` pushes `circuitRelayTransport()`
8
- * unconditionally), and listening on a relay's circuit addr is what makes libp2p dial
9
- * that relay and hold a reservation. Everything downstream of the listen list —
10
- * the reservation itself, `CadreNode.circuitRelayTargets()` picking delegate-announce
11
- * targets, strand nodes inheriting the listen addrs, the `/p2p-circuit`-first address
12
- * publishing in `registerSelf` — then works unchanged.
13
- *
14
- * So `relayAddrs` is a second, friendlier door onto an existing mechanism:
15
- *
16
- * effective listen addrs = listenAddrs ++ relayAddrs.map(a => a + '/p2p-circuit')
17
- *
18
- * NOTE: this is the FAIL-FAST route to a reservation — libp2p's transport manager
19
- * throws when a configured circuit listener cannot listen, so naming a relay that
20
- * is down means the node does not start. The fail-soft route is the bare
21
- * `/p2p-circuit` search listener driven by `relay-reservation.ts`. The two are
22
- * ALTERNATIVES, not layers: configuring `relayAddrs` on a node that already
23
- * reserves via the search listener adds the fatal listener back and the node
24
- * stops booting when its relay is down.
3
+ * setting — into the listen entry a node needs before it can hold a circuit-relay
4
+ * slot, and validate the operator's entries while doing it.
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.
34
+ *
35
+ * The bare `/p2p-circuit` search listener cannot open a connection on its own:
36
+ * libp2p fills a pending reservation from `RelayDiscovery`, which nominates a peer
37
+ * only once the relay-hop protocol id is in that peer's peer-store protocol list,
38
+ * and that list is written exclusively by IDENTIFY — which `@optimystic/db-p2p`
39
+ * namespaces per network, so a cadre node and a stock relay never identify each
40
+ * other. See `relay-reservation.ts`'s module doc and `docs/architecture.md`.
25
41
  */
26
42
  import type { NetworkConfig } from './types.js';
43
+ import type { RelayReservationState } from './relay-reservation.js';
44
+ /**
45
+ * libp2p's relay SEARCH listener: registers one pending reservation and dials
46
+ * nothing. One entry covers every configured relay — the pending reservation is
47
+ * per-listener, not per-relay, and `relay-reservation.ts` fills it with whichever
48
+ * relay answers first.
49
+ */
50
+ export declare const RELAY_SEARCH_LISTEN_ADDR = "/p2p-circuit";
51
+ /**
52
+ * Which listener shape `network.relayAddrs` resolves to for this caller — see the
53
+ * table in the module doc. Defaults to `'configured'` everywhere except the
54
+ * control node's own libp2p options.
55
+ */
56
+ export type RelayListenRoute = 'configured' | 'search';
27
57
  /**
28
58
  * The circuit-listen multiaddr for each configured relay: `<relayAddr>/p2p-circuit`.
29
59
  * An entry that already carries `/p2p-circuit` is passed through unchanged.
@@ -38,8 +68,28 @@ import type { NetworkConfig } from './types.js';
38
68
  export declare function relayCircuitAddrs(relayAddrs: readonly string[]): string[];
39
69
  /**
40
70
  * The listen multiaddrs a node actually binds: configured `listenAddrs` plus the
41
- * circuit-listen addr of every configured relay, deduplicated, order-stable
42
- * (configured entries first). Returns `undefined` when neither field is set, so
43
- * callers keep omitting `listenAddrs` and inherit db-p2p's default.
71
+ * relay listen entry `route` calls for, deduplicated, order-stable (configured
72
+ * entries first). Returns `undefined` when neither field is set, so callers keep
73
+ * omitting `listenAddrs` and inherit db-p2p's default.
74
+ *
75
+ * Every `relayAddrs` entry is validated on BOTH routes — the `'search'` route
76
+ * discards the resolved circuit addrs, but an operator typo must fail at config
77
+ * resolution either way.
78
+ */
79
+ export declare function resolveListenAddrs(network: NetworkConfig | undefined, route?: RelayListenRoute): string[] | undefined;
80
+ /**
81
+ * Thrown out of `CadreNode.start()` when the boot-path reservation drive for
82
+ * `network.relayAddrs` produces no `/p2p-circuit` address on its FIRST attempt.
83
+ *
84
+ * This is what keeps `network.relayAddrs` fail-fast now that the control node
85
+ * takes the `'search'` route: libp2p's own `UnsupportedListenAddressesError` used
86
+ * to abort start from inside `listen()`, and an operator who names a relay is
87
+ * telling the node it has no other reachability — coming up undialable is worse
88
+ * than not coming up. Names the relays and the reservation's own reason, both of
89
+ * which the libp2p error omitted.
44
90
  */
45
- export declare function resolveListenAddrs(network: NetworkConfig | undefined): string[] | undefined;
91
+ export declare class RelayReservationFailedError extends Error {
92
+ readonly relayAddrs: readonly string[];
93
+ readonly state: RelayReservationState;
94
+ constructor(relayAddrs: readonly string[], state: RelayReservationState);
95
+ }
@@ -1,27 +1,43 @@
1
1
  /**
2
2
  * Translate `network.relayAddrs` — the operator-facing "use these relay servers"
3
- * setting — into the mechanism this repo already has for reserving a circuit-relay
4
- * slot: a `…/p2p/<relayPeerId>/p2p-circuit` entry in the node's listen addrs.
3
+ * setting — into the listen entry a node needs before it can hold a circuit-relay
4
+ * slot, and validate the operator's entries while doing it.
5
5
  *
6
- * `@libp2p/circuit-relay-v2`'s transport is always present on the Node path
7
- * (`../optimystic/packages/db-p2p/src/libp2p-node.ts` pushes `circuitRelayTransport()`
8
- * unconditionally), and listening on a relay's circuit addr is what makes libp2p dial
9
- * that relay and hold a reservation. Everything downstream of the listen list —
10
- * the reservation itself, `CadreNode.circuitRelayTargets()` picking delegate-announce
11
- * targets, strand nodes inheriting the listen addrs, the `/p2p-circuit`-first address
12
- * publishing in `registerSelf` — then works unchanged.
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}):
13
9
  *
14
- * So `relayAddrs` is a second, friendlier door onto an existing mechanism:
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 |
15
14
  *
16
- * effective listen addrs = listenAddrs ++ relayAddrs.map(a => a + '/p2p-circuit')
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()`.
17
22
  *
18
- * NOTE: this is the FAIL-FAST route to a reservation — libp2p's transport manager
19
- * throws when a configured circuit listener cannot listen, so naming a relay that
20
- * is down means the node does not start. The fail-soft route is the bare
21
- * `/p2p-circuit` search listener driven by `relay-reservation.ts`. The two are
22
- * ALTERNATIVES, not layers: configuring `relayAddrs` on a node that already
23
- * reserves via the search listener adds the fatal listener back and the node
24
- * stops booting when its relay is down.
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.
34
+ *
35
+ * The bare `/p2p-circuit` search listener cannot open a connection on its own:
36
+ * libp2p fills a pending reservation from `RelayDiscovery`, which nominates a peer
37
+ * only once the relay-hop protocol id is in that peer's peer-store protocol list,
38
+ * and that list is written exclusively by IDENTIFY — which `@optimystic/db-p2p`
39
+ * namespaces per network, so a cadre node and a stock relay never identify each
40
+ * other. See `relay-reservation.ts`'s module doc and `docs/architecture.md`.
25
41
  */
26
42
  import { multiaddr } from '@multiformats/multiaddr';
27
43
  import { circuitRelayTargetOrThrow } from './delegate-admission.js';
@@ -36,6 +52,13 @@ import { circuitRelayTargetOrThrow } from './delegate-admission.js';
36
52
  * ever changes its default, this constant has to follow.
37
53
  */
38
54
  const DEFAULT_DIRECT_LISTEN_ADDR = '/ip4/0.0.0.0/tcp/0';
55
+ /**
56
+ * 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.
60
+ */
61
+ export const RELAY_SEARCH_LISTEN_ADDR = '/p2p-circuit';
39
62
  /**
40
63
  * The circuit-listen multiaddr for each configured relay: `<relayAddr>/p2p-circuit`.
41
64
  * An entry that already carries `/p2p-circuit` is passed through unchanged.
@@ -52,20 +75,85 @@ export function relayCircuitAddrs(relayAddrs) {
52
75
  }
53
76
  /**
54
77
  * The listen multiaddrs a node actually binds: configured `listenAddrs` plus the
55
- * circuit-listen addr of every configured relay, deduplicated, order-stable
56
- * (configured entries first). Returns `undefined` when neither field is set, so
57
- * callers keep omitting `listenAddrs` and inherit db-p2p's default.
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.
81
+ *
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.
58
85
  */
59
- export function resolveListenAddrs(network) {
60
- const circuits = relayCircuitAddrs(network?.relayAddrs ?? []);
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;
61
91
  const listenAddrs = network?.listenAddrs;
62
- if (!listenAddrs && circuits.length === 0) {
92
+ if (route === 'search') {
93
+ rejectConfiguredCircuitListenAddrs(listenAddrs ?? []);
94
+ }
95
+ if (!listenAddrs && relayEntries.length === 0) {
63
96
  return undefined;
64
97
  }
65
98
  // An explicitly empty `listenAddrs` (the React Native "cannot listen" case) stays
66
- // empty — but a relay named alongside it still gets its circuit listener, since
67
- // acquiring one is the whole point of configuring a relay.
68
- return dedupe([...(listenAddrs ?? [DEFAULT_DIRECT_LISTEN_ADDR]), ...circuits]);
99
+ // empty — but a relay named alongside it still gets its listen entry, since
100
+ // acquiring a slot is the whole point of configuring a relay.
101
+ return dedupe([...(listenAddrs ?? [DEFAULT_DIRECT_LISTEN_ADDR]), ...relayEntries]);
102
+ }
103
+ /**
104
+ * Thrown out of `CadreNode.start()` when the boot-path reservation drive for
105
+ * `network.relayAddrs` produces no `/p2p-circuit` address on its FIRST attempt.
106
+ *
107
+ * This is what keeps `network.relayAddrs` fail-fast now that the control node
108
+ * takes the `'search'` route: libp2p's own `UnsupportedListenAddressesError` used
109
+ * to abort start from inside `listen()`, and an operator who names a relay is
110
+ * telling the node it has no other reachability — coming up undialable is worse
111
+ * than not coming up. Names the relays and the reservation's own reason, both of
112
+ * which the libp2p error omitted.
113
+ */
114
+ export class RelayReservationFailedError extends Error {
115
+ constructor(relayAddrs, state) {
116
+ super(`network.relayAddrs reservation failed (status: ${state.status}): ` +
117
+ `${state.error ?? 'no /p2p-circuit address appeared'} — relays: ${relayAddrs.join(', ')}`);
118
+ this.relayAddrs = relayAddrs;
119
+ this.state = state;
120
+ this.name = 'RelayReservationFailedError';
121
+ }
122
+ }
123
+ /**
124
+ * 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
126
+ * dials that relay from inside `listen()`, the bring-up quiet period
127
+ * (`membership-connection-gater.ts`) denies exactly that dial, `listen()` fails, and
128
+ * the transport manager's default `FATAL_ALL` aborts `libp2p.start()`. The operator
129
+ * would see `UnsupportedListenAddressesError` from deep inside bring-up with nothing
130
+ * naming the cause, so name it here instead.
131
+ *
132
+ * `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.
134
+ */
135
+ function rejectConfiguredCircuitListenAddrs(listenAddrs) {
136
+ const configured = listenAddrs.filter(isConfiguredCircuitListenAddr);
137
+ if (configured.length > 0) {
138
+ throw new Error('network.listenAddrs names a relay directly ' +
139
+ `(${configured.join(', ')}), which a control node cannot listen on: libp2p would ` +
140
+ 'dial that relay during control-database bring-up, when the node accepts no ' +
141
+ 'connections. Move the relay to network.relayAddrs, which reserves after bring-up.');
142
+ }
143
+ }
144
+ /** `<something>/p2p-circuit` — the configured shape, as opposed to the bare search addr. */
145
+ function isConfiguredCircuitListenAddr(listenAddr) {
146
+ if (listenAddr === RELAY_SEARCH_LISTEN_ADDR) {
147
+ return false;
148
+ }
149
+ try {
150
+ return multiaddr(listenAddr).getComponents().some((c) => c.name === 'p2p-circuit');
151
+ }
152
+ catch {
153
+ // Not our error to raise: libp2p reports an unparsable listen addr itself, and
154
+ // this helper only ever ADDS a denial.
155
+ return false;
156
+ }
69
157
  }
70
158
  /**
71
159
  * One entry of {@link relayCircuitAddrs}: the addr the node listens on for this relay.
@@ -1 +1 @@
1
- {"version":3,"file":"relay-addrs.js","sourceRoot":"","sources":["../src/relay-addrs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,SAAS,EAAkB,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAGpE;;;;;;;;;GASG;AACH,MAAM,0BAA0B,GAAG,oBAAoB,CAAC;AAExD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA6B;IAC7D,OAAO,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC;AACnD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAkC;IACnE,MAAM,QAAQ,GAAG,iBAAiB,CAAC,OAAO,EAAE,UAAU,IAAI,EAAE,CAAC,CAAC;IAC9D,MAAM,WAAW,GAAG,OAAO,EAAE,WAAW,CAAC;IACzC,IAAI,CAAC,WAAW,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1C,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,kFAAkF;IAClF,gFAAgF;IAChF,2DAA2D;IAC3D,OAAO,MAAM,CAAC,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,0BAA0B,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC;AACjF,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,iBAAiB,CAAC,SAAiB;IAC1C,MAAM,cAAc,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC;IACxF,MAAM,UAAU,GAAG,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,cAAc,CAAC;IAC3E,IAAI,CAAC;QACH,yBAAyB,CAAC,UAAU,CAAC,CAAC,CAAC,6BAA6B;IACtE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,4FAA4F,SAAS,EAAE,EACvG,EAAE,KAAK,EAAE,GAAG,EAAE,CACf,CAAC;IACJ,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,2FAA2F;AAC3F,SAAS,eAAe,CAAC,SAAiB;IACxC,IAAI,CAAC;QACH,OAAO,SAAS,CAAC,SAAS,CAAC,CAAC,aAAa,EAAE,CAAC;IAC9C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,sDAAsD,SAAS,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IACrG,CAAC;AACH,CAAC;AAED,yFAAyF;AACzF,SAAS,MAAM,CAAC,KAAwB;IACtC,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;AAC7B,CAAC"}
1
+ {"version":3,"file":"relay-addrs.js","sourceRoot":"","sources":["../src/relay-addrs.ts"],"names":[],"mappings":"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"}