@serfab/cadre-core 0.13.0 → 1.1.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 (131) hide show
  1. package/README.md +20 -6
  2. package/dist/bootstrap-peer-store-file.js +2 -2
  3. package/dist/bootstrap-peer-store.d.ts +32 -12
  4. package/dist/bootstrap-peer-store.js +41 -15
  5. package/dist/bootstrap-peer-store.js.map +1 -1
  6. package/dist/cadre-node.d.ts +558 -80
  7. package/dist/cadre-node.js +1060 -170
  8. package/dist/cadre-node.js.map +1 -1
  9. package/dist/control-authorization.d.ts +2 -2
  10. package/dist/control-authorization.js +1 -0
  11. package/dist/control-authorization.js.map +1 -1
  12. package/dist/control-cohort.d.ts +13 -26
  13. package/dist/control-cohort.js +0 -26
  14. package/dist/control-cohort.js.map +1 -1
  15. package/dist/control-database.d.ts +299 -44
  16. package/dist/control-database.js +469 -69
  17. package/dist/control-database.js.map +1 -1
  18. package/dist/control-read-retry.d.ts +40 -11
  19. package/dist/control-read-retry.js +48 -15
  20. package/dist/control-read-retry.js.map +1 -1
  21. package/dist/control-retry.d.ts +58 -3
  22. package/dist/control-retry.js +58 -3
  23. package/dist/control-retry.js.map +1 -1
  24. package/dist/control-schema.d.ts +1 -1
  25. package/dist/control-schema.js +103 -19
  26. package/dist/control-schema.js.map +1 -1
  27. package/dist/control-write-retry.d.ts +80 -11
  28. package/dist/control-write-retry.js +176 -29
  29. package/dist/control-write-retry.js.map +1 -1
  30. package/dist/enrolled-machine-store.js +2 -2
  31. package/dist/index.d.ts +14 -7
  32. package/dist/index.js +33 -6
  33. package/dist/index.js.map +1 -1
  34. package/dist/node-local-snapshot.d.ts +15 -7
  35. package/dist/node-local-snapshot.js +23 -8
  36. package/dist/node-local-snapshot.js.map +1 -1
  37. package/dist/peer-dial.d.ts +143 -0
  38. package/dist/peer-dial.js +176 -0
  39. package/dist/peer-dial.js.map +1 -0
  40. package/dist/relay-addrs.d.ts +90 -52
  41. package/dist/relay-addrs.js +91 -59
  42. package/dist/relay-addrs.js.map +1 -1
  43. package/dist/relay-reservation.d.ts +71 -3
  44. package/dist/relay-reservation.js +116 -8
  45. package/dist/relay-reservation.js.map +1 -1
  46. package/dist/seed-bootstrap.d.ts +30 -1
  47. package/dist/seed-bootstrap.js +62 -17
  48. package/dist/seed-bootstrap.js.map +1 -1
  49. package/dist/storage-scope.d.ts +62 -0
  50. package/dist/storage-scope.js +75 -0
  51. package/dist/storage-scope.js.map +1 -0
  52. package/dist/strand-database.d.ts +34 -16
  53. package/dist/strand-database.js +38 -24
  54. package/dist/strand-database.js.map +1 -1
  55. package/dist/strand-first-sync-gate.d.ts +152 -0
  56. package/dist/strand-first-sync-gate.js +208 -0
  57. package/dist/strand-first-sync-gate.js.map +1 -0
  58. package/dist/strand-formation-manager.d.ts +87 -5
  59. package/dist/strand-formation-manager.js +109 -5
  60. package/dist/strand-formation-manager.js.map +1 -1
  61. package/dist/strand-formation-protocol.d.ts +21 -1
  62. package/dist/strand-formation-protocol.js +30 -0
  63. package/dist/strand-formation-protocol.js.map +1 -1
  64. package/dist/strand-instance-manager.d.ts +243 -6
  65. package/dist/strand-instance-manager.js +470 -37
  66. package/dist/strand-instance-manager.js.map +1 -1
  67. package/dist/strand-member-key.d.ts +20 -12
  68. package/dist/strand-member-key.js +20 -12
  69. package/dist/strand-member-key.js.map +1 -1
  70. package/dist/strand-member-registry.js +3 -0
  71. package/dist/strand-member-registry.js.map +1 -1
  72. package/dist/strand-membership-reconciler.d.ts +361 -0
  73. package/dist/strand-membership-reconciler.js +548 -0
  74. package/dist/strand-membership-reconciler.js.map +1 -0
  75. package/dist/strand-membership-writer.d.ts +131 -11
  76. package/dist/strand-membership-writer.js +396 -134
  77. package/dist/strand-membership-writer.js.map +1 -1
  78. package/dist/strand-network-config.d.ts +46 -14
  79. package/dist/strand-network-config.js +79 -23
  80. package/dist/strand-network-config.js.map +1 -1
  81. package/dist/strand-solicitation.d.ts +10 -1
  82. package/dist/strand-solicitation.js +5 -0
  83. package/dist/strand-solicitation.js.map +1 -1
  84. package/dist/strand-wake-protocol.d.ts +11 -7
  85. package/dist/strand-wake-protocol.js +20 -52
  86. package/dist/strand-wake-protocol.js.map +1 -1
  87. package/dist/strand-watcher.js +6 -0
  88. package/dist/strand-watcher.js.map +1 -1
  89. package/dist/timed-step.d.ts +12 -0
  90. package/dist/timed-step.js +28 -0
  91. package/dist/timed-step.js.map +1 -0
  92. package/dist/timeout-scheduler.d.ts +19 -0
  93. package/dist/timeout-scheduler.js +22 -0
  94. package/dist/timeout-scheduler.js.map +1 -0
  95. package/dist/types.d.ts +262 -30
  96. package/dist/types.js.map +1 -1
  97. package/package.json +8 -8
  98. package/src/bootstrap-peer-store-file.ts +2 -2
  99. package/src/bootstrap-peer-store.ts +69 -27
  100. package/src/cadre-node.ts +7301 -6353
  101. package/src/control-authorization.ts +2 -1
  102. package/src/control-cohort.ts +122 -130
  103. package/src/control-database.ts +537 -69
  104. package/src/control-read-retry.ts +202 -167
  105. package/src/control-retry.ts +297 -190
  106. package/src/control-schema.ts +103 -19
  107. package/src/control-write-retry.ts +533 -383
  108. package/src/enrolled-machine-store.ts +2 -2
  109. package/src/index.ts +83 -6
  110. package/src/node-local-snapshot.ts +25 -8
  111. package/src/peer-dial.ts +215 -0
  112. package/src/relay-addrs.ts +458 -436
  113. package/src/relay-reservation.ts +951 -828
  114. package/src/seed-bootstrap.ts +76 -18
  115. package/src/storage-scope.ts +78 -0
  116. package/src/strand-database.ts +50 -27
  117. package/src/strand-first-sync-gate.ts +258 -0
  118. package/src/strand-formation-manager.ts +140 -5
  119. package/src/strand-formation-protocol.ts +43 -1
  120. package/src/strand-instance-manager.ts +577 -37
  121. package/src/strand-member-key.ts +20 -12
  122. package/src/strand-member-registry.ts +3 -0
  123. package/src/strand-membership-reconciler.ts +680 -0
  124. package/src/strand-membership-writer.ts +527 -189
  125. package/src/strand-network-config.ts +102 -24
  126. package/src/strand-solicitation.ts +16 -1
  127. package/src/strand-wake-protocol.ts +21 -66
  128. package/src/strand-watcher.ts +6 -0
  129. package/src/timed-step.ts +28 -0
  130. package/src/timeout-scheduler.ts +28 -0
  131. package/src/types.ts +1789 -1554
@@ -38,6 +38,7 @@ export const CONTROL_TABLES = [
38
38
  'OwnerKey',
39
39
  'ValidationKey',
40
40
  'Strand',
41
+ 'StrandPartyKey',
41
42
  'CadrePeer',
42
43
  'DeviceToken',
43
44
  'FormationInvite',
@@ -63,7 +64,7 @@ export type ControlTable = typeof CONTROL_TABLES[number];
63
64
  * (`control-database.ts`) so the lightweight signers — `peer-authorization.ts`'s
64
65
  * `revocationDigest` — can type against it without pulling in the runtime.
65
66
  */
66
- export type RevocableTable = Extract<ControlTable, 'OwnerKey' | 'CadrePeer' | 'ValidationKey' | 'Strand' | 'DeviceToken'>;
67
+ export type RevocableTable = Extract<ControlTable, 'OwnerKey' | 'CadrePeer' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' | 'DeviceToken'>;
67
68
 
68
69
  /**
69
70
  * What a signature authorizes, scoped to one table rule — or, for
@@ -1,130 +1,122 @@
1
- /**
2
- * Control-cohort dial selection: the pure policy behind
3
- * {@link CadreNode.reconcileControlCohort}. Decides WHICH known cadre siblings a
4
- * node proactively dials each pass to keep the `CadreControl` collections' cohort
5
- * connected (and thus replicating), without devolving into an N² mesh.
6
- *
7
- * FRET only needs a *connected* graph with the block clusters reachable, not a
8
- * full mesh. Storage/owner nodes are the stable, publicly-dialable backbone
9
- * that hold the control blocks, so the policy is **backbone-preferential with a
10
- * bounded out-degree**:
11
- *
12
- * - **Always dial every backbone (owner) member** — the owner set is
13
- * small by design, and routing cohort formation through publicly-dialable
14
- * owner nodes keeps the cohort ≥2 where owner-signed writes originate.
15
- * - **Fill the remainder** up to `targetDegree` with non-owner members,
16
- * ordered deterministically (peerId sort) so every node makes the same stable
17
- * choice across passes and load spreads predictably.
18
- *
19
- * For a small party (members ≤ degree) this degenerates to a full mesh, which is
20
- * correct, cheap, and matches what the convergence tests do by hand; for a large
21
- * party it bounds out-degree to `backbone + targetDegree`.
22
- */
23
-
24
- import { ed25519PublicKeyB64FromPeerId } from './seed-bootstrap.js';
25
- import type { CohortPeerRow } from './strand-cohort.js';
26
-
27
- /**
28
- * Default recurring cadence for {@link CadreNode.reconcileControlCohort} (ms).
29
- * In the same spirit as the 5s strand-watch poll but lighter, since a control
30
- * cohort changes far less often than strand activity.
31
- */
32
- export const DEFAULT_CONTROL_COHORT_RECONCILE_MS = 15_000;
33
-
34
- /**
35
- * Default cap on the number of **non-owner** members a single reconcile pass
36
- * proactively dials. Backbone (owner) members are always dialed and do NOT
37
- * count against this cap.
38
- */
39
- export const DEFAULT_CONTROL_COHORT_TARGET_DEGREE = 6;
40
-
41
- /**
42
- * Default per-sibling budget for one proactive control-cohort dial, in ms.
43
- *
44
- * `libp2p.dial(addrs)` is handed EVERY address a sibling resolved to, and each
45
- * one carries its own attempt timeout (js-libp2p's default is ~10 s, and
46
- * `@optimystic/db-p2p` does not override it). Some addresses cost more than one
47
- * attempt on their own: a `…/p2p-circuit/webrtc/…` address makes
48
- * `@libp2p/webrtc` dial the signalling leg to the relay hop FIRST and only then
49
- * the peer, so a two-address WebRTC sibling can block the sequential dial loop
50
- * for four attempt timeouts. Against unroutable addresses none of those attempts
51
- * ever answers, so an offline sibling's cost is address fan-out × relay hops ×
52
- * ~10 s — a number nothing in this package chose or bounds.
53
- *
54
- * This budget makes the SIBLING the unit instead of the address: one pass costs
55
- * at most (dialed siblings) × this, whatever each sibling's addresses look like.
56
- * Deliberately wider than one attempt timeout so a reachable sibling whose first
57
- * address is stale still gets a genuine try at its later ones; a sibling that
58
- * needs longer than this is simply retried next pass (every
59
- * {@link DEFAULT_CONTROL_COHORT_RECONCILE_MS}), which is what the dial loop's
60
- * best-effort contract already does with any other dial failure.
61
- *
62
- * Override per node with `network.controlCohort.dialTimeoutMs` — tests that
63
- * drive dead addresses on purpose set it low so a pass's duration is a chosen
64
- * number rather than a transitive libp2p default stretched by machine load.
65
- */
66
- export const DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS = 20_000;
67
-
68
- /** Outcome of {@link selectControlCohortDials}: who to dial + what the cap dropped. */
69
- export interface ControlCohortSelection {
70
- /**
71
- * Siblings to dial this pass, backbone first then the bounded non-owner
72
- * fill. Already self-excluded (the caller must pass self-excluded `siblings`).
73
- * Still includes peers that may turn out to be already-connected — the caller
74
- * diffs against live connections separately.
75
- */
76
- dials: CohortPeerRow[];
77
- /** Non-owner members dropped by the `targetDegree` cap (0 when none). */
78
- cappedNonOwner: number;
79
- }
80
-
81
- /** Stable peerId ordering so the bounded fill is identical across passes/nodes. */
82
- function byPeerId(a: CohortPeerRow, b: CohortPeerRow): number {
83
- if (a.peerId < b.peerId) return -1;
84
- if (a.peerId > b.peerId) return 1;
85
- return 0;
86
- }
87
-
88
- /**
89
- * Backbone-preferential, bounded-out-degree selection of control-cohort siblings
90
- * to dial.
91
- *
92
- * A member is **backbone** iff the ed25519 key derived from its peerId
93
- * (see {@link ed25519PublicKeyB64FromPeerId}) is in `ownerKeys`. All backbone
94
- * members are selected; non-owner members fill up to `targetDegree` in
95
- * deterministic peerId order.
96
- *
97
- * @param siblings - cadre members EXCLUDING self (the caller filters self out).
98
- * @param ownerKeys - the converged `OwnerKey` set; may be empty/partial
99
- * before owner convergence, in which case no member classifies as backbone
100
- * yet and the bounded fill still makes progress (preference sharpens later).
101
- * @param targetDegree - cap on non-owner dials (negative is treated as 0).
102
- */
103
- export function selectControlCohortDials(
104
- siblings: CohortPeerRow[],
105
- ownerKeys: ReadonlySet<string>,
106
- targetDegree: number,
107
- ): ControlCohortSelection {
108
- const backbone: CohortPeerRow[] = [];
109
- const rest: CohortPeerRow[] = [];
110
-
111
- for (const peer of siblings) {
112
- const pubKey = ed25519PublicKeyB64FromPeerId(peer.peerId);
113
- if (pubKey !== null && ownerKeys.has(pubKey)) {
114
- backbone.push(peer);
115
- } else {
116
- rest.push(peer);
117
- }
118
- }
119
-
120
- backbone.sort(byPeerId);
121
- rest.sort(byPeerId);
122
-
123
- const cap = Math.max(0, targetDegree);
124
- const fill = rest.slice(0, cap);
125
-
126
- return {
127
- dials: [...backbone, ...fill],
128
- cappedNonOwner: rest.length - fill.length,
129
- };
130
- }
1
+ /**
2
+ * Control-cohort dial selection: the pure policy behind
3
+ * {@link CadreNode.reconcileControlCohort}. Decides WHICH known cadre siblings a
4
+ * node proactively dials each pass to keep the `CadreControl` collections' cohort
5
+ * connected (and thus replicating), without devolving into an N² mesh.
6
+ *
7
+ * FRET only needs a *connected* graph with the block clusters reachable, not a
8
+ * full mesh. Storage/owner nodes are the stable, publicly-dialable backbone
9
+ * that hold the control blocks, so the policy is **backbone-preferential with a
10
+ * bounded out-degree**:
11
+ *
12
+ * - **Always dial every backbone (owner) member** — the owner set is
13
+ * small by design, and routing cohort formation through publicly-dialable
14
+ * owner nodes keeps the cohort ≥2 where owner-signed writes originate.
15
+ * - **Fill the remainder** up to `targetDegree` with non-owner members,
16
+ * ordered deterministically (peerId sort) so every node makes the same stable
17
+ * choice across passes and load spreads predictably.
18
+ *
19
+ * For a small party (members ≤ degree) this degenerates to a full mesh, which is
20
+ * correct, cheap, and matches what the convergence tests do by hand; for a large
21
+ * party it bounds out-degree to `backbone + targetDegree`.
22
+ */
23
+
24
+ import { ed25519PublicKeyB64FromPeerId } from './seed-bootstrap.js';
25
+ import type { CohortPeerRow } from './strand-cohort.js';
26
+
27
+ /**
28
+ * Default recurring cadence for {@link CadreNode.reconcileControlCohort} (ms).
29
+ * In the same spirit as the 5s strand-watch poll but lighter, since a control
30
+ * cohort changes far less often than strand activity.
31
+ */
32
+ export const DEFAULT_CONTROL_COHORT_RECONCILE_MS = 15_000;
33
+
34
+ /**
35
+ * Default cap on the number of **non-owner** members a single reconcile pass
36
+ * proactively dials. Backbone (owner) members are always dialed and do NOT
37
+ * count against this cap.
38
+ */
39
+ export const DEFAULT_CONTROL_COHORT_TARGET_DEGREE = 6;
40
+
41
+ // The per-sibling and per-address dial time limits live in `peer-dial.ts`
42
+ // (`DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS`,
43
+ // `DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS`), beside the dial they
44
+ // bound, which `seed-bootstrap.ts` also uses and cannot import from here.
45
+
46
+ /** Outcome of {@link selectControlCohortDials}: who to dial + what the cap dropped. */
47
+ export interface ControlCohortSelection {
48
+ /**
49
+ * Siblings to dial this pass, backbone first then the bounded non-owner
50
+ * fill. Already self-excluded (the caller must pass self-excluded `siblings`).
51
+ * Still includes peers that may turn out to be already-connected — the caller
52
+ * diffs against live connections separately.
53
+ */
54
+ dials: CohortPeerRow[];
55
+ /** Non-owner members dropped by the `targetDegree` cap (0 when none). */
56
+ cappedNonOwner: number;
57
+ }
58
+
59
+ /** What one {@link CadreNode.reconcileControlCohort} pass did. */
60
+ export interface ControlCohortReconcileResult {
61
+ /**
62
+ * Peer ids this pass dialled and whose dial resolved, in dial order: selected
63
+ * siblings on the steady-state pass, retained bootstrap peers on the cold-start
64
+ * pass (the pass takes one branch or the other, never both). A peer already
65
+ * connected when the pass began is never listed — the pass skips it. A resolved
66
+ * dial can still be refused moments later by the remote's membership gate, so
67
+ * an entry means "this pass opened a connection", not "that connection is still
68
+ * open".
69
+ */
70
+ dialed: string[];
71
+ }
72
+
73
+ /** Stable peerId ordering so the bounded fill is identical across passes/nodes. */
74
+ function byPeerId(a: CohortPeerRow, b: CohortPeerRow): number {
75
+ if (a.peerId < b.peerId) return -1;
76
+ if (a.peerId > b.peerId) return 1;
77
+ return 0;
78
+ }
79
+
80
+ /**
81
+ * Backbone-preferential, bounded-out-degree selection of control-cohort siblings
82
+ * to dial.
83
+ *
84
+ * A member is **backbone** iff the ed25519 key derived from its peerId
85
+ * (see {@link ed25519PublicKeyB64FromPeerId}) is in `ownerKeys`. All backbone
86
+ * members are selected; non-owner members fill up to `targetDegree` in
87
+ * deterministic peerId order.
88
+ *
89
+ * @param siblings - cadre members EXCLUDING self (the caller filters self out).
90
+ * @param ownerKeys - the converged `OwnerKey` set; may be empty/partial
91
+ * before owner convergence, in which case no member classifies as backbone
92
+ * yet and the bounded fill still makes progress (preference sharpens later).
93
+ * @param targetDegree - cap on non-owner dials (negative is treated as 0).
94
+ */
95
+ export function selectControlCohortDials(
96
+ siblings: CohortPeerRow[],
97
+ ownerKeys: ReadonlySet<string>,
98
+ targetDegree: number,
99
+ ): ControlCohortSelection {
100
+ const backbone: CohortPeerRow[] = [];
101
+ const rest: CohortPeerRow[] = [];
102
+
103
+ for (const peer of siblings) {
104
+ const pubKey = ed25519PublicKeyB64FromPeerId(peer.peerId);
105
+ if (pubKey !== null && ownerKeys.has(pubKey)) {
106
+ backbone.push(peer);
107
+ } else {
108
+ rest.push(peer);
109
+ }
110
+ }
111
+
112
+ backbone.sort(byPeerId);
113
+ rest.sort(byPeerId);
114
+
115
+ const cap = Math.max(0, targetDegree);
116
+ const fill = rest.slice(0, cap);
117
+
118
+ return {
119
+ dials: [...backbone, ...fill],
120
+ cappedNonOwner: rest.length - fill.length,
121
+ };
122
+ }