@serfab/cadre-core 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/announce-addrs.d.ts +49 -0
- package/dist/announce-addrs.js +78 -0
- package/dist/announce-addrs.js.map +1 -0
- package/dist/cadre-node.d.ts +607 -53
- package/dist/cadre-node.js +1268 -157
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-database.d.ts +174 -23
- package/dist/control-database.js +317 -107
- package/dist/control-database.js.map +1 -1
- package/dist/control-read-retry.d.ts +103 -0
- package/dist/control-read-retry.js +145 -0
- package/dist/control-read-retry.js.map +1 -0
- package/dist/control-retry.d.ts +101 -0
- package/dist/control-retry.js +108 -0
- package/dist/control-retry.js.map +1 -0
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +50 -6
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +59 -35
- package/dist/control-write-retry.js +21 -92
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +17 -15
- package/dist/delegate-admission.js +17 -15
- package/dist/delegate-admission.js.map +1 -1
- package/dist/enrolled-machine-store-file.d.ts +13 -0
- package/dist/enrolled-machine-store-file.js +41 -0
- package/dist/enrolled-machine-store-file.js.map +1 -0
- package/dist/enrolled-machine-store.d.ts +81 -0
- package/dist/enrolled-machine-store.js +225 -0
- package/dist/enrolled-machine-store.js.map +1 -0
- package/dist/file-durable-slot.js +5 -3
- package/dist/file-durable-slot.js.map +1 -1
- package/dist/index.d.ts +10 -6
- package/dist/index.js +39 -12
- package/dist/index.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +226 -26
- package/dist/membership-connection-gater.js +316 -30
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +6 -0
- package/dist/node-local-snapshot.js +7 -1
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/{strand-backfill.d.ts → peer-join-backfill.d.ts} +49 -20
- package/dist/{strand-backfill.js → peer-join-backfill.js} +146 -56
- package/dist/peer-join-backfill.js.map +1 -0
- package/dist/peer-record.d.ts +26 -0
- package/dist/peer-record.js +32 -0
- package/dist/peer-record.js.map +1 -1
- package/dist/relay-addrs.d.ts +145 -22
- package/dist/relay-addrs.js +314 -31
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +20 -15
- package/dist/relay-reservation.js +18 -14
- package/dist/relay-reservation.js.map +1 -1
- package/dist/strand-addr-protocol.js +16 -0
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-database.d.ts +12 -0
- package/dist/strand-database.js +16 -0
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-formation-manager.d.ts +22 -9
- package/dist/strand-formation-manager.js +20 -12
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +83 -9
- package/dist/strand-formation-protocol.js +86 -10
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +151 -17
- package/dist/strand-instance-manager.js +237 -32
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +118 -14
- package/dist/strand-membership-writer.js +203 -15
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-network-config.d.ts +84 -0
- package/dist/strand-network-config.js +136 -0
- package/dist/strand-network-config.js.map +1 -0
- package/dist/strand-revocation-enforcer.d.ts +392 -0
- package/dist/strand-revocation-enforcer.js +533 -0
- package/dist/strand-revocation-enforcer.js.map +1 -0
- package/dist/strand-solicitation.d.ts +10 -3
- package/dist/strand-solicitation.js +12 -7
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +47 -2
- package/dist/strand-wake-protocol.js +76 -6
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/strand-watcher.js +6 -1
- package/dist/strand-watcher.js.map +1 -1
- package/dist/types.d.ts +282 -43
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/package.json +16 -8
- package/src/announce-addrs.ts +93 -0
- package/src/cadre-node.ts +1366 -171
- package/src/control-database.ts +2628 -2406
- package/src/control-read-retry.ts +167 -0
- package/src/control-retry.ts +190 -0
- package/src/control-schema.ts +50 -6
- package/src/control-write-retry.ts +30 -130
- package/src/delegate-admission.ts +17 -15
- package/src/enrolled-machine-store-file.ts +43 -0
- package/src/enrolled-machine-store.ts +280 -0
- package/src/file-durable-slot.ts +5 -3
- package/src/index.ts +477 -407
- package/src/membership-connection-gater.ts +539 -204
- package/src/node-local-snapshot.ts +13 -1
- package/src/peer-addr-book.ts +251 -250
- package/src/{strand-backfill.ts → peer-join-backfill.ts} +187 -75
- package/src/peer-record.ts +33 -0
- package/src/relay-addrs.ts +436 -111
- package/src/relay-reservation.ts +828 -823
- package/src/strand-addr-protocol.ts +17 -0
- package/src/strand-database.ts +17 -0
- package/src/strand-formation-manager.ts +511 -491
- package/src/strand-formation-protocol.ts +808 -692
- package/src/strand-instance-manager.ts +899 -611
- package/src/strand-membership-writer.ts +232 -15
- package/src/strand-network-config.ts +172 -0
- package/src/strand-revocation-enforcer.ts +653 -0
- package/src/strand-solicitation.ts +484 -472
- package/src/strand-wake-protocol.ts +94 -8
- package/src/strand-watcher.ts +319 -314
- package/src/types.ts +1554 -1309
- package/dist/strand-backfill.js.map +0 -1
package/dist/types.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { ConnectionGater, Libp2p, PeerId, PrivateKey } from '@libp2p/interface';
|
|
2
2
|
import type { IRawStorage, Libp2pTransports } from '@optimystic/db-p2p';
|
|
3
3
|
import type { IPeerNetwork, IRepo } from '@optimystic/db-core';
|
|
4
|
-
import type {
|
|
4
|
+
import type { PeerJoinBackfillConfig } from './peer-join-backfill.js';
|
|
5
|
+
import type { StrandRevocationEnforcementConfig } from './strand-revocation-enforcer.js';
|
|
5
6
|
import type { StrandDatabase } from './strand-database.js';
|
|
6
7
|
import type { SeedTrustPolicy } from './seed-trust-policy.js';
|
|
7
8
|
import type { KeyStore, KeyId } from './key-store.js';
|
|
8
9
|
import type { TrustedOwnerStore, TrustSource } from './trusted-owner-store.js';
|
|
9
10
|
import type { BootstrapPeerStore } from './bootstrap-peer-store.js';
|
|
11
|
+
import type { EnrolledMachineStore } from './enrolled-machine-store.js';
|
|
10
12
|
import type { PushNotifier } from './push-notifier.js';
|
|
11
13
|
import type { RevocableTable } from './control-authorization.js';
|
|
12
14
|
/**
|
|
@@ -20,7 +22,7 @@ export interface Libp2pNodeWithRepo extends Libp2p {
|
|
|
20
22
|
* `coordinatedRepo` (`libp2p-node-base.ts`). Optional deliberately: the base
|
|
21
23
|
* factory always assigns it, but it is not part of the upstream node type, so
|
|
22
24
|
* declaring it required would be a claim this repo cannot enforce. The strand
|
|
23
|
-
* backfill (`
|
|
25
|
+
* backfill (`peer-join-backfill.ts`) logs once and stays inert when it is absent.
|
|
24
26
|
*/
|
|
25
27
|
keyNetwork?: IPeerNetwork;
|
|
26
28
|
}
|
|
@@ -98,12 +100,12 @@ export declare const HIBERNATION_TIMEOUTS: Record<LatencyHint, HibernationTimeou
|
|
|
98
100
|
* cadre-core disposes only its own cache wrapper — it never closes the store you
|
|
99
101
|
* returned. Closing the underlying handle stays the embedder's job.
|
|
100
102
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
103
|
+
* Either form is safe with a persistent backend. One instance handed to every scope
|
|
104
|
+
* also shares ONE cache wrapper (`wrapStorageWithCache` memoizes per inner instance),
|
|
105
|
+
* which counts its holders: a scope stopping releases only its own claim, and the
|
|
106
|
+
* wrapper is retired when the last scope releases it. The factory form is still the
|
|
107
|
+
* better default for a different reason — it partitions each strand's data, which
|
|
108
|
+
* simplifies cleanup.
|
|
107
109
|
*/
|
|
108
110
|
export type RawStorageProvider = IRawStorage | ((strandId: string) => IRawStorage);
|
|
109
111
|
/**
|
|
@@ -140,28 +142,75 @@ export interface StorageConfig {
|
|
|
140
142
|
* Network configuration for libp2p
|
|
141
143
|
*/
|
|
142
144
|
export interface NetworkConfig {
|
|
145
|
+
/**
|
|
146
|
+
* Addresses this machine's libp2p nodes bind. Omitted, `@optimystic/db-p2p`'s own
|
|
147
|
+
* default (`/ip4/0.0.0.0/tcp/0`) applies; `[]` means "cannot listen" and is honored
|
|
148
|
+
* as written (React Native hosts rely on that).
|
|
149
|
+
*
|
|
150
|
+
* The machine runs one control node plus one node per strand, so a FIXED port here
|
|
151
|
+
* can only belong to one of them. The control node binds it as configured; each
|
|
152
|
+
* strand node binds the same entry with its port rewritten to `0`, keeping the
|
|
153
|
+
* interface and transport (`/ws`, a specific-interface bind). A
|
|
154
|
+
* `<relay>/p2p-circuit` entry is untouched — the port inside it is the relay's, not
|
|
155
|
+
* a local bind. See `strand-network-config.ts`.
|
|
156
|
+
*
|
|
157
|
+
* **Only TCP, WebSocket (`/ws`, `/wss`) and circuit-relay addresses are bindable**
|
|
158
|
+
* unless {@link transports} supplies the factories for something else. A `/ws` entry
|
|
159
|
+
* switches the WebSocket transport on by itself; anything else — `/quic-v1`,
|
|
160
|
+
* `/webrtc`, `/webtransport` — refuses node start naming the address and the libp2p
|
|
161
|
+
* package it would need, rather than being dropped in silence by libp2p's transport
|
|
162
|
+
* manager. See `relay-addrs.ts` → `resolveTransportOptions`.
|
|
163
|
+
*/
|
|
143
164
|
listenAddrs?: string[];
|
|
144
165
|
/**
|
|
145
|
-
* Addresses to advertise to peers instead of `listenAddrs` — for a node behind
|
|
166
|
+
* Addresses to advertise to peers **instead of** `listenAddrs` — for a node behind
|
|
146
167
|
* NAT or a reverse proxy that is reachable at a different address than it binds
|
|
147
|
-
* (e.g. `mynode.example.com:4001` in front of a `0.0.0.0:4001` listener).
|
|
168
|
+
* (e.g. `mynode.example.com:4001` in front of a `0.0.0.0:4001` listener). Reaches
|
|
169
|
+
* libp2p's `addresses.announce` via `@optimystic/db-p2p`'s `NodeOptions.announceAddrs`.
|
|
148
170
|
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* in this repo to wire this into. `CadreNode.start()` warns when this is set,
|
|
154
|
-
* rather than pretending it works. Unblocked by the `announce-addrs-option`
|
|
155
|
-
* request filed against `@optimystic/db-p2p`; wiring lands here once this repo's
|
|
156
|
-
* dependency range moves to a version carrying it.
|
|
171
|
+
* **Applies to this machine's CONTROL node only.** Strand nodes drop it: any concrete
|
|
172
|
+
* entry names a port, and that port is the control node's, so a strand node
|
|
173
|
+
* advertising it would send peers to the wrong node (`strand-network-config.ts`).
|
|
174
|
+
* Entries are still validated at node start whichever node ends up using them.
|
|
157
175
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
176
|
+
* **A non-empty value REPLACES the advertised set entirely.** Observed addresses,
|
|
177
|
+
* and the `/p2p-circuit` address earned from a {@link relayAddrs} reservation, are
|
|
178
|
+
* all dropped from what peers are told — so a node configured with both this and
|
|
179
|
+
* `relayAddrs` stops being reachable through its relay. `CadreNode.start()` warns
|
|
180
|
+
* when it sees that combination; it does not refuse it, since a node whose relay
|
|
181
|
+
* slot is decorative may legitimately want only the announced address.
|
|
182
|
+
*
|
|
183
|
+
* Reach for {@link appendAnnounceAddrs} instead in the common case — one publicly
|
|
184
|
+
* reachable address ADDED to everything else the node advertises. Or for
|
|
185
|
+
* {@link relayAddrs} alone, when the node has no stable public address to name and
|
|
186
|
+
* a relay reservation is what makes it dialable. (`inviteAddressResolver` is a
|
|
187
|
+
* narrower tool again: it substitutes a reachable address into invite payloads
|
|
188
|
+
* only, and changes nothing about what this node advertises on the wire.)
|
|
189
|
+
*
|
|
190
|
+
* An empty array means "unset" — it is dropped rather than forwarded, so it cannot
|
|
191
|
+
* land as an explicit empty announce set. An empty *entry* is a different thing and
|
|
192
|
+
* is rejected: a list of one blank string is non-empty, so it would replace every
|
|
193
|
+
* advertised address with nothing. Malformed entries are rejected on the same terms,
|
|
194
|
+
* both at node start — libp2p itself would not notice either until its first address
|
|
195
|
+
* lookup (see `announce-addrs.ts`), so this repo checks each entry up front.
|
|
163
196
|
*/
|
|
164
197
|
announceAddrs?: string[];
|
|
198
|
+
/**
|
|
199
|
+
* Addresses to advertise **in addition to** `listenAddrs` — the field most
|
|
200
|
+
* deployments actually want, since it makes a node reachable at a public address
|
|
201
|
+
* without discarding the observed and `/p2p-circuit` addresses it would otherwise
|
|
202
|
+
* advertise. Reaches libp2p's `addresses.appendAnnounce` via
|
|
203
|
+
* `@optimystic/db-p2p`'s `NodeOptions.appendAnnounceAddrs`.
|
|
204
|
+
*
|
|
205
|
+
* **Control node only**, on the same terms as {@link announceAddrs}.
|
|
206
|
+
*
|
|
207
|
+
* **Ignored while {@link announceAddrs} is non-empty** — that is libp2p's own
|
|
208
|
+
* precedence, applied upstream; this repo does not merge the two locally. Set one
|
|
209
|
+
* or the other, not both.
|
|
210
|
+
*
|
|
211
|
+
* Empty-array, blank-entry and malformed-entry handling all match {@link announceAddrs}.
|
|
212
|
+
*/
|
|
213
|
+
appendAnnounceAddrs?: string[];
|
|
165
214
|
/**
|
|
166
215
|
* Circuit-relay servers this node reserves a slot on, so peers can reach it
|
|
167
216
|
* from behind NAT. Each entry is the relay's DIRECT dial multiaddr ending in
|
|
@@ -169,18 +218,26 @@ export interface NetworkConfig {
|
|
|
169
218
|
* `/p2p-circuit` suffix (one is appended for you; an entry that already has
|
|
170
219
|
* one is passed through unchanged).
|
|
171
220
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
221
|
+
* On the CONTROL node this adds libp2p's bare `/p2p-circuit` SEARCH listener to
|
|
222
|
+
* {@link listenAddrs} (`relay-addrs.ts`) and `CadreNode.start()` drives the
|
|
223
|
+
* reservation itself once the control database is up — the ordering matters,
|
|
224
|
+
* because a listener that dials its relay from inside `libp2p.start()` put a
|
|
225
|
+
* sibling in this node's cohort before its own database existed, and a sibling
|
|
226
|
+
* that had not yet replicated this node's membership row refused the bring-up.
|
|
227
|
+
* STRAND nodes inherit the older per-relay `<relay>/p2p-circuit` listener, which
|
|
228
|
+
* has no such ordering hazard. A relay named here also becomes a
|
|
229
|
+
* delegate-announce target, so this node's strand nodes may reserve on it too
|
|
230
|
+
* (see `delegate-admission.ts`).
|
|
177
231
|
*
|
|
178
232
|
* Setting this gives the node a listener even when `listenAddrs` is `[]` —
|
|
179
233
|
* that is the point of naming a relay, but note that React Native hosts
|
|
180
234
|
* (`reference-app-rn`'s `cadre-phone.ts`) deliberately do NOT listen, and
|
|
181
235
|
* must not acquire a listener by accident.
|
|
182
236
|
*
|
|
183
|
-
*
|
|
237
|
+
* FAIL-FAST both ways: a malformed entry throws at node start rather than being
|
|
238
|
+
* silently dropped, and a first reservation attempt that lands no
|
|
239
|
+
* `/p2p-circuit` address throws `RelayReservationFailedError` out of `start()`.
|
|
240
|
+
* The fail-soft posture over the same machinery is `CadreNode.reserveRelays()`.
|
|
184
241
|
*/
|
|
185
242
|
relayAddrs?: string[];
|
|
186
243
|
/**
|
|
@@ -189,6 +246,16 @@ export interface NetworkConfig {
|
|
|
189
246
|
* better connectivity and uptime), false for transaction profile nodes.
|
|
190
247
|
*/
|
|
191
248
|
enableRelay?: boolean;
|
|
249
|
+
/**
|
|
250
|
+
* Cap on concurrent circuit-relay reservations this node's relay server grants to
|
|
251
|
+
* peers it cannot (yet) recognize as authorized members — the boot-ordering window
|
|
252
|
+
* where a genuine member reserves its relay slot before its membership row has
|
|
253
|
+
* replicated here (see `membership-connection-gater.ts` → "The relay-reservation
|
|
254
|
+
* seam"). Authorized members and announced delegate peers are never counted against
|
|
255
|
+
* it. Default `MAX_UNAUTHORIZED_RELAY_RESERVATIONS` (8); 0 refuses every
|
|
256
|
+
* unauthorized reservation. Only meaningful while {@link enableRelay} is on.
|
|
257
|
+
*/
|
|
258
|
+
unauthorizedRelayReservationCap?: number;
|
|
192
259
|
/**
|
|
193
260
|
* Custom libp2p transports. When omitted, the default transports from
|
|
194
261
|
* `@optimystic/db-p2p` are used (TCP + circuit relay for Node.js).
|
|
@@ -211,6 +278,11 @@ export interface NetworkConfig {
|
|
|
211
278
|
* (and other owner-address contexts). When unset, `libp2pNode.getMultiaddrs()`
|
|
212
279
|
* is used. Hosts behind NAT supply this to substitute their DDNS hostname
|
|
213
280
|
* and externally-mapped port — see `@serfab/cadre-host`'s NatService.
|
|
281
|
+
*
|
|
282
|
+
* Returned addresses need NOT carry a `/p2p/<peerId>` suffix: `CadreNode`
|
|
283
|
+
* appends its own before publishing or dialing any of them, so a resolver
|
|
284
|
+
* cannot leave a sibling with the mixed suffixed/unsuffixed list that
|
|
285
|
+
* `libp2p.dial` rejects. Returning them already suffixed is equally fine.
|
|
214
286
|
*/
|
|
215
287
|
inviteAddressResolver?: () => Promise<string[]>;
|
|
216
288
|
/**
|
|
@@ -225,8 +297,14 @@ export interface NetworkConfig {
|
|
|
225
297
|
* On the CONTROL node this gater is composed with the built-in membership
|
|
226
298
|
* admission gate (`membership-connection-gater.ts`): every hook supplied here
|
|
227
299
|
* is honored unchanged, and on inbound encrypted connections a deny from
|
|
228
|
-
* either this gater or the membership policy denies.
|
|
229
|
-
*
|
|
300
|
+
* either this gater or the membership policy denies.
|
|
301
|
+
*
|
|
302
|
+
* Strand cohort nodes: an OPEN strand's node receives this gater as-is (its
|
|
303
|
+
* peers are legitimately cross-party). A CLOSED strand's node composes
|
|
304
|
+
* revoked-peer denial onto it (`strand-revocation-enforcer.ts`) — every hook
|
|
305
|
+
* supplied here is still honored unchanged, and on the composed hooks
|
|
306
|
+
* (inbound encrypted connection, outbound peer dial, relay reservation) a
|
|
307
|
+
* deny from either this gater or the revocation check denies.
|
|
230
308
|
*/
|
|
231
309
|
connectionGater?: ConnectionGater;
|
|
232
310
|
/**
|
|
@@ -307,8 +385,14 @@ export interface ControlNetworkConfig {
|
|
|
307
385
|
* by omission); the strand breadth defaults to
|
|
308
386
|
* {@link CadreNodeConfig.strandClusterSize} while its policy is the fixed
|
|
309
387
|
* {@link STRAND_CLUSTER_POLICY}.
|
|
388
|
+
*
|
|
389
|
+
* The two `*ClusterPolicy` BUILDERS are the same objects with the block-repair
|
|
390
|
+
* corroboration yardstick declared from the machines enrolled in this party
|
|
391
|
+
* ({@link resolveRepairYardstick}); handed no count, each returns its frozen base
|
|
392
|
+
* constant unchanged. A network picks the derived number up when its libp2p node is
|
|
393
|
+
* built, which for a strand is every wake from hibernation.
|
|
310
394
|
*/
|
|
311
|
-
export { MIN_CLUSTER_SIZE, CONTROL_REPLICATION_BREADTH, CONTROL_CLUSTER_POLICY, DEFAULT_STRAND_CLUSTER_SIZE, STRAND_CLUSTER_POLICY, resolveStrandClusterSize } from '@serfab/quereus-plugin-sereus';
|
|
395
|
+
export { MIN_CLUSTER_SIZE, CONTROL_REPLICATION_BREADTH, CONTROL_CLUSTER_POLICY, DEFAULT_STRAND_CLUSTER_SIZE, STRAND_CLUSTER_POLICY, resolveStrandClusterSize, resolveRepairYardstick, controlClusterPolicy, strandClusterPolicy } from '@serfab/quereus-plugin-sereus';
|
|
312
396
|
/**
|
|
313
397
|
* Main configuration for a CadreNode
|
|
314
398
|
*/
|
|
@@ -323,8 +407,8 @@ export interface CadreNodeConfig {
|
|
|
323
407
|
* Pluggable secure store for node key material. When set, the node loads its
|
|
324
408
|
* identity from `keyStore` under {@link identityKeyId}, generating + persisting
|
|
325
409
|
* a fresh Ed25519 key on first run (protobuf bytes are the canonical stored
|
|
326
|
-
* form). Mutually exclusive with {@link privateKey}. Absent ⇒
|
|
327
|
-
*
|
|
410
|
+
* form). Mutually exclusive with {@link privateKey}. Absent ⇒ use `privateKey`
|
|
411
|
+
* when set, else libp2p generates an ephemeral key.
|
|
328
412
|
*
|
|
329
413
|
* In the single-key reference model the owner signing key is *derived from*
|
|
330
414
|
* the node identity (see {@link CadreNode.getIdentityOwnerKey}), so
|
|
@@ -369,17 +453,42 @@ export interface CadreNodeConfig {
|
|
|
369
453
|
*/
|
|
370
454
|
strandClusterSize?: number;
|
|
371
455
|
/**
|
|
372
|
-
* Tuning for the strand peer-join block catch-up (see `
|
|
456
|
+
* Tuning for the strand peer-join block catch-up (see `peer-join-backfill.ts`):
|
|
373
457
|
* when a strand's libp2p node connects to a peer this runtime has not yet
|
|
374
458
|
* caught up, every block in the strand's own raw store is pushed to that peer,
|
|
375
459
|
* so a machine that joined the strand after blocks were committed still ends
|
|
376
|
-
* up physically holding them. Applies to every strand this node starts
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
|
|
460
|
+
* up physically holding them. Applies to every strand this node starts.
|
|
461
|
+
* Omit for the defaults (`DEFAULT_PEER_JOIN_BACKFILL`); `{ enabled: false }`
|
|
462
|
+
* restores the no-backfill behaviour. The control network runs its own,
|
|
463
|
+
* separately tuned catch-up — see {@link controlBackfill}.
|
|
464
|
+
*/
|
|
465
|
+
strandBackfill?: PeerJoinBackfillConfig;
|
|
466
|
+
/**
|
|
467
|
+
* Tuning for the CONTROL network's peer-join block catch-up (same module,
|
|
468
|
+
* `peer-join-backfill.ts`, wired in `CadreNode.start`): when the control
|
|
469
|
+
* libp2p node connects to an AUTHORIZED party member this runtime has not yet
|
|
470
|
+
* caught up, every block in the control database's own raw store is pushed to
|
|
471
|
+
* it. This is what makes a control block committed while the writer was alone
|
|
472
|
+
* (collection headers written at genesis above all) physically reach members
|
|
473
|
+
* that joined later — without it, such a member that restarts offline reads
|
|
474
|
+
* the affected tables as empty. Pushes are gated on
|
|
475
|
+
* `CadreNode.isAuthorizedMember`, checked at push time, because the control
|
|
476
|
+
* network's inbound gate deliberately admits non-members in several states
|
|
477
|
+
* (seed delivery, enrollment, bootstrap/relay peers). Omit for the defaults
|
|
478
|
+
* (`DEFAULT_PEER_JOIN_BACKFILL`); `{ enabled: false }` disables it.
|
|
381
479
|
*/
|
|
382
|
-
|
|
480
|
+
controlBackfill?: PeerJoinBackfillConfig;
|
|
481
|
+
/**
|
|
482
|
+
* Tuning for the CLOSED-strand revoked-peer gate
|
|
483
|
+
* (`strand-revocation-enforcer.ts`), applied to every closed strand this node
|
|
484
|
+
* starts: each strand node materializes the peer ids bound to REMOVED members
|
|
485
|
+
* (orphaned `Strand.MemberPeer` rows) and refuses them at the stream and
|
|
486
|
+
* connection layers, so removing a party cuts the network to its machines
|
|
487
|
+
* rather than only deleting its row. Open strands never arm it. Omit for the
|
|
488
|
+
* defaults (`DEFAULT_REVOCATION_POLL_INTERVAL_MS` refresh cadence);
|
|
489
|
+
* `{ enabled: false }` restores the pre-existing behaviour.
|
|
490
|
+
*/
|
|
491
|
+
strandRevocationEnforcement?: StrandRevocationEnforcementConfig;
|
|
383
492
|
/** Hibernation configuration */
|
|
384
493
|
hibernation?: HibernationConfig;
|
|
385
494
|
/** Polling interval for strand watcher in ms (default: 5000) */
|
|
@@ -463,6 +572,39 @@ export interface CadreNodeConfig {
|
|
|
463
572
|
*/
|
|
464
573
|
store?: BootstrapPeerStore;
|
|
465
574
|
};
|
|
575
|
+
/**
|
|
576
|
+
* Node-local record of how many machines this party had enrolled the last time
|
|
577
|
+
* this node looked (see `enrolled-machine-store.ts`): the block-repair
|
|
578
|
+
* corroboration yardstick the CONTROL node declares when its libp2p node is
|
|
579
|
+
* built. Sibling of {@link trustedOwners} / {@link bootstrapPeers} — same
|
|
580
|
+
* NON-replicated, per-party, injected-backend shape — and, like
|
|
581
|
+
* {@link bootstrapPeers}, nothing here is trust-bearing: it is a repair hint,
|
|
582
|
+
* recomputed from `CadrePeer` rows the moment the control database is up.
|
|
583
|
+
*
|
|
584
|
+
* It exists only because the control node is created BEFORE that database, so
|
|
585
|
+
* the live count is unreadable at the one moment it is needed; remembering it
|
|
586
|
+
* across the restart is what breaks that deadlock. Absent ⇒ an in-memory store
|
|
587
|
+
* is created at start(), which cold-starts on every launch and therefore
|
|
588
|
+
* declares nothing — byte-for-byte the behaviour before this record existed.
|
|
589
|
+
* No embedder is forced to change.
|
|
590
|
+
*
|
|
591
|
+
* A party that grows applies the larger number on each node's NEXT launch,
|
|
592
|
+
* deliberately: Optimystic freezes the policy at node construction and offers
|
|
593
|
+
* no runtime setter, and a node still holding the old value runs at exactly
|
|
594
|
+
* today's behaviour, so there is no forced rebuild.
|
|
595
|
+
*
|
|
596
|
+
* A new platform needs no new store class: supply a `DurableSlot` for the
|
|
597
|
+
* platform's storage and inject `PersistentEnrolledMachineStore.open(slot, partyId)`.
|
|
598
|
+
*/
|
|
599
|
+
enrolledMachines?: {
|
|
600
|
+
/**
|
|
601
|
+
* Injected store instance — e.g. a `FileEnrolledMachineStore` from the
|
|
602
|
+
* Node-only subpath `@serfab/cadre-core/enrolled-machine-store-file`,
|
|
603
|
+
* persisted in the node's state directory. Its `partyId` must match
|
|
604
|
+
* `controlNetwork.partyId`; start() fails closed on a mismatch.
|
|
605
|
+
*/
|
|
606
|
+
store?: EnrolledMachineStore;
|
|
607
|
+
};
|
|
466
608
|
/**
|
|
467
609
|
* Platform push-delivery for suspended mobile peers. When present, the node's
|
|
468
610
|
* server fan-out can deliver strand-wake data messages over the platform push
|
|
@@ -555,6 +697,17 @@ export interface StrandRow {
|
|
|
555
697
|
Id: string;
|
|
556
698
|
MemberPrivateKey: string | null;
|
|
557
699
|
Type: 'o' | 'c';
|
|
700
|
+
/**
|
|
701
|
+
* ed25519 (base64url) owner key of the MACHINE that published this row — the
|
|
702
|
+
* signer of the owner-signed `Strand` insert (`== context.OwnerKey`, pinned by
|
|
703
|
+
* the schema's `AuthorizedInsert`). Identifies the founding machine, so a
|
|
704
|
+
* launch with no explicit `founder` flag derives "am I the founder?" by
|
|
705
|
+
* comparing it to the node's own owner key (see `CadreNode.launchStrand`).
|
|
706
|
+
* `null` on a consent-seated strand (the unsigned `FormationUsage` branch
|
|
707
|
+
* records no trustworthy signer). Provenance, not content —
|
|
708
|
+
* `strandRowMismatches` excludes it from the identical-content comparison.
|
|
709
|
+
*/
|
|
710
|
+
FounderOwnerKey: string | null;
|
|
558
711
|
}
|
|
559
712
|
/**
|
|
560
713
|
* sApp configuration provided by the hosting application when creating a strand.
|
|
@@ -592,11 +745,62 @@ export interface StrandConfig {
|
|
|
592
745
|
* paths; the same party that calls {@link CadreNode.publishStrand}). The founder
|
|
593
746
|
* runs the one-time membership bootstrap at bring-up (writes `Strand.Header`, and
|
|
594
747
|
* for a closed strand the founding `Member`+`Manager`). A joiner leaves this
|
|
595
|
-
*
|
|
596
|
-
*
|
|
748
|
+
* unset and writes nothing — it receives those rows via Optimystic sync.
|
|
749
|
+
*
|
|
750
|
+
* When unset, founder-ness is DERIVED from the row: this node founds iff
|
|
751
|
+
* {@link StrandRow.FounderOwnerKey} equals its own owner key. An explicit
|
|
752
|
+
* `true`/`false` wins over the derivation — the formation/responder flows pass
|
|
753
|
+
* it deliberately, since a consent-seated row carries a null column.
|
|
597
754
|
*/
|
|
598
755
|
founder?: boolean;
|
|
599
756
|
}
|
|
757
|
+
/**
|
|
758
|
+
* What {@link CadreNode.foundStrand} needs to publish a strand's control row AND start it
|
|
759
|
+
* locally as its founder — the resumable one-call form of `publishStrand` + `addStrand`.
|
|
760
|
+
*
|
|
761
|
+
* No `strandRow`: the row is the OUTPUT of founding, either freshly published or read back
|
|
762
|
+
* from an interrupted earlier attempt. `StrandConfig` (the join/attach shape) takes one
|
|
763
|
+
* because there the row already exists and came from the control network.
|
|
764
|
+
*/
|
|
765
|
+
export interface FoundStrandConfig {
|
|
766
|
+
/** Unique strand identifier. Trimmed; blank is rejected. */
|
|
767
|
+
strandId: string;
|
|
768
|
+
/** `'o'` for open (default) or `'c'` for closed. */
|
|
769
|
+
type?: 'o' | 'c';
|
|
770
|
+
/**
|
|
771
|
+
* Membership key gating a closed strand. Required in practice for `type: 'c'` (the
|
|
772
|
+
* founder bootstrap rejects a null key), and IGNORED when the strand is already
|
|
773
|
+
* published — the stored key wins, so read the resolved key back off the returned
|
|
774
|
+
* instance rather than trusting a freshly minted one.
|
|
775
|
+
*/
|
|
776
|
+
memberPrivateKey?: string;
|
|
777
|
+
/** sApp configuration the hosting application provides, as for {@link StrandConfig}. */
|
|
778
|
+
sAppConfig: SAppConfig;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* What {@link CadreNode.foundStrand} hands back: the running instance PLUS the `Strand` row
|
|
782
|
+
* the strand actually runs under.
|
|
783
|
+
*
|
|
784
|
+
* The row is returned separately rather than left for the caller to read off the instance
|
|
785
|
+
* because on a resumed founding it is NOT the caller's input — a closed strand adopts the
|
|
786
|
+
* stored `MemberPrivateKey`, discarding a freshly minted one. A caller that must carry the
|
|
787
|
+
* membership key onward (to mint an invitation, say) reads it from HERE.
|
|
788
|
+
*/
|
|
789
|
+
export interface FoundStrandResult {
|
|
790
|
+
/** The active local instance, as {@link CadreNode.addStrand} would have returned. */
|
|
791
|
+
instance: StrandInstance;
|
|
792
|
+
/** The live control-plane row: freshly published, or the one already there. */
|
|
793
|
+
strandRow: StrandRow;
|
|
794
|
+
/**
|
|
795
|
+
* Whether THIS machine actually founded (ran / will have run the one-time
|
|
796
|
+
* founder bootstrap). `false` when the resolved row was published by a
|
|
797
|
+
* DIFFERENT machine's owner key — e.g. a sibling won a concurrent founding
|
|
798
|
+
* race — in which case the call attached as a joiner instead, which is the
|
|
799
|
+
* correct outcome (two machines bootstrapping one strand on separate replicas
|
|
800
|
+
* is the double-`Header` hazard).
|
|
801
|
+
*/
|
|
802
|
+
founded: boolean;
|
|
803
|
+
}
|
|
600
804
|
/**
|
|
601
805
|
* Result of creating a new cadre peer
|
|
602
806
|
*/
|
|
@@ -654,6 +858,21 @@ export interface FormStrandResult {
|
|
|
654
858
|
* from {@link invitePrivateKey} (the initiator's own generated signing key).
|
|
655
859
|
*/
|
|
656
860
|
memberPrivateKey?: string;
|
|
861
|
+
/**
|
|
862
|
+
* The responder's live STRAND-network multiaddrs for the formed strand — the only
|
|
863
|
+
* cross-party discovery seed there is, since the strand-addr RPC that resolves a
|
|
864
|
+
* sibling's strand addresses answers own-party callers only.
|
|
865
|
+
*
|
|
866
|
+
* Always an array, frequently EMPTY: the responder discloses none when it holds no
|
|
867
|
+
* live strand node for the strand it just provisioned (the responder-provisions path
|
|
868
|
+
* mints a strand that has not launched yet). `CadreNode.formStrand` records these
|
|
869
|
+
* itself, so an embedding app only needs them to seed a strand it launches OUTSIDE
|
|
870
|
+
* that node.
|
|
871
|
+
*
|
|
872
|
+
* One-shot and in-memory: they are the responder's addresses at the moment of
|
|
873
|
+
* formation and are never re-resolved — see `docs/strands.md`.
|
|
874
|
+
*/
|
|
875
|
+
strandAddrs: string[];
|
|
657
876
|
}
|
|
658
877
|
/**
|
|
659
878
|
* Disclosure object provided during strand formation.
|
|
@@ -706,6 +925,26 @@ export interface CadreNodeEvents {
|
|
|
706
925
|
'strand:waking': {
|
|
707
926
|
strandId: string;
|
|
708
927
|
};
|
|
928
|
+
/**
|
|
929
|
+
* Emitted when this node discovers it is no longer a member of a CLOSED
|
|
930
|
+
* strand — its party was removed by a manager, or it left. Detected by the
|
|
931
|
+
* strand's revoked-peer gate (`strand-revocation-enforcer.ts`) seeing this
|
|
932
|
+
* node's own peer id in the revoked set, so it is BEST-EFFORT: it fires only
|
|
933
|
+
* if the removal replicated here before the rest of the strand cut us off,
|
|
934
|
+
* and a node that was already offline may never see it at all.
|
|
935
|
+
*
|
|
936
|
+
* Nothing is stopped or torn down for you. Treat it as "this strand will no
|
|
937
|
+
* longer sync": remaining members refuse this node's streams, dials, and
|
|
938
|
+
* connections, so reads keep working against whatever is stored locally while
|
|
939
|
+
* writes stop propagating. Stopping the strand (`stopStrand`) or deleting its
|
|
940
|
+
* data is the app's decision. Emitted once per removal — re-armed if a manager
|
|
941
|
+
* re-admits this party, so a second removal is reported again — and a
|
|
942
|
+
* hibernation wake rebuilds the gate, which may re-emit for a strand still
|
|
943
|
+
* revoked.
|
|
944
|
+
*/
|
|
945
|
+
'strand:revoked': {
|
|
946
|
+
strandId: string;
|
|
947
|
+
};
|
|
709
948
|
/**
|
|
710
949
|
* Emitted when the control network advertises a strand this node has no
|
|
711
950
|
* registered `sAppConfig` for — i.e. a strand created by another member. The
|
package/dist/types.js
CHANGED
|
@@ -51,6 +51,12 @@ export const DEFAULT_CHECKIN_WINDOW_MS = 15 * 1000;
|
|
|
51
51
|
* by omission); the strand breadth defaults to
|
|
52
52
|
* {@link CadreNodeConfig.strandClusterSize} while its policy is the fixed
|
|
53
53
|
* {@link STRAND_CLUSTER_POLICY}.
|
|
54
|
+
*
|
|
55
|
+
* The two `*ClusterPolicy` BUILDERS are the same objects with the block-repair
|
|
56
|
+
* corroboration yardstick declared from the machines enrolled in this party
|
|
57
|
+
* ({@link resolveRepairYardstick}); handed no count, each returns its frozen base
|
|
58
|
+
* constant unchanged. A network picks the derived number up when its libp2p node is
|
|
59
|
+
* built, which for a strand is every wake from hibernation.
|
|
54
60
|
*/
|
|
55
|
-
export { MIN_CLUSTER_SIZE, CONTROL_REPLICATION_BREADTH, CONTROL_CLUSTER_POLICY, DEFAULT_STRAND_CLUSTER_SIZE, STRAND_CLUSTER_POLICY, resolveStrandClusterSize } from '@serfab/quereus-plugin-sereus';
|
|
61
|
+
export { MIN_CLUSTER_SIZE, CONTROL_REPLICATION_BREADTH, CONTROL_CLUSTER_POLICY, DEFAULT_STRAND_CLUSTER_SIZE, STRAND_CLUSTER_POLICY, resolveStrandClusterSize, resolveRepairYardstick, controlClusterPolicy, strandClusterPolicy } from '@serfab/quereus-plugin-sereus';
|
|
56
62
|
//# sourceMappingURL=types.js.map
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA6EA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAA6C;IAC5E,QAAQ,EAAE;QACR,WAAW,EAAE,QAAQ,EAAS,aAAa;QAC3C,gBAAgB,EAAE,QAAQ,EAAI,kBAAkB;QAChD,eAAe,EAAE,QAAQ,EAAK,MAAM;QACpC,oBAAoB,EAAE,CAAC;QACvB,kBAAkB,EAAE,QAAQ,CAAE,MAAM;KACrC;IACD,WAAW,EAAE;QACX,WAAW,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAI,YAAY;QAC1C,gBAAgB,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,wBAAwB;QAC1D,eAAe,EAAE,EAAE,GAAG,IAAI,EAAI,mBAAmB;QACjD,oBAAoB,EAAE,CAAC;QACvB,kBAAkB,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,eAAe;KACnD;IACD,UAAU,EAAE;QACV,WAAW,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAI,WAAW;QACzC,gBAAgB,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAG,uBAAuB;QACzD,eAAe,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAI,kBAAkB;QACpD,oBAAoB,EAAE,CAAC;QACvB,kBAAkB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,gBAAgB;KACxD;IACD,OAAO,EAAE;QACP,WAAW,EAAE,EAAE,GAAG,IAAI,EAAQ,aAAa;QAC3C,gBAAgB,EAAE,EAAE,GAAG,IAAI,EAAG,wBAAwB;QACtD,eAAe,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAG,eAAe;QACjD,oBAAoB,EAAE,CAAC;QACvB,kBAAkB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,eAAe;KAC5D;CACF,CAAC;AA2RF;;;GAGG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,GAAG,IAAI,CAAC;AAYnD;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EACL,gBAAgB,EAChB,2BAA2B,EAC3B,sBAAsB,EACtB,2BAA2B,EAC3B,qBAAqB,EACrB,wBAAwB,EACxB,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,+BAA+B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serfab/cadre-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core library for Sereus cadre nodes - manages control networks and strand participation",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"types": "./dist/bootstrap-peer-store-file.d.ts",
|
|
33
33
|
"import": "./dist/bootstrap-peer-store-file.js"
|
|
34
34
|
},
|
|
35
|
+
"./enrolled-machine-store-file": {
|
|
36
|
+
"types": "./dist/enrolled-machine-store-file.d.ts",
|
|
37
|
+
"import": "./dist/enrolled-machine-store-file.js"
|
|
38
|
+
},
|
|
35
39
|
"./push-node": {
|
|
36
40
|
"types": "./dist/push-node.d.ts",
|
|
37
41
|
"import": "./dist/push-node.js"
|
|
@@ -64,13 +68,17 @@
|
|
|
64
68
|
"@chainsafe/libp2p-yamux": "^8.0.1",
|
|
65
69
|
"@libp2p/circuit-relay-v2": "^4.1.3",
|
|
66
70
|
"@libp2p/identify": "4.0.10",
|
|
71
|
+
"@libp2p/logger": "^6.2.2",
|
|
72
|
+
"@libp2p/peer-store": "^12.0.10",
|
|
67
73
|
"@libp2p/tcp": "^11.0.10",
|
|
68
74
|
"@libp2p/webrtc": "6.0.14",
|
|
69
75
|
"@libp2p/websockets": "^10.1.3",
|
|
70
|
-
"@optimystic/db-p2p-storage-fs": "^0.
|
|
76
|
+
"@optimystic/db-p2p-storage-fs": "^1.0.0-beta.2",
|
|
71
77
|
"@types/debug": "^4.1.12",
|
|
72
78
|
"@types/node": "^25.0.10",
|
|
73
79
|
"@vitest/coverage-v8": "^4.0.17",
|
|
80
|
+
"datastore-core": "^11.0.1",
|
|
81
|
+
"main-event": "^1.0.1",
|
|
74
82
|
"rimraf": "^6.1.2",
|
|
75
83
|
"typescript": "^5.9.3",
|
|
76
84
|
"vitest": "^4.0.17"
|
|
@@ -80,12 +88,12 @@
|
|
|
80
88
|
"@libp2p/interface": "^3.1.0",
|
|
81
89
|
"@libp2p/peer-id": "^6.0.4",
|
|
82
90
|
"@multiformats/multiaddr": "^12.5.1",
|
|
83
|
-
"@optimystic/db-core": "^0.
|
|
84
|
-
"@optimystic/db-p2p": "^0.
|
|
85
|
-
"@optimystic/quereus-plugin-crypto": "^0.
|
|
86
|
-
"@optimystic/quereus-plugin-optimystic": "^0.
|
|
87
|
-
"@quereus/quereus": "^4.
|
|
88
|
-
"@serfab/quereus-plugin-sereus": "^0.
|
|
91
|
+
"@optimystic/db-core": "^1.0.0-beta.2",
|
|
92
|
+
"@optimystic/db-p2p": "^1.0.0-beta.2",
|
|
93
|
+
"@optimystic/quereus-plugin-crypto": "^1.0.0-beta.2",
|
|
94
|
+
"@optimystic/quereus-plugin-optimystic": "^1.0.0-beta.2",
|
|
95
|
+
"@quereus/quereus": "^4.19.0",
|
|
96
|
+
"@serfab/quereus-plugin-sereus": "^0.13.0",
|
|
89
97
|
"debug": "^4.4.3",
|
|
90
98
|
"libp2p": "^3.1.3",
|
|
91
99
|
"uint8arrays": "^5.1.0"
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
|
|
29
|
+
import { multiaddr, type Multiaddr } from '@multiformats/multiaddr';
|
|
30
|
+
import type { NetworkConfig } from './types.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The announce fields to spread into `createLibp2pNode` options. A field is present
|
|
34
|
+
* only when the operator configured a non-empty list for it, so a node that configures
|
|
35
|
+
* neither passes neither.
|
|
36
|
+
*/
|
|
37
|
+
export interface ResolvedAnnounceAddrs {
|
|
38
|
+
announceAddrs?: string[];
|
|
39
|
+
appendAnnounceAddrs?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve both announce fields off a `NetworkConfig`. Throws — naming the offending
|
|
44
|
+
* config field and entry — when any entry is not a parsable multiaddr.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveAnnounceAddrs(network: NetworkConfig | undefined): ResolvedAnnounceAddrs {
|
|
47
|
+
const announceAddrs = configuredAddrs('network.announceAddrs', network?.announceAddrs);
|
|
48
|
+
const appendAnnounceAddrs = configuredAddrs('network.appendAnnounceAddrs', network?.appendAnnounceAddrs);
|
|
49
|
+
return {
|
|
50
|
+
...(announceAddrs && { announceAddrs }),
|
|
51
|
+
...(appendAnnounceAddrs && { appendAnnounceAddrs })
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether this node's advertised set will be REPLACED rather than extended — i.e.
|
|
57
|
+
* whether {@link ResolvedAnnounceAddrs.announceAddrs} is in play at all. The single
|
|
58
|
+
* predicate behind `CadreNode.start()`'s operator warning, kept here so the warning
|
|
59
|
+
* and the forwarding agree on what "set" means (non-empty, not merely present).
|
|
60
|
+
*/
|
|
61
|
+
export function replacesAdvertisedAddrs(network: NetworkConfig | undefined): boolean {
|
|
62
|
+
return (network?.announceAddrs?.length ?? 0) > 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The entries to forward for one announce field, or `undefined` when it is unset or empty. */
|
|
66
|
+
function configuredAddrs(field: string, addrs: readonly string[] | undefined): string[] | undefined {
|
|
67
|
+
if (!addrs || addrs.length === 0) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return addrs.map((addr) => validated(field, addr));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** `addr` unchanged, having proved it names a real address — config field named on failure. */
|
|
74
|
+
function validated(field: string, addr: string): string {
|
|
75
|
+
if (parsed(field, addr).getComponents().length === 0) {
|
|
76
|
+
// `''` and `'/'` PARSE: both yield a component-less multiaddr that stringifies to
|
|
77
|
+
// `/`. Forwarded, they make an announce set that is non-empty but names nothing —
|
|
78
|
+
// which by the replacement rule above costs the node every address it would
|
|
79
|
+
// otherwise advertise, silently. A templated `cadre.yaml` whose address variable went
|
|
80
|
+
// unsubstituted is the realistic way in, so it is rejected rather than forwarded.
|
|
81
|
+
throw new Error(`${field} entry names no address: ${JSON.stringify(addr)}`);
|
|
82
|
+
}
|
|
83
|
+
return addr;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** `addr` as a multiaddr, with the config field named on a parse failure. */
|
|
87
|
+
function parsed(field: string, addr: string): Multiaddr {
|
|
88
|
+
try {
|
|
89
|
+
return multiaddr(addr);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
throw new Error(`${field} entry is not a valid multiaddr: ${addr}`, { cause: err });
|
|
92
|
+
}
|
|
93
|
+
}
|