@serfab/cadre-core 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cadre-invite-protocol.d.ts +35 -13
- package/dist/cadre-invite-protocol.js +90 -25
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-node.d.ts +586 -255
- package/dist/cadre-node.js +1176 -564
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-cohort.d.ts +66 -9
- package/dist/control-cohort.js +67 -9
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +451 -193
- package/dist/control-database.js +701 -359
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +46 -43
- package/dist/control-formation-recorder.js +77 -78
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +6 -0
- package/dist/control-protocol-guard.js +14 -2
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +48 -31
- package/dist/control-schema-version.js +67 -45
- package/dist/control-schema-version.js.map +1 -1
- package/dist/control-schema.d.ts +8 -8
- package/dist/control-schema.js +16 -16
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +42 -7
- package/dist/control-write-retry.js +87 -19
- package/dist/control-write-retry.js.map +1 -1
- package/dist/enrolled-machine-store.d.ts +3 -3
- package/dist/enrolled-machine-store.js +3 -3
- package/dist/formation-token.d.ts +48 -0
- package/dist/formation-token.js +26 -0
- package/dist/formation-token.js.map +1 -0
- package/dist/index.d.ts +14 -13
- package/dist/index.js +16 -13
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +26 -9
- package/dist/joined-strand-store.js +63 -24
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/link-budget.d.ts +9 -7
- package/dist/link-budget.js +9 -7
- package/dist/link-budget.js.map +1 -1
- package/dist/member-redemption.d.ts +15 -9
- package/dist/member-redemption.js +13 -8
- package/dist/member-redemption.js.map +1 -1
- package/dist/membership-view.d.ts +91 -0
- package/dist/membership-view.js +110 -0
- package/dist/membership-view.js.map +1 -0
- package/dist/owner-grant-protocol.d.ts +11 -7
- package/dist/owner-grant-protocol.js +31 -17
- package/dist/owner-grant-protocol.js.map +1 -1
- package/dist/peer-addr-book.d.ts +10 -1
- package/dist/peer-addr-book.js +4 -3
- package/dist/peer-addr-book.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +89 -20
- package/dist/peer-join-backfill.js +265 -108
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/pending-join-runner.d.ts +48 -18
- package/dist/pending-join-runner.js +74 -56
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +47 -40
- package/dist/seed-bootstrap.js +66 -61
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/strand-database.d.ts +23 -2
- package/dist/strand-database.js +26 -6
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +176 -89
- package/dist/strand-first-sync-gate.js +288 -122
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +9 -9
- package/dist/strand-formation-deadlines.js +9 -9
- package/dist/strand-formation-manager.d.ts +23 -17
- package/dist/strand-formation-manager.js +46 -44
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +5 -8
- package/dist/strand-formation-protocol.js +101 -16
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +26 -17
- package/dist/strand-instance-manager.js +119 -50
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +16 -13
- package/dist/strand-membership-reconciler.js +30 -22
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +40 -28
- package/dist/strand-membership-writer.js +212 -153
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.js +5 -2
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-revocation-enforcer.d.ts +134 -60
- package/dist/strand-revocation-enforcer.js +144 -71
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +50 -39
- package/dist/strand-solicitation.js +10 -3
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +80 -13
- package/dist/strand-watcher.js +152 -48
- package/dist/strand-watcher.js.map +1 -1
- package/dist/table-change-watch.d.ts +63 -0
- package/dist/table-change-watch.js +209 -0
- package/dist/table-change-watch.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +4 -3
- package/dist/timeout-scheduler.js +4 -3
- package/dist/timeout-scheduler.js.map +1 -1
- package/dist/types.d.ts +90 -49
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/cadre-invite-protocol.ts +119 -31
- package/src/cadre-node.ts +1282 -584
- package/src/control-cohort.ts +86 -9
- package/src/control-database.ts +848 -403
- package/src/control-formation-recorder.ts +359 -353
- package/src/control-protocol-guard.ts +16 -2
- package/src/control-schema-version.ts +101 -51
- package/src/control-schema.ts +16 -16
- package/src/control-write-retry.ts +91 -19
- package/src/delegate-admission.ts +351 -351
- package/src/enrolled-machine-store.ts +3 -3
- package/src/formation-token.ts +63 -0
- package/src/index.ts +30 -17
- package/src/joined-strand-store.ts +69 -24
- package/src/link-budget.ts +9 -7
- package/src/member-redemption.ts +20 -13
- package/src/membership-view.ts +144 -0
- package/src/owner-grant-protocol.ts +33 -18
- package/src/peer-addr-book.ts +4 -3
- package/src/peer-join-backfill.ts +971 -761
- package/src/pending-join-runner.ts +76 -53
- package/src/seed-bootstrap.ts +72 -67
- package/src/strand-database.ts +352 -322
- package/src/strand-first-sync-gate.ts +334 -134
- package/src/strand-formation-deadlines.ts +227 -227
- package/src/strand-formation-manager.ts +731 -725
- package/src/strand-formation-protocol.ts +122 -24
- package/src/strand-instance-manager.ts +2092 -2026
- package/src/strand-membership-reconciler.ts +33 -25
- package/src/strand-membership-writer.ts +2093 -2019
- package/src/strand-move.ts +5 -2
- package/src/strand-revocation-enforcer.ts +740 -654
- package/src/strand-solicitation.ts +61 -34
- package/src/strand-watcher.ts +175 -56
- package/src/table-change-watch.ts +285 -0
- package/src/timeout-scheduler.ts +29 -28
- 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(
|
|
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(
|
|
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
|
|
788
|
-
* rather than left running. A dial that settles in the same tick the
|
|
789
|
-
* the race to the timeout rejection and would leave its stream open with
|
|
790
|
-
* it, so a
|
|
791
|
-
* `exchangeFrame` (`control-stream.ts`) gives a stream
|
|
792
|
-
* rejection is the abort itself, or reaches the caller through
|
|
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<
|
|
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
|
|
837
|
+
const pending = dialFormationChannel(node, addrs, protocolId, signal);
|
|
812
838
|
void pending.then(
|
|
813
|
-
(
|
|
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
|
|
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
|
|