@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.
- package/README.md +23 -8
- package/dist/bootstrap-peer-store-file.js +2 -2
- package/dist/bootstrap-peer-store.d.ts +32 -12
- package/dist/bootstrap-peer-store.js +41 -15
- package/dist/bootstrap-peer-store.js.map +1 -1
- package/dist/cadre-node.d.ts +883 -102
- package/dist/cadre-node.js +1800 -209
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +2 -2
- package/dist/control-authorization.js +1 -0
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +13 -26
- package/dist/control-cohort.js +0 -26
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +318 -44
- package/dist/control-database.js +516 -74
- package/dist/control-database.js.map +1 -1
- package/dist/control-read-retry.d.ts +40 -11
- package/dist/control-read-retry.js +48 -15
- package/dist/control-read-retry.js.map +1 -1
- package/dist/control-retry.d.ts +58 -3
- package/dist/control-retry.js +58 -3
- package/dist/control-retry.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +133 -21
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +80 -11
- package/dist/control-write-retry.js +168 -29
- package/dist/control-write-retry.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 +17 -8
- package/dist/index.js +54 -9
- package/dist/index.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +15 -1
- package/dist/membership-connection-gater.js +9 -3
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +21 -7
- package/dist/node-local-snapshot.js +30 -9
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/peer-dial.d.ts +143 -0
- package/dist/peer-dial.js +176 -0
- package/dist/peer-dial.js.map +1 -0
- package/dist/{strand-backfill.d.ts → peer-join-backfill.d.ts} +49 -20
- package/dist/{strand-backfill.js → peer-join-backfill.js} +133 -52
- package/dist/peer-join-backfill.js.map +1 -0
- package/dist/relay-addrs.d.ts +160 -49
- package/dist/relay-addrs.js +287 -60
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +71 -3
- package/dist/relay-reservation.js +116 -8
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +30 -1
- package/dist/seed-bootstrap.js +62 -17
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/storage-scope.d.ts +62 -0
- package/dist/storage-scope.js +75 -0
- package/dist/storage-scope.js.map +1 -0
- package/dist/strand-database.d.ts +41 -11
- package/dist/strand-database.js +48 -18
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +152 -0
- package/dist/strand-first-sync-gate.js +208 -0
- package/dist/strand-first-sync-gate.js.map +1 -0
- package/dist/strand-formation-manager.d.ts +99 -5
- package/dist/strand-formation-manager.js +119 -8
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +97 -3
- package/dist/strand-formation-protocol.js +108 -2
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +389 -19
- package/dist/strand-instance-manager.js +678 -65
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-key.d.ts +20 -12
- package/dist/strand-member-key.js +20 -12
- package/dist/strand-member-key.js.map +1 -1
- package/dist/strand-member-registry.js +3 -0
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +361 -0
- package/dist/strand-membership-reconciler.js +548 -0
- package/dist/strand-membership-reconciler.js.map +1 -0
- package/dist/strand-membership-writer.d.ts +143 -11
- package/dist/strand-membership-writer.js +419 -135
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-network-config.d.ts +116 -0
- package/dist/strand-network-config.js +192 -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 +17 -1
- package/dist/strand-solicitation.js +13 -2
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +11 -7
- package/dist/strand-wake-protocol.js +20 -52
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/strand-watcher.js +12 -1
- package/dist/strand-watcher.js.map +1 -1
- package/dist/timed-step.d.ts +12 -0
- package/dist/timed-step.js +28 -0
- package/dist/timed-step.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +19 -0
- package/dist/timeout-scheduler.js +22 -0
- package/dist/timeout-scheduler.js.map +1 -0
- package/dist/types.d.ts +472 -46
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/package.json +16 -8
- package/src/bootstrap-peer-store-file.ts +2 -2
- package/src/bootstrap-peer-store.ts +69 -27
- package/src/cadre-node.ts +1926 -228
- package/src/control-authorization.ts +2 -1
- package/src/control-cohort.ts +122 -130
- package/src/control-database.ts +586 -74
- package/src/control-read-retry.ts +202 -167
- package/src/control-retry.ts +297 -190
- package/src/control-schema.ts +133 -21
- package/src/control-write-retry.ts +525 -383
- 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 +134 -15
- package/src/membership-connection-gater.ts +9 -3
- package/src/node-local-snapshot.ts +38 -9
- package/src/peer-addr-book.ts +251 -250
- package/src/peer-dial.ts +215 -0
- package/src/{strand-backfill.ts → peer-join-backfill.ts} +478 -389
- package/src/relay-addrs.ts +458 -218
- package/src/relay-reservation.ts +951 -828
- package/src/seed-bootstrap.ts +76 -18
- package/src/storage-scope.ts +78 -0
- package/src/strand-database.ts +61 -21
- package/src/strand-first-sync-gate.ts +258 -0
- package/src/strand-formation-manager.ts +162 -8
- package/src/strand-formation-protocol.ts +850 -692
- package/src/strand-instance-manager.ts +877 -74
- package/src/strand-member-key.ts +20 -12
- package/src/strand-member-registry.ts +3 -0
- package/src/strand-membership-reconciler.ts +680 -0
- package/src/strand-membership-writer.ts +550 -190
- package/src/strand-network-config.ts +250 -0
- package/src/strand-revocation-enforcer.ts +653 -0
- package/src/strand-solicitation.ts +499 -471
- package/src/strand-wake-protocol.ts +21 -66
- package/src/strand-watcher.ts +325 -314
- package/src/timed-step.ts +28 -0
- package/src/timeout-scheduler.ts +28 -0
- package/src/types.ts +487 -52
- package/dist/strand-backfill.js.map +0 -1
package/src/types.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
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';
|
|
6
|
+
import type { StrandMembershipReconciliationConfig } from './strand-membership-reconciler.js';
|
|
7
|
+
import type { StrandFirstSyncConfig } from './strand-first-sync-gate.js';
|
|
5
8
|
import type { StrandDatabase } from './strand-database.js';
|
|
6
9
|
import type { SeedTrustPolicy } from './seed-trust-policy.js';
|
|
7
10
|
import type { KeyStore, KeyId } from './key-store.js';
|
|
8
11
|
import type { TrustedOwnerStore, TrustSource } from './trusted-owner-store.js';
|
|
9
12
|
import type { BootstrapPeerStore } from './bootstrap-peer-store.js';
|
|
13
|
+
import type { EnrolledMachineStore } from './enrolled-machine-store.js';
|
|
10
14
|
import type { PushNotifier } from './push-notifier.js';
|
|
11
15
|
import type { RevocableTable } from './control-authorization.js';
|
|
16
|
+
import type { ControlRetryAbandonment } from './control-retry.js';
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* Extended Libp2p node with the coordinatedRepo attached by db-p2p's
|
|
@@ -21,7 +26,7 @@ export interface Libp2pNodeWithRepo extends Libp2p {
|
|
|
21
26
|
* `coordinatedRepo` (`libp2p-node-base.ts`). Optional deliberately: the base
|
|
22
27
|
* factory always assigns it, but it is not part of the upstream node type, so
|
|
23
28
|
* declaring it required would be a claim this repo cannot enforce. The strand
|
|
24
|
-
* backfill (`
|
|
29
|
+
* backfill (`peer-join-backfill.ts`) logs once and stays inert when it is absent.
|
|
25
30
|
*/
|
|
26
31
|
keyNetwork?: IPeerNetwork;
|
|
27
32
|
}
|
|
@@ -113,12 +118,27 @@ export const HIBERNATION_TIMEOUTS: Record<LatencyHint, HibernationTimeouts> = {
|
|
|
113
118
|
|
|
114
119
|
/**
|
|
115
120
|
* Storage provider - either an `IRawStorage` instance or a factory function.
|
|
116
|
-
* Factory functions are useful for creating
|
|
121
|
+
* Factory functions are useful for creating per-scope storage instances.
|
|
117
122
|
*
|
|
118
|
-
* **
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
123
|
+
* **The argument is an opaque scope key.** Use it directly as a file name, directory
|
|
124
|
+
* name or database name: every key cadre-core mints stays within `[A-Za-z0-9._-]`, so
|
|
125
|
+
* no escaping is needed and none should be assumed. Do not parse it;
|
|
126
|
+
* `controlStorageScope` / `isControlStorageScope` (`storage-scope.ts`) are the
|
|
127
|
+
* supported way to mint and recognize the control key. The strand arm of that charset
|
|
128
|
+
* rule is intended but not yet checked — a strand id replicated in from another node
|
|
129
|
+
* in the party reaches this callback verbatim; see
|
|
130
|
+
* `tickets/backlog/bug-strand-scope-key-charset-unenforced`.
|
|
131
|
+
*
|
|
132
|
+
* **Called once per scope per runtime lifetime.** The scopes are the control database
|
|
133
|
+
* (once per `CadreNode.start()`) and each strand id (once per `startStrand`).
|
|
134
|
+
* Hibernation — `quiesceStrand` then `resumeStrand` — reuses the store already
|
|
135
|
+
* resolved for that strand and does NOT re-enter this callback.
|
|
136
|
+
*
|
|
137
|
+
* **The control scope is party-specific.** It is `controlStorageScope(partyId)`, not a
|
|
138
|
+
* fixed string, because the control database holds one party's own records — its
|
|
139
|
+
* strands, owner keys, peers, invitations, revocations. Two parties on one device ask
|
|
140
|
+
* for two different keys and MUST get two different stores, or a node started for one
|
|
141
|
+
* party reads the other's rows as its own.
|
|
122
142
|
*
|
|
123
143
|
* It IS re-entered for a scope after that scope's runtime has stopped
|
|
124
144
|
* (`stopStrand`, or a `stop()` then `start()` cycle on one `CadreNode`), so it must
|
|
@@ -128,14 +148,16 @@ export const HIBERNATION_TIMEOUTS: Record<LatencyHint, HibernationTimeouts> = {
|
|
|
128
148
|
* cadre-core disposes only its own cache wrapper — it never closes the store you
|
|
129
149
|
* returned. Closing the underlying handle stays the embedder's job.
|
|
130
150
|
*
|
|
131
|
-
*
|
|
151
|
+
* **The single-instance form shares data across scopes by construction** — one store
|
|
152
|
+
* for every strand AND for every party's control database. An embedder that can serve
|
|
153
|
+
* more than one party must use the factory form. One instance handed to every scope
|
|
132
154
|
* also shares ONE cache wrapper (`wrapStorageWithCache` memoizes per inner instance),
|
|
133
155
|
* which counts its holders: a scope stopping releases only its own claim, and the
|
|
134
|
-
* wrapper is retired when the last scope releases it. The factory form is
|
|
135
|
-
*
|
|
156
|
+
* wrapper is retired when the last scope releases it. The factory form is the better
|
|
157
|
+
* default for the further reason that it partitions each strand's data, which
|
|
136
158
|
* simplifies cleanup.
|
|
137
159
|
*/
|
|
138
|
-
export type RawStorageProvider = IRawStorage | ((
|
|
160
|
+
export type RawStorageProvider = IRawStorage | ((scope: string) => IRawStorage);
|
|
139
161
|
|
|
140
162
|
/**
|
|
141
163
|
* Storage configuration for storage profile nodes
|
|
@@ -143,19 +165,24 @@ export type RawStorageProvider = IRawStorage | ((strandId: string) => IRawStorag
|
|
|
143
165
|
export interface StorageConfig {
|
|
144
166
|
/**
|
|
145
167
|
* Storage provider - either an IRawStorage instance or a factory function.
|
|
146
|
-
* See {@link RawStorageProvider} for how often cadre-core calls it
|
|
168
|
+
* See {@link RawStorageProvider} for how often cadre-core calls it, and for why
|
|
169
|
+
* the single-instance form cannot serve more than one party.
|
|
170
|
+
*
|
|
171
|
+
* The `scope` a factory receives is an opaque key already safe as a path or
|
|
172
|
+
* database-name segment (always within `[A-Za-z0-9._-]`), so each example below
|
|
173
|
+
* interpolates it directly.
|
|
147
174
|
*
|
|
148
175
|
* For Node.js environments, use FileRawStorage from @optimystic/db-p2p-storage-fs:
|
|
149
176
|
* ```typescript
|
|
150
177
|
* import { FileRawStorage } from '@optimystic/db-p2p-storage-fs';
|
|
151
|
-
* storage: { provider: (
|
|
178
|
+
* storage: { provider: (scope) => new FileRawStorage(`./data/${scope}`) }
|
|
152
179
|
* ```
|
|
153
180
|
*
|
|
154
181
|
* For React Native, use the appropriate storage from @optimystic/db-p2p-storage-rn:
|
|
155
182
|
* ```typescript
|
|
156
183
|
* import { LevelDBRawStorage } from '@optimystic/db-p2p-storage-rn';
|
|
157
184
|
* // `db` is an open LevelDB handle (see `openOptimysticRNDb` over `rn-leveldb`)
|
|
158
|
-
* storage: { provider: (
|
|
185
|
+
* storage: { provider: (scope) => new LevelDBRawStorage(openDb(`sereus-${scope}`)) }
|
|
159
186
|
* ```
|
|
160
187
|
*
|
|
161
188
|
* For in-memory storage (testing):
|
|
@@ -172,13 +199,36 @@ export interface StorageConfig {
|
|
|
172
199
|
* Network configuration for libp2p
|
|
173
200
|
*/
|
|
174
201
|
export interface NetworkConfig {
|
|
202
|
+
/**
|
|
203
|
+
* Addresses this machine's libp2p nodes bind. Omitted, `@optimystic/db-p2p`'s own
|
|
204
|
+
* default (`/ip4/0.0.0.0/tcp/0`) applies; `[]` means "cannot listen" and is honored
|
|
205
|
+
* as written (React Native hosts rely on that).
|
|
206
|
+
*
|
|
207
|
+
* The machine runs one control node plus one node per strand, so a FIXED port here
|
|
208
|
+
* can only belong to one of them. The control node binds it as configured; each
|
|
209
|
+
* strand node binds the same entry with its port rewritten to `0`, keeping the
|
|
210
|
+
* interface and transport (`/ws`, a specific-interface bind). A
|
|
211
|
+
* `<relay>/p2p-circuit` entry is untouched — the port inside it is the relay's, not
|
|
212
|
+
* a local bind. See `strand-network-config.ts`.
|
|
213
|
+
*
|
|
214
|
+
* **Only TCP, WebSocket (`/ws`, `/wss`) and circuit-relay addresses are bindable**
|
|
215
|
+
* unless {@link transports} supplies the factories for something else. A `/ws` entry
|
|
216
|
+
* switches the WebSocket transport on by itself; anything else — `/quic-v1`,
|
|
217
|
+
* `/webrtc`, `/webtransport` — refuses node start naming the address and the libp2p
|
|
218
|
+
* package it would need, rather than being dropped in silence by libp2p's transport
|
|
219
|
+
* manager. See `relay-addrs.ts` → `resolveTransportOptions`.
|
|
220
|
+
*/
|
|
175
221
|
listenAddrs?: string[];
|
|
176
222
|
/**
|
|
177
223
|
* Addresses to advertise to peers **instead of** `listenAddrs` — for a node behind
|
|
178
224
|
* NAT or a reverse proxy that is reachable at a different address than it binds
|
|
179
225
|
* (e.g. `mynode.example.com:4001` in front of a `0.0.0.0:4001` listener). Reaches
|
|
180
|
-
* libp2p's `addresses.announce` via `@optimystic/db-p2p`'s `NodeOptions.announceAddrs
|
|
181
|
-
*
|
|
226
|
+
* libp2p's `addresses.announce` via `@optimystic/db-p2p`'s `NodeOptions.announceAddrs`.
|
|
227
|
+
*
|
|
228
|
+
* **Applies to this machine's CONTROL node only.** Strand nodes drop it: any concrete
|
|
229
|
+
* entry names a port, and that port is the control node's, so a strand node
|
|
230
|
+
* advertising it would send peers to the wrong node (`strand-network-config.ts`).
|
|
231
|
+
* Entries are still validated at node start whichever node ends up using them.
|
|
182
232
|
*
|
|
183
233
|
* **A non-empty value REPLACES the advertised set entirely.** Observed addresses,
|
|
184
234
|
* and the `/p2p-circuit` address earned from a {@link relayAddrs} reservation, are
|
|
@@ -209,6 +259,8 @@ export interface NetworkConfig {
|
|
|
209
259
|
* advertise. Reaches libp2p's `addresses.appendAnnounce` via
|
|
210
260
|
* `@optimystic/db-p2p`'s `NodeOptions.appendAnnounceAddrs`.
|
|
211
261
|
*
|
|
262
|
+
* **Control node only**, on the same terms as {@link announceAddrs}.
|
|
263
|
+
*
|
|
212
264
|
* **Ignored while {@link announceAddrs} is non-empty** — that is libp2p's own
|
|
213
265
|
* precedence, applied upstream; this repo does not merge the two locally. Set one
|
|
214
266
|
* or the other, not both.
|
|
@@ -229,8 +281,9 @@ export interface NetworkConfig {
|
|
|
229
281
|
* because a listener that dials its relay from inside `libp2p.start()` put a
|
|
230
282
|
* sibling in this node's cohort before its own database existed, and a sibling
|
|
231
283
|
* that had not yet replicated this node's membership row refused the bring-up.
|
|
232
|
-
* STRAND nodes
|
|
233
|
-
*
|
|
284
|
+
* STRAND nodes take the same search shape, one bare listener plus one reservation
|
|
285
|
+
* supervisor PER relay, fail-soft (`strand-network-config.ts`,
|
|
286
|
+
* `strand-instance-manager.ts`). A relay named here also becomes a
|
|
234
287
|
* delegate-announce target, so this node's strand nodes may reserve on it too
|
|
235
288
|
* (see `delegate-admission.ts`).
|
|
236
289
|
*
|
|
@@ -240,11 +293,46 @@ export interface NetworkConfig {
|
|
|
240
293
|
* must not acquire a listener by accident.
|
|
241
294
|
*
|
|
242
295
|
* FAIL-FAST both ways: a malformed entry throws at node start rather than being
|
|
243
|
-
* silently dropped, and
|
|
244
|
-
* `/p2p-circuit` address throws
|
|
245
|
-
* The fail-soft posture over
|
|
296
|
+
* silently dropped, and — while {@link requireRelay} stays at its default —
|
|
297
|
+
* a first reservation attempt that lands no `/p2p-circuit` address throws
|
|
298
|
+
* `RelayReservationFailedError` out of `start()`. The fail-soft posture over
|
|
299
|
+
* the same machinery is `CadreNode.reserveRelays()`.
|
|
246
300
|
*/
|
|
247
301
|
relayAddrs?: string[];
|
|
302
|
+
/**
|
|
303
|
+
* Must a relay named in {@link relayAddrs} have granted a reservation before
|
|
304
|
+
* `start()` is allowed to succeed? Default `true`: an operator who names a
|
|
305
|
+
* relay is telling this machine it has no other reachability, so a relay
|
|
306
|
+
* that will not have us on the first attempt should stop the boot loudly
|
|
307
|
+
* rather than leave a node nobody can reach.
|
|
308
|
+
*
|
|
309
|
+
* Set `false` for a node that must still boot with no network — a phone or a
|
|
310
|
+
* browser tab. The first attempt is still driven at the same point in
|
|
311
|
+
* `start()`; a lost reservation just logs instead of throwing, and the
|
|
312
|
+
* retry supervisor keeps trying in the background exactly as it does after a
|
|
313
|
+
* `CadreNode.reserveRelays()` call. A caller on this posture cannot infer
|
|
314
|
+
* dialability from `start()` resolving — read
|
|
315
|
+
* {@link CadreNode.getRelayReservationState} to find out.
|
|
316
|
+
*
|
|
317
|
+
* It buys a node that BOOTS, not a node that boots fast: `start()` still waits
|
|
318
|
+
* out that first attempt, which costs the drive's whole timeout
|
|
319
|
+
* (`DEFAULT_RELAY_RESERVE_TIMEOUT_MS`, 10 s) against a relay that is unreachable
|
|
320
|
+
* rather than merely refusing (`relay-reservation.ts` polls to the deadline, in
|
|
321
|
+
* case libp2p's own discovery lands a reservation independently).
|
|
322
|
+
*
|
|
323
|
+
* Softens only the RESERVATION half of {@link relayAddrs}'s fail-fast
|
|
324
|
+
* contract. A malformed `relayAddrs` entry still throws at config
|
|
325
|
+
* resolution regardless of this setting — a typo is an operator error
|
|
326
|
+
* whatever the posture — and a hand-written `<relay>/p2p-circuit` entry in
|
|
327
|
+
* {@link listenAddrs} is still rejected on the control node, since that
|
|
328
|
+
* rejection is about the listener shape, not about whether reachability is
|
|
329
|
+
* required.
|
|
330
|
+
*
|
|
331
|
+
* Has no effect on STRAND nodes, which are already fail-soft over
|
|
332
|
+
* `relayAddrs` regardless of this field (`strand-network-config.ts`,
|
|
333
|
+
* `strand-instance-manager.ts`).
|
|
334
|
+
*/
|
|
335
|
+
requireRelay?: boolean;
|
|
248
336
|
/**
|
|
249
337
|
* Enable circuit relay server - allows this node to relay connections for other peers.
|
|
250
338
|
* When undefined, defaults to true for storage profile nodes (they typically have
|
|
@@ -302,8 +390,14 @@ export interface NetworkConfig {
|
|
|
302
390
|
* On the CONTROL node this gater is composed with the built-in membership
|
|
303
391
|
* admission gate (`membership-connection-gater.ts`): every hook supplied here
|
|
304
392
|
* is honored unchanged, and on inbound encrypted connections a deny from
|
|
305
|
-
* either this gater or the membership policy denies.
|
|
306
|
-
*
|
|
393
|
+
* either this gater or the membership policy denies.
|
|
394
|
+
*
|
|
395
|
+
* Strand cohort nodes: an OPEN strand's node receives this gater as-is (its
|
|
396
|
+
* peers are legitimately cross-party). A CLOSED strand's node composes
|
|
397
|
+
* revoked-peer denial onto it (`strand-revocation-enforcer.ts`) — every hook
|
|
398
|
+
* supplied here is still honored unchanged, and on the composed hooks
|
|
399
|
+
* (inbound encrypted connection, outbound peer dial, relay reservation) a
|
|
400
|
+
* deny from either this gater or the revocation check denies.
|
|
307
401
|
*/
|
|
308
402
|
connectionGater?: ConnectionGater;
|
|
309
403
|
/**
|
|
@@ -313,7 +407,8 @@ export interface NetworkConfig {
|
|
|
313
407
|
* Every field is optional; omit for the defaults
|
|
314
408
|
* ({@link DEFAULT_CONTROL_COHORT_TARGET_DEGREE} /
|
|
315
409
|
* {@link DEFAULT_CONTROL_COHORT_RECONCILE_MS} /
|
|
316
|
-
* {@link DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS} /
|
|
410
|
+
* {@link DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS} /
|
|
411
|
+
* {@link DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS} / `STRAND_PEER_ADDR_REFRESH_MS`).
|
|
317
412
|
*/
|
|
318
413
|
controlCohort?: {
|
|
319
414
|
/**
|
|
@@ -328,12 +423,20 @@ export interface NetworkConfig {
|
|
|
328
423
|
*/
|
|
329
424
|
reconcileMs?: number;
|
|
330
425
|
/**
|
|
331
|
-
*
|
|
332
|
-
* (dialed siblings) × this
|
|
333
|
-
*
|
|
334
|
-
*
|
|
426
|
+
* Limit on dialing ONE peer — all of its candidate addresses together — in
|
|
427
|
+
* ms. Bounds a reconcile pass at (dialed siblings) × this. The same limit
|
|
428
|
+
* applies to the owner dials of `applySeed` and to `dialInvite`. Defaults to
|
|
429
|
+
* {@link DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS}.
|
|
335
430
|
*/
|
|
336
431
|
dialTimeoutMs?: number;
|
|
432
|
+
/**
|
|
433
|
+
* Limit on ONE candidate address's dial attempt, in ms, inside
|
|
434
|
+
* {@link dialTimeoutMs}. Each address is dialed on its own under this limit,
|
|
435
|
+
* so an address that never answers cannot use up the time the peer's other
|
|
436
|
+
* addresses needed. Defaults to
|
|
437
|
+
* {@link DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS}.
|
|
438
|
+
*/
|
|
439
|
+
perAddressDialTimeoutMs?: number;
|
|
337
440
|
/**
|
|
338
441
|
* How often each running strand re-resolves its siblings' strand-network
|
|
339
442
|
* addresses into its own libp2p address book — a step of the reconcile pass,
|
|
@@ -388,6 +491,12 @@ export interface ControlNetworkConfig {
|
|
|
388
491
|
* by omission); the strand breadth defaults to
|
|
389
492
|
* {@link CadreNodeConfig.strandClusterSize} while its policy is the fixed
|
|
390
493
|
* {@link STRAND_CLUSTER_POLICY}.
|
|
494
|
+
*
|
|
495
|
+
* The two `*ClusterPolicy` BUILDERS are the same objects with the block-repair
|
|
496
|
+
* corroboration yardstick declared from the machines enrolled in this party
|
|
497
|
+
* ({@link resolveRepairYardstick}); handed no count, each returns its frozen base
|
|
498
|
+
* constant unchanged. A network picks the derived number up when its libp2p node is
|
|
499
|
+
* built, which for a strand is every wake from hibernation.
|
|
391
500
|
*/
|
|
392
501
|
export {
|
|
393
502
|
MIN_CLUSTER_SIZE,
|
|
@@ -395,7 +504,10 @@ export {
|
|
|
395
504
|
CONTROL_CLUSTER_POLICY,
|
|
396
505
|
DEFAULT_STRAND_CLUSTER_SIZE,
|
|
397
506
|
STRAND_CLUSTER_POLICY,
|
|
398
|
-
resolveStrandClusterSize
|
|
507
|
+
resolveStrandClusterSize,
|
|
508
|
+
resolveRepairYardstick,
|
|
509
|
+
controlClusterPolicy,
|
|
510
|
+
strandClusterPolicy
|
|
399
511
|
} from '@serfab/quereus-plugin-sereus';
|
|
400
512
|
|
|
401
513
|
/**
|
|
@@ -467,17 +579,76 @@ export interface CadreNodeConfig {
|
|
|
467
579
|
strandClusterSize?: number;
|
|
468
580
|
|
|
469
581
|
/**
|
|
470
|
-
* Tuning for the strand peer-join block catch-up (see `
|
|
582
|
+
* Tuning for the strand peer-join block catch-up (see `peer-join-backfill.ts`):
|
|
471
583
|
* when a strand's libp2p node connects to a peer this runtime has not yet
|
|
472
584
|
* caught up, every block in the strand's own raw store is pushed to that peer,
|
|
473
585
|
* so a machine that joined the strand after blocks were committed still ends
|
|
474
|
-
* up physically holding them. Applies to every strand this node starts
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
|
|
586
|
+
* up physically holding them. Applies to every strand this node starts.
|
|
587
|
+
* Omit for the defaults (`DEFAULT_PEER_JOIN_BACKFILL`); `{ enabled: false }`
|
|
588
|
+
* restores the no-backfill behaviour. The control network runs its own,
|
|
589
|
+
* separately tuned catch-up — see {@link controlBackfill}.
|
|
590
|
+
*/
|
|
591
|
+
strandBackfill?: PeerJoinBackfillConfig;
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Tuning for the CONTROL network's peer-join block catch-up (same module,
|
|
595
|
+
* `peer-join-backfill.ts`, wired in `CadreNode.start`): when the control
|
|
596
|
+
* libp2p node connects to an AUTHORIZED party member this runtime has not yet
|
|
597
|
+
* caught up, every block in the control database's own raw store is pushed to
|
|
598
|
+
* it. This is what makes a control block committed while the writer was alone
|
|
599
|
+
* (collection headers written at genesis above all) physically reach members
|
|
600
|
+
* that joined later — without it, such a member that restarts offline reads
|
|
601
|
+
* the affected tables as empty. Pushes are gated on
|
|
602
|
+
* `CadreNode.isAuthorizedMember`, checked at push time, because the control
|
|
603
|
+
* network's inbound gate deliberately admits non-members in several states
|
|
604
|
+
* (seed delivery, enrollment, bootstrap/relay peers). Omit for the defaults
|
|
605
|
+
* (`DEFAULT_PEER_JOIN_BACKFILL`); `{ enabled: false }` disables it.
|
|
606
|
+
*/
|
|
607
|
+
controlBackfill?: PeerJoinBackfillConfig;
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Tuning for the CLOSED-strand revoked-peer gate
|
|
611
|
+
* (`strand-revocation-enforcer.ts`), applied to every closed strand this node
|
|
612
|
+
* starts: each strand node materializes the peer ids bound to REMOVED members
|
|
613
|
+
* (orphaned `Strand.MemberPeer` rows) and refuses them at the stream and
|
|
614
|
+
* connection layers, so removing a party cuts the network to its machines
|
|
615
|
+
* rather than only deleting its row. Open strands never arm it. Omit for the
|
|
616
|
+
* defaults (`DEFAULT_REVOCATION_POLL_INTERVAL_MS` refresh cadence);
|
|
617
|
+
* `{ enabled: false }` restores the pre-existing behaviour.
|
|
479
618
|
*/
|
|
480
|
-
|
|
619
|
+
strandRevocationEnforcement?: StrandRevocationEnforcementConfig;
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Tuning for the CLOSED-strand membership reconciler
|
|
623
|
+
* (`strand-membership-reconciler.ts`), applied to every closed strand this
|
|
624
|
+
* node launches with a party identity key: at bring-up each machine redeems a
|
|
625
|
+
* pending formation invitation (seating the party's `Strand.Member` row) and
|
|
626
|
+
* writes its own machine→party `Strand.MemberPeer` binding, retrying in the
|
|
627
|
+
* background without blocking bring-up. `pollIntervalMs` is the IDLE cadence
|
|
628
|
+
* (how often a machine nobody has admitted re-checks) and the cap of the
|
|
629
|
+
* unfinished-join retry ladder, NOT the retry cadence itself — an unfinished
|
|
630
|
+
* join retries from 1 s, doubling. Omit for the defaults (it mirrors
|
|
631
|
+
* {@link strandRevocationEnforcement}'s `pollIntervalMs`);
|
|
632
|
+
* `{ enabled: false }` disarms the loop — for test fixtures that hand-drive
|
|
633
|
+
* the membership writers and assert exact row sets.
|
|
634
|
+
*/
|
|
635
|
+
strandMembershipReconciliation?: StrandMembershipReconciliationConfig;
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Tuning for the JOINING machine's first-sync write gate
|
|
639
|
+
* (`strand-first-sync-gate.ts`), applied to every strand this node launches as a
|
|
640
|
+
* non-founder: the strand's database is withheld from the app (`StrandInstance.database`
|
|
641
|
+
* unset, status `'syncing'`) until the strand's `Strand.Header` has been received from
|
|
642
|
+
* another member and every `App` table has been read once, because a machine that writes
|
|
643
|
+
* to a table it has never fetched creates a private copy of it that never merges.
|
|
644
|
+
* `timeoutMs` bounds how long {@link CadreNode.addStrand} waits before rejecting with
|
|
645
|
+
* `StrandAwaitingFirstSyncError` (retryable — the launch stays up and keeps probing);
|
|
646
|
+
* `pollIntervalMs` is the probe cadence. Omit for the defaults (`DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS`, 30 s;
|
|
647
|
+
* `DEFAULT_STRAND_FIRST_SYNC_POLL_MS`, 500 ms). There is deliberately no way to disable
|
|
648
|
+
* the gate: a machine that already holds the Header is never gated, so nothing that works
|
|
649
|
+
* today is blocked by it.
|
|
650
|
+
*/
|
|
651
|
+
strandFirstSync?: StrandFirstSyncConfig;
|
|
481
652
|
|
|
482
653
|
/** Hibernation configuration */
|
|
483
654
|
hibernation?: HibernationConfig;
|
|
@@ -568,6 +739,40 @@ export interface CadreNodeConfig {
|
|
|
568
739
|
store?: BootstrapPeerStore;
|
|
569
740
|
};
|
|
570
741
|
|
|
742
|
+
/**
|
|
743
|
+
* Node-local record of how many machines this party had enrolled the last time
|
|
744
|
+
* this node looked (see `enrolled-machine-store.ts`): the block-repair
|
|
745
|
+
* corroboration yardstick the CONTROL node declares when its libp2p node is
|
|
746
|
+
* built. Sibling of {@link trustedOwners} / {@link bootstrapPeers} — same
|
|
747
|
+
* NON-replicated, per-party, injected-backend shape — and, like
|
|
748
|
+
* {@link bootstrapPeers}, nothing here is trust-bearing: it is a repair hint,
|
|
749
|
+
* recomputed from `CadrePeer` rows the moment the control database is up.
|
|
750
|
+
*
|
|
751
|
+
* It exists only because the control node is created BEFORE that database, so
|
|
752
|
+
* the live count is unreadable at the one moment it is needed; remembering it
|
|
753
|
+
* across the restart is what breaks that deadlock. Absent ⇒ an in-memory store
|
|
754
|
+
* is created at start(), which cold-starts on every launch and therefore
|
|
755
|
+
* declares nothing — byte-for-byte the behaviour before this record existed.
|
|
756
|
+
* No embedder is forced to change.
|
|
757
|
+
*
|
|
758
|
+
* A party that grows applies the larger number on each node's NEXT launch,
|
|
759
|
+
* deliberately: Optimystic freezes the policy at node construction and offers
|
|
760
|
+
* no runtime setter, and a node still holding the old value runs at exactly
|
|
761
|
+
* today's behaviour, so there is no forced rebuild.
|
|
762
|
+
*
|
|
763
|
+
* A new platform needs no new store class: supply a `DurableSlot` for the
|
|
764
|
+
* platform's storage and inject `PersistentEnrolledMachineStore.open(slot, partyId)`.
|
|
765
|
+
*/
|
|
766
|
+
enrolledMachines?: {
|
|
767
|
+
/**
|
|
768
|
+
* Injected store instance — e.g. a `FileEnrolledMachineStore` from the
|
|
769
|
+
* Node-only subpath `@serfab/cadre-core/enrolled-machine-store-file`,
|
|
770
|
+
* persisted in the node's state directory. Its `partyId` must match
|
|
771
|
+
* `controlNetwork.partyId`; start() fails closed on a mismatch.
|
|
772
|
+
*/
|
|
773
|
+
store?: EnrolledMachineStore;
|
|
774
|
+
};
|
|
775
|
+
|
|
571
776
|
/**
|
|
572
777
|
* Platform push-delivery for suspended mobile peers. When present, the node's
|
|
573
778
|
* server fan-out can deliver strand-wake data messages over the platform push
|
|
@@ -598,15 +803,23 @@ export interface CadreNodeConfig {
|
|
|
598
803
|
}
|
|
599
804
|
|
|
600
805
|
/**
|
|
601
|
-
* Status of a strand instance
|
|
806
|
+
* Status of a strand instance.
|
|
807
|
+
*
|
|
808
|
+
* `'syncing'` is a JOINING machine whose runtime is up (strand libp2p node, background
|
|
809
|
+
* loops) but whose database is held back from the app until the strand's `Strand.Header`
|
|
810
|
+
* has been received from another member — see `strand-first-sync-gate.ts`. In that state
|
|
811
|
+
* `StrandInstance.database` is unset; the instance goes `'active'` (and `CadreNode` emits
|
|
812
|
+
* `strand:writable`) the moment the Header is readable. A founder, or a machine that has
|
|
813
|
+
* synced this strand before (restart, hibernation resume), never passes through it.
|
|
602
814
|
*/
|
|
603
|
-
export type StrandStatus =
|
|
604
|
-
| 'starting'
|
|
605
|
-
| '
|
|
606
|
-
| '
|
|
607
|
-
| '
|
|
608
|
-
| '
|
|
609
|
-
| '
|
|
815
|
+
export type StrandStatus =
|
|
816
|
+
| 'starting'
|
|
817
|
+
| 'syncing'
|
|
818
|
+
| 'active'
|
|
819
|
+
| 'idle'
|
|
820
|
+
| 'hibernating'
|
|
821
|
+
| 'stopping'
|
|
822
|
+
| 'stopped'
|
|
610
823
|
| 'error';
|
|
611
824
|
|
|
612
825
|
/**
|
|
@@ -627,10 +840,16 @@ export interface StrandInstance {
|
|
|
627
840
|
status: StrandStatus;
|
|
628
841
|
sAppInfo?: SAppInfo;
|
|
629
842
|
|
|
630
|
-
/** The libp2p node for this strand (only when active/idle) */
|
|
843
|
+
/** The libp2p node for this strand (only when live: syncing/active/idle) */
|
|
631
844
|
libp2pNode?: Libp2p;
|
|
632
845
|
|
|
633
|
-
/**
|
|
846
|
+
/**
|
|
847
|
+
* The Quereus database for this strand — set only while the strand is live AND
|
|
848
|
+
* this machine may write to it. A joining machine that has not yet received the
|
|
849
|
+
* strand's `Strand.Header` from another member runs with `libp2pNode` set and this
|
|
850
|
+
* unset (status `'syncing'`); it is published when the Header arrives
|
|
851
|
+
* (`strand:writable`). Unset while hibernating.
|
|
852
|
+
*/
|
|
634
853
|
database?: StrandDatabase;
|
|
635
854
|
|
|
636
855
|
/** Membership info for closed strands */
|
|
@@ -678,6 +897,17 @@ export interface StrandRow {
|
|
|
678
897
|
Id: string;
|
|
679
898
|
MemberPrivateKey: string | null;
|
|
680
899
|
Type: 'o' | 'c';
|
|
900
|
+
/**
|
|
901
|
+
* ed25519 (base64url) owner key of the MACHINE that published this row — the
|
|
902
|
+
* signer of the owner-signed `Strand` insert (`== context.OwnerKey`, pinned by
|
|
903
|
+
* the schema's `AuthorizedInsert`). Identifies the founding machine, so a
|
|
904
|
+
* launch with no explicit `founder` flag derives "am I the founder?" by
|
|
905
|
+
* comparing it to the node's own owner key (see `CadreNode.launchStrand`).
|
|
906
|
+
* `null` on a consent-seated strand (the unsigned `FormationUsage` branch
|
|
907
|
+
* records no trustworthy signer). Provenance, not content —
|
|
908
|
+
* `strandRowMismatches` excludes it from the identical-content comparison.
|
|
909
|
+
*/
|
|
910
|
+
FounderOwnerKey: string | null;
|
|
681
911
|
}
|
|
682
912
|
|
|
683
913
|
/**
|
|
@@ -717,10 +947,95 @@ export interface StrandConfig {
|
|
|
717
947
|
* paths; the same party that calls {@link CadreNode.publishStrand}). The founder
|
|
718
948
|
* runs the one-time membership bootstrap at bring-up (writes `Strand.Header`, and
|
|
719
949
|
* for a closed strand the founding `Member`+`Manager`). A joiner leaves this
|
|
720
|
-
*
|
|
721
|
-
*
|
|
950
|
+
* unset and writes nothing — it receives those rows via Optimystic sync.
|
|
951
|
+
*
|
|
952
|
+
* When unset, founder-ness is DERIVED from the row: this node founds iff
|
|
953
|
+
* {@link StrandRow.FounderOwnerKey} equals its own owner key. An explicit
|
|
954
|
+
* `true`/`false` wins over the derivation — the formation/responder flows pass
|
|
955
|
+
* it deliberately, since a consent-seated row carries a null column.
|
|
722
956
|
*/
|
|
723
957
|
founder?: boolean;
|
|
958
|
+
/**
|
|
959
|
+
* THIS party's own strand membership private key (base64 protobuf, as
|
|
960
|
+
* `generateStrandMemberKey` mints) for a CLOSED strand — the identity whose public
|
|
961
|
+
* key seats the founding `Member`/`Manager`. The explicit sibling of
|
|
962
|
+
* {@link founder}: normally the launch reads it from the party's control-layer
|
|
963
|
+
* `StrandPartyKey` row (minting one on the founding machine when absent), and an
|
|
964
|
+
* explicit value here WINS over that read — for callers whose row carries no
|
|
965
|
+
* founder provenance to heal against (an explicit `founder: true` over a hand-built
|
|
966
|
+
* row, as the test harness does). Deliberately NOT
|
|
967
|
+
* {@link StrandRow.MemberPrivateKey}, the strand-wide read secret every joining
|
|
968
|
+
* party receives — that key derives nobody's identity.
|
|
969
|
+
*/
|
|
970
|
+
partyMemberPrivateKey?: string;
|
|
971
|
+
/**
|
|
972
|
+
* Whether {@link CadreNode.addStrand} waits for a JOINING machine's first sync before it
|
|
973
|
+
* resolves. Default `true`: the returned instance is writable (`database` set, status
|
|
974
|
+
* `'active'`), or the call rejects with `StrandAwaitingFirstSyncError` after
|
|
975
|
+
* `CadreNodeConfig.strandFirstSync.timeoutMs` — retryable, the launch stays up. `false`
|
|
976
|
+
* returns as soon as the runtime is launched, possibly `'syncing'` with no `database`;
|
|
977
|
+
* the caller then wires its own peers (a hand-dialed test fixture) and awaits
|
|
978
|
+
* {@link CadreNode.whenStrandWritable} or the `strand:writable` event. A founder, or a
|
|
979
|
+
* machine that already holds the strand's `Strand.Header`, is never gated either way.
|
|
980
|
+
*/
|
|
981
|
+
awaitFirstSync?: boolean;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* What {@link CadreNode.foundStrand} needs to publish a strand's control row AND start it
|
|
986
|
+
* locally as its founder — the resumable one-call form of `publishStrand` + `addStrand`.
|
|
987
|
+
*
|
|
988
|
+
* No `strandRow`: the row is the OUTPUT of founding, either freshly published or read back
|
|
989
|
+
* from an interrupted earlier attempt. `StrandConfig` (the join/attach shape) takes one
|
|
990
|
+
* because there the row already exists and came from the control network.
|
|
991
|
+
*/
|
|
992
|
+
export interface FoundStrandConfig {
|
|
993
|
+
/** Unique strand identifier. Trimmed; blank is rejected. */
|
|
994
|
+
strandId: string;
|
|
995
|
+
/** `'o'` for open (default) or `'c'` for closed. */
|
|
996
|
+
type?: 'o' | 'c';
|
|
997
|
+
/**
|
|
998
|
+
* Membership key gating a closed strand. Required in practice for `type: 'c'` (the
|
|
999
|
+
* founder bootstrap rejects a null key), and IGNORED when the strand is already
|
|
1000
|
+
* published — the stored key wins, so read the resolved key back off the returned
|
|
1001
|
+
* instance rather than trusting a freshly minted one.
|
|
1002
|
+
*/
|
|
1003
|
+
memberPrivateKey?: string;
|
|
1004
|
+
/** sApp configuration the hosting application provides, as for {@link StrandConfig}. */
|
|
1005
|
+
sAppConfig: SAppConfig;
|
|
1006
|
+
/**
|
|
1007
|
+
* Forwarded to the attach ({@link StrandConfig.awaitFirstSync}). Matters only when the
|
|
1008
|
+
* call ATTACHES rather than founds — the stored row was published by a sibling machine
|
|
1009
|
+
* that won the founding race — in which case this machine is a joiner whose database
|
|
1010
|
+
* is withheld until that sibling's `Strand.Header` reaches it; default `true` waits
|
|
1011
|
+
* for that inside the call. A founding launch is never gated.
|
|
1012
|
+
*/
|
|
1013
|
+
awaitFirstSync?: boolean;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* What {@link CadreNode.foundStrand} hands back: the running instance PLUS the `Strand` row
|
|
1018
|
+
* the strand actually runs under.
|
|
1019
|
+
*
|
|
1020
|
+
* The row is returned separately rather than left for the caller to read off the instance
|
|
1021
|
+
* because on a resumed founding it is NOT the caller's input — a closed strand adopts the
|
|
1022
|
+
* stored `MemberPrivateKey`, discarding a freshly minted one. A caller that must carry the
|
|
1023
|
+
* membership key onward (to mint an invitation, say) reads it from HERE.
|
|
1024
|
+
*/
|
|
1025
|
+
export interface FoundStrandResult {
|
|
1026
|
+
/** The active local instance, as {@link CadreNode.addStrand} would have returned. */
|
|
1027
|
+
instance: StrandInstance;
|
|
1028
|
+
/** The live control-plane row: freshly published, or the one already there. */
|
|
1029
|
+
strandRow: StrandRow;
|
|
1030
|
+
/**
|
|
1031
|
+
* Whether THIS machine actually founded (ran / will have run the one-time
|
|
1032
|
+
* founder bootstrap). `false` when the resolved row was published by a
|
|
1033
|
+
* DIFFERENT machine's owner key — e.g. a sibling won a concurrent founding
|
|
1034
|
+
* race — in which case the call attached as a joiner instead, which is the
|
|
1035
|
+
* correct outcome (two machines bootstrapping one strand on separate replicas
|
|
1036
|
+
* is the double-`Header` hazard).
|
|
1037
|
+
*/
|
|
1038
|
+
founded: boolean;
|
|
724
1039
|
}
|
|
725
1040
|
|
|
726
1041
|
/**
|
|
@@ -775,6 +1090,27 @@ export interface OpenInvitation {
|
|
|
775
1090
|
bootstrap: string[];
|
|
776
1091
|
}
|
|
777
1092
|
|
|
1093
|
+
/**
|
|
1094
|
+
* A single-use strand membership invitation carried on a closed-strand formation
|
|
1095
|
+
* result: the `Strand.Invite` keypair the responder (the host strand's founder party)
|
|
1096
|
+
* issued for this redemption. The joiner spends it — `consumeInvite` on its own strand
|
|
1097
|
+
* replica — to seat a `Strand.Member` row under its OWN party key, so joining no longer
|
|
1098
|
+
* hands out the founder's identity. Structurally identical to
|
|
1099
|
+
* `strand-membership-writer`'s `IssuedInvite`; declared here so the wire/type layer does
|
|
1100
|
+
* not import the writer.
|
|
1101
|
+
*/
|
|
1102
|
+
export interface StrandMembershipInvite {
|
|
1103
|
+
/** The invite ed25519 PUBLIC key (base64url) — the `Strand.Invite.Key` row. */
|
|
1104
|
+
inviteKey: string;
|
|
1105
|
+
/**
|
|
1106
|
+
* The invite ed25519 PRIVATE seed (base64url). A single-use bearer credential:
|
|
1107
|
+
* whoever holds it can `consumeInvite` exactly once. Same sensitivity class and
|
|
1108
|
+
* handling as `memberPrivateKey` — delivered only inside the validated,
|
|
1109
|
+
* post-approval formation result, never written to either side's control DB.
|
|
1110
|
+
*/
|
|
1111
|
+
invitePrivateKey: string;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
778
1114
|
/**
|
|
779
1115
|
* Result of forming a strand via open invitation
|
|
780
1116
|
*/
|
|
@@ -792,6 +1128,30 @@ export interface FormStrandResult {
|
|
|
792
1128
|
* from {@link invitePrivateKey} (the initiator's own generated signing key).
|
|
793
1129
|
*/
|
|
794
1130
|
memberPrivateKey?: string;
|
|
1131
|
+
/**
|
|
1132
|
+
* The joiner's own single-use membership invitation into the (closed, bound) host
|
|
1133
|
+
* strand — see {@link StrandMembershipInvite}. Present only when the responder's
|
|
1134
|
+
* host strand is closed and its runtime issued one; absent for open strands and the
|
|
1135
|
+
* responder-provisions (unbound) path. `CadreNode.formStrand` itself persists the
|
|
1136
|
+
* joiner's party key and caches this invitation for the strand bring-up to redeem,
|
|
1137
|
+
* so an embedding app normally never touches it.
|
|
1138
|
+
*/
|
|
1139
|
+
membershipInvite?: StrandMembershipInvite;
|
|
1140
|
+
/**
|
|
1141
|
+
* The responder's live STRAND-network multiaddrs for the formed strand — the only
|
|
1142
|
+
* cross-party discovery seed there is, since the strand-addr RPC that resolves a
|
|
1143
|
+
* sibling's strand addresses answers own-party callers only.
|
|
1144
|
+
*
|
|
1145
|
+
* Always an array, frequently EMPTY: the responder discloses none when it holds no
|
|
1146
|
+
* live strand node for the strand it just provisioned (the responder-provisions path
|
|
1147
|
+
* mints a strand that has not launched yet). `CadreNode.formStrand` records these
|
|
1148
|
+
* itself, so an embedding app only needs them to seed a strand it launches OUTSIDE
|
|
1149
|
+
* that node.
|
|
1150
|
+
*
|
|
1151
|
+
* One-shot and in-memory: they are the responder's addresses at the moment of
|
|
1152
|
+
* formation and are never re-resolved — see `docs/strands.md`.
|
|
1153
|
+
*/
|
|
1154
|
+
strandAddrs: string[];
|
|
795
1155
|
}
|
|
796
1156
|
|
|
797
1157
|
/**
|
|
@@ -834,16 +1194,81 @@ export interface CadreNodeEvents {
|
|
|
834
1194
|
'strand:idle': { strandId: string };
|
|
835
1195
|
'strand:hibernating': { strandId: string };
|
|
836
1196
|
'strand:waking': { strandId: string };
|
|
1197
|
+
/**
|
|
1198
|
+
* Emitted when a strand that came up `'syncing'` — a joining machine whose database was
|
|
1199
|
+
* withheld until it received the strand's `Strand.Header` from another member — becomes
|
|
1200
|
+
* writable: `StrandInstance.database` is now set and the status is `'active'`. A strand
|
|
1201
|
+
* whose `strand:started` already reported `'active'` (a founder, or a machine that had
|
|
1202
|
+
* synced before) never emits this. The event form of what {@link CadreNode.addStrand}
|
|
1203
|
+
* awaits; an app that attaches without waiting (`awaitFirstSync: false`, or a strand the
|
|
1204
|
+
* watcher auto-launched) hangs its "waiting for the other member" screen on the pair.
|
|
1205
|
+
*/
|
|
1206
|
+
'strand:writable': { strandId: string };
|
|
1207
|
+
/**
|
|
1208
|
+
* Emitted when this node discovers it is no longer a member of a CLOSED
|
|
1209
|
+
* strand — its party was removed by a manager, or it left. Detected by the
|
|
1210
|
+
* strand's revoked-peer gate (`strand-revocation-enforcer.ts`) seeing this
|
|
1211
|
+
* node's own peer id in the revoked set, so it is BEST-EFFORT: it fires only
|
|
1212
|
+
* if the removal replicated here before the rest of the strand cut us off,
|
|
1213
|
+
* and a node that was already offline may never see it at all.
|
|
1214
|
+
*
|
|
1215
|
+
* Nothing is stopped or torn down for you. Treat it as "this strand will no
|
|
1216
|
+
* longer sync": remaining members refuse this node's streams, dials, and
|
|
1217
|
+
* connections, so reads keep working against whatever is stored locally while
|
|
1218
|
+
* writes stop propagating. Stopping the strand (`stopStrand`) or deleting its
|
|
1219
|
+
* data is the app's decision. Emitted once per removal — re-armed if a manager
|
|
1220
|
+
* re-admits this party, so a second removal is reported again — and a
|
|
1221
|
+
* hibernation wake rebuilds the gate, which may re-emit for a strand still
|
|
1222
|
+
* revoked.
|
|
1223
|
+
*/
|
|
1224
|
+
'strand:revoked': { strandId: string };
|
|
837
1225
|
/**
|
|
838
1226
|
* Emitted when the control network advertises a strand this node has no
|
|
839
|
-
* registered `sAppConfig` for — i.e. a strand created by another member
|
|
840
|
-
*
|
|
841
|
-
*
|
|
842
|
-
*
|
|
1227
|
+
* registered `sAppConfig` for — i.e. a strand created by another member, or
|
|
1228
|
+
* one this node ran in a previous session (sApp configs are in-memory only and
|
|
1229
|
+
* do not survive `stop()`). The hosting app decides whether to join it
|
|
1230
|
+
* (register a config + `addStrand`, e.g. via a chat `joinChatStrand` helper).
|
|
1231
|
+
* Carries the full {@link StrandRow} so the app can join without re-querying
|
|
1232
|
+
* the control DB.
|
|
1233
|
+
*
|
|
1234
|
+
* **Fired exactly ONCE per strand per session, and it can fire before your
|
|
1235
|
+
* listener is attached.** The strand watcher's first poll runs inside
|
|
1236
|
+
* `CadreNode.start()` (100 ms after the watcher starts), so every strand
|
|
1237
|
+
* already stored for this party is normally offered while the embedding app is
|
|
1238
|
+
* still inside its own `start()` await. The watcher then records the strand as
|
|
1239
|
+
* seen and no later poll re-offers it, so a listener attached a moment late
|
|
1240
|
+
* misses those strands for the life of the process.
|
|
1241
|
+
*
|
|
1242
|
+
* So an app that auto-joins discovered strands must **subscribe first, then
|
|
1243
|
+
* drain `CadreNode.getDiscoveredStrands()`** — the map of strands no local
|
|
1244
|
+
* config claims. In that order a strand discovered between the two steps is
|
|
1245
|
+
* handled twice rather than not at all, which is why the join handler needs to
|
|
1246
|
+
* be idempotent (guard on an in-flight set, not only on
|
|
1247
|
+
* `getStrands().has(id)` — the strand manager tracks an instance only once
|
|
1248
|
+
* `addStrand` has resolved).
|
|
843
1249
|
*/
|
|
844
1250
|
'strand:discovered': { strandId: string; strand: StrandRow };
|
|
845
1251
|
'control:connected': void;
|
|
846
1252
|
'control:disconnected': void;
|
|
1253
|
+
/**
|
|
1254
|
+
* Emitted when the control-write retry funnel GAVE UP on a local control write — the
|
|
1255
|
+
* classifier declined the failure as non-transient, or every attempt (or the elapsed
|
|
1256
|
+
* budget) ran out. One event per abandoned write, carrying the operation label, how far
|
|
1257
|
+
* it got, why it stopped and the error.
|
|
1258
|
+
*
|
|
1259
|
+
* Exists because an abandoned BACKGROUND write is otherwise invisible: the node's own
|
|
1260
|
+
* self-address republish, the post-connect drain and the replication drain all fire
|
|
1261
|
+
* their writes unawaited with a `debug`-only catch, and that namespace is off unless
|
|
1262
|
+
* something enabled it — so the write is lost and nobody is told. A FOREGROUND write's
|
|
1263
|
+
* caller sees the rethrown error as well as this event.
|
|
1264
|
+
*
|
|
1265
|
+
* What an app does with it is its own call; the node itself only escalates ONE case to
|
|
1266
|
+
* the operator (a self-address republish that has been failing longer than a peer
|
|
1267
|
+
* record stays fresh, at which point other machines are already discarding this node's
|
|
1268
|
+
* address). Treat the rest as telemetry — a control write that was lost, not a strand
|
|
1269
|
+
* or connection state change.
|
|
1270
|
+
*/
|
|
1271
|
+
'control:write-abandoned': ControlRetryAbandonment;
|
|
847
1272
|
/** Emitted when a seed is received via the seed protocol */
|
|
848
1273
|
'seed:received': { partyId: string; peerId: string };
|
|
849
1274
|
/** Emitted when a seed is successfully applied */
|
|
@@ -916,6 +1341,8 @@ export type CadrePeerVoucherFields = Pick<CadrePeerRow, 'peerId' | 'stampId' | '
|
|
|
916
1341
|
* `ControlDatabase.queryRevocations`: the identity triple plus the `ReissuedAt`
|
|
917
1342
|
* counter an owner bumps (`ControlDatabase.reissueRevocations`) to re-write — and
|
|
918
1343
|
* therefore re-broadcast — a tombstone that committed while the node was alone.
|
|
1344
|
+
* Never the ledger marker: that row names no guarded table, and `queryRevocations`
|
|
1345
|
+
* skips it, which is what keeps `tableName` a {@link RevocableTable}.
|
|
919
1346
|
*/
|
|
920
1347
|
export interface RevocationRow {
|
|
921
1348
|
/** Which guarded table's stamp was retired. */
|
|
@@ -928,6 +1355,14 @@ export interface RevocationRow {
|
|
|
928
1355
|
reissuedAt: number;
|
|
929
1356
|
}
|
|
930
1357
|
|
|
1358
|
+
/**
|
|
1359
|
+
* Outcome of `ControlDatabase.openRevocationLedger`: `'opened'` when this call filed the
|
|
1360
|
+
* singleton `Revocation` ledger marker, `'already-open'` when it was already filed (seen
|
|
1361
|
+
* by the in-lock guard, or filed first by another owner and refused on the primary key).
|
|
1362
|
+
* Either way the marker exists afterwards.
|
|
1363
|
+
*/
|
|
1364
|
+
export type RevocationLedgerOpenResult = 'opened' | 'already-open';
|
|
1365
|
+
|
|
931
1366
|
/**
|
|
932
1367
|
* Outcome of {@link CadreNode.registerSelf}, surfaced so callers (e.g. the CLI
|
|
933
1368
|
* `--owner` branch) can log what happened:
|