@serfab/cadre-core 1.9.0 → 1.10.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 (87) hide show
  1. package/dist/cadre-node.d.ts +148 -17
  2. package/dist/cadre-node.js +361 -45
  3. package/dist/cadre-node.js.map +1 -1
  4. package/dist/control-authorization.d.ts +2 -2
  5. package/dist/control-authorization.js +1 -0
  6. package/dist/control-authorization.js.map +1 -1
  7. package/dist/control-cohort.d.ts +1 -1
  8. package/dist/control-cohort.js +1 -1
  9. package/dist/control-database.d.ts +105 -14
  10. package/dist/control-database.js +242 -73
  11. package/dist/control-database.js.map +1 -1
  12. package/dist/control-formation-recorder.d.ts +6 -6
  13. package/dist/control-formation-recorder.js +7 -8
  14. package/dist/control-formation-recorder.js.map +1 -1
  15. package/dist/control-schema.d.ts +1 -1
  16. package/dist/control-schema.js +90 -3
  17. package/dist/control-schema.js.map +1 -1
  18. package/dist/control-write-retry.d.ts +21 -15
  19. package/dist/control-write-retry.js +21 -15
  20. package/dist/control-write-retry.js.map +1 -1
  21. package/dist/index.d.ts +5 -3
  22. package/dist/index.js +6 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/invitation-bootstrap.d.ts +32 -0
  25. package/dist/invitation-bootstrap.js +30 -0
  26. package/dist/invitation-bootstrap.js.map +1 -0
  27. package/dist/link-budget.d.ts +204 -119
  28. package/dist/link-budget.js +208 -120
  29. package/dist/link-budget.js.map +1 -1
  30. package/dist/membership-connection-gater.d.ts +3 -3
  31. package/dist/membership-connection-gater.js +3 -3
  32. package/dist/peer-dial.d.ts +18 -14
  33. package/dist/peer-dial.js +18 -14
  34. package/dist/peer-dial.js.map +1 -1
  35. package/dist/peer-join-backfill.d.ts +3 -3
  36. package/dist/pending-join-runner.d.ts +186 -0
  37. package/dist/pending-join-runner.js +649 -0
  38. package/dist/pending-join-runner.js.map +1 -0
  39. package/dist/push-fanout.d.ts +1 -1
  40. package/dist/push-fanout.js +1 -1
  41. package/dist/seed-bootstrap.d.ts +2 -2
  42. package/dist/seed-bootstrap.js +1 -1
  43. package/dist/strand-addr-protocol.js +2 -2
  44. package/dist/strand-formation-deadlines.d.ts +5 -5
  45. package/dist/strand-formation-deadlines.js +5 -5
  46. package/dist/strand-formation-manager.d.ts +18 -4
  47. package/dist/strand-formation-manager.js +48 -33
  48. package/dist/strand-formation-manager.js.map +1 -1
  49. package/dist/strand-formation-protocol.d.ts +71 -14
  50. package/dist/strand-formation-protocol.js +222 -47
  51. package/dist/strand-formation-protocol.js.map +1 -1
  52. package/dist/strand-formation-rejection.d.ts +94 -0
  53. package/dist/strand-formation-rejection.js +77 -0
  54. package/dist/strand-formation-rejection.js.map +1 -0
  55. package/dist/strand-instance-manager.js +4 -1
  56. package/dist/strand-instance-manager.js.map +1 -1
  57. package/dist/strand-solicitation.d.ts +11 -3
  58. package/dist/strand-solicitation.js +9 -0
  59. package/dist/strand-solicitation.js.map +1 -1
  60. package/dist/types.d.ts +103 -17
  61. package/dist/types.js +5 -9
  62. package/dist/types.js.map +1 -1
  63. package/package.json +22 -22
  64. package/src/cadre-node.ts +399 -46
  65. package/src/control-authorization.ts +117 -116
  66. package/src/control-cohort.ts +1 -1
  67. package/src/control-database.ts +303 -82
  68. package/src/control-formation-recorder.ts +8 -9
  69. package/src/control-schema.ts +90 -3
  70. package/src/control-write-retry.ts +21 -15
  71. package/src/index.ts +21 -1
  72. package/src/invitation-bootstrap.ts +40 -0
  73. package/src/link-budget.ts +221 -120
  74. package/src/membership-connection-gater.ts +3 -3
  75. package/src/peer-dial.ts +18 -14
  76. package/src/peer-join-backfill.ts +3 -3
  77. package/src/pending-join-runner.ts +735 -0
  78. package/src/push-fanout.ts +1 -1
  79. package/src/seed-bootstrap.ts +2 -2
  80. package/src/strand-addr-protocol.ts +2 -2
  81. package/src/strand-formation-deadlines.ts +5 -5
  82. package/src/strand-formation-manager.ts +62 -37
  83. package/src/strand-formation-protocol.ts +1036 -802
  84. package/src/strand-formation-rejection.ts +129 -0
  85. package/src/strand-instance-manager.ts +4 -1
  86. package/src/strand-solicitation.ts +17 -4
  87. package/src/types.ts +99 -17
@@ -1,6 +1,6 @@
1
1
  import type { Libp2p, PeerId } from '@libp2p/interface';
2
2
  import type { Multiaddr } from '@multiformats/multiaddr';
3
- import type { CadreNodeConfig, StrandInstance, StrandRow, StrandConfig, FoundStrandConfig, FoundStrandResult, SAppConfig, CadreNodeEvents, ControlNetworkSeed, ApplySeedResult, AddDroneOptions, AddPhoneOptions, DroneInitResult, InviteResult, CadreInvite, OpenInvitation, FormStrandResult, StrandFormationDisclosure, StrandMembershipInvite, ResolveOpts, SelfRegistrationOutcome, ServiceWakeResult, PushPlatform, DeviceTokenRecord, ResolveDeviceTokenOpts } from './types.js';
3
+ import type { CadreNodeConfig, StrandInstance, StrandRow, StrandConfig, FoundStrandConfig, FoundStrandResult, SAppConfig, CadreNodeEvents, ControlNetworkSeed, ApplySeedResult, AddDroneOptions, AddPhoneOptions, DroneInitResult, InviteResult, CadreInvite, OpenInvitation, FormStrandResult, StrandFormationDisclosure, StrandMembershipInvite, ResolveOpts, SelfRegistrationOutcome, ServiceWakeResult, PushPlatform, DeviceTokenRecord, ResolveDeviceTokenOpts, PendingJoinStatus } from './types.js';
4
4
  import { type Ed25519KeyPair } from './ed25519-key.js';
5
5
  import { type TrustedOwnerStore, type TrustSource } from './trusted-owner-store.js';
6
6
  import { type BootstrapPeerStore } from './bootstrap-peer-store.js';
@@ -84,6 +84,15 @@ export declare class CadreNode implements SAppIdLookup {
84
84
  private enrollmentService;
85
85
  private seedBootstrapService;
86
86
  private strandSolicitationService;
87
+ /**
88
+ * The service whose formation handler is registered on the control node. Trails
89
+ * {@link strandSolicitationService} while {@link initializeStrandSolicitation} swaps a new
90
+ * one in: the field is set at once so concurrent callers reuse it, the handler only when
91
+ * its queued swap runs.
92
+ */
93
+ private registeredSolicitation;
94
+ /** Tail of the queued formation-handler swaps ({@link swapFormationResponder}). */
95
+ private solicitationSwaps;
87
96
  private strandWakeService;
88
97
  /**
89
98
  * Control-network strand-address responder. Answers a co-cadre sibling's
@@ -382,6 +391,20 @@ export declare class CadreNode implements SAppIdLookup {
382
391
  * `unpublishStrand` / `forgetJoinedStrand` as {@link formationStrandAddrs} is.
383
392
  */
384
393
  private readonly pendingMembershipInvites;
394
+ /**
395
+ * Invite keys of the membership invitations this process has staged, by
396
+ * {@link adoptFormationMembershipInvite} or from a `joined` `PendingJoin` row
397
+ * ({@link stageMembershipInvitesFromPendingJoins}), so one the reconciler has settled is not
398
+ * staged again by the next pending-join pass. Same lifetime as
399
+ * {@link pendingMembershipInvites}: a restarted process stages each live one once more, and the
400
+ * reconciler settles it again (spent, or the party already seated).
401
+ */
402
+ private readonly stagedMembershipInviteKeys;
403
+ /**
404
+ * The pending-join retry loop (`pending-join-runner.ts`): built by {@link start}, stopped by
405
+ * {@link cleanup}. It acts only while this machine is an enrolled owner.
406
+ */
407
+ private pendingJoinRunner;
385
408
  /**
386
409
  * Founder launches refused because the strand was founded before the per-party
387
410
  * identity split (`PreSplitStrandIdentityError`), keyed by strandId. Read by
@@ -966,11 +989,9 @@ export declare class CadreNode implements SAppIdLookup {
966
989
  * `StrandSolicitationService.hasOutstandingInvitation`). A formation
967
990
  * initiator is another party's peer by design and its token is only
968
991
  * checkable inside the protocol, so the gate asks the coarser question
969
- * "does this node expect a stranger at all?". Merely REGISTERING the
970
- * responder ({@link initializeStrandSolicitation}) no longer suspends
971
- * stranger denial — eager registration (as `reference-app-rn` does at
972
- * bring-up) and `formStrand`'s lazy initialization both leave the gate
973
- * armed, because neither mints an invitation.
992
+ * "does this node expect a stranger at all?". REGISTERING the responder
993
+ * does not suspend stranger denial: every node registers one at
994
+ * {@link start}, and registering mints no invitation.
974
995
  *
975
996
  * Ordering is semantically free (the checks are OR'd) but decides who pays:
976
997
  * checks 1-3 are in-memory, 4/5 share one control-DB read, and only a peer
@@ -1002,6 +1023,11 @@ export declare class CadreNode implements SAppIdLookup {
1002
1023
  * layer is fail-open behind `ADMISSION_DECISION_TIMEOUT_MS`, so the live
1003
1024
  * read is safe here — unlike the per-stream gate, which must consult the
1004
1025
  * materialized {@link authorizedControlPeers} snapshot instead.
1026
+ * NOTE: check 6 adds a second control read (`hasOutstandingFormationInvite`) for a
1027
+ * stranger with no locally minted invitation in play, on every node now that every
1028
+ * node runs the responder — a relay-enabled storage node included. If stranger
1029
+ * connections to such a node ever arrive fast enough for that read to show, cache
1030
+ * the answer for a few seconds.
1005
1031
  * NOTE: on a relay-DISABLED node, a sibling whose membership row has not yet
1006
1032
  * replicated here is denied until the row converges (typically via the
1007
1033
  * owner); either side's next outbound reconcile dial (outbound is never
@@ -1344,6 +1370,12 @@ export declare class CadreNode implements SAppIdLookup {
1344
1370
  * handling, is what keeps a mixed list from taking a whole peer offline.
1345
1371
  */
1346
1372
  resolvePeerAddrs(peerId: string, opts?: ResolveOpts): Promise<Multiaddr[]>;
1373
+ /**
1374
+ * {@link resolvePeerAddrs} with the record's `UpdatedAt` kept beside the addresses, for a
1375
+ * caller that ranks peers by how recently they published. `null` wherever that returns `[]`
1376
+ * for a failed gate.
1377
+ */
1378
+ private resolvePeerRecord;
1347
1379
  /**
1348
1380
  * Guarantee every address handed to a control-network dial terminates in
1349
1381
  * `/p2p/<peerId>`, with no duplicates — the invariant every dial path
@@ -1537,15 +1569,18 @@ export declare class CadreNode implements SAppIdLookup {
1537
1569
  *
1538
1570
  * The list may name transports this node cannot dial (a lent node reports TCP
1539
1571
  * and `/ws` addresses to a phone that dials WebSockets only). That needs no
1540
- * filtering here: libp2p's dial queue (`calculateMultiaddrs`, libp2p 3.1.3)
1572
+ * filtering here: libp2p's dial queue (`calculateMultiaddrs`, libp2p 3.3.11)
1541
1573
  * rejects an address no transport can dial before touching the network, so
1542
1574
  * such an address costs a log line.
1543
1575
  *
1544
1576
  * The list is NOT handed to one `dial()`. libp2p tries a multi-address dial's
1545
- * addresses one after another under a single deadline, with no limit per
1546
- * address, and sorts loopback addresses last — so one or two addresses that
1547
- * never answer used up the whole deadline before the address that worked was
1548
- * tried. {@link dialPeerAddrs} dials each address on its own time limit instead.
1577
+ * addresses one after another under a single deadline, and sorts loopback
1578
+ * addresses last — so one or two addresses that never answer use up most of
1579
+ * the deadline before the address that works is tried. libp2p 3.3's
1580
+ * per-address `addressDialTimeout` does not prevent that: cadre sizes it for a
1581
+ * cold relayed open through two admission decisions (`optimysticDialLimits`,
1582
+ * 39 s at the default declared link).
1583
+ * {@link dialPeerAddrs} dials each address on its own time limit instead.
1549
1584
  */
1550
1585
  private resolveControlDialAddrs;
1551
1586
  /**
@@ -2613,9 +2648,9 @@ export declare class CadreNode implements SAppIdLookup {
2613
2648
  * caller runs the drive regardless).
2614
2649
  *
2615
2650
  * NOTE: against a relay that is DOWN this hook costs up to two strand-addr
2616
- * timeouts (dial by peer id, then by addr; 23 s each at the default declared link round trip)
2651
+ * timeouts (dial by peer id, then by addr; 28.5 s each at the default declared link round trip)
2617
2652
  * before the reservation drive even starts, so one failed re-drive holds the supervisor
2618
- * `driving` for those 46 s plus the drive's own deadline — 64 s at the default, and longer on
2653
+ * `driving` for those 57 s plus the drive's own deadline — 75 s at the default, and longer on
2619
2654
  * a host that declared a slower one (`link-budget.ts`).
2620
2655
  * Bounded and harmless while the relay is unreachable anyway; if recovery
2621
2656
  * latency after a relay comes back ever matters, skip the announce when the
@@ -3435,32 +3470,128 @@ export declare class CadreNode implements SAppIdLookup {
3435
3470
  */
3436
3471
  dialInvite(invite: CadreInvite): Promise<void>;
3437
3472
  /**
3438
- * Initialize the strand solicitation service.
3439
- * This enables forming strands with other parties via open invitations.
3473
+ * Replace this node's strand solicitation service — the formation responder every node
3474
+ * installs at {@link start}, and the initiator side of {@link formStrand}. Optional: call it
3475
+ * only to customize the responder (an approver, a provisioner, formation deadlines). The
3476
+ * new service's handler replaces the previous one's on the control node, and invitations
3477
+ * the previous service minted stay outstanding.
3478
+ *
3479
+ * Without `options.formationUsageRecorder`, tokens are checked against this party's
3480
+ * replicated `FormationInvite`/`FormationUsage` rows ({@link ControlFormationUsageRecorder}).
3481
+ * A responder that accepts every token is reachable only by constructing
3482
+ * {@link StrandSolicitationService} directly.
3440
3483
  *
3441
3484
  * @param options - Configuration for the solicitation service
3442
3485
  */
3443
3486
  initializeStrandSolicitation(options?: StrandSolicitationServiceOptions): Promise<void>;
3444
3487
  /**
3445
- * Get the strand solicitation service (for advanced use)
3488
+ * Answer formation from start. Log-and-continue: a node that cannot is still useful, and
3489
+ * {@link createOpenInvitation} / {@link formStrand} install the responder when it is missing.
3490
+ */
3491
+ private installDefaultFormationResponder;
3492
+ /**
3493
+ * Move the control node's formation handler from the registered service to `service`.
3494
+ * Queued by {@link initializeStrandSolicitation} so swaps run one at a time: libp2p allows
3495
+ * one handler per protocol id, and two overlapping swaps would each find the other's
3496
+ * handler in the way. A failed swap registers the previous service again, points
3497
+ * {@link strandSolicitationService} back at it unless a later call has replaced it, and
3498
+ * rethrows.
3499
+ *
3500
+ * NOTE: same stray-handler caveat as {@link installSeedBootstrapService} — a failed
3501
+ * peer-store merge leaves the formation handler registered, so the restore below (and a
3502
+ * retry) rejects as a duplicate.
3503
+ */
3504
+ private swapFormationResponder;
3505
+ /**
3506
+ * Get the strand solicitation service (for advanced use). Non-null on a started node unless
3507
+ * the install at {@link start} failed.
3446
3508
  */
3447
3509
  getStrandSolicitationService(): StrandSolicitationService | null;
3448
3510
  /**
3449
3511
  * Create an open invitation for others to form strands with this party.
3450
3512
  *
3513
+ * The invitation's bootstrap list names this machine first (it is the one most likely to
3514
+ * run the host strand), then a few of the party's other machines
3515
+ * ({@link siblingInvitationAddrs}), so a joiner can still form while this one is offline.
3516
+ *
3451
3517
  * @param sAppId - The sApp to use for formed strands
3452
3518
  * @param expirationMs - How long the invitation is valid (ms from now)
3453
3519
  * @returns The open invitation to share out-of-band
3520
+ * @throws when neither this machine nor any other machine of the party has an address
3454
3521
  */
3455
3522
  createOpenInvitation(sAppId: string, expirationMs?: number): Promise<OpenInvitation>;
3456
3523
  /**
3457
- * Form a strand with a responder via an open invitation.
3524
+ * Addresses of the party's other machines for an invitation's bootstrap list
3525
+ * ({@link selectInvitationSiblingAddrs} picks which). The source is each authorized
3526
+ * member's signed `CadrePeer` record ({@link resolvePeerRecord}), so a machine whose record
3527
+ * is missing, stale or untrusted is left out; live connections only order them.
3528
+ *
3529
+ * Best-effort: a failed read leaves the invitation naming this machine alone, which is what
3530
+ * it named before siblings were added.
3531
+ */
3532
+ private siblingInvitationAddrs;
3533
+ /**
3534
+ * Form a strand with a responder via an open invitation: one attempt, from this machine, with
3535
+ * nothing recorded party-wide. {@link requestJoin} is the path that keeps trying.
3458
3536
  *
3459
3537
  * @param invitation - The open invitation received out-of-band
3460
3538
  * @param disclosure - Identity/context information to share with the responder
3461
3539
  * @returns The member key and strand info if successful
3540
+ * @throws `FormationRejectedError` or `FormationUnreachableError` when the formation fails, and
3541
+ * `FormationPostApprovalError` when it was approved and a step on this machine then failed
3462
3542
  */
3463
3543
  formStrand(invitation: OpenInvitation, disclosure?: StrandFormationDisclosure): Promise<FormStrandResult>;
3544
+ /**
3545
+ * Ask to join through `invitation` and have the party keep trying until the join works, the
3546
+ * invitation is used up or expires, or the request is dismissed: across restarts, and from
3547
+ * any owner machine of the party (`docs/strands.md` → "Joining while the inviter is
3548
+ * offline"). Records a party-wide `PendingJoin` row, runs one attempt on this machine at
3549
+ * once, and returns the status after it: `'joined'` when the inviter answered yes, `'waiting'`
3550
+ * when it was unreachable or not ready, `'failed'` when it refused for good.
3551
+ *
3552
+ * Asking again for an invitation whose request is still pending adopts that request as it
3553
+ * is; asking again after it finished starts a fresh one. Later changes arrive as
3554
+ * `pendingJoin:changed`. A joined strand is offered through `strand:discovered` like any
3555
+ * {@link formStrand} join. Do not also call {@link formStrand} with the same invitation:
3556
+ * nothing coordinates the two, and one of them gets `token-spent`.
3557
+ *
3558
+ * @throws on a machine that is not an enrolled owner (the row is owner-signed), and on an
3559
+ * invitation already past its expiration
3560
+ */
3561
+ requestJoin(invitation: OpenInvitation, disclosure?: StrandFormationDisclosure): Promise<PendingJoinStatus>;
3562
+ /**
3563
+ * Every join asked for with {@link requestJoin} and not dismissed, pending or finished, as this
3564
+ * machine sees it (see `PendingJoinStatus` for which states are machine-local).
3565
+ */
3566
+ listPendingJoins(): Promise<PendingJoinStatus[]>;
3567
+ /**
3568
+ * Remove a join request party-wide. On a pending request this cancels it: every owner machine
3569
+ * stops at its next read, and an attempt already running does not record its outcome (a join
3570
+ * it made stays on that machine, as any {@link formStrand} join does). Owner machines only.
3571
+ * Resolves `false` when there was no such request.
3572
+ */
3573
+ dismissPendingJoin(id: string): Promise<boolean>;
3574
+ private requirePendingJoins;
3575
+ /** This machine's owner key and signer for an owner-signed control write, or a throw carrying `refusal`. */
3576
+ private requireOwnerSigner;
3577
+ /**
3578
+ * Write a join request's row, or adopt the party's existing row for the same invitation: a
3579
+ * pending one as it is, a finished one replaced by `requested` (the user asked again).
3580
+ */
3581
+ private recordJoinRequest;
3582
+ /** Build and start the pending-join retry loop over this node (see {@link pendingJoinRunner}). */
3583
+ private startPendingJoinRunner;
3584
+ /** The invitation a `PendingJoin` row stores. A failure names the row, never the invitation, which is a bearer credential. */
3585
+ private pendingJoinInvitation;
3586
+ private pendingJoinSAppId;
3587
+ /**
3588
+ * Stage the membership invitation of each `joined` row this process has not staged yet, so a
3589
+ * closed strand joined on another owner machine is seated by whichever machine launches it
3590
+ * first; the party key it admits is the replicated `StrandPartyKey` row the finishing machine
3591
+ * seated. A strand that already has an invitation staged keeps it, for the reconciler to
3592
+ * settle first.
3593
+ */
3594
+ private stageMembershipInvitesFromPendingJoins;
3464
3595
  /**
3465
3596
  * Remember the strand a formation just joined (see {@link joinedStrandStore}), so it
3466
3597
  * is re-offered after a restart even when the app is killed before its `addStrand`, and