@serfab/cadre-core 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +8 -6
  2. package/dist/cadre-node.d.ts +548 -137
  3. package/dist/cadre-node.js +1208 -383
  4. package/dist/cadre-node.js.map +1 -1
  5. package/dist/control-authorization.d.ts +2 -2
  6. package/dist/control-authorization.js +1 -0
  7. package/dist/control-authorization.js.map +1 -1
  8. package/dist/control-cohort.d.ts +6 -0
  9. package/dist/control-cohort.js +6 -0
  10. package/dist/control-cohort.js.map +1 -1
  11. package/dist/control-database.d.ts +46 -8
  12. package/dist/control-database.js +97 -18
  13. package/dist/control-database.js.map +1 -1
  14. package/dist/control-formation-recorder.d.ts +37 -14
  15. package/dist/control-formation-recorder.js +60 -10
  16. package/dist/control-formation-recorder.js.map +1 -1
  17. package/dist/control-read-retry.d.ts +13 -12
  18. package/dist/control-read-retry.js +14 -12
  19. package/dist/control-read-retry.js.map +1 -1
  20. package/dist/control-schema.d.ts +1 -1
  21. package/dist/control-schema.js +67 -5
  22. package/dist/control-schema.js.map +1 -1
  23. package/dist/control-stream.d.ts +9 -1
  24. package/dist/control-stream.js +24 -1
  25. package/dist/control-stream.js.map +1 -1
  26. package/dist/control-write-retry.d.ts +12 -2
  27. package/dist/control-write-retry.js +13 -2
  28. package/dist/control-write-retry.js.map +1 -1
  29. package/dist/delegate-admission.d.ts +11 -8
  30. package/dist/delegate-admission.js +19 -11
  31. package/dist/delegate-admission.js.map +1 -1
  32. package/dist/diagnostics/connection-path.d.ts +0 -7
  33. package/dist/diagnostics/connection-path.js +0 -7
  34. package/dist/diagnostics/connection-path.js.map +1 -1
  35. package/dist/formation-approval.d.ts +7 -0
  36. package/dist/formation-approval.js +9 -3
  37. package/dist/formation-approval.js.map +1 -1
  38. package/dist/fs-atomic.d.ts +11 -2
  39. package/dist/fs-atomic.js +13 -3
  40. package/dist/fs-atomic.js.map +1 -1
  41. package/dist/hibernation-manager.d.ts +111 -8
  42. package/dist/hibernation-manager.js +234 -49
  43. package/dist/hibernation-manager.js.map +1 -1
  44. package/dist/index.d.ts +13 -6
  45. package/dist/index.js +27 -5
  46. package/dist/index.js.map +1 -1
  47. package/dist/joined-strand-store.d.ts +177 -0
  48. package/dist/joined-strand-store.js +431 -0
  49. package/dist/joined-strand-store.js.map +1 -0
  50. package/dist/key-store-file.js +14 -5
  51. package/dist/key-store-file.js.map +1 -1
  52. package/dist/key-store.d.ts +6 -0
  53. package/dist/key-store.js.map +1 -1
  54. package/dist/link-budget.d.ts +367 -69
  55. package/dist/link-budget.js +375 -72
  56. package/dist/link-budget.js.map +1 -1
  57. package/dist/membership-connection-gater.d.ts +30 -43
  58. package/dist/membership-connection-gater.js +31 -42
  59. package/dist/membership-connection-gater.js.map +1 -1
  60. package/dist/node-local-snapshot.d.ts +5 -4
  61. package/dist/node-local-snapshot.js +6 -3
  62. package/dist/node-local-snapshot.js.map +1 -1
  63. package/dist/peer-dial.d.ts +24 -4
  64. package/dist/peer-dial.js +74 -5
  65. package/dist/peer-dial.js.map +1 -1
  66. package/dist/peer-join-backfill.d.ts +35 -15
  67. package/dist/peer-join-backfill.js +115 -55
  68. package/dist/peer-join-backfill.js.map +1 -1
  69. package/dist/push-fanout.d.ts +6 -0
  70. package/dist/push-fanout.js +6 -0
  71. package/dist/push-fanout.js.map +1 -1
  72. package/dist/relay-reservation.d.ts +3 -1
  73. package/dist/relay-reservation.js +3 -1
  74. package/dist/relay-reservation.js.map +1 -1
  75. package/dist/relay-server.d.ts +79 -0
  76. package/dist/relay-server.js +74 -0
  77. package/dist/relay-server.js.map +1 -0
  78. package/dist/seed-bootstrap.d.ts +57 -4
  79. package/dist/seed-bootstrap.js +107 -83
  80. package/dist/seed-bootstrap.js.map +1 -1
  81. package/dist/storage-scope.d.ts +33 -17
  82. package/dist/storage-scope.js +44 -23
  83. package/dist/storage-scope.js.map +1 -1
  84. package/dist/strand-addr-protocol.d.ts +58 -21
  85. package/dist/strand-addr-protocol.js +133 -85
  86. package/dist/strand-addr-protocol.js.map +1 -1
  87. package/dist/strand-database.d.ts +20 -2
  88. package/dist/strand-database.js +42 -5
  89. package/dist/strand-database.js.map +1 -1
  90. package/dist/strand-first-sync-gate.d.ts +59 -21
  91. package/dist/strand-first-sync-gate.js +58 -19
  92. package/dist/strand-first-sync-gate.js.map +1 -1
  93. package/dist/strand-formation-deadlines.d.ts +151 -0
  94. package/dist/strand-formation-deadlines.js +171 -0
  95. package/dist/strand-formation-deadlines.js.map +1 -0
  96. package/dist/strand-formation-manager.d.ts +63 -30
  97. package/dist/strand-formation-manager.js +67 -41
  98. package/dist/strand-formation-manager.js.map +1 -1
  99. package/dist/strand-formation-protocol.d.ts +45 -35
  100. package/dist/strand-formation-protocol.js +59 -122
  101. package/dist/strand-formation-protocol.js.map +1 -1
  102. package/dist/strand-instance-manager.d.ts +152 -6
  103. package/dist/strand-instance-manager.js +331 -42
  104. package/dist/strand-instance-manager.js.map +1 -1
  105. package/dist/strand-membership-writer.d.ts +6 -0
  106. package/dist/strand-membership-writer.js +11 -0
  107. package/dist/strand-membership-writer.js.map +1 -1
  108. package/dist/strand-peer-book-file.d.ts +12 -0
  109. package/dist/strand-peer-book-file.js +37 -0
  110. package/dist/strand-peer-book-file.js.map +1 -0
  111. package/dist/strand-peer-book-protocol.d.ts +168 -0
  112. package/dist/strand-peer-book-protocol.js +356 -0
  113. package/dist/strand-peer-book-protocol.js.map +1 -0
  114. package/dist/strand-peer-book-swap.d.ts +104 -0
  115. package/dist/strand-peer-book-swap.js +284 -0
  116. package/dist/strand-peer-book-swap.js.map +1 -0
  117. package/dist/strand-peer-book.d.ts +159 -0
  118. package/dist/strand-peer-book.js +361 -0
  119. package/dist/strand-peer-book.js.map +1 -0
  120. package/dist/strand-peer-observer.d.ts +88 -0
  121. package/dist/strand-peer-observer.js +180 -0
  122. package/dist/strand-peer-observer.js.map +1 -0
  123. package/dist/strand-revocation-enforcer.js +2 -1
  124. package/dist/strand-revocation-enforcer.js.map +1 -1
  125. package/dist/strand-solicitation.d.ts +36 -20
  126. package/dist/strand-solicitation.js +4 -4
  127. package/dist/strand-solicitation.js.map +1 -1
  128. package/dist/strand-wake-protocol.d.ts +80 -23
  129. package/dist/strand-wake-protocol.js +89 -64
  130. package/dist/strand-wake-protocol.js.map +1 -1
  131. package/dist/types.d.ts +181 -33
  132. package/dist/types.js +4 -3
  133. package/dist/types.js.map +1 -1
  134. package/package.json +11 -7
  135. package/src/cadre-node.ts +1316 -398
  136. package/src/control-authorization.ts +2 -1
  137. package/src/control-cohort.ts +6 -0
  138. package/src/control-database.ts +122 -18
  139. package/src/control-formation-recorder.ts +354 -305
  140. package/src/control-read-retry.ts +14 -12
  141. package/src/control-schema.ts +67 -5
  142. package/src/control-stream.ts +25 -1
  143. package/src/control-write-retry.ts +13 -2
  144. package/src/delegate-admission.ts +23 -13
  145. package/src/diagnostics/connection-path.ts +0 -7
  146. package/src/formation-approval.ts +610 -604
  147. package/src/fs-atomic.ts +14 -4
  148. package/src/hibernation-manager.ts +638 -437
  149. package/src/index.ts +91 -6
  150. package/src/joined-strand-store.ts +498 -0
  151. package/src/key-store-file.ts +14 -5
  152. package/src/key-store.ts +6 -0
  153. package/src/link-budget.ts +393 -74
  154. package/src/membership-connection-gater.ts +31 -45
  155. package/src/node-local-snapshot.ts +293 -289
  156. package/src/peer-dial.ts +81 -7
  157. package/src/peer-join-backfill.ts +120 -57
  158. package/src/push-fanout.ts +6 -0
  159. package/src/relay-reservation.ts +3 -1
  160. package/src/relay-server.ts +98 -0
  161. package/src/seed-bootstrap.ts +133 -91
  162. package/src/storage-scope.ts +44 -23
  163. package/src/strand-addr-protocol.ts +547 -458
  164. package/src/strand-database.ts +287 -242
  165. package/src/strand-first-sync-gate.ts +58 -19
  166. package/src/strand-formation-deadlines.ts +227 -0
  167. package/src/strand-formation-manager.ts +700 -647
  168. package/src/strand-formation-protocol.ts +103 -150
  169. package/src/strand-instance-manager.ts +387 -45
  170. package/src/strand-membership-writer.ts +12 -0
  171. package/src/strand-peer-book-file.ts +39 -0
  172. package/src/strand-peer-book-protocol.ts +466 -0
  173. package/src/strand-peer-book-swap.ts +338 -0
  174. package/src/strand-peer-book.ts +475 -0
  175. package/src/strand-peer-observer.ts +235 -0
  176. package/src/strand-revocation-enforcer.ts +2 -1
  177. package/src/strand-solicitation.ts +519 -500
  178. package/src/strand-wake-protocol.ts +401 -355
  179. package/src/types.ts +185 -33
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ConnectionGater, Libp2p, PeerId, PrivateKey } from '@libp2p/interface';
2
+ import type { CircuitRelayServerInit } from '@libp2p/circuit-relay-v2';
2
3
  import type { IRawStorage, Libp2pConnectionMonitorInit, Libp2pTransports, NoiseCryptoInterface } from '@optimystic/db-p2p';
3
4
  import type { IPeerNetwork, IRepo } from '@optimystic/db-core';
4
5
  import type { PeerJoinBackfillConfig } from './peer-join-backfill.js';
@@ -10,7 +11,9 @@ import type { SeedTrustPolicy } from './seed-trust-policy.js';
10
11
  import type { KeyStore, KeyId } from './key-store.js';
11
12
  import type { TrustedOwnerStore, TrustSource } from './trusted-owner-store.js';
12
13
  import type { BootstrapPeerStore } from './bootstrap-peer-store.js';
14
+ import type { StrandPeerBookStore } from './strand-peer-book.js';
13
15
  import type { EnrolledMachineStore } from './enrolled-machine-store.js';
16
+ import type { JoinedStrandStore } from './joined-strand-store.js';
14
17
  import type { PushNotifier } from './push-notifier.js';
15
18
  import type { RevocableTable } from './control-authorization.js';
16
19
  import type { ControlRetryAbandonment } from './control-retry.js';
@@ -121,10 +124,12 @@ export const HIBERNATION_TIMEOUTS: Record<LatencyHint, HibernationTimeouts> = {
121
124
  * Factory functions are useful for creating per-scope storage instances.
122
125
  *
123
126
  * **The argument is an opaque scope key.** Use it directly as a file name, directory
124
- * name or database name: every key stays within `[A-Za-z0-9._-]`, so no escaping is
125
- * needed and none should be assumed. Do not parse it; `controlStorageScope` /
126
- * `isControlStorageScope` (`storage-scope.ts`) are the supported way to mint and
127
- * recognize the control key. The control key holds the charset by base64url encoding;
127
+ * name or database name: every key stays within `[a-z0-9._-]`, so no escaping is
128
+ * needed and none should be assumed, and two different keys are two different names
129
+ * even on a filesystem that ignores case (Windows, macOS). Do not parse it;
130
+ * `controlStorageScope` / `isControlStorageScope` (`storage-scope.ts`) are the supported
131
+ * way to mint and recognize the control key. The control key holds the charset by
132
+ * lowercase hex encoding;
128
133
  * a strand's key — its strand id, which may have replicated in from another node in
129
134
  * the party — holds it because `StrandInstanceManager.startStrand` runs
130
135
  * `assertStrandScopeKey` on every launch and refuses a strand that fails it.
@@ -169,7 +174,7 @@ export interface StorageConfig {
169
174
  * the single-instance form cannot serve more than one party.
170
175
  *
171
176
  * 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
177
+ * database-name segment (always within `[a-z0-9._-]`), so each example below
173
178
  * interpolates it directly.
174
179
  *
175
180
  * For Node.js environments, use FileRawStorage from @optimystic/db-p2p-storage-fs:
@@ -317,7 +322,8 @@ export interface NetworkConfig {
317
322
  * It buys a node that BOOTS, not a node that boots fast: `start()` still waits
318
323
  * out that first attempt, which costs the drive's whole timeout
319
324
  * (`DEFAULT_RELAY_RESERVE_TIMEOUT_MS`, four link round trips at
320
- * {@link linkRoundTripMs} — 14 s at its default) against a relay that is unreachable
325
+ * {@link linkRoundTripMs} plus two admission decisions — 18 s at its default) against a relay
326
+ * that is unreachable
321
327
  * rather than merely refusing (`relay-reservation.ts` polls to the deadline, in
322
328
  * case libp2p's own discovery lands a reservation independently).
323
329
  *
@@ -340,6 +346,31 @@ export interface NetworkConfig {
340
346
  * better connectivity and uptime), false for transaction profile nodes.
341
347
  */
342
348
  enableRelay?: boolean;
349
+ /**
350
+ * Settings for this machine's circuit-relay SERVER, handed to `@libp2p/circuit-relay-v2`'s
351
+ * `circuitRelayServer(...)` (through db-p2p's `NodeOptions.relayServerInit`) on the control
352
+ * node and on every strand node. Only meaningful while {@link enableRelay} is on.
353
+ *
354
+ * Omitted, a party-run relay FORWARDS WITHOUT LIMIT: `reservations.applyDefaultLimit` is
355
+ * `false`, where libp2p's own default caps every relayed connection at 128 KiB or two
356
+ * minutes and resets it — which cut off any database sync or chat history forwarded
357
+ * through a party's own always-on machine. The store also holds
358
+ * `PARTY_RELAY_MAX_RESERVATIONS` (128) reservations instead of libp2p's 15, and each lives
359
+ * `PARTY_RELAY_RESERVATION_TTL_MS` (2 h) unrefreshed. See `relay-server.ts`.
360
+ *
361
+ * A value is MERGED over those defaults, not substituted for them: `reservations` key by
362
+ * key, every other top-level key as given. Setting only `reservations.maxReservations`
363
+ * keeps the limit off; `reservations: { applyDefaultLimit: true }` is how to turn libp2p's
364
+ * cap back on.
365
+ *
366
+ * The accepted tradeoff of the unlimited default: once a slot is granted, nothing caps what
367
+ * is forwarded through it, and a holder that keeps refreshing keeps it indefinitely — the
368
+ * 2 h lifetime only reclaims a slot whose holder went away. On the control node a peer it
369
+ * cannot place as a party member is bounded by COUNT ({@link unauthorizedRelayReservationCap}).
370
+ * A strand node's relay server has no such budget (its peers are cross-party by design), so
371
+ * any peer that reaches it is bounded only by `maxReservations`.
372
+ */
373
+ relayServerInit?: CircuitRelayServerInit;
343
374
  /**
344
375
  * Cap on concurrent circuit-relay reservations this node's relay server grants to
345
376
  * peers it cannot (yet) recognize as authorized members — the boot-ordering window
@@ -411,16 +442,21 @@ export interface NetworkConfig {
411
442
  * How long ONE cohort peer gets to answer ONE read-path request, in milliseconds, for the
412
443
  * control node and every strand node — as {@link connectionMonitor} is, and for the same
413
444
  * reason: the setting describes the LINK, and a phone's control node and its strand nodes
414
- * ride the same one. Omitted takes {@link COHORT_READ_DEADLINE_MS} (5000 ms), chosen for two
415
- * parties reaching each other only through a relay; Optimystic's own default is 1000 ms.
445
+ * ride the same one. Omitted, the deadline is DERIVED from {@link linkRoundTripMs}: one
446
+ * request and its answer over an open circuit costs two link round trips, so 7 000 ms at the
447
+ * default declaration (`cohortReadDeadlineMs` in `link-budget.ts`; the plugin's
448
+ * {@link COHORT_READ_DEADLINE_MS} is the same number). Optimystic's own default is 1000 ms.
416
449
  *
417
- * Raise it for a link slower still, lower it for a deployment that is all LAN and wants a
418
- * departed peer to stop holding up a read sooner. The cost of a larger value is that a peer
419
- * which is truly gone holds a read of a block missing locally for that long before the read
420
- * is declined and retried, and a joining machine's first sync runs several such consults —
421
- * so a change here should be weighed against
422
- * {@link CadreNodeConfig.strandFirstSync}'s budget. The measurement behind the default, and
423
- * what it costs, are on {@link COHORT_READ_DEADLINE_MS}.
450
+ * Set this only to break the derivation on purpose: an explicit value wins over whatever
451
+ * {@link linkRoundTripMs} would derive, so a deployment that is all LAN can make a departed
452
+ * peer stop holding up a read sooner without declaring a faster link (which would also
453
+ * shorten the dial budgets). A deployment on a slower link should raise
454
+ * {@link linkRoundTripMs} instead, which moves this and every dial budget together. The
455
+ * cost of a larger value is that a peer which is truly gone holds a read of a block missing
456
+ * locally for that long before the read is declined and retried, and a joining machine's
457
+ * first sync runs several such consults — so a change here should be weighed against
458
+ * {@link CadreNodeConfig.strandFirstSync}'s budget. The derivation, its history and what it
459
+ * costs are on {@link COHORT_READ_DEADLINE_MS}.
424
460
  *
425
461
  * Handed to db-p2p's `clusterPolicy.cohortQueryTimeoutMs` unchanged and NOT re-validated
426
462
  * here. Optimystic refuses a value that is not a finite number above zero, or is above its
@@ -439,11 +475,14 @@ export interface NetworkConfig {
439
475
  * This is NOT a timeout. It is the one stated assumption that cadre's own dial and
440
476
  * reservation deadlines are DERIVED from, each by the number of round trips that operation
441
477
  * was measured to cost: a peer-join catch-up's dial to one peer, its push response, one relay
442
- * reservation drive, the control-cohort dial budgets, and libp2p's own `dialTimeout` and
443
- * `inboundUpgradeTimeout` on every node. Reaching another machine through a relay costs a
444
- * fixed number of exchanges, so a deadline written as milliseconds has a link speed above
445
- * which it can never open a connection — which is the defect this declaration exists to make
446
- * impossible to reintroduce one budget at a time. The counts, the measurement behind them,
478
+ * reservation drive, the control-cohort dial budgets, and the per-peer cohort read deadline
479
+ * ({@link cohortQueryTimeoutMs}, unless set explicitly). It is also handed to Optimystic
480
+ * (`NodeOptions.linkRoundTripMs`), default included, which derives its own deadlines from it:
481
+ * its request dials and responses, its block pushes, and libp2p's `dialTimeout` and
482
+ * `inboundUpgradeTimeout` on every node. Reaching another machine through a
483
+ * relay costs a fixed number of exchanges, so a deadline written as milliseconds has a link
484
+ * speed above which it can never open a connection — which is the defect this declaration
485
+ * exists to make impossible to reintroduce one budget at a time. The counts, the measurement behind them,
447
486
  * and what still fails at the supported link are in `link-budget.ts`.
448
487
  *
449
488
  * The default covers the slowest link sereus supports, a 3-second round trip through a relay.
@@ -457,7 +496,8 @@ export interface NetworkConfig {
457
496
  * Refused where the libp2p node is built — inside `CadreNode.start()` for the control
458
497
  * network, inside `CadreNode.addStrand` for a strand — if it is not a finite number above
459
498
  * zero, because every consumer multiplies it into a deadline where a zero means "give up at
460
- * once" and a `NaN` means "never".
499
+ * once" and a `NaN` means "never". Optimystic refuses, in the same place, a value above its
500
+ * `MAX_LINK_ROUND_TRIP_MS` (about 1.66 days).
461
501
  */
462
502
  linkRoundTripMs?: number;
463
503
  /**
@@ -528,14 +568,19 @@ export interface NetworkConfig {
528
568
  * {@link dialTimeoutMs}. Each address is dialed on its own under this limit,
529
569
  * so an address that never answers cannot use up the time the peer's other
530
570
  * addresses needed. Defaults to
531
- * {@link DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS}.
571
+ * {@link DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS}. A value given
572
+ * here replaces that whole derived budget, including its allowance for the
573
+ * called machine's admission decision (`relayedDialBudgetMs`).
532
574
  */
533
575
  perAddressDialTimeoutMs?: number;
534
576
  /**
535
- * How often each running strand re-resolves its siblings' strand-network
536
- * addresses into its own libp2p address book — a step of the reconcile pass,
537
- * on its own much longer throttle. Defaults to `STRAND_PEER_ADDR_REFRESH_MS`
538
- * (10 min); must stay well under the peerStore's one-hour address expiry.
577
+ * How long a running strand waits, after a sibling answers its strand-addr
578
+ * RPC, before asking that sibling again for its strand-network addresses — a
579
+ * step of the reconcile pass, scheduled per (sibling, strand). A sibling that
580
+ * did not answer is retried sooner, on `STRAND_PEER_ADDR_RETRY_MS` (or this
581
+ * interval, when it is set shorter). Defaults to
582
+ * `STRAND_PEER_ADDR_REFRESH_MS` (10 min); must stay well under the peerStore's
583
+ * one-hour address expiry.
539
584
  */
540
585
  strandAddrRefreshMs?: number;
541
586
  };
@@ -654,9 +699,10 @@ export interface ControlNetworkConfig {
654
699
  *
655
700
  * The two `*ClusterPolicy` BUILDERS are the same objects with the block-repair
656
701
  * corroboration yardstick declared from the machines enrolled in this party
657
- * ({@link resolveRepairYardstick}) and, if the host set one, its own per-peer read
658
- * deadline in place of {@link COHORT_READ_DEADLINE_MS}. Both arrive in one named
659
- * declarations object, because both are plain numbers meaning unrelated things;
702
+ * ({@link resolveRepairYardstick}) and, if the host set one or declared its link, a
703
+ * per-peer read deadline in place of {@link COHORT_READ_DEADLINE_MS}
704
+ * (`declaredCohortReadDeadlineMs` in `link-budget.ts` settles which). Both arrive in one
705
+ * named declarations object, because both are plain numbers meaning unrelated things;
660
706
  * declaring neither returns the frozen base constant unchanged. A network picks the
661
707
  * derived numbers up when its libp2p node is built, which for a strand is every wake
662
708
  * from hibernation.
@@ -720,6 +766,22 @@ export interface CadreNodeConfig {
720
766
  /** Which strands to participate in */
721
767
  strandFilter?: StrandFilter;
722
768
 
769
+ /**
770
+ * Run every strand {@link strandFilter} admits, including ones no app on this machine
771
+ * has claimed with `addStrand`, as a **storage replica**: the strand's own libp2p node,
772
+ * storage and `Strand` membership schema, storing and serving its blocks, without the
773
+ * app's schema (no sApp config needed, none of the app's code or schema runs). This is
774
+ * how an always-on machine keeps a copy of every shared workspace its party runs, so a
775
+ * lost phone loses nothing that had reached it.
776
+ *
777
+ * The strand is still announced as `strand:discovered` (once) and stays in
778
+ * `getDiscoveredStrands()` until an app claims it. A replica is always a joiner.
779
+ *
780
+ * Default: `profile === 'storage'` — always-on machines host replicas, phones
781
+ * (`'transaction'`) keep today's announce-only behaviour.
782
+ */
783
+ hostUnclaimedStrands?: boolean;
784
+
723
785
  /** Storage configuration (only for storage profile) */
724
786
  storage?: StorageConfig;
725
787
 
@@ -947,6 +1009,55 @@ export interface CadreNodeConfig {
947
1009
  store?: EnrolledMachineStore;
948
1010
  };
949
1011
 
1012
+ /**
1013
+ * Node-local record of the strands this node joined from ANOTHER party and has not yet
1014
+ * published to the party-wide `JoinedStrand` table (see `joined-strand-store.ts`): id,
1015
+ * type and the closed strand's read secret, recorded by `formStrand` and by a joining
1016
+ * (not `founder: true`) `addStrand` whose row this party's control database does not
1017
+ * name, offered on every start as `strand:discovered` until an owner machine's
1018
+ * reconcile pass publishes it — so an embedding app keeps no list of its own.
1019
+ *
1020
+ * Absent ⇒ a `KeyStoreJoinedStrandStore` over {@link keyStore} when one is
1021
+ * configured (the record carries a secret, and the KeyStore is where every platform
1022
+ * already keeps secrets), else an in-memory store that warns once and forgets every
1023
+ * join on restart. An embedder that passes {@link privateKey} rather than a
1024
+ * `keyStore` injects a durable store here, e.g.
1025
+ * `new KeyStoreJoinedStrandStore(new FileKeyStore(dir), partyId)`.
1026
+ */
1027
+ joinedStrands?: {
1028
+ /** Its `partyId` must match `controlNetwork.partyId`; start() fails closed on a mismatch. */
1029
+ store?: JoinedStrandStore;
1030
+ };
1031
+
1032
+ /**
1033
+ * Node-local strand peer book (see `strand-peer-book.ts`): per strand, the strand
1034
+ * peers this node has met — the other parties' strand transport peers a formation
1035
+ * carried back, and every strand peer observed on a live connection — with their
1036
+ * last-known addresses. Read on every launch, hibernation resume and periodic
1037
+ * address refresh, so a restarted machine dials the peers it was talking to before
1038
+ * anything else, instead of coming back up alone (gotchoices/sereus#18). Sibling
1039
+ * of {@link bootstrapPeers} — same NON-replicated, per-party, injected-backend
1040
+ * shape — and, like it, nothing here is trust-bearing: dial hints only, each
1041
+ * bound to its peer id. Entries age out after 14 days (`STRAND_PEER_MAX_AGE_MS`,
1042
+ * overridable per store via `StrandPeerBookOptions.maxAgeMs`).
1043
+ *
1044
+ * Absent ⇒ an in-memory store is created at start() (ephemeral: the book dies
1045
+ * with the process, and a cross-party strand whose only contact was the other
1046
+ * party does not re-mesh after a restart until a fresh formation). Every
1047
+ * reference embedder injects a durable backend over the same slot kind as its
1048
+ * bootstrap-peer store. A new platform needs no new store class: supply a
1049
+ * `DurableSlot` and inject `PersistentStrandPeerBookStore.open(slot, partyId)`.
1050
+ */
1051
+ strandPeers?: {
1052
+ /**
1053
+ * Injected store instance — e.g. a `FileStrandPeerBookStore` from the
1054
+ * Node-only subpath `@serfab/cadre-core/strand-peer-book-file`, persisted in
1055
+ * the node's state directory. Its `partyId` must match
1056
+ * `controlNetwork.partyId`; start() fails closed on a mismatch.
1057
+ */
1058
+ store?: StrandPeerBookStore;
1059
+ };
1060
+
950
1061
  /**
951
1062
  * Platform push-delivery for suspended mobile peers. When present, the node's
952
1063
  * server fan-out can deliver strand-wake data messages over the platform push
@@ -1032,6 +1143,12 @@ export interface StrandInstance {
1032
1143
 
1033
1144
  /** Activity tracking */
1034
1145
  connectedPeers: number;
1146
+ /**
1147
+ * When the strand last saw activity: the app's (`recordStrandActivity`), a requested wake
1148
+ * (`wakeStrand`), or a peer delivering a gated joiner's `Strand.Header`. Always a fresh
1149
+ * `Date`. Bringing the runtime up is NOT activity — a check-in window decides whether to
1150
+ * re-hibernate by whether this changed since before its resume.
1151
+ */
1035
1152
  lastActivity: Date;
1036
1153
  nextCheckIn?: Date;
1037
1154
 
@@ -1394,6 +1511,10 @@ export interface CadreNodeEvents {
1394
1511
  * re-admits this party, so a second removal is reported again — and a
1395
1512
  * hibernation wake rebuilds the gate, which may re-emit for a strand still
1396
1513
  * revoked.
1514
+ *
1515
+ * For a strand joined from another party, the node also forgets its remembered
1516
+ * join (see {@link CadreNodeConfig.joinedStrands}): it keeps running this session
1517
+ * and is not re-offered after the next start.
1397
1518
  */
1398
1519
  'strand:revoked': { strandId: string };
1399
1520
  /**
@@ -1427,6 +1548,12 @@ export interface CadreNodeEvents {
1427
1548
  * Carries the full {@link StrandRow} so the app can join without re-querying
1428
1549
  * the control DB.
1429
1550
  *
1551
+ * Also emitted for a strand this node joined from ANOTHER party, which no control
1552
+ * row names: the node remembers every such join (see
1553
+ * {@link CadreNodeConfig.joinedStrands}) and offers it here on each start, as a row
1554
+ * with `FounderOwnerKey: null` carrying the closed strand's `MemberPrivateKey`. That
1555
+ * row is the product of the formation's consent, so claiming it needs no second one.
1556
+ *
1430
1557
  * **Fired once per strand per session, and it can fire before your listener is
1431
1558
  * attached.** The strand watcher's first poll runs inside `CadreNode.start()`
1432
1559
  * (100 ms after the watcher starts), so every strand already stored for this
@@ -1448,6 +1575,12 @@ export interface CadreNodeEvents {
1448
1575
  * be idempotent (guard on an in-flight set, not only on
1449
1576
  * `getStrands().has(id)` — the strand manager tracks an instance only once
1450
1577
  * `addStrand` has resolved).
1578
+ *
1579
+ * On a node that hosts storage replicas ({@link CadreNodeConfig.hostUnclaimedStrands},
1580
+ * the default for `profile: 'storage'`) the event still fires once, and the node launches
1581
+ * the strand as a replica right after it — so there `getStrands().has(id)` soon turns
1582
+ * true for a strand no app has claimed; an unclaimed instance has no `sAppInfo`. A
1583
+ * launch that fails is retried by the watcher without a second `strand:discovered`.
1451
1584
  */
1452
1585
  'strand:discovered': { strandId: string; strand: StrandRow };
1453
1586
  'control:connected': void;
@@ -1826,7 +1959,11 @@ export interface WakeRequest {
1826
1959
  export interface WakeAck {
1827
1960
  /** Whether the receiver honored the wake (member + participated strand). */
1828
1961
  accepted: boolean;
1829
- /** The strand's status after the wake (present when `accepted`). */
1962
+ /**
1963
+ * The strand's status when the receiver accepted the wake (present when `accepted`). The
1964
+ * receiver replies before the wake runs, so `hibernating` or `idle` here means "a wake was
1965
+ * started", not "the strand is now up".
1966
+ */
1830
1967
  status?: StrandStatus;
1831
1968
  /** Reason for rejection (non-member, unknown strand, malformed frame). */
1832
1969
  reason?: string;
@@ -1855,17 +1992,32 @@ export interface StrandAddrRequest {
1855
1992
  delegatePeerId?: string;
1856
1993
  }
1857
1994
 
1995
+ /**
1996
+ * How a strand-addr responder handled a {@link StrandAddrRequest}. Carried so the
1997
+ * asker can tell "I have nothing" from "I could not answer" — the two need
1998
+ * different retry timing, and an empty address list alone cannot say which.
1999
+ *
2000
+ * - `ok` — looked up; `multiaddrs` is the truth, possibly empty (strand not running here).
2001
+ * - `unavailable` — the responder could not answer: over its concurrency cap, the
2002
+ * request was unreadable, or its own lookup threw (e.g. a control-database read failed).
2003
+ * - `refused` — the requester is not an authorized member in the responder's
2004
+ * current view (possibly because its `CadrePeer` row has not replicated there yet);
2005
+ * any `delegatePeerId` it carried was NOT recorded.
2006
+ */
2007
+ export type StrandAddrStatus = 'ok' | 'unavailable' | 'refused';
2008
+
1858
2009
  /**
1859
2010
  * Response to a {@link StrandAddrRequest}, carrying the responder's strand-node
1860
2011
  * multiaddrs, returned on the same stream.
1861
2012
  */
1862
2013
  export interface StrandAddrResponse {
1863
- /** Echoes the requested strand id (empty on a reject/error reply). */
2014
+ /** How the responder handled the request; a reply without one is malformed. */
2015
+ status: StrandAddrStatus;
2016
+ /** Echoes the requested strand id; always empty on an `unavailable` reply. */
1864
2017
  strandId: string;
1865
2018
  /**
1866
2019
  * Dialable strand-network multiaddr strings (signaling/`p2p-circuit` first);
1867
- * empty when the responder is a non-member, or does not currently run that
1868
- * strand, or the exchange failed.
2020
+ * empty unless `status` is `ok` and the responder runs that strand.
1869
2021
  */
1870
2022
  multiaddrs: string[];
1871
2023
  }