@serfab/cadre-core 1.17.0 → 1.19.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 (148) hide show
  1. package/README.md +3 -2
  2. package/dist/cadre-invite-protocol.d.ts +35 -13
  3. package/dist/cadre-invite-protocol.js +90 -25
  4. package/dist/cadre-invite-protocol.js.map +1 -1
  5. package/dist/cadre-node.d.ts +586 -255
  6. package/dist/cadre-node.js +1182 -566
  7. package/dist/cadre-node.js.map +1 -1
  8. package/dist/control-cohort.d.ts +66 -9
  9. package/dist/control-cohort.js +67 -9
  10. package/dist/control-cohort.js.map +1 -1
  11. package/dist/control-database.d.ts +451 -193
  12. package/dist/control-database.js +701 -359
  13. package/dist/control-database.js.map +1 -1
  14. package/dist/control-formation-recorder.d.ts +46 -43
  15. package/dist/control-formation-recorder.js +77 -78
  16. package/dist/control-formation-recorder.js.map +1 -1
  17. package/dist/control-protocol-guard.d.ts +6 -0
  18. package/dist/control-protocol-guard.js +14 -2
  19. package/dist/control-protocol-guard.js.map +1 -1
  20. package/dist/control-schema-version.d.ts +48 -31
  21. package/dist/control-schema-version.js +67 -45
  22. package/dist/control-schema-version.js.map +1 -1
  23. package/dist/control-schema.d.ts +8 -8
  24. package/dist/control-schema.js +16 -16
  25. package/dist/control-schema.js.map +1 -1
  26. package/dist/control-write-retry.d.ts +42 -7
  27. package/dist/control-write-retry.js +87 -19
  28. package/dist/control-write-retry.js.map +1 -1
  29. package/dist/enrolled-machine-store.d.ts +3 -3
  30. package/dist/enrolled-machine-store.js +3 -3
  31. package/dist/formation-token.d.ts +48 -0
  32. package/dist/formation-token.js +26 -0
  33. package/dist/formation-token.js.map +1 -0
  34. package/dist/index.d.ts +15 -14
  35. package/dist/index.js +17 -14
  36. package/dist/index.js.map +1 -1
  37. package/dist/joined-strand-store.d.ts +26 -9
  38. package/dist/joined-strand-store.js +63 -24
  39. package/dist/joined-strand-store.js.map +1 -1
  40. package/dist/link-budget.d.ts +9 -7
  41. package/dist/link-budget.js +9 -7
  42. package/dist/link-budget.js.map +1 -1
  43. package/dist/member-redemption.d.ts +15 -9
  44. package/dist/member-redemption.js +13 -8
  45. package/dist/member-redemption.js.map +1 -1
  46. package/dist/membership-view.d.ts +91 -0
  47. package/dist/membership-view.js +110 -0
  48. package/dist/membership-view.js.map +1 -0
  49. package/dist/owner-grant-protocol.d.ts +11 -7
  50. package/dist/owner-grant-protocol.js +31 -17
  51. package/dist/owner-grant-protocol.js.map +1 -1
  52. package/dist/peer-addr-book.d.ts +10 -1
  53. package/dist/peer-addr-book.js +4 -3
  54. package/dist/peer-addr-book.js.map +1 -1
  55. package/dist/peer-join-backfill.d.ts +96 -30
  56. package/dist/peer-join-backfill.js +272 -128
  57. package/dist/peer-join-backfill.js.map +1 -1
  58. package/dist/pending-join-runner.d.ts +48 -18
  59. package/dist/pending-join-runner.js +74 -56
  60. package/dist/pending-join-runner.js.map +1 -1
  61. package/dist/seed-bootstrap.d.ts +47 -40
  62. package/dist/seed-bootstrap.js +66 -61
  63. package/dist/seed-bootstrap.js.map +1 -1
  64. package/dist/self-address-update.d.ts +13 -0
  65. package/dist/self-address-update.js +19 -0
  66. package/dist/self-address-update.js.map +1 -0
  67. package/dist/strand-database.d.ts +23 -2
  68. package/dist/strand-database.js +26 -6
  69. package/dist/strand-database.js.map +1 -1
  70. package/dist/strand-first-sync-gate.d.ts +176 -89
  71. package/dist/strand-first-sync-gate.js +288 -122
  72. package/dist/strand-first-sync-gate.js.map +1 -1
  73. package/dist/strand-formation-deadlines.d.ts +9 -9
  74. package/dist/strand-formation-deadlines.js +9 -9
  75. package/dist/strand-formation-manager.d.ts +23 -17
  76. package/dist/strand-formation-manager.js +46 -44
  77. package/dist/strand-formation-manager.js.map +1 -1
  78. package/dist/strand-formation-protocol.d.ts +5 -8
  79. package/dist/strand-formation-protocol.js +101 -16
  80. package/dist/strand-formation-protocol.js.map +1 -1
  81. package/dist/strand-instance-manager.d.ts +26 -17
  82. package/dist/strand-instance-manager.js +119 -50
  83. package/dist/strand-instance-manager.js.map +1 -1
  84. package/dist/strand-membership-reconciler.d.ts +16 -13
  85. package/dist/strand-membership-reconciler.js +28 -22
  86. package/dist/strand-membership-reconciler.js.map +1 -1
  87. package/dist/strand-membership-writer.d.ts +40 -28
  88. package/dist/strand-membership-writer.js +212 -153
  89. package/dist/strand-membership-writer.js.map +1 -1
  90. package/dist/strand-move.js +5 -2
  91. package/dist/strand-move.js.map +1 -1
  92. package/dist/strand-revocation-enforcer.d.ts +134 -60
  93. package/dist/strand-revocation-enforcer.js +144 -71
  94. package/dist/strand-revocation-enforcer.js.map +1 -1
  95. package/dist/strand-solicitation.d.ts +50 -39
  96. package/dist/strand-solicitation.js +10 -3
  97. package/dist/strand-solicitation.js.map +1 -1
  98. package/dist/strand-watcher.d.ts +80 -13
  99. package/dist/strand-watcher.js +152 -48
  100. package/dist/strand-watcher.js.map +1 -1
  101. package/dist/table-change-watch.d.ts +63 -0
  102. package/dist/table-change-watch.js +209 -0
  103. package/dist/table-change-watch.js.map +1 -0
  104. package/dist/timeout-scheduler.d.ts +4 -3
  105. package/dist/timeout-scheduler.js +4 -3
  106. package/dist/timeout-scheduler.js.map +1 -1
  107. package/dist/types.d.ts +90 -49
  108. package/dist/types.js +4 -4
  109. package/dist/types.js.map +1 -1
  110. package/package.json +8 -8
  111. package/src/cadre-invite-protocol.ts +119 -31
  112. package/src/cadre-node.ts +1289 -588
  113. package/src/control-cohort.ts +86 -9
  114. package/src/control-database.ts +848 -403
  115. package/src/control-formation-recorder.ts +359 -353
  116. package/src/control-protocol-guard.ts +16 -2
  117. package/src/control-schema-version.ts +101 -51
  118. package/src/control-schema.ts +16 -16
  119. package/src/control-write-retry.ts +91 -19
  120. package/src/delegate-admission.ts +351 -351
  121. package/src/enrolled-machine-store.ts +3 -3
  122. package/src/formation-token.ts +63 -0
  123. package/src/index.ts +30 -18
  124. package/src/joined-strand-store.ts +69 -24
  125. package/src/link-budget.ts +9 -7
  126. package/src/member-redemption.ts +20 -13
  127. package/src/membership-view.ts +144 -0
  128. package/src/owner-grant-protocol.ts +33 -18
  129. package/src/peer-addr-book.ts +4 -3
  130. package/src/peer-join-backfill.ts +961 -761
  131. package/src/pending-join-runner.ts +76 -53
  132. package/src/seed-bootstrap.ts +72 -67
  133. package/src/self-address-update.ts +19 -0
  134. package/src/strand-database.ts +352 -322
  135. package/src/strand-first-sync-gate.ts +334 -134
  136. package/src/strand-formation-deadlines.ts +227 -227
  137. package/src/strand-formation-manager.ts +731 -725
  138. package/src/strand-formation-protocol.ts +122 -24
  139. package/src/strand-instance-manager.ts +2092 -2026
  140. package/src/strand-membership-reconciler.ts +31 -25
  141. package/src/strand-membership-writer.ts +2093 -2019
  142. package/src/strand-move.ts +5 -2
  143. package/src/strand-revocation-enforcer.ts +740 -654
  144. package/src/strand-solicitation.ts +61 -34
  145. package/src/strand-watcher.ts +175 -56
  146. package/src/table-change-watch.ts +285 -0
  147. package/src/timeout-scheduler.ts +29 -28
  148. package/src/types.ts +89 -51
@@ -20,7 +20,7 @@
20
20
 
21
21
  import debug from 'debug';
22
22
  import { multiaddr, type Multiaddr } from '@multiformats/multiaddr';
23
- import type { Libp2p } from '@libp2p/interface';
23
+ import type { Connection, Libp2p } from '@libp2p/interface';
24
24
  import type { StrandFormationDisclosure, StrandMembershipInvite } from './types.js';
25
25
  import { type ControlStream, writeFrame, withDeadline, withTimeout } from './control-stream.js';
26
26
  import { canonicalJson } from './canonical-json.js';
@@ -41,6 +41,7 @@ import {
41
41
  FormationUnreachableError,
42
42
  type FormationRejectionCode
43
43
  } from './strand-formation-rejection.js';
44
+ import type { FormationTokenCheck, FormationTokenRead } from './formation-token.js';
44
45
 
45
46
  const log = debug('sereus:cadre:formation-proto');
46
47
 
@@ -147,10 +148,6 @@ export interface FormationRejection {
147
148
  reason: string;
148
149
  }
149
150
 
150
- /** What the responder's token check yields; an invalid token names which way it failed. */
151
- export type FormationTokenCheck =
152
- | { valid: true }
153
- | { valid: false; code: Extract<FormationRejectionCode, 'token-unknown' | 'token-spent'> };
154
151
 
155
152
  /** Initiator → Responder: carries the real token + disclosure + initiator cadre. */
156
153
  export interface FormationContactMessage {
@@ -387,8 +384,15 @@ export interface FormationListenerOptions {
387
384
  * `signal` is aborted when the listener's work budget expires: a hook that observes it
388
385
  * BEFORE writing abandons the redemption and leaves the invite unspent. Optional so
389
386
  * signal-unaware hooks (tests, mocks) stay assignable.
387
+ *
388
+ * `read` is the token as {@link validateToken} read it for this session, so the hook acts on
389
+ * that read instead of reading the invite again; absent when the token check read nothing.
390
390
  */
391
- provisionStrand(contact: FormationContactMessage, signal?: AbortSignal): Promise<ResponderProvisionOutcome>;
391
+ provisionStrand(
392
+ contact: FormationContactMessage,
393
+ signal?: AbortSignal,
394
+ read?: FormationTokenRead
395
+ ): Promise<ResponderProvisionOutcome>;
392
396
  /** Responder identity, disclosed only AFTER token + disclosure validation passes. */
393
397
  getResponderIdentity(): { partyId: string; cadrePeerAddrs: string[] };
394
398
  /**
@@ -511,10 +515,14 @@ export class FormationListener {
511
515
  * Deliberately not `withDeadline`: that exposes neither the timed-out flag nor the signal
512
516
  * outside its `op`, and both are needed here.
513
517
  */
514
- private async provision(id: number, contact: FormationContactMessage): Promise<ResponderProvisionOutcome | undefined> {
518
+ private async provision(
519
+ id: number,
520
+ contact: FormationContactMessage,
521
+ read: FormationTokenRead | undefined
522
+ ): Promise<ResponderProvisionOutcome | undefined> {
515
523
  const controller = new AbortController();
516
524
  let timedOut = false;
517
- const pending = this.options.provisionStrand(contact, controller.signal);
525
+ const pending = this.options.provisionStrand(contact, controller.signal, read);
518
526
  try {
519
527
  return await withTimeout(
520
528
  this.provisionWorkMs,
@@ -674,7 +682,7 @@ export class FormationListener {
674
682
  return;
675
683
  }
676
684
 
677
- const outcome = await this.provision(id, contact);
685
+ const outcome = await this.provision(id, contact, tokenResult.read);
678
686
  if (!outcome) {
679
687
  // Reported as its own retryable reason rather than falling through to the generic
680
688
  // catch below, which would report the misleading 'Internal formation error'.
@@ -781,16 +789,34 @@ export interface FormationDialResult {
781
789
  strandAddrs: string[];
782
790
  }
783
791
 
792
+ /** The formation stream and the connection it runs on. */
793
+ interface FormationChannel {
794
+ stream: ControlStream;
795
+ connection: Connection;
796
+ }
797
+
798
+ /**
799
+ * Connections a formation dial opened, each with the number of formation exchanges still using
800
+ * it; the entry is dropped when the last one finishes and closes the connection
801
+ * ({@link releaseFormationConnection}). A connection the node already held when its dial started
802
+ * is never entered, so formation never closes someone else's connection.
803
+ *
804
+ * Counted per connection rather than per dial: two formations to one responder machine at once
805
+ * (two pending joins on invitations from one inviter) share the connection libp2p's dial queue
806
+ * opened for the first, and the first to finish must leave it to the second.
807
+ */
808
+ const formationConnectionUsers = new WeakMap<Connection, number>();
809
+
784
810
  /**
785
811
  * Open the formation stream under the dial deadline, cancelling the dial when it expires.
786
812
  *
787
- * The deadline's signal goes into `dialProtocol`, so an expired dial is abandoned by libp2p
788
- * rather than left running. A dial that settles in the same tick the deadline fires loses
789
- * the race to the timeout rejection and would leave its stream open with nobody to close
790
- * it, so a stream that arrives after the signal aborted is reset here — the same treatment
791
- * `exchangeFrame` (`control-stream.ts`) gives a stream dialed past its deadline. A late
792
- * rejection is the abort itself, or reaches the caller through the awaited copy, so the
793
- * handler below only stops it being reported as unhandled.
813
+ * The deadline's signal goes into the dial and the stream negotiation, so an expired dial is
814
+ * abandoned by libp2p rather than left running. A dial that settles in the same tick the
815
+ * deadline fires loses the race to the timeout rejection and would leave its stream open with
816
+ * nobody to close it, so a channel that arrives after the signal aborted is reset here, and its
817
+ * connection released — the same treatment `exchangeFrame` (`control-stream.ts`) gives a stream
818
+ * dialed past its deadline. A late rejection is the abort itself, or reaches the caller through
819
+ * the awaited copy, so the handler below only stops it being reported as unhandled.
794
820
  *
795
821
  * NOTE: libp2p tries `addrs` one after another, and they all share this one dial budget. Its
796
822
  * per-address limit (`addressDialTimeout`: ten link round trips plus two admission decisions, as
@@ -806,25 +832,96 @@ export interface FormationDialResult {
806
832
  * jobs by peer id — see `peer-dial.ts` → `dialAddr`), which then holds this one on that job's
807
833
  * address; the remedy, when that is seen, is the same per-address dial.
808
834
  */
809
- function openFormationStream(node: Libp2p, addrs: Multiaddr[], protocolId: string, dialTimeoutMs: number): Promise<ControlStream> {
835
+ function openFormationStream(node: Libp2p, addrs: Multiaddr[], protocolId: string, dialTimeoutMs: number): Promise<FormationChannel> {
810
836
  return withDeadline(dialTimeoutMs, 'Formation dial-connect', async (signal) => {
811
- const pending = node.dialProtocol(addrs, protocolId, { signal });
837
+ const pending = dialFormationChannel(node, addrs, protocolId, signal);
812
838
  void pending.then(
813
- (stream) => {
814
- if (signal.aborted) {
815
- (stream as unknown as ControlStream).abort(new Error('Formation dial-connect resolved after its deadline'));
816
- }
839
+ (channel) => {
840
+ if (signal.aborted) abandonLateChannel(channel);
817
841
  },
818
842
  () => { /* surfaced through the awaited copy, or the abort itself */ }
819
843
  );
820
844
  try {
821
- return await pending as unknown as ControlStream;
845
+ return await pending;
822
846
  } catch (error) {
823
847
  throw describeAllAddressesFailed(error);
824
848
  }
825
849
  });
826
850
  }
827
851
 
852
+ /**
853
+ * Dial the responder machine, then open the formation stream on the connection. The ids of the
854
+ * node's connections are read before the dial: a connection not among them was opened by this
855
+ * dial (or by another formation dial it was joined to), and is closed when formation is done
856
+ * with it. A stream that cannot be opened releases the connection at once.
857
+ *
858
+ * NOTE: a dial of the responder that something other than formation started, and this dial was
859
+ * joined to, also counts as opened by formation and is closed afterwards. Nothing else dials an
860
+ * inviter's control node before formation does; if something starts to, tell the two apart by
861
+ * the connection's own opening time rather than by this snapshot.
862
+ */
863
+ async function dialFormationChannel(node: Libp2p, addrs: Multiaddr[], protocolId: string, signal: AbortSignal): Promise<FormationChannel> {
864
+ const heldBefore = new Set(node.getConnections().map((connection) => connection.id));
865
+ const connection = await node.dial(addrs, { signal });
866
+ useFormationConnection(connection, heldBefore.has(connection.id));
867
+ try {
868
+ signal.throwIfAborted();
869
+ const stream = await connection.newStream(protocolId, { signal });
870
+ return { stream: stream as unknown as ControlStream, connection };
871
+ } catch (error) {
872
+ void releaseFormationConnection(connection);
873
+ throw error;
874
+ }
875
+ }
876
+
877
+ function abandonLateChannel({ stream, connection }: FormationChannel): void {
878
+ stream.abort(new Error('Formation dial-connect resolved after its deadline'));
879
+ void releaseFormationConnection(connection);
880
+ }
881
+
882
+ /**
883
+ * Count one more formation exchange on `connection` when a formation dial opened it — this one
884
+ * (`heldBefore` false) or an earlier one still using it.
885
+ */
886
+ function useFormationConnection(connection: Connection, heldBefore: boolean): void {
887
+ const users = formationConnectionUsers.get(connection);
888
+ if (users !== undefined) formationConnectionUsers.set(connection, users + 1);
889
+ else if (!heldBefore) formationConnectionUsers.set(connection, 1);
890
+ }
891
+
892
+ /**
893
+ * End one formation exchange's use of `connection`, closing it when a formation dial opened it
894
+ * and no other exchange still uses it. Never throws.
895
+ *
896
+ * Only the joiner closes: it opened the connection, so it is the side that knows the connection
897
+ * did not exist before. The joiner's and the inviter's control nodes run on different FRET
898
+ * networks, so a connection left open after formation carries nothing but FRET pings that fail,
899
+ * until FRET has been refused three times and leaves that peer alone (p2p-fret 1.2.1). FRET can
900
+ * still redial the other control node on its own while it re-probes a peer it has seen
901
+ * (`complete/fret-maintenance-probes-peers-off-its-ring`).
902
+ *
903
+ * The close is graceful, so the responder's side can finish its own stream close; a close that
904
+ * fails or overruns libp2p's close timeout is logged and the connection aborted, as libp2p's own
905
+ * `hangUp` does. When the responder is a machine of the joiner's own party the connection
906
+ * normally pre-exists (the cadre mesh) and is left alone; if this dial opened it, it is closed,
907
+ * and the mesh re-dials that sibling as after any dropped connection.
908
+ */
909
+ async function releaseFormationConnection(connection: Connection): Promise<void> {
910
+ const users = formationConnectionUsers.get(connection);
911
+ if (users === undefined) return;
912
+ if (users > 1) {
913
+ formationConnectionUsers.set(connection, users - 1);
914
+ return;
915
+ }
916
+ formationConnectionUsers.delete(connection);
917
+ try {
918
+ await connection.close();
919
+ } catch (error) {
920
+ log('closing the formation connection to %s failed; aborting it: %o', connection.remotePeer.toString(), error);
921
+ connection.abort(error instanceof Error ? error : new Error(String(error)));
922
+ }
923
+ }
924
+
828
925
  const NO_RESPONDER_ADDRS = 'No responder addresses available for formation';
829
926
 
830
927
  /**
@@ -939,13 +1036,14 @@ async function exchangeContact(
939
1036
  contact: FormationContactMessage,
940
1037
  budgets: { protocolId: string; dialTimeoutMs: number; awaitResponseMs: number }
941
1038
  ): Promise<FormationResultMessage> {
942
- const stream = await openFormationStream(node, addrs, budgets.protocolId, budgets.dialTimeoutMs);
1039
+ const { stream, connection } = await openFormationStream(node, addrs, budgets.protocolId, budgets.dialTimeoutMs);
943
1040
  try {
944
1041
  const reader = new FrameReader(stream);
945
1042
  writeFrame(stream, contact);
946
1043
  return await withTimeout(budgets.awaitResponseMs, 'Formation await-response', () => reader.read<FormationResultMessage>());
947
1044
  } finally {
948
1045
  try { await stream.close(); } catch { /* ignore */ }
1046
+ void releaseFormationConnection(connection);
949
1047
  }
950
1048
  }
951
1049