@serfab/cadre-core 1.7.0 → 1.9.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 +8 -6
- package/dist/cadre-node.d.ts +527 -206
- package/dist/cadre-node.js +1084 -528
- 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 +6 -0
- package/dist/control-cohort.js +6 -0
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +46 -8
- package/dist/control-database.js +97 -18
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +37 -14
- package/dist/control-formation-recorder.js +60 -10
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-read-retry.d.ts +13 -12
- package/dist/control-read-retry.js +14 -12
- package/dist/control-read-retry.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +67 -5
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +9 -1
- package/dist/control-stream.js +24 -1
- package/dist/control-stream.js.map +1 -1
- package/dist/control-write-retry.d.ts +12 -2
- package/dist/control-write-retry.js +13 -2
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +11 -8
- package/dist/delegate-admission.js +19 -11
- package/dist/delegate-admission.js.map +1 -1
- package/dist/diagnostics/connection-path.d.ts +0 -7
- package/dist/diagnostics/connection-path.js +0 -7
- package/dist/diagnostics/connection-path.js.map +1 -1
- package/dist/formation-approval.d.ts +7 -0
- package/dist/formation-approval.js +9 -3
- package/dist/formation-approval.js.map +1 -1
- package/dist/fs-atomic.d.ts +11 -2
- package/dist/fs-atomic.js +13 -3
- package/dist/fs-atomic.js.map +1 -1
- package/dist/hibernation-manager.d.ts +111 -8
- package/dist/hibernation-manager.js +234 -49
- package/dist/hibernation-manager.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.js +25 -20
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +90 -25
- package/dist/joined-strand-store.js +223 -55
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store-file.js +14 -5
- package/dist/key-store-file.js.map +1 -1
- package/dist/link-budget.d.ts +367 -69
- package/dist/link-budget.js +375 -72
- package/dist/link-budget.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +30 -43
- package/dist/membership-connection-gater.js +31 -42
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +6 -4
- package/dist/node-local-snapshot.js +7 -3
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/peer-dial.d.ts +24 -4
- package/dist/peer-dial.js +74 -5
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +2 -10
- package/dist/peer-join-backfill.js +8 -8
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/push-fanout.d.ts +6 -0
- package/dist/push-fanout.js +6 -0
- package/dist/push-fanout.js.map +1 -1
- package/dist/relay-reservation.d.ts +3 -1
- package/dist/relay-reservation.js +3 -1
- package/dist/relay-reservation.js.map +1 -1
- package/dist/relay-server.d.ts +79 -0
- package/dist/relay-server.js +74 -0
- package/dist/relay-server.js.map +1 -0
- package/dist/seed-bootstrap.d.ts +57 -4
- package/dist/seed-bootstrap.js +107 -83
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/storage-scope.d.ts +33 -17
- package/dist/storage-scope.js +44 -23
- package/dist/storage-scope.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +58 -21
- package/dist/strand-addr-protocol.js +133 -85
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-database.d.ts +20 -2
- package/dist/strand-database.js +42 -5
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +59 -21
- package/dist/strand-first-sync-gate.js +58 -19
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +151 -0
- package/dist/strand-formation-deadlines.js +171 -0
- package/dist/strand-formation-deadlines.js.map +1 -0
- package/dist/strand-formation-manager.d.ts +63 -30
- package/dist/strand-formation-manager.js +67 -41
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +45 -46
- package/dist/strand-formation-protocol.js +62 -126
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-fret-addrs.d.ts +54 -0
- package/dist/strand-fret-addrs.js +108 -0
- package/dist/strand-fret-addrs.js.map +1 -0
- package/dist/strand-instance-manager.d.ts +122 -44
- package/dist/strand-instance-manager.js +270 -114
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +6 -0
- package/dist/strand-membership-writer.js +11 -0
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-network-state-file.d.ts +12 -0
- package/dist/strand-network-state-file.js +38 -0
- package/dist/strand-network-state-file.js.map +1 -0
- package/dist/strand-network-state.d.ts +111 -0
- package/dist/strand-network-state.js +191 -0
- package/dist/strand-network-state.js.map +1 -0
- package/dist/strand-revocation-enforcer.js +2 -1
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +36 -20
- package/dist/strand-solicitation.js +4 -4
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +80 -23
- package/dist/strand-wake-protocol.js +89 -64
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/types.d.ts +148 -61
- package/dist/types.js +4 -3
- package/dist/types.js.map +1 -1
- package/package.json +11 -10
- package/src/cadre-node.ts +8400 -7776
- package/src/control-authorization.ts +2 -1
- package/src/control-cohort.ts +6 -0
- package/src/control-database.ts +122 -18
- package/src/control-formation-recorder.ts +354 -305
- package/src/control-read-retry.ts +14 -12
- package/src/control-schema.ts +67 -5
- package/src/control-stream.ts +25 -1
- package/src/control-write-retry.ts +13 -2
- package/src/delegate-admission.ts +23 -13
- package/src/diagnostics/connection-path.ts +0 -7
- package/src/formation-approval.ts +610 -604
- package/src/fs-atomic.ts +14 -4
- package/src/hibernation-manager.ts +638 -437
- package/src/index.ts +43 -65
- package/src/joined-strand-store.ts +247 -57
- package/src/key-store-file.ts +14 -5
- package/src/link-budget.ts +393 -74
- package/src/membership-connection-gater.ts +31 -45
- package/src/node-local-snapshot.ts +12 -7
- package/src/peer-dial.ts +81 -7
- package/src/peer-join-backfill.ts +10 -9
- package/src/push-fanout.ts +6 -0
- package/src/relay-reservation.ts +3 -1
- package/src/relay-server.ts +98 -0
- package/src/seed-bootstrap.ts +133 -91
- package/src/storage-scope.ts +44 -23
- package/src/strand-addr-protocol.ts +547 -458
- package/src/strand-database.ts +287 -242
- package/src/strand-first-sync-gate.ts +58 -19
- package/src/strand-formation-deadlines.ts +227 -0
- package/src/strand-formation-manager.ts +700 -647
- package/src/strand-formation-protocol.ts +106 -154
- package/src/strand-fret-addrs.ts +145 -0
- package/src/strand-instance-manager.ts +307 -140
- package/src/strand-membership-writer.ts +12 -0
- package/src/strand-network-state-file.ts +40 -0
- package/src/strand-network-state.ts +243 -0
- package/src/strand-revocation-enforcer.ts +2 -1
- package/src/strand-solicitation.ts +519 -500
- package/src/strand-wake-protocol.ts +401 -355
- package/src/types.ts +150 -61
- package/dist/strand-peer-book-file.d.ts +0 -12
- package/dist/strand-peer-book-file.js +0 -37
- package/dist/strand-peer-book-file.js.map +0 -1
- package/dist/strand-peer-book-protocol.d.ts +0 -159
- package/dist/strand-peer-book-protocol.js +0 -360
- package/dist/strand-peer-book-protocol.js.map +0 -1
- package/dist/strand-peer-book-swap.d.ts +0 -104
- package/dist/strand-peer-book-swap.js +0 -284
- package/dist/strand-peer-book-swap.js.map +0 -1
- package/dist/strand-peer-book.d.ts +0 -159
- package/dist/strand-peer-book.js +0 -354
- package/dist/strand-peer-book.js.map +0 -1
- package/dist/strand-peer-observer.d.ts +0 -88
- package/dist/strand-peer-observer.js +0 -180
- package/dist/strand-peer-observer.js.map +0 -1
- package/src/strand-peer-book-file.ts +0 -39
- package/src/strand-peer-book-protocol.ts +0 -463
- package/src/strand-peer-book-swap.ts +0 -338
- package/src/strand-peer-book.ts +0 -468
- package/src/strand-peer-observer.ts +0 -235
package/src/index.ts
CHANGED
|
@@ -117,10 +117,10 @@ export {
|
|
|
117
117
|
type EnrolledMachineStore
|
|
118
118
|
} from './enrolled-machine-store.js';
|
|
119
119
|
|
|
120
|
-
// Node-local record of the strands this node joined from ANOTHER party
|
|
121
|
-
// every start as `strand:discovered`.
|
|
122
|
-
// KeyStore seam above (the record carries a
|
|
123
|
-
// no file-backed subpath.
|
|
120
|
+
// Node-local record of the strands this node joined from ANOTHER party and has not yet
|
|
121
|
+
// published party-wide, re-offered on every start as `strand:discovered`.
|
|
122
|
+
// Cross-platform: its durable form rides the KeyStore seam above (the record carries a
|
|
123
|
+
// closed strand's read secret), so there is no file-backed subpath.
|
|
124
124
|
export {
|
|
125
125
|
MemoryJoinedStrandStore,
|
|
126
126
|
KeyStoreJoinedStrandStore,
|
|
@@ -128,66 +128,27 @@ export {
|
|
|
128
128
|
type JoinedStrandRecord
|
|
129
129
|
} from './joined-strand-store.js';
|
|
130
130
|
|
|
131
|
-
// Node-local strand
|
|
132
|
-
//
|
|
133
|
-
// re-
|
|
134
|
-
// above — interface + in-memory + slot-backed stores
|
|
135
|
-
//
|
|
136
|
-
export {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
type StrandPeerEntry
|
|
147
|
-
} from './strand-peer-book.js';
|
|
148
|
-
export {
|
|
149
|
-
StrandPeerObserver,
|
|
150
|
-
dialableAddrs,
|
|
151
|
-
connectedIdentifiedPeers,
|
|
152
|
-
STRAND_PEER_OBSERVE_THROTTLE_MS,
|
|
153
|
-
type StrandPeerObservation,
|
|
154
|
-
type StrandPeerObserverDeps,
|
|
155
|
-
type StrandPeerObserverOptions,
|
|
156
|
-
type ConnectedIdentifiedPeer
|
|
157
|
-
} from './strand-peer-observer.js';
|
|
158
|
-
// The signed book SWAP between strand peers (`/sereus/strand-peers/1.0.0`): the wire
|
|
159
|
-
// protocol, signing and verification, and the per-strand driver that arms it.
|
|
160
|
-
export {
|
|
161
|
-
STRAND_PEER_BOOK_PROTOCOL,
|
|
162
|
-
SIGNED_STRAND_PEER_ENTRY_VERSION,
|
|
163
|
-
STRAND_PEER_ISSUED_AT_SKEW_MS,
|
|
164
|
-
MAX_BOOK_FRAME_SIZE,
|
|
165
|
-
StrandPeerBookService,
|
|
166
|
-
exchangeStrandPeerBook,
|
|
167
|
-
signStrandPeerEntry,
|
|
168
|
-
signedStrandPeerEntryPayload,
|
|
169
|
-
verifySignedStrandPeerEntry,
|
|
170
|
-
verifyStrandPeerBookFrame,
|
|
171
|
-
trimBookFrameToFit,
|
|
172
|
-
assertBookFrameFits,
|
|
173
|
-
type SignedStrandPeerEntry,
|
|
174
|
-
type StrandPeerBookFrame,
|
|
175
|
-
type StrandPeerEntryVerdict,
|
|
176
|
-
type StrandPeerBookServiceDeps,
|
|
177
|
-
type ExchangeStrandPeerBookOptions
|
|
178
|
-
} from './strand-peer-book-protocol.js';
|
|
179
|
-
export {
|
|
180
|
-
StrandPeerBookSwap,
|
|
181
|
-
STRAND_PEER_BOOK_SWAP_THROTTLE_MS,
|
|
182
|
-
OWN_ENTRY_RESIGN_DEBOUNCE_MS,
|
|
183
|
-
type StrandPeerBookSwapDeps,
|
|
184
|
-
type StrandPeerBookSwapOptions
|
|
185
|
-
} from './strand-peer-book-swap.js';
|
|
131
|
+
// Node-local strand network state: per strand, the state Optimystic's db-p2p saves for
|
|
132
|
+
// the strand node (its FRET routing table with each peer's signed address record), so a
|
|
133
|
+
// restarted strand node re-imports the peers it was talking to. Same cross-platform
|
|
134
|
+
// split as the bootstrap-peer store above — interface + in-memory + slot-backed stores
|
|
135
|
+
// here, Node-only file backend behind '@serfab/cadre-core/strand-network-state-file'.
|
|
136
|
+
export {
|
|
137
|
+
MemoryStrandNetworkStateStore,
|
|
138
|
+
PersistentStrandNetworkStateStore,
|
|
139
|
+
strandNetworkStatePersistence,
|
|
140
|
+
type StrandNetworkStateStore
|
|
141
|
+
} from './strand-network-state.js';
|
|
142
|
+
// The signed address records a running strand node's FRET table holds, as per-peer dial
|
|
143
|
+
// addresses: what the periodic refresh re-merges into the node's address book, and a
|
|
144
|
+
// diagnostic for "whom could this strand node redial right now?".
|
|
145
|
+
export { strandFretPeerAddrs, type FretAddrHost, type FretPeerAddrs } from './strand-fret-addrs.js';
|
|
186
146
|
|
|
187
147
|
// Storage scope keys — what `CadreNodeConfig.storage.provider` is called with. A
|
|
188
148
|
// strand's key is its strand id; the control database's key carries the party id,
|
|
189
149
|
// so two parties on one device never share a control store. Every key stays within
|
|
190
|
-
// `[
|
|
150
|
+
// `[a-z0-9._-]` — lowercase, so distinct keys stay distinct on a case-insensitive
|
|
151
|
+
// filesystem: the control key by lowercase hex encoding, a strand's by
|
|
191
152
|
// `assertStrandScopeKey`, which every strand launch runs.
|
|
192
153
|
export {
|
|
193
154
|
controlStorageScope,
|
|
@@ -305,6 +266,8 @@ export {
|
|
|
305
266
|
createDefaultFormationResponseValidator,
|
|
306
267
|
type DisclosureValidator,
|
|
307
268
|
type FormationUsageRecorder,
|
|
269
|
+
type FormationUsageParams,
|
|
270
|
+
type AuthorizedFormationUsage,
|
|
308
271
|
type ResolvedHostStrand,
|
|
309
272
|
type StrandProvisioner,
|
|
310
273
|
type FormationResponseValidator,
|
|
@@ -430,14 +393,16 @@ export {
|
|
|
430
393
|
} from './strand-wake-protocol.js';
|
|
431
394
|
|
|
432
395
|
// Strand Address (control-network strand-address RPC) — wire types come via
|
|
433
|
-
// `export * from './types.js'` (StrandAddrRequest / StrandAddrResponse).
|
|
396
|
+
// `export * from './types.js'` (StrandAddrRequest / StrandAddrResponse / StrandAddrStatus).
|
|
434
397
|
export {
|
|
435
398
|
StrandAddrService,
|
|
436
399
|
collectStrandAddrs,
|
|
437
400
|
STRAND_ADDR_PROTOCOL,
|
|
438
401
|
type StrandAddrServiceOptions,
|
|
439
402
|
type StrandAddrPeer,
|
|
440
|
-
type CollectStrandAddrsOptions
|
|
403
|
+
type CollectStrandAddrsOptions,
|
|
404
|
+
type StrandAddrOutcome,
|
|
405
|
+
type StrandAddrCollection
|
|
441
406
|
} from './strand-addr-protocol.js';
|
|
442
407
|
|
|
443
408
|
// Peer-address record (self-published, signed, freshness-stamped CadrePeer row)
|
|
@@ -504,6 +469,7 @@ export {
|
|
|
504
469
|
// limit, so an address that never answers cannot starve the rest.
|
|
505
470
|
export {
|
|
506
471
|
dialPeerAddrs,
|
|
472
|
+
SelfRelayOnlyError,
|
|
507
473
|
tryAddrsInTurn,
|
|
508
474
|
directBeforeRelayed,
|
|
509
475
|
DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS,
|
|
@@ -521,12 +487,14 @@ export {
|
|
|
521
487
|
RELAYED_DIAL_ROUND_TRIPS,
|
|
522
488
|
RELAY_RESERVATION_ROUND_TRIPS,
|
|
523
489
|
CIRCUIT_REQUEST_ROUND_TRIPS,
|
|
490
|
+
RELAYED_REQUEST_ROUND_TRIPS,
|
|
524
491
|
PUSH_TRANSFER_ALLOWANCE_MS,
|
|
492
|
+
ADMISSION_DECISION_TIMEOUT_MS,
|
|
525
493
|
resolveLinkRoundTripMs,
|
|
526
494
|
relayedDialBudgetMs,
|
|
527
|
-
connectionManagerTimeouts,
|
|
528
495
|
relayReservationBudgetMs,
|
|
529
496
|
circuitRequestBudgetMs,
|
|
497
|
+
relayedRequestBudgetMs,
|
|
530
498
|
peerJoinPushBudget,
|
|
531
499
|
type PeerJoinPushBudget
|
|
532
500
|
} from './link-budget.js';
|
|
@@ -568,6 +536,7 @@ export {
|
|
|
568
536
|
type FormationDialResult,
|
|
569
537
|
type ResponderProvisionOutcome
|
|
570
538
|
} from './strand-formation-protocol.js';
|
|
539
|
+
export { formationDeadlines, type FormationDeadlines } from './strand-formation-deadlines.js';
|
|
571
540
|
|
|
572
541
|
// Strand Formation manager (drives the native transport)
|
|
573
542
|
export {
|
|
@@ -576,6 +545,7 @@ export {
|
|
|
576
545
|
MEMBERSHIP_INVITE_TTL_MS,
|
|
577
546
|
MEMBERSHIP_INVITE_UNAVAILABLE_REASON,
|
|
578
547
|
HOST_STRAND_MUST_BE_RECREATED_REASON,
|
|
548
|
+
type MembershipInviteIssuer,
|
|
579
549
|
type StrandFormationManagerConfig,
|
|
580
550
|
type StrandFormationManagerOptions
|
|
581
551
|
} from './strand-formation-manager.js';
|
|
@@ -585,11 +555,9 @@ export {
|
|
|
585
555
|
createMembershipConnectionGater,
|
|
586
556
|
STRANGER_OPEN_PROTOCOLS,
|
|
587
557
|
DEFAULT_ENROLLMENT_WINDOW_MS,
|
|
588
|
-
ADMISSION_DECISION_TIMEOUT_MS,
|
|
589
558
|
RELAY_ADMISSION_RESERVE_DEADLINE_MS,
|
|
590
559
|
RELAY_ADMISSION_CLOSE_TIMEOUT_MS,
|
|
591
560
|
MAX_UNAUTHORIZED_RELAY_RESERVATIONS,
|
|
592
|
-
UNAUTHORIZED_RESERVATION_TTL_MS,
|
|
593
561
|
UnauthorizedReservationBudget,
|
|
594
562
|
type InboundAdmissionPolicy,
|
|
595
563
|
type InboundConnectionVerdict
|
|
@@ -615,6 +583,16 @@ export {
|
|
|
615
583
|
resolveListenAddrs
|
|
616
584
|
} from './relay-addrs.js';
|
|
617
585
|
export { strandNodeAddrs, type StrandNodeAddrs } from './strand-network-config.js';
|
|
586
|
+
// The circuit-relay SERVER a node runs, and with which init — one resolution shared by the
|
|
587
|
+
// control node and every strand node. Exported alongside the listen derivations above for the
|
|
588
|
+
// same reason: an embedder can assert on what its `network` block resolves to.
|
|
589
|
+
export {
|
|
590
|
+
resolveRelayServer,
|
|
591
|
+
PARTY_RELAY_MAX_RESERVATIONS,
|
|
592
|
+
PARTY_RELAY_RESERVATION_TTL_MS,
|
|
593
|
+
type ResolvedRelayServer,
|
|
594
|
+
type ResolvedRelayReservations
|
|
595
|
+
} from './relay-server.js';
|
|
618
596
|
|
|
619
597
|
// Relay reservation via the bare `/p2p-circuit` search listener — the one route
|
|
620
598
|
// every control node takes; `network.relayAddrs` is its fail-fast posture
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The strands this party joined from ANOTHER party, held in two records.
|
|
3
3
|
*
|
|
4
4
|
* A party's own strands come back after a restart through its control database: the
|
|
5
5
|
* `Strand` table holds their rows, the strand watcher polls it, and each row no sApp
|
|
6
6
|
* config claims is offered as `strand:discovered`. A strand joined through formation with
|
|
7
|
-
* another party (`CadreNode.formStrand`) has
|
|
8
|
-
* database
|
|
9
|
-
* kept its own list of joins, read secret included (gotchoices/sereus#18).
|
|
7
|
+
* another party (`CadreNode.formStrand`) has its `Strand` row in the OTHER party's control
|
|
8
|
+
* database, so it needs a record of its own (gotchoices/sereus#18):
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
* the
|
|
13
|
-
*
|
|
10
|
+
* - the PARTY-WIDE record, a `CadreControl.JoinedStrand` row ({@link PartyJoinedStrandLedger}).
|
|
11
|
+
* Once it exists it is the authority: every machine of the party offers it, a storage
|
|
12
|
+
* replica host launches it, and removing it is how the party leaves the strand;
|
|
13
|
+
* - the MACHINE-LOCAL record ({@link JoinedStrandStore}, below), a queue of joins this
|
|
14
|
+
* machine made that the party does not know about yet. A join is recorded here first,
|
|
15
|
+
* published by an owner machine's connected reconcile pass, and forgotten as soon as the
|
|
16
|
+
* party-wide row is visible, so a local record always means "joined here, not yet
|
|
17
|
+
* published". A machine that is not an owner never publishes; its joins stay here.
|
|
14
18
|
*
|
|
15
|
-
*
|
|
19
|
+
* {@link JoinedStrandSession} moves joins between the two and is what the strand watcher
|
|
20
|
+
* polls beside the control rows.
|
|
21
|
+
*
|
|
22
|
+
* Two store implementations:
|
|
16
23
|
* - {@link MemoryJoinedStrandStore} — joins die with the process. The default only for a
|
|
17
24
|
* node configured with no `keyStore`.
|
|
18
25
|
* - {@link KeyStoreJoinedStrandStore} — one {@link KeyStore} slot per join. A record holds
|
|
@@ -35,7 +42,7 @@ import type { StrandRow } from './types.js';
|
|
|
35
42
|
|
|
36
43
|
const log = debug('sereus:cadre:joined-strand-store');
|
|
37
44
|
|
|
38
|
-
/** One strand this node joined from ANOTHER party
|
|
45
|
+
/** One strand this node joined from ANOTHER party and has not yet published party-wide. */
|
|
39
46
|
export interface JoinedStrandRecord {
|
|
40
47
|
/** The strand id (a valid scope key, see storage-scope.ts). */
|
|
41
48
|
Id: string;
|
|
@@ -222,85 +229,268 @@ function parseRecord(strandId: string, bytes: Uint8Array): JoinedStrandRecord |
|
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
232
|
+
* The party-wide joined-strand table (`CadreControl.JoinedStrand`), as the node's control
|
|
233
|
+
* database exposes it. `CadreNode` builds it over `ControlDatabase` and the owner signing
|
|
234
|
+
* key; this module sees only the seam, so it stays free of the runtime.
|
|
235
|
+
*/
|
|
236
|
+
export interface PartyJoinedStrandLedger {
|
|
237
|
+
/** Every party-wide join, as offerable rows (`FounderOwnerKey: null`). */
|
|
238
|
+
list(): Promise<StrandRow[]>;
|
|
239
|
+
/** Whether this party's control database names `strandId`, as its own strand or as a party-wide join. */
|
|
240
|
+
names(strandId: string): Promise<boolean>;
|
|
241
|
+
/** Whether this machine can sign {@link publish} and {@link remove}: it holds an owner key the party enrolls. */
|
|
242
|
+
canSign(): Promise<boolean>;
|
|
243
|
+
/** Owner-signed insert. Resolves when the row landed or a sibling's join already holds the id. */
|
|
244
|
+
publish(record: JoinedStrandRecord): Promise<void>;
|
|
245
|
+
/** Owner-signed delete + tombstone; false when there is no row. Throws when a row exists and this machine cannot sign. */
|
|
246
|
+
remove(strandId: string): Promise<boolean>;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* One node session's joined strands: what the strand watcher sees of the two records, and
|
|
251
|
+
* the moves between them. Rebuilt at every start, which is what ends the per-session state
|
|
252
|
+
* below.
|
|
228
253
|
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* re-attach them, but still offered until this session ends, because `strand:revoked`
|
|
237
|
-
* promises that nothing is torn down for the app.
|
|
254
|
+
* The watcher reads a row missing from a poll as a removal and detaches the strand, so this
|
|
255
|
+
* holds the state a plain union would get wrong:
|
|
256
|
+
* - the last list each record answered, reused when a later read fails, so a failed read
|
|
257
|
+
* is not "every join was forgotten";
|
|
258
|
+
* - joins this party was removed from ({@link forgetAfterThisSession}): offered until this
|
|
259
|
+
* session ends, because `strand:revoked` promises that nothing is torn down for the app,
|
|
260
|
+
* and queued for party-wide removal so the next start does not re-attach them.
|
|
238
261
|
*/
|
|
239
|
-
export class
|
|
240
|
-
private
|
|
241
|
-
private
|
|
262
|
+
export class JoinedStrandSession {
|
|
263
|
+
private lastUnpublished: JoinedStrandRecord[] = [];
|
|
264
|
+
private lastPartyWide: StrandRow[] = [];
|
|
265
|
+
private readonly keptForSession = new Map<string, StrandRow>();
|
|
266
|
+
/** Strands whose party-wide row {@link syncWithParty} removes, queued by a self-revocation. */
|
|
267
|
+
private readonly pendingRemovals = new Set<string>();
|
|
268
|
+
/**
|
|
269
|
+
* Tail of the moves that must not interleave ({@link serialized}): a publish that found no
|
|
270
|
+
* party-wide row, then a {@link leave} that removed nothing, then the publish's insert
|
|
271
|
+
* would leave the party holding a strand the app just left.
|
|
272
|
+
*/
|
|
273
|
+
private exclusive: Promise<unknown> = Promise.resolve();
|
|
242
274
|
|
|
243
|
-
constructor(private readonly store: JoinedStrandStore) {}
|
|
275
|
+
constructor(private readonly store: JoinedStrandStore, private readonly ledger: PartyJoinedStrandLedger) {}
|
|
244
276
|
|
|
245
277
|
/**
|
|
246
|
-
* `control` plus
|
|
247
|
-
*
|
|
248
|
-
*
|
|
278
|
+
* `control` plus every join, one row per id, in this precedence: the control rows (the
|
|
279
|
+
* party's own strands), then the party-wide joins, then this machine's unpublished joins
|
|
280
|
+
* and the joins kept for this session. A local record named by either of the first two in
|
|
281
|
+
* a read that succeeded this poll is stale — published, or the strand is the party's own
|
|
282
|
+
* now — so it is forgotten here; this also cleans up after a crash between a publish and
|
|
283
|
+
* its local forget. One named only by the last-good party-wide list is kept: that list may
|
|
284
|
+
* predate a leave the record re-joined after.
|
|
249
285
|
*/
|
|
250
286
|
async withControlRows(control: readonly StrandRow[]): Promise<StrandRow[]> {
|
|
251
287
|
const controlIds = new Set(control.map((row) => row.Id));
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
288
|
+
const named = new Set(controlIds);
|
|
289
|
+
const offered = new Map<string, StrandRow>(control.map((row) => [row.Id, row]));
|
|
290
|
+
const partyWide = await this.partyWide();
|
|
291
|
+
// NOTE: a party-wide join whose id the party's own `Strand` table also holds (the party
|
|
292
|
+
// founded AND joined it — contrived) is shadowed here, not deleted: a watcher poll must
|
|
293
|
+
// not make owner-signed writes. If the party later unpublishes its own row the join row
|
|
294
|
+
// resurfaces; if that is ever seen, remove the join row inside `unpublishStrand`.
|
|
295
|
+
for (const row of partyWide.rows) {
|
|
296
|
+
if (!offered.has(row.Id)) {
|
|
297
|
+
offered.set(row.Id, row);
|
|
298
|
+
}
|
|
299
|
+
if (partyWide.fresh) {
|
|
300
|
+
named.add(row.Id);
|
|
301
|
+
}
|
|
255
302
|
}
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
if (controlIds.has(record.Id)) {
|
|
303
|
+
for (const record of await this.unpublished()) {
|
|
304
|
+
if (named.has(record.Id)) {
|
|
259
305
|
await this.forgetSuperseded(record.Id);
|
|
260
|
-
} else {
|
|
261
|
-
|
|
306
|
+
} else if (!offered.has(record.Id)) {
|
|
307
|
+
offered.set(record.Id, joinedStrandRow(record));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const [strandId, row] of this.keptForSession) {
|
|
311
|
+
if (controlIds.has(strandId)) {
|
|
312
|
+
this.keptForSession.delete(strandId);
|
|
313
|
+
} else if (!offered.has(strandId)) {
|
|
314
|
+
offered.set(strandId, row);
|
|
262
315
|
}
|
|
263
316
|
}
|
|
264
|
-
return
|
|
317
|
+
return [...offered.values()];
|
|
265
318
|
}
|
|
266
319
|
|
|
267
|
-
/**
|
|
268
|
-
async
|
|
269
|
-
this.
|
|
270
|
-
await this.store.
|
|
320
|
+
/** Record a join `formStrand` just made. A re-join also cancels a removal a revocation queued. */
|
|
321
|
+
async remember(record: JoinedStrandRecord): Promise<void> {
|
|
322
|
+
this.pendingRemovals.delete(record.Id);
|
|
323
|
+
await this.store.record(record);
|
|
271
324
|
}
|
|
272
325
|
|
|
273
326
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
327
|
+
* Record `row`, joined by `addStrand`, unless this party's control database already names
|
|
328
|
+
* it (its own strand, or a join already published) or an identical record is already here
|
|
329
|
+
* (an app re-claiming a remembered join on every start). Keeps the first `joinedAt`. Like
|
|
330
|
+
* {@link remember}, cancels a queued removal.
|
|
277
331
|
*/
|
|
278
|
-
async
|
|
279
|
-
|
|
280
|
-
if (
|
|
332
|
+
async rememberForeign(row: StrandRow): Promise<void> {
|
|
333
|
+
this.pendingRemovals.delete(row.Id);
|
|
334
|
+
if (await this.namedByParty(row.Id)) {
|
|
281
335
|
return;
|
|
282
336
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
337
|
+
const existing = (await this.store.list()).find((record) => record.Id === row.Id);
|
|
338
|
+
if (existing?.Type === row.Type && existing.MemberPrivateKey === row.MemberPrivateKey) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
await this.store.record({
|
|
342
|
+
Id: row.Id,
|
|
343
|
+
Type: row.Type,
|
|
344
|
+
MemberPrivateKey: row.MemberPrivateKey,
|
|
345
|
+
joinedAt: existing?.joinedAt ?? Date.now()
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Leave a join for the whole party: remove the party-wide row, then this machine's record.
|
|
351
|
+
* Throws, keeping both, when a party-wide row exists and this machine cannot sign.
|
|
352
|
+
*/
|
|
353
|
+
leave(strandId: string): Promise<void> {
|
|
354
|
+
return this.serialized(async () => {
|
|
355
|
+
await this.ledger.remove(strandId);
|
|
356
|
+
this.pendingRemovals.delete(strandId);
|
|
357
|
+
this.keptForSession.delete(strandId);
|
|
358
|
+
this.lastPartyWide = this.lastPartyWide.filter((row) => row.Id !== strandId);
|
|
359
|
+
await this.store.forget(strandId);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* For a strand this party was removed from: keep offering it until this session ends, and
|
|
365
|
+
* queue its party-wide row for removal by {@link syncWithParty}, so it does not re-attach
|
|
366
|
+
* on every start of every machine. A no-op for a strand with neither record (one of the
|
|
367
|
+
* party's own). The queue is in memory: if the process dies first, the next start
|
|
368
|
+
* relaunches the strand, the revoked-peer gate raises `strand:revoked` again, and this
|
|
369
|
+
* runs again.
|
|
370
|
+
*/
|
|
371
|
+
forgetAfterThisSession(strandId: string): Promise<void> {
|
|
372
|
+
return this.serialized(async () => {
|
|
373
|
+
const local = (await this.unpublished()).find((record) => record.Id === strandId);
|
|
374
|
+
const row = local ? joinedStrandRow(local) : this.lastPartyWide.find((listed) => listed.Id === strandId);
|
|
375
|
+
if (!row) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// Kept BEFORE the store forgets, so no poll can union without it. Queued even for a
|
|
379
|
+
// local-only join, which a sibling may have published already.
|
|
380
|
+
this.keptForSession.set(strandId, row);
|
|
381
|
+
this.pendingRemovals.add(strandId);
|
|
382
|
+
if (local) {
|
|
383
|
+
await this.store.forget(strandId);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* The owner machine's half, run by the node's connected reconcile pass: publish every
|
|
390
|
+
* unpublished join party-wide, then remove the party-wide rows queued by
|
|
391
|
+
* {@link forgetAfterThisSession}. Does nothing on a machine that cannot sign, whose joins
|
|
392
|
+
* stay machine-local. Per strand, a failure is logged and kept for the next pass; a
|
|
393
|
+
* failure to list the store or to check the signer throws.
|
|
394
|
+
*/
|
|
395
|
+
syncWithParty(): Promise<void> {
|
|
396
|
+
return this.serialized(async () => {
|
|
397
|
+
// Straight from the store, never the last-good list: a stale list could republish a
|
|
398
|
+
// join that was left since.
|
|
399
|
+
const unpublished = await this.store.list();
|
|
400
|
+
if (unpublished.length === 0 && this.pendingRemovals.size === 0) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (!(await this.ledger.canSign())) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
for (const record of unpublished) {
|
|
407
|
+
await this.publish(record);
|
|
408
|
+
}
|
|
409
|
+
for (const strandId of [...this.pendingRemovals]) {
|
|
410
|
+
await this.removeQueued(strandId);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// NOTE: a sibling that joined the same strand and has not yet published publishes it after
|
|
416
|
+
// a leave, bringing the strand back party-wide. Needs two devices joining one strand within
|
|
417
|
+
// a reconcile interval of the leave; if it is ever seen, check the strand's `Revocation`
|
|
418
|
+
// tombstone before publishing.
|
|
419
|
+
private async publish(record: JoinedStrandRecord): Promise<void> {
|
|
420
|
+
try {
|
|
421
|
+
if (!(await this.ledger.names(record.Id))) {
|
|
422
|
+
await this.ledger.publish(record);
|
|
423
|
+
// Into the last-good list too: its local record goes next, so a party-wide read that
|
|
424
|
+
// fails before one succeeds would otherwise drop the strand from the poll and hide it
|
|
425
|
+
// from a self-revocation.
|
|
426
|
+
this.lastPartyWide = [...this.lastPartyWide.filter((row) => row.Id !== record.Id), joinedStrandRow(record)];
|
|
427
|
+
log('joined strand %s published party-wide (party=%s)', record.Id, this.store.partyId);
|
|
428
|
+
}
|
|
429
|
+
await this.store.forget(record.Id);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
log('joined strand %s: publishing party-wide failed; kept for the next pass: %o', record.Id, error);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private async removeQueued(strandId: string): Promise<void> {
|
|
436
|
+
try {
|
|
437
|
+
const removed = await this.ledger.remove(strandId);
|
|
438
|
+
this.pendingRemovals.delete(strandId);
|
|
439
|
+
log('joined strand %s: party-wide row %s after this party was removed from it',
|
|
440
|
+
strandId, removed ? 'removed' : 'already absent');
|
|
441
|
+
} catch (error) {
|
|
442
|
+
log('joined strand %s: removing the party-wide row failed; queued for the next pass: %o', strandId, error);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** The party-wide joins; `fresh` is false when this read failed and the last list it answered stands in. */
|
|
447
|
+
private async partyWide(): Promise<{ rows: StrandRow[]; fresh: boolean }> {
|
|
448
|
+
try {
|
|
449
|
+
this.lastPartyWide = await this.ledger.list();
|
|
450
|
+
return { rows: this.lastPartyWide, fresh: true };
|
|
451
|
+
} catch (error) {
|
|
452
|
+
log('party-wide joined-strand read failed; offering the last list it answered (%d join(s)): %o',
|
|
453
|
+
this.lastPartyWide.length, error);
|
|
454
|
+
return { rows: this.lastPartyWide, fresh: false };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* {@link PartyJoinedStrandLedger.names}, answering false when the control database cannot
|
|
460
|
+
* be read (a machine cut off from its party before it received the `JoinedStrand` block).
|
|
461
|
+
* Recording is the safe side: a local record the party turns out to name is forgotten by
|
|
462
|
+
* the next poll that reads it, while a join with no record is not offered after a restart.
|
|
463
|
+
*/
|
|
464
|
+
private async namedByParty(strandId: string): Promise<boolean> {
|
|
465
|
+
try {
|
|
466
|
+
return await this.ledger.names(strandId);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
log('joined strand %s: could not read whether the control database names it; recording it locally: %o', strandId, error);
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Run `work` after every earlier serialized move settles; see {@link exclusive}. */
|
|
474
|
+
private serialized<T>(work: () => Promise<T>): Promise<T> {
|
|
475
|
+
const result = this.exclusive.then(work);
|
|
476
|
+
this.exclusive = result.catch(() => undefined);
|
|
477
|
+
return result;
|
|
287
478
|
}
|
|
288
479
|
|
|
289
|
-
private async
|
|
480
|
+
private async unpublished(): Promise<JoinedStrandRecord[]> {
|
|
290
481
|
try {
|
|
291
|
-
this.
|
|
482
|
+
this.lastUnpublished = await this.store.list();
|
|
292
483
|
} catch (error) {
|
|
293
484
|
log('joined-strand store list failed; offering the last list it answered (%d join(s)): %o',
|
|
294
|
-
this.
|
|
485
|
+
this.lastUnpublished.length, error);
|
|
295
486
|
}
|
|
296
|
-
return this.
|
|
487
|
+
return this.lastUnpublished;
|
|
297
488
|
}
|
|
298
489
|
|
|
299
490
|
private async forgetSuperseded(strandId: string): Promise<void> {
|
|
300
|
-
this.keptForSession.delete(strandId);
|
|
301
491
|
try {
|
|
302
492
|
await this.store.forget(strandId);
|
|
303
|
-
log('joined strand %s
|
|
493
|
+
log('joined strand %s is named by this party\'s control database — local record forgotten', strandId);
|
|
304
494
|
} catch (error) {
|
|
305
495
|
log('forgetting superseded joined strand %s failed; the next poll retries: %o', strandId, error);
|
|
306
496
|
}
|
package/src/key-store-file.ts
CHANGED
|
@@ -45,19 +45,28 @@ function encodeKeyId(keyId: KeyId): string {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Reverse {@link encodeKeyId}: strip the slot suffix and percent-decode. Returns
|
|
48
|
-
* `undefined` for a `.key` filename this store did not write
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
48
|
+
* `undefined` for a `.key` filename this store did not write, so
|
|
49
|
+
* {@link FileKeyStore.list} can skip it rather than letting one undecodable
|
|
50
|
+
* entry throw and break enumeration of every real slot. That includes a name
|
|
51
|
+
* that decodes but is not the one {@link encodeKeyId} produces for the result
|
|
52
|
+
* (e.g. a slot written before uppercase letters were escaped):
|
|
53
|
+
* {@link FileKeyStore.get} looks only at the canonical name, so listing it
|
|
54
|
+
* would name a key id that reads back as absent.
|
|
52
55
|
*/
|
|
53
56
|
function decodeKeyId(fileName: string): KeyId | undefined {
|
|
54
57
|
const encoded = fileName.slice(0, -SLOT_SUFFIX.length);
|
|
58
|
+
let keyId: KeyId;
|
|
55
59
|
try {
|
|
56
|
-
|
|
60
|
+
keyId = decodeURIComponent(encoded);
|
|
57
61
|
} catch (error) {
|
|
58
62
|
log('FileKeyStore: skipping undecodable slot filename %s: %o', fileName, error);
|
|
59
63
|
return undefined;
|
|
60
64
|
}
|
|
65
|
+
if (encodeKeyId(keyId) !== encoded) {
|
|
66
|
+
log('FileKeyStore: skipping non-canonical slot filename %s', fileName);
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return keyId;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
/**
|