@serfab/cadre-core 0.12.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/cadre-node.d.ts +328 -25
- package/dist/cadre-node.js +760 -59
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-database.d.ts +19 -0
- package/dist/control-database.js +51 -9
- package/dist/control-database.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +30 -2
- package/dist/control-schema.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 +5 -3
- package/dist/index.js +23 -5
- 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 +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} +133 -52
- package/dist/peer-join-backfill.js.map +1 -0
- package/dist/relay-addrs.d.ts +73 -0
- package/dist/relay-addrs.js +195 -0
- package/dist/relay-addrs.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 +12 -0
- package/dist/strand-formation-manager.js +10 -3
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +76 -2
- package/dist/strand-formation-protocol.js +78 -2
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +147 -14
- package/dist/strand-instance-manager.js +225 -45
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +12 -0
- package/dist/strand-membership-writer.js +23 -1
- 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 +7 -0
- package/dist/strand-solicitation.js +8 -2
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.js +6 -1
- package/dist/strand-watcher.js.map +1 -1
- package/dist/types.d.ts +210 -16
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/package.json +16 -8
- package/src/cadre-node.ts +6353 -5603
- package/src/control-database.ts +53 -9
- package/src/control-schema.ts +30 -2
- 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 +52 -10
- package/src/membership-connection-gater.ts +9 -3
- 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} +478 -389
- package/src/relay-addrs.ts +218 -0
- package/src/strand-database.ts +17 -0
- package/src/strand-formation-manager.ts +22 -3
- package/src/strand-formation-protocol.ts +808 -692
- package/src/strand-instance-manager.ts +317 -54
- package/src/strand-membership-writer.ts +23 -1
- package/src/strand-network-config.ts +172 -0
- package/src/strand-revocation-enforcer.ts +653 -0
- package/src/strand-solicitation.ts +484 -471
- package/src/strand-watcher.ts +319 -314
- package/src/types.ts +1554 -1354
- package/dist/strand-backfill.js.map +0 -1
package/README.md
CHANGED
|
@@ -168,9 +168,10 @@ The main entry point for cadre participation.
|
|
|
168
168
|
| `stop()` | Gracefully disconnect from all networks |
|
|
169
169
|
| `getStrands()` | Get all active strand instances |
|
|
170
170
|
| `getStrand(id)` | Get a specific strand instance |
|
|
171
|
-
| `
|
|
171
|
+
| `foundStrand(config)` | **Create a strand**: publish its `Strand` row and start it locally as founder, in one call that is safe to re-run after an interruption. Returns the instance plus the row it actually runs under — for a closed strand read the membership key from there, since a resumed founding keeps the stored key |
|
|
172
|
+
| `addStrand(row)` | Attach only — start a local instance for a row that already exists (the join path; also testing/direct API). Does **not** publish |
|
|
172
173
|
| `stopStrand(id)` | Stop a strand on this node only (the shared `Strand` row stays; rediscovered on restart) |
|
|
173
|
-
| `publishStrand(id, type?, memberKey?)` | Owner-signed `Strand` row insert — makes the strand visible cadre-wide |
|
|
174
|
+
| `publishStrand(id, type?, memberKey?)` | Owner-signed `Strand` row insert — makes the strand visible cadre-wide. Idempotent: a live row with the same `(Type, MemberPrivateKey)` is returned unwritten; different content on the same id throws. Prefer `foundStrand` when creating a strand |
|
|
174
175
|
| `unpublishStrand(id)` | Owner-signed party-wide removal — deletes this party's `Strand` row; every node stops the strand. Destroys a closed strand's `MemberPrivateKey` |
|
|
175
176
|
| `getEnrollmentService()` | Access peer enrollment API |
|
|
176
177
|
|
package/dist/cadre-node.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Libp2p, PeerId } from '@libp2p/interface';
|
|
2
2
|
import type { Multiaddr } from '@multiformats/multiaddr';
|
|
3
|
-
import type { CadreNodeConfig, StrandInstance, StrandConfig, SAppConfig, CadreNodeEvents, ControlNetworkSeed, ApplySeedResult, AddDroneOptions, AddPhoneOptions, DroneInitResult, InviteResult, CadreInvite, OpenInvitation, FormStrandResult, StrandFormationDisclosure, ResolveOpts, SelfRegistrationOutcome, ServiceWakeResult, PushPlatform, DeviceTokenRecord, ResolveDeviceTokenOpts } from './types.js';
|
|
3
|
+
import type { CadreNodeConfig, StrandInstance, StrandRow, StrandConfig, FoundStrandConfig, FoundStrandResult, SAppConfig, CadreNodeEvents, ControlNetworkSeed, ApplySeedResult, AddDroneOptions, AddPhoneOptions, DroneInitResult, InviteResult, CadreInvite, OpenInvitation, FormStrandResult, StrandFormationDisclosure, ResolveOpts, SelfRegistrationOutcome, ServiceWakeResult, PushPlatform, DeviceTokenRecord, ResolveDeviceTokenOpts } from './types.js';
|
|
4
4
|
import { type Ed25519KeyPair } from './ed25519-key.js';
|
|
5
5
|
import { type TrustedOwnerStore, type TrustSource } from './trusted-owner-store.js';
|
|
6
6
|
import { type BootstrapPeerStore } from './bootstrap-peer-store.js';
|
|
7
|
+
import { type EnrolledMachineStore } from './enrolled-machine-store.js';
|
|
7
8
|
import { type SAppIdLookup } from './strand-watcher.js';
|
|
8
9
|
import { EnrollmentService } from './enrollment.js';
|
|
9
10
|
import { ControlDatabase } from './control-database.js';
|
|
@@ -57,6 +58,14 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
57
58
|
private trustedOwnerStore;
|
|
58
59
|
private controlNode;
|
|
59
60
|
private controlDatabase;
|
|
61
|
+
/**
|
|
62
|
+
* The CONTROL network's peer-join block catch-up — same module as the
|
|
63
|
+
* per-strand ones, but membership-gated (see {@link startControlBackfill}).
|
|
64
|
+
* Created in {@link start} after the control node and database are up, stopped
|
|
65
|
+
* and dropped in {@link cleanup} before either is torn down, so a stop()→
|
|
66
|
+
* start() cycle rebuilds it with a fresh caught-up-peer memo.
|
|
67
|
+
*/
|
|
68
|
+
private controlBackfill;
|
|
60
69
|
private strandWatcher;
|
|
61
70
|
private strandManager;
|
|
62
71
|
private hibernationManager;
|
|
@@ -237,6 +246,36 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
237
246
|
* stale stamp.
|
|
238
247
|
*/
|
|
239
248
|
private readonly strandPeerAddrRefreshAt;
|
|
249
|
+
/**
|
|
250
|
+
* CROSS-PARTY strand-network addresses learned at formation, keyed by strandId — the
|
|
251
|
+
* responder's live strand addrs, carried back on the formation result
|
|
252
|
+
* (`FormationResultMessage.strandAddrs`) and recorded by {@link formStrand}.
|
|
253
|
+
*
|
|
254
|
+
* This is the ONLY cross-party discovery input there is. The strand-addr RPC that
|
|
255
|
+
* resolves a strand's addresses is membership-gated and answers own-party siblings
|
|
256
|
+
* only, so without this map a joiner's cohort seed for a two-party strand is empty and
|
|
257
|
+
* the mesh never forms.
|
|
258
|
+
*
|
|
259
|
+
* Read by {@link resolveCohortSeed} (launch + hibernation resume) and re-merged by
|
|
260
|
+
* every {@link refreshStrandPeerAddrs} pass — the re-merge is what keeps the entries
|
|
261
|
+
* alive past the peerStore's one-hour address expiry, since nothing can re-resolve
|
|
262
|
+
* them.
|
|
263
|
+
*
|
|
264
|
+
* IN-MEMORY and ONE-SHOT, deliberately (see `docs/strands.md`): the addresses die with
|
|
265
|
+
* the process, and a restarted initiator with no sibling running the strand is back to
|
|
266
|
+
* an empty seed until the durability work lands
|
|
267
|
+
* (`backlog/feat-cross-party-strand-addr-durability`). They are also never refreshed,
|
|
268
|
+
* so a responder whose relay reservation rotates before the initiator dials leaves a
|
|
269
|
+
* dead entry behind; recovery today is a fresh invitation.
|
|
270
|
+
*
|
|
271
|
+
* NOTE: entries are kept for the node's lifetime, including across a
|
|
272
|
+
* {@link stopStrand} — a stopped strand may be rediscovered by the watcher and must
|
|
273
|
+
* still get its seed. Each strand's list is capped at `MAX_STRAND_ADDRS`, but the
|
|
274
|
+
* number of KEYS is bounded only by the strands this node has ever formed as an
|
|
275
|
+
* initiator (not by time); if a node ever forms strands at scale, evict on
|
|
276
|
+
* `unpublishStrand` or cap the map.
|
|
277
|
+
*/
|
|
278
|
+
private readonly crossPartyStrandAddrs;
|
|
240
279
|
/**
|
|
241
280
|
* Cold-start bootstrap dial targets: the owner-flagged peers of every seed
|
|
242
281
|
* this node has applied, keyed by peer id, valued by the seed's multiaddr
|
|
@@ -271,6 +310,34 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
271
310
|
* the party if it is ever stranded again.
|
|
272
311
|
*/
|
|
273
312
|
private bootstrapPeerStore;
|
|
313
|
+
/**
|
|
314
|
+
* Node-local record of the party's enrolled-machine count (see
|
|
315
|
+
* `enrolled-machine-store.ts`), kept so the CONTROL node can declare a
|
|
316
|
+
* block-repair yardstick at bring-up. Constructed (or adopted from
|
|
317
|
+
* `config.enrolledMachines.store`) by {@link initializeEnrolledMachineStore};
|
|
318
|
+
* written by {@link refreshAuthorizedControlPeers}. Like
|
|
319
|
+
* {@link bootstrapPeerStore} it is deliberately NOT cleared by {@link cleanup},
|
|
320
|
+
* so it survives a stop()→start() cycle of the same node instance.
|
|
321
|
+
*/
|
|
322
|
+
private enrolledMachineStore;
|
|
323
|
+
/**
|
|
324
|
+
* The enrolled-machine count this node's CONTROL libp2p node was (or will be)
|
|
325
|
+
* built with — read out of {@link enrolledMachineStore} during {@link start},
|
|
326
|
+
* before {@link createControlNode}, and consumed by
|
|
327
|
+
* {@link buildControlNodeOptions}.
|
|
328
|
+
*
|
|
329
|
+
* A captured FIELD rather than a live `store.count()` read, deliberately: the
|
|
330
|
+
* store moves as membership changes, but Optimystic froze the policy when the
|
|
331
|
+
* node was built, so this field is the honest answer to "what did this node
|
|
332
|
+
* actually declare?" and does not drift away from the running node. It is
|
|
333
|
+
* re-read on each {@link start}, which is what makes a stop()→start() cycle pick
|
|
334
|
+
* up a count recorded during the previous run.
|
|
335
|
+
*
|
|
336
|
+
* `undefined` — a brand-new node, an unreadable slot, or the ephemeral default
|
|
337
|
+
* store — means "this node does not know", which `controlClusterPolicy` answers
|
|
338
|
+
* with the frozen base policy itself.
|
|
339
|
+
*/
|
|
340
|
+
private declaredEnrolledMachines;
|
|
274
341
|
/** Initial self-registration timer (see {@link scheduleSelfRegistration}). */
|
|
275
342
|
private selfRegistrationTimer;
|
|
276
343
|
/** TTL heartbeat that re-publishes the self record before it goes stale. */
|
|
@@ -502,6 +569,32 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
502
569
|
* must never enter this node's dial loop.
|
|
503
570
|
*/
|
|
504
571
|
private initializeBootstrapPeerStore;
|
|
572
|
+
/**
|
|
573
|
+
* Construct (or adopt) the node-local enrolled-machine store and capture the
|
|
574
|
+
* count this run will declare into {@link declaredEnrolledMachines}.
|
|
575
|
+
*
|
|
576
|
+
* Synchronous for the same reason {@link initializeBootstrapPeerStore} is: an
|
|
577
|
+
* injected store has already loaded its persisted count by the time it is handed
|
|
578
|
+
* in (its `open` is the async part), and the in-memory default has nothing to
|
|
579
|
+
* load. Must run BEFORE {@link createControlNode} — that is the whole point of
|
|
580
|
+
* the record; see `enrolled-machine-store.ts`.
|
|
581
|
+
*
|
|
582
|
+
* The store instance is kept across stop()→start() (so a count recorded during
|
|
583
|
+
* the previous run is not lost with the ephemeral default), but the DECLARED
|
|
584
|
+
* count is re-read every time — which is how a restart applies a number the
|
|
585
|
+
* previous run learned.
|
|
586
|
+
*
|
|
587
|
+
* A store scoped to a different party is a configuration error, fail closed. It
|
|
588
|
+
* is only a repair hint, but a hint sized by a foreign party's membership is a
|
|
589
|
+
* number nobody chose, and the mismatch always means a miswired embedder.
|
|
590
|
+
*/
|
|
591
|
+
private initializeEnrolledMachineStore;
|
|
592
|
+
/**
|
|
593
|
+
* The node-local enrolled-machine store (null before {@link start}) — what this
|
|
594
|
+
* node last knew about its party's size. Exposed for diagnostics and for a host
|
|
595
|
+
* that wants to show which repair yardstick the next launch will declare.
|
|
596
|
+
*/
|
|
597
|
+
getEnrolledMachineStore(): EnrolledMachineStore | null;
|
|
505
598
|
/**
|
|
506
599
|
* The node-local cold-start bootstrap-peer store (null before {@link start}) —
|
|
507
600
|
* the retained owner addresses {@link reconcileControlCohort}'s cold-start
|
|
@@ -534,6 +627,44 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
534
627
|
*/
|
|
535
628
|
trustOwnerKeys(keys: Iterable<string>, source: Exclude<TrustSource, 'genesis'>): Promise<void>;
|
|
536
629
|
private createControlNode;
|
|
630
|
+
/**
|
|
631
|
+
* The control libp2p node's Optimystic network name — ONE binding for every
|
|
632
|
+
* derivation from it (`db-p2p` namespaces all of the node's protocol ids as
|
|
633
|
+
* `/optimystic/<networkName>/...`), so the node options and the block-transfer
|
|
634
|
+
* protocol prefix the control backfill dials can never drift apart.
|
|
635
|
+
*/
|
|
636
|
+
private controlNetworkName;
|
|
637
|
+
/**
|
|
638
|
+
* Arm the CONTROL network's peer-join block catch-up: push every block in the
|
|
639
|
+
* control database's own raw store to each newly connected AUTHORIZED member
|
|
640
|
+
* this runtime has not yet caught up. This is what physically replicates
|
|
641
|
+
* control blocks committed while the writer was alone — the named
|
|
642
|
+
* collection-header blocks written once at genesis above all, whose revision
|
|
643
|
+
* never moves again, so no later commit ever carries them to a member that
|
|
644
|
+
* joined after them. Without it, such a member that restarts offline reads
|
|
645
|
+
* the affected control tables as EMPTY, silently (`isMember()` answers false
|
|
646
|
+
* for peers it knew about before the restart).
|
|
647
|
+
*
|
|
648
|
+
* Unlike the per-strand instances (see `strand-instance-manager.ts`), pushes
|
|
649
|
+
* are gated on {@link isAuthorizedMember}, judged at push time: the control
|
|
650
|
+
* network's inbound connection gate deliberately admits non-members in
|
|
651
|
+
* several states (seed delivery to an un-enrolled node, an open enrollment
|
|
652
|
+
* window, an outstanding invitation, configured bootstrap/relay peers), and
|
|
653
|
+
* pushing the whole control store to such a peer would hand a stranger the
|
|
654
|
+
* party's entire membership, addresses and strand list. The receiving side
|
|
655
|
+
* needs no work of its own: `createLibp2pNode` registers the block-transfer
|
|
656
|
+
* handler on every node it builds, control node included, and this node's
|
|
657
|
+
* per-stream gate (`authorizeInboundControlStream`) covers the inbound
|
|
658
|
+
* direction. A denied peer is retried on its next `connection:open`, and —
|
|
659
|
+
* because the production join order is connect-then-authorize — on every
|
|
660
|
+
* committed membership change via {@link refreshAuthorizedControlPeers}'s
|
|
661
|
+
* `scheduleConnectedPeers()` call.
|
|
662
|
+
*
|
|
663
|
+
* No-ops (logged) when the embedder configured no control storage or the
|
|
664
|
+
* node exposes no key network — the backfill would have nothing to read or
|
|
665
|
+
* no way to dial.
|
|
666
|
+
*/
|
|
667
|
+
private startControlBackfill;
|
|
537
668
|
/**
|
|
538
669
|
* The control database's cache-wrapped raw storage, resolved from
|
|
539
670
|
* `config.storage.provider` on first use and then held in {@link controlStorage}
|
|
@@ -857,6 +988,24 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
857
988
|
* producing an unresolvable row.
|
|
858
989
|
*/
|
|
859
990
|
private getSelfSigningKey;
|
|
991
|
+
/**
|
|
992
|
+
* Does the `Strand` row name THIS machine as its founder — i.e. is its
|
|
993
|
+
* `FounderOwnerKey` this node's own owner key (the key behind its PeerId)?
|
|
994
|
+
* A null/absent column (a consent-seated strand, or a hand-built row) and a
|
|
995
|
+
* node with no owner key both derive `false`: without a positive match this
|
|
996
|
+
* machine must attach, never bootstrap. Pure key derivation, no I/O — cheap
|
|
997
|
+
* enough for {@link launchStrand}'s tracked-instance early return.
|
|
998
|
+
*
|
|
999
|
+
* NOTE: reads "one owner key per machine" — the reference model, where the owner
|
|
1000
|
+
* key IS the key behind the PeerId. Two machines running the SAME identity key
|
|
1001
|
+
* would both derive `true` and each bootstrap on its own replica, which is the
|
|
1002
|
+
* double-`Header` hazard this derivation exists to avoid. Unreachable today: that
|
|
1003
|
+
* configuration also gives both machines one PeerId, which already breaks control
|
|
1004
|
+
* networking well before any strand launches. Revisit if machines ever share an
|
|
1005
|
+
* owner key while holding distinct transport identities — the derivation would
|
|
1006
|
+
* then need a per-machine discriminator (the `CadrePeer` PeerId) on the row.
|
|
1007
|
+
*/
|
|
1008
|
+
private isSelfFoundedRow;
|
|
860
1009
|
/**
|
|
861
1010
|
* The owner keypair (base64url Ed25519) derived from this node's resolved
|
|
862
1011
|
* identity key. In the single-key reference model the owner signing key is
|
|
@@ -939,10 +1088,15 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
939
1088
|
* One address bound to `peerId`, or `null` when it cannot be — logged either
|
|
940
1089
|
* way, never thrown, so every caller's list-shaping stays total.
|
|
941
1090
|
*
|
|
1091
|
+
* Used both to normalize addresses this node DIALS (a `CadrePeer` row, a
|
|
1092
|
+
* seed-supplied bootstrap addr) and to normalize the ones it ANNOUNCES
|
|
1093
|
+
* ({@link getStrandMultiaddrs}); the rule is the same either way — an address
|
|
1094
|
+
* that does not reach `peerId` is not an address for `peerId`.
|
|
1095
|
+
*
|
|
942
1096
|
* `withTrailingPeerId` encapsulates `/p2p/<peerId>`, which throws on a peer id
|
|
943
1097
|
* that does not parse. Unreachable for a `CadrePeer` row (the binding gate
|
|
944
|
-
* above parsed it already), reachable for a
|
|
945
|
-
* ({@link bootstrapDialAddrs}).
|
|
1098
|
+
* above parsed it already) and for our own node's id, reachable for a
|
|
1099
|
+
* seed-supplied one ({@link bootstrapDialAddrs}).
|
|
946
1100
|
*/
|
|
947
1101
|
private bindAddrToPeer;
|
|
948
1102
|
/** Parse multiaddr strings, dropping (and logging) any that fail to parse. */
|
|
@@ -1375,8 +1529,8 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1375
1529
|
/**
|
|
1376
1530
|
* Rebuild a quiesced strand's runtime, re-resolving the volatile cohort input
|
|
1377
1531
|
* first: the discovery seed may have grown since the strand last ran. Shared by
|
|
1378
|
-
* the wake (`handleStrandWake`) and check-in (`handleStrandCheckIn`) paths so
|
|
1379
|
-
*
|
|
1532
|
+
* the wake (`handleStrandWake`) and check-in (`handleStrandCheckIn`) paths so both
|
|
1533
|
+
* apply the same fresh resolution. `resumeStrand` is idempotent
|
|
1380
1534
|
* (returns the live instance unchanged) as a backstop against double-resume.
|
|
1381
1535
|
*/
|
|
1382
1536
|
private resumeStrandRuntime;
|
|
@@ -1437,6 +1591,14 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1437
1591
|
* Add a strand with its sApp configuration.
|
|
1438
1592
|
* The hosting application must provide the sApp schema when creating a strand.
|
|
1439
1593
|
*
|
|
1594
|
+
* This is the ATTACH half only — it starts the local instance and never publishes the
|
|
1595
|
+
* `Strand` row. A joiner (the row arrived over the control network) wants exactly this; a
|
|
1596
|
+
* FOUNDER wants {@link foundStrand}, which publishes and attaches in one resumable call.
|
|
1597
|
+
* With no explicit `founder` flag, founder-ness is derived from the row's
|
|
1598
|
+
* `FounderOwnerKey` (see {@link StrandConfig.founder}) — so attaching a row THIS machine
|
|
1599
|
+
* published (e.g. re-attaching its own orphan after a restart) founds it, and attaching
|
|
1600
|
+
* anyone else's row joins, without the caller needing to know which it is.
|
|
1601
|
+
*
|
|
1440
1602
|
* A rejected call leaves nothing running but DOES leave the sApp config
|
|
1441
1603
|
* registered, deliberately: both an explicit retry and the {@link StrandWatcher}'s
|
|
1442
1604
|
* automatic relaunch need it. So once the strand's row is visible on the control
|
|
@@ -1451,16 +1613,41 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1451
1613
|
* owner identity, so other cadre members discover it via control-network
|
|
1452
1614
|
* sync (their {@link StrandWatcher} fires `strand:discovered`).
|
|
1453
1615
|
*
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1458
|
-
* `addStrand` (the row already exists).
|
|
1616
|
+
* **Founding a strand is TWO steps** — this one (publish the row cadre-wide) and
|
|
1617
|
+
* {@link addStrand} (start the local instance, `founder: true` to write the strand's
|
|
1618
|
+
* `Header`/founding membership). `addStrand` deliberately omits the insert: it only
|
|
1619
|
+
* starts the LOCAL instance, whereas publishing makes the strand visible cadre-wide. A
|
|
1620
|
+
* discovering peer only does `addStrand` (the row already exists). Callers founding a
|
|
1621
|
+
* strand should use {@link foundStrand}, which performs both steps and is safe to
|
|
1622
|
+
* re-run — hand-rolling the pair is what left strands half-founded when an app was
|
|
1623
|
+
* killed between them.
|
|
1624
|
+
*
|
|
1625
|
+
* **Idempotent for identical content.** A live row whose `(Type, MemberPrivateKey)` match
|
|
1626
|
+
* the arguments is the state this call would have produced, so the call logs and returns
|
|
1627
|
+
* that row instead of writing — a publish interrupted after it committed can be repeated.
|
|
1628
|
+
* DIFFERENT content on the same id throws, naming the columns that differ, rather than
|
|
1629
|
+
* surfacing the raw `UNIQUE constraint failed: Strand.Id`: silently accepting it would
|
|
1630
|
+
* let a retry reopen a closed strand or swap the key gating its reads. The read and the
|
|
1631
|
+
* insert are not atomic, so a concurrent founder (two machines of one party, same id) is
|
|
1632
|
+
* caught on the insert's uniqueness rejection and resolved the same way — re-read, no-op
|
|
1633
|
+
* on a match, rethrow otherwise. Never an overwrite either way.
|
|
1634
|
+
*
|
|
1635
|
+
* A tombstoned strand is NOT resurrected here: {@link unpublishStrand} deletes the row, so
|
|
1636
|
+
* the idempotent branch is unreachable and the ordinary publish path re-seats it, exactly
|
|
1637
|
+
* as that method documents.
|
|
1638
|
+
*
|
|
1639
|
+
* **Already stuck on `UNIQUE constraint failed: Strand.Id`?** The row is already published
|
|
1640
|
+
* — attach, do NOT republish: `addStrand` (or {@link foundStrand}) with the id and, for a
|
|
1641
|
+
* closed strand, the `MemberPrivateKey` read back from the row. `unpublishStrand` + a
|
|
1642
|
+
* fresh publish also clears it for an OPEN strand, but is destructive for a closed one
|
|
1643
|
+
* (the key exists nowhere else). No app-data wipe is needed for either.
|
|
1459
1644
|
*
|
|
1460
1645
|
* The insert is signed with the ed25519 key behind this node's PeerId — which
|
|
1461
1646
|
* {@link ed25519KeyPairFromLibp2p} also exposes as the node's owner keypair,
|
|
1462
|
-
* so peer identity and owner key are one and the same. That key
|
|
1463
|
-
*
|
|
1647
|
+
* so peer identity and owner key are one and the same. That key is also persisted on the
|
|
1648
|
+
* row as `FounderOwnerKey` (the schema pins the column to the verified signer), which is
|
|
1649
|
+
* what later lets any launch derive "this machine is the founder" from the row alone.
|
|
1650
|
+
* The key must be enrolled in `OwnerKey` (e.g. via {@link ControlDatabase.ensureOwnerKey}
|
|
1464
1651
|
* at genesis) or the schema's `Strand.AuthorizedInsert` constraint rejects the write.
|
|
1465
1652
|
* Failing loudly here is intentional: a silently-unpublished strand would run
|
|
1466
1653
|
* as a local-only island that no peer could ever discover or join.
|
|
@@ -1469,10 +1656,62 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1469
1656
|
* {@link addStrand}).
|
|
1470
1657
|
* @param type - `'o'` for open (default) or `'c'` for closed.
|
|
1471
1658
|
* @param memberPrivateKey - Optional membership key for a closed strand.
|
|
1472
|
-
* @
|
|
1473
|
-
*
|
|
1474
|
-
|
|
1475
|
-
|
|
1659
|
+
* @returns The live `Strand` row — the one just inserted, or the matching one already
|
|
1660
|
+
* there. A closed strand's caller should carry THIS row's `MemberPrivateKey` forward:
|
|
1661
|
+
* on a repeat it is the stored key, not the argument.
|
|
1662
|
+
* @throws if the node is not started, exposes no owner signing key, the id is blank, a
|
|
1663
|
+
* row with the same id holds different content, or the control DB rejects the
|
|
1664
|
+
* (unauthorized) insert.
|
|
1665
|
+
*/
|
|
1666
|
+
publishStrand(strandId: string, type?: 'o' | 'c', memberPrivateKey?: string): Promise<StrandRow>;
|
|
1667
|
+
/**
|
|
1668
|
+
* Found a strand — publish its row cadre-wide AND start the local instance as its
|
|
1669
|
+
* founder — in one resumable call. The single entry point for creating a strand; a
|
|
1670
|
+
* caller joining one someone else founded uses {@link addStrand} alone.
|
|
1671
|
+
*
|
|
1672
|
+
* Safe to re-run from any point of interruption, which hand-rolling
|
|
1673
|
+
* {@link publishStrand} + {@link addStrand} is not. Either half can already have
|
|
1674
|
+
* happened:
|
|
1675
|
+
*
|
|
1676
|
+
* - **row already published** → the stored row is adopted rather than re-published, so a
|
|
1677
|
+
* run killed after the insert committed no longer dies on
|
|
1678
|
+
* `UNIQUE constraint failed: Strand.Id`. For a closed strand the row's STORED
|
|
1679
|
+
* `MemberPrivateKey` wins over `config.memberPrivateKey`: a caller that mints a key per
|
|
1680
|
+
* attempt (as the reference apps do) would otherwise present a key that does not match
|
|
1681
|
+
* the membership already seated in the strand.
|
|
1682
|
+
* - **instance already running** → the tracked instance is returned, and — because the
|
|
1683
|
+
* `Strand` row records its publishing machine (`FounderOwnerKey`) — a founder request
|
|
1684
|
+
* against an instance that was first launched as a joiner now runs the (idempotent,
|
|
1685
|
+
* insert-if-absent) founder bootstrap on it rather than silently skipping it
|
|
1686
|
+
* ({@link StrandInstanceManager.foundExistingStrand}). So a strand that something else
|
|
1687
|
+
* attached first — the reference RN app's `strand:discovered` handler after a restart,
|
|
1688
|
+
* or this node's own {@link StrandWatcher} poll winning `launchStrand`'s
|
|
1689
|
+
* `resolveCohortSeed` window — still ends up with its `Strand.Header` written.
|
|
1690
|
+
*
|
|
1691
|
+
* Founder-ness is resolved FROM THE ROW, not assumed: this machine founds iff the
|
|
1692
|
+
* resolved row's `FounderOwnerKey` is its own owner key. A fresh publish records this
|
|
1693
|
+
* machine, so the common path founds; a machine that lost a concurrent founding race to
|
|
1694
|
+
* a sibling adopts the sibling's row and ATTACHES instead — deliberately, since two
|
|
1695
|
+
* machines bootstrapping the same strand on separate replicas is the double-`Header`
|
|
1696
|
+
* hazard. The returned {@link FoundStrandResult.founded} says which happened.
|
|
1697
|
+
*
|
|
1698
|
+
* `Type` is compared, not adopted: a stored row of the other type means the caller and
|
|
1699
|
+
* the control plane disagree about what this strand IS, so it throws.
|
|
1700
|
+
*
|
|
1701
|
+
* @returns The instance AND the row the strand actually runs under — read the membership
|
|
1702
|
+
* key from the returned row, not from a freshly minted one ({@link FoundStrandResult}).
|
|
1703
|
+
* @throws if the node is not started, the id is blank, a published row of the same id has
|
|
1704
|
+
* a different `Type`, or either half rejects.
|
|
1705
|
+
*/
|
|
1706
|
+
foundStrand(config: FoundStrandConfig): Promise<FoundStrandResult>;
|
|
1707
|
+
/**
|
|
1708
|
+
* Resume onto an already-published `Strand` row: keep its stored content, reject a type
|
|
1709
|
+
* disagreement. The stored `MemberPrivateKey` is authoritative — see {@link foundStrand}
|
|
1710
|
+
* for why a caller-minted key must lose here (and why that is NOT the same rule as
|
|
1711
|
+
* {@link publishStrand}'s, which throws on a key mismatch because it is being asked to
|
|
1712
|
+
* WRITE that key, not to resume onto what already exists).
|
|
1713
|
+
*/
|
|
1714
|
+
private adoptPublishedStrand;
|
|
1476
1715
|
/**
|
|
1477
1716
|
* Remove this party's `Strand` row from the shared control database — the owner-signed
|
|
1478
1717
|
* inverse of {@link publishStrand}.
|
|
@@ -1624,14 +1863,25 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1624
1863
|
* seed, starts the strand, and registers it with the hibernation manager
|
|
1625
1864
|
* before emitting `strand:started`.
|
|
1626
1865
|
*
|
|
1627
|
-
*
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
1631
|
-
*
|
|
1866
|
+
* Founder-ness: an explicit `founder` argument wins (the formation/responder flows pass
|
|
1867
|
+
* one deliberately — their consent-seated rows carry a null `FounderOwnerKey`); when
|
|
1868
|
+
* unset it is DERIVED from the row, so the control-discovered path
|
|
1869
|
+
* (`handleStrandAdded`) and a restart's re-attach found this machine's own strands
|
|
1870
|
+
* without the caller having to know. The derivation is pure key comparison — no I/O.
|
|
1871
|
+
*
|
|
1872
|
+
* Idempotent when the strand manager already tracks `strand.Id` — the watcher
|
|
1873
|
+
* rediscovers a row this node already started and calls this again via
|
|
1874
|
+
* `handleStrandAdded`; without the guard that re-entry would resolve a fresh
|
|
1632
1875
|
* (already-connected) cohort seed and re-emit `strand:started` for an
|
|
1633
1876
|
* instance that never stopped. The guard runs before the cohort-seed RPC
|
|
1634
|
-
* fan-out, so a rediscovery costs nothing beyond the map lookup
|
|
1877
|
+
* fan-out, so a rediscovery costs nothing beyond the map lookup plus the (pure)
|
|
1878
|
+
* founder derivation. NOT a silent no-op any more when the launch resolves as a
|
|
1879
|
+
* FOUNDER: the tracked instance may have been launched first as a joiner (an app
|
|
1880
|
+
* attach, or this node's own watcher poll winning the `resolveCohortSeed` window
|
|
1881
|
+
* below), which used to drop the founder request and leave the strand headerless —
|
|
1882
|
+
* now the tracked instance is founded in place
|
|
1883
|
+
* ({@link StrandInstanceManager.foundExistingStrand}), waking it first if quiesced
|
|
1884
|
+
* so the bootstrap actually runs before this resolves.
|
|
1635
1885
|
*/
|
|
1636
1886
|
private launchStrand;
|
|
1637
1887
|
/**
|
|
@@ -1661,6 +1911,13 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1661
1911
|
* for a relay we are not yet connected to.
|
|
1662
1912
|
*/
|
|
1663
1913
|
private resolveCohortSeed;
|
|
1914
|
+
/**
|
|
1915
|
+
* The own-party half of {@link resolveCohortSeed}: strand-network addresses resolved
|
|
1916
|
+
* from CONNECTED cohort siblings over the control-mesh strand-addr RPC, doubling as
|
|
1917
|
+
* the delegate announcement when `delegatePeerId` is given. Empty when the control DB
|
|
1918
|
+
* or node is absent (not yet started / torn down).
|
|
1919
|
+
*/
|
|
1920
|
+
private resolveSiblingSeed;
|
|
1664
1921
|
/**
|
|
1665
1922
|
* The co-cadre siblings worth sending a strand-addr RPC to right now: cohort
|
|
1666
1923
|
* members (self excluded) we already hold an open control connection to. They
|
|
@@ -1774,9 +2031,18 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1774
2031
|
*/
|
|
1775
2032
|
private refreshStrandPeerAddrs;
|
|
1776
2033
|
/**
|
|
1777
|
-
* One strand's share of {@link refreshStrandPeerAddrs}: RPC the siblings,
|
|
1778
|
-
*
|
|
1779
|
-
* swallowed so one strand's failure never
|
|
2034
|
+
* One strand's share of {@link refreshStrandPeerAddrs}: RPC the siblings, union their
|
|
2035
|
+
* answers with this strand's cross-party contact addresses, and merge the lot into the
|
|
2036
|
+
* strand's address book. Errors are logged and swallowed so one strand's failure never
|
|
2037
|
+
* costs the others their refresh.
|
|
2038
|
+
*
|
|
2039
|
+
* `targets` may be EMPTY — a solo node, or one whose only control peers are strangers.
|
|
2040
|
+
* The cross-party addresses still have to be re-merged in that case: they were learned
|
|
2041
|
+
* at formation and can never be re-resolved, so this periodic re-merge is the ONLY
|
|
2042
|
+
* thing standing between them and the peerStore's one-hour expiry. A strand with
|
|
2043
|
+
* neither a sibling to ask nor a contact entry does nothing and leaves its throttle
|
|
2044
|
+
* unstamped, so the next reconcile tick retries rather than sitting out the whole
|
|
2045
|
+
* refresh interval having done nothing.
|
|
1780
2046
|
*/
|
|
1781
2047
|
private refreshOneStrandPeerAddrs;
|
|
1782
2048
|
/**
|
|
@@ -1797,6 +2063,15 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
1797
2063
|
* handshake, and a bad entry costs one failed dial that ages out at the
|
|
1798
2064
|
* peerStore's one-hour expiry. No new gating here — cross-party strand trust is
|
|
1799
2065
|
* `backlog/strand-network-nat-relay-reachability`.
|
|
2066
|
+
*
|
|
2067
|
+
* NOTE: one input does NOT age out — the cross-party addresses a formation
|
|
2068
|
+
* carried back ({@link crossPartyStrandAddrs}). Nothing can re-resolve them, so
|
|
2069
|
+
* {@link refreshStrandPeerAddrs} re-merges the same stored list every pass, which
|
|
2070
|
+
* also means a junk entry from a responder survives for the node's lifetime rather
|
|
2071
|
+
* than an hour. Bounded to {@link MAX_STRAND_ADDRS} per strand, from a party this
|
|
2072
|
+
* node deliberately formed a strand with, and still authority-free — so the
|
|
2073
|
+
* exposure is a handful of failed dials, not a trust hole. Evict on
|
|
2074
|
+
* `unpublishStrand` if that ever stops being an acceptable price.
|
|
1800
2075
|
*/
|
|
1801
2076
|
private mergeStrandPeerAddrs;
|
|
1802
2077
|
/**
|
|
@@ -2191,6 +2466,23 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
2191
2466
|
* best-effort (a failed read keeps the previous snapshot); never rejects.
|
|
2192
2467
|
*/
|
|
2193
2468
|
refreshMembershipGate(reason?: string): Promise<void>;
|
|
2469
|
+
/**
|
|
2470
|
+
* Re-materialize a CLOSED strand's revoked-peer deny set now, and hang up any
|
|
2471
|
+
* connected peer it newly covers — the strand-side counterpart to
|
|
2472
|
+
* {@link refreshMembershipGate}.
|
|
2473
|
+
*
|
|
2474
|
+
* Unlike that one, NOTHING calls this automatically: strand membership is
|
|
2475
|
+
* written through the strand's own `Database` handle
|
|
2476
|
+
* (`strand-membership-writer.ts`), which raises no notification this runtime
|
|
2477
|
+
* can hook, and a revocation that arrives by REPLICATION raises none either.
|
|
2478
|
+
* The gate therefore polls (default 30 s). An app that has just called
|
|
2479
|
+
* `revokeMember` or `leaveStrand` should follow the write with this call so
|
|
2480
|
+
* the cut is immediate rather than up to one poll interval late.
|
|
2481
|
+
*
|
|
2482
|
+
* Quiet no-op for a strand that is not running, is quiesced, is open, or has
|
|
2483
|
+
* the gate disabled. Never rejects; resolves once the sweep has finished.
|
|
2484
|
+
*/
|
|
2485
|
+
refreshRevocationEnforcement(strandId: string): Promise<void>;
|
|
2194
2486
|
/**
|
|
2195
2487
|
* The single in-flight refresh loop behind {@link refreshMembershipGate}: read
|
|
2196
2488
|
* until the stale flag stays clear, then release the slot.
|
|
@@ -2337,6 +2629,17 @@ export declare class CadreNode implements SAppIdLookup {
|
|
|
2337
2629
|
* @returns The member key and strand info if successful
|
|
2338
2630
|
*/
|
|
2339
2631
|
formStrand(invitation: OpenInvitation, disclosure?: StrandFormationDisclosure): Promise<FormStrandResult>;
|
|
2632
|
+
/**
|
|
2633
|
+
* Remember the responder's strand-network addresses for a strand this node just
|
|
2634
|
+
* formed, so the strand's discovery seed has something to work with when the app
|
|
2635
|
+
* launches it (see {@link crossPartyStrandAddrs}).
|
|
2636
|
+
*
|
|
2637
|
+
* Scoped strictly to `strandId`: these addresses reach ONE strand node of ONE other
|
|
2638
|
+
* party and must never seed another strand's mesh or the control peerStore. An empty
|
|
2639
|
+
* list is recorded as no entry at all rather than an empty one, so a later formation
|
|
2640
|
+
* against the same strand that DOES disclose addresses is not shadowed.
|
|
2641
|
+
*/
|
|
2642
|
+
private recordCrossPartyStrandAddrs;
|
|
2340
2643
|
/**
|
|
2341
2644
|
* Encode an open invitation for out-of-band delivery (QR, link, etc.).
|
|
2342
2645
|
*/
|