@serfab/cadre-core 1.13.0 → 1.14.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 (120) hide show
  1. package/dist/cadre-invite-protocol.d.ts +348 -0
  2. package/dist/cadre-invite-protocol.js +605 -0
  3. package/dist/cadre-invite-protocol.js.map +1 -0
  4. package/dist/cadre-node.d.ts +523 -180
  5. package/dist/cadre-node.js +921 -293
  6. package/dist/cadre-node.js.map +1 -1
  7. package/dist/claim-proof.d.ts +45 -0
  8. package/dist/claim-proof.js +132 -0
  9. package/dist/claim-proof.js.map +1 -0
  10. package/dist/control-authorization.d.ts +80 -19
  11. package/dist/control-authorization.js +55 -13
  12. package/dist/control-authorization.js.map +1 -1
  13. package/dist/control-cohort.js +1 -1
  14. package/dist/control-cohort.js.map +1 -1
  15. package/dist/control-database.d.ts +514 -97
  16. package/dist/control-database.js +1033 -195
  17. package/dist/control-database.js.map +1 -1
  18. package/dist/control-protocol-guard.d.ts +142 -0
  19. package/dist/control-protocol-guard.js +293 -0
  20. package/dist/control-protocol-guard.js.map +1 -0
  21. package/dist/control-schema.d.ts +1 -1
  22. package/dist/control-schema.js +801 -1067
  23. package/dist/control-schema.js.map +1 -1
  24. package/dist/control-stream.d.ts +4 -0
  25. package/dist/control-stream.js +22 -7
  26. package/dist/control-stream.js.map +1 -1
  27. package/dist/delegate-admission.d.ts +1 -1
  28. package/dist/delegate-admission.js +1 -1
  29. package/dist/device-token.d.ts +11 -9
  30. package/dist/device-token.js +14 -12
  31. package/dist/device-token.js.map +1 -1
  32. package/dist/ed25519-key.d.ts +15 -0
  33. package/dist/ed25519-key.js +28 -1
  34. package/dist/ed25519-key.js.map +1 -1
  35. package/dist/index.d.ts +14 -9
  36. package/dist/index.js +27 -9
  37. package/dist/index.js.map +1 -1
  38. package/dist/link-budget.d.ts +16 -29
  39. package/dist/link-budget.js +16 -31
  40. package/dist/link-budget.js.map +1 -1
  41. package/dist/membership-connection-gater.d.ts +190 -151
  42. package/dist/membership-connection-gater.js +235 -173
  43. package/dist/membership-connection-gater.js.map +1 -1
  44. package/dist/node-claim-payload.d.ts +19 -0
  45. package/dist/node-claim-payload.js +120 -0
  46. package/dist/node-claim-payload.js.map +1 -0
  47. package/dist/owner-anchor-sync.d.ts +22 -0
  48. package/dist/owner-anchor-sync.js +143 -0
  49. package/dist/owner-anchor-sync.js.map +1 -0
  50. package/dist/peer-authorization.d.ts +113 -18
  51. package/dist/peer-authorization.js +225 -29
  52. package/dist/peer-authorization.js.map +1 -1
  53. package/dist/peer-dial.d.ts +19 -0
  54. package/dist/peer-dial.js +22 -1
  55. package/dist/peer-dial.js.map +1 -1
  56. package/dist/peer-join-backfill.d.ts +26 -2
  57. package/dist/peer-join-backfill.js +54 -12
  58. package/dist/peer-join-backfill.js.map +1 -1
  59. package/dist/peer-record.d.ts +13 -9
  60. package/dist/peer-record.js +16 -12
  61. package/dist/peer-record.js.map +1 -1
  62. package/dist/pending-join-runner.d.ts +66 -58
  63. package/dist/pending-join-runner.js +136 -145
  64. package/dist/pending-join-runner.js.map +1 -1
  65. package/dist/push-fanout.d.ts +8 -2
  66. package/dist/push-fanout.js +8 -2
  67. package/dist/push-fanout.js.map +1 -1
  68. package/dist/seed-bootstrap.d.ts +147 -122
  69. package/dist/seed-bootstrap.js +284 -269
  70. package/dist/seed-bootstrap.js.map +1 -1
  71. package/dist/seed-trust-policy.d.ts +98 -37
  72. package/dist/seed-trust-policy.js +188 -2
  73. package/dist/seed-trust-policy.js.map +1 -1
  74. package/dist/strand-addr-protocol.d.ts +24 -26
  75. package/dist/strand-addr-protocol.js +19 -26
  76. package/dist/strand-addr-protocol.js.map +1 -1
  77. package/dist/strand-formation-protocol.js +1 -1
  78. package/dist/strand-formation-protocol.js.map +1 -1
  79. package/dist/strand-revocation-enforcer.d.ts +11 -11
  80. package/dist/strand-revocation-enforcer.js +11 -11
  81. package/dist/strand-wake-protocol.d.ts +13 -16
  82. package/dist/strand-wake-protocol.js +13 -20
  83. package/dist/strand-wake-protocol.js.map +1 -1
  84. package/dist/trusted-owner-store.d.ts +35 -5
  85. package/dist/trusted-owner-store.js +50 -8
  86. package/dist/trusted-owner-store.js.map +1 -1
  87. package/dist/types.d.ts +302 -87
  88. package/dist/types.js.map +1 -1
  89. package/package.json +8 -7
  90. package/src/cadre-invite-protocol.ts +864 -0
  91. package/src/cadre-node.ts +9381 -8686
  92. package/src/claim-proof.ts +147 -0
  93. package/src/control-authorization.ts +203 -117
  94. package/src/control-cohort.ts +1 -1
  95. package/src/control-database.ts +1216 -218
  96. package/src/control-protocol-guard.ts +348 -0
  97. package/src/control-schema.ts +812 -1078
  98. package/src/control-stream.ts +26 -7
  99. package/src/delegate-admission.ts +1 -1
  100. package/src/device-token.ts +14 -10
  101. package/src/ed25519-key.ts +28 -1
  102. package/src/index.ts +84 -10
  103. package/src/link-budget.ts +16 -32
  104. package/src/membership-connection-gater.ts +637 -570
  105. package/src/node-claim-payload.ts +125 -0
  106. package/src/owner-anchor-sync.ts +171 -0
  107. package/src/peer-authorization.ts +258 -29
  108. package/src/peer-dial.ts +22 -1
  109. package/src/peer-join-backfill.ts +56 -11
  110. package/src/peer-record.ts +16 -11
  111. package/src/pending-join-runner.ts +729 -735
  112. package/src/push-fanout.ts +8 -2
  113. package/src/seed-bootstrap.ts +344 -321
  114. package/src/seed-trust-policy.ts +353 -140
  115. package/src/strand-addr-protocol.ts +26 -35
  116. package/src/strand-formation-protocol.ts +1 -1
  117. package/src/strand-revocation-enforcer.ts +11 -11
  118. package/src/strand-wake-protocol.ts +13 -23
  119. package/src/trusted-owner-store.ts +242 -171
  120. package/src/types.ts +2471 -2242
@@ -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, PendingJoinStatus } from './types.js';
3
+ import type { CadreNodeConfig, StrandInstance, StrandRow, StrandConfig, FoundStrandConfig, FoundStrandResult, SAppConfig, CadreNodeEvents, ControlNetworkSeed, ApplySeedResult, SeedAckMessage, SeedDeliveryTarget, AddDroneOptions, DroneInitResult, OpenInvitation, FormStrandResult, StrandFormationDisclosure, StrandMembershipInvite, ResolveOpts, SelfRegistrationOutcome, ServiceWakeResult, PushPlatform, DeviceTokenRecord, CadreInviteStatus, CreateCadreInvitationOptions, CreateCadreInvitationResult, RedeemCadreInvitationResult, 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';
@@ -10,8 +10,9 @@ import { type SAppIdLookup } from './strand-watcher.js';
10
10
  import { type ControlCohortReconcileResult } from './control-cohort.js';
11
11
  import { EnrollmentService } from './enrollment.js';
12
12
  import { ControlDatabase } from './control-database.js';
13
+ import { type CadreInvitation } from './cadre-invite-protocol.js';
13
14
  import { SeedBootstrapService } from './seed-bootstrap.js';
14
- import type { SeedTrustPolicy } from './seed-trust-policy.js';
15
+ import { type SeedTrustPolicy } from './seed-trust-policy.js';
15
16
  import { StrandSolicitationService, type StrandSolicitationServiceOptions } from './strand-solicitation.js';
16
17
  import { type RelayReservationState, type RelayReservationSupervisorOptions } from './relay-reservation.js';
17
18
  import type { WakeAck } from './types.js';
@@ -38,6 +39,15 @@ export declare const STRAND_PEER_ADDR_REFRESH_MS: number;
38
39
  * minute rather than one a tick.
39
40
  */
40
41
  export declare const STRAND_PEER_ADDR_RETRY_MS: number;
42
+ /**
43
+ * Default lifetime of an UNTARGETED, OWNER-GRANTING cadre invitation
44
+ * ({@link CadreNode.createCadreInvitation}): whoever holds the bundle becomes an owner, so
45
+ * it is a bearer credential for admin rights and lives only long enough to be pasted or
46
+ * scanned by the device it was minted for.
47
+ */
48
+ export declare const OWNER_INVITATION_DEFAULT_TTL_MS: number;
49
+ /** Default lifetime of every other cadre invitation: targeted, or granting membership only. */
50
+ export declare const CADRE_INVITATION_DEFAULT_TTL_MS: number;
41
51
  type EventHandler<T> = (data: T) => void;
42
52
  /**
43
53
  * CadreNode is the main entry point for a cadre member.
@@ -78,11 +88,25 @@ export declare class CadreNode implements SAppIdLookup {
78
88
  * start() cycle rebuilds it with a fresh caught-up-peer memo.
79
89
  */
80
90
  private controlBackfill;
91
+ /**
92
+ * Devices whose invitation redemption this member has verified and is admitting right now,
93
+ * each with its count of overlapping redemptions. The control backfill's gate admits them
94
+ * as it admits a member, for that window only ({@link catchUpRedeemingDevice}).
95
+ */
96
+ private readonly admittingDevices;
81
97
  private strandWatcher;
82
98
  private strandManager;
83
99
  private hibernationManager;
84
100
  private enrollmentService;
85
101
  private seedBootstrapService;
102
+ /**
103
+ * The claim-secret trust policy of a node started with `config.claim`, built once by
104
+ * {@link initializeClaimPolicy} over {@link trustedOwnerStore} and handed to every
105
+ * seed service this node constructs ({@link inboundSeedTrustPolicy}). Kept across
106
+ * stop()→start() like the anchor it writes: its in-process latch and failure window
107
+ * are per node, not per seed service. Null when the node has no claim secret.
108
+ */
109
+ private claimTrustPolicy;
86
110
  private strandSolicitationService;
87
111
  /**
88
112
  * The service whose formation handler is registered on the control node. Trails
@@ -101,6 +125,13 @@ export declare class CadreNode implements SAppIdLookup {
101
125
  * stops conflating control addresses with strand seeding.
102
126
  */
103
127
  private strandAddrService;
128
+ /**
129
+ * Cadre invitation redemption handler (`/sereus/cadre-invite/1.0.0`): a device holding an
130
+ * invitation dials this machine and proves possession; the handler seats the row and
131
+ * admits the device against this node's control database. Registered by {@link start}
132
+ * once that database is up, on every node.
133
+ */
134
+ private cadreInviteHandler;
104
135
  /**
105
136
  * Server-side push-wake fan-out. Constructed by {@link start} only when
106
137
  * `config.push` (an injected `PushNotifier` + policy) is present — without it
@@ -205,24 +236,10 @@ export declare class CadreNode implements SAppIdLookup {
205
236
  /**
206
237
  * Most-recently pushed invite addresses (see {@link setInviteAddresses}).
207
238
  * When non-null these take priority over `libp2pNode.getMultiaddrs()` when
208
- * minting invites — the host pushes NAT-resolved addresses here so the
209
- * control-network node never needs to dial back to the manager.
239
+ * minting cadre invitations — the host pushes NAT-resolved addresses here so
240
+ * the control-network node never needs to dial back to the manager.
210
241
  */
211
242
  private latestInviteAddresses;
212
- /**
213
- * Epoch ms until which the control-network inbound gate admits
214
- * not-yet-authorized peers (see {@link admitInboundControlConnection} /
215
- * {@link openEnrollmentWindow}). 0 = no window open. Opened automatically by
216
- * {@link createInvite}; deliberately NOT reset on stop() — a stop/start cycle
217
- * inside an outstanding invite's validity must not strand the invitee.
218
- *
219
- * NOTE: in-memory only, so a PROCESS restart mid-invite closes the door until
220
- * the owner re-mints or the invitee is authorized. Fine while invites are
221
- * short-lived and redeemed promptly; if long-lived invites or restart-prone
222
- * hosts become normal, persist the window (or derive it from the issued-invite
223
- * records `SeedBootstrapService` already keeps).
224
- */
225
- private enrollmentWindowUntil;
226
243
  /**
227
244
  * Lazily-parsed PeerIds of `controlNetwork.bootstrapNodes` (see
228
245
  * {@link getBootstrapPeerIds}) — infrastructure the inbound gate always admits.
@@ -284,10 +301,8 @@ export declare class CadreNode implements SAppIdLookup {
284
301
  * Does this node's CONTROL libp2p run the circuit-relay server, and with which
285
302
  * init? Resolved once from `network` and `profile` by the same function every
286
303
  * strand node's build uses (`relay-server.ts`). Read by
287
- * {@link buildControlNodeOptions} (which configures the server from it),
288
- * {@link admitInboundControlConnection} (whose deny/admit-for-relay branch must
289
- * agree with whether a reservation is even servable here), and the
290
- * unauthorized budget above (whose TTL is the server's).
304
+ * {@link buildControlNodeOptions} (which configures the server from it) and
305
+ * the unauthorized budget above (whose TTL is the server's).
291
306
  */
292
307
  private readonly relayServer;
293
308
  /**
@@ -393,7 +408,7 @@ export declare class CadreNode implements SAppIdLookup {
393
408
  private readonly pendingMembershipInvites;
394
409
  /**
395
410
  * Invite keys of the membership invitations this process has staged, by
396
- * {@link adoptFormationMembershipInvite} or from a `joined` `PendingJoin` row
411
+ * {@link adoptFormationMembershipInvite} or from a `JoinSuccess` row
397
412
  * ({@link stageMembershipInvitesFromPendingJoins}), so one the reconciler has settled is not
398
413
  * staged again by the next pending-join pass. Same lifetime as
399
414
  * {@link pendingMembershipInvites}: a restarted process stages each live one once more, and the
@@ -737,17 +752,63 @@ export declare class CadreNode implements SAppIdLookup {
737
752
  private resolveIdentityKey;
738
753
  /**
739
754
  * Construct (or adopt) the node-local trusted-owner anchor and seed the
740
- * out-of-band pinned keys from `config.trustedOwners`. The store is NEVER
741
- * sourced from the replicated control DB — its entries come only from
742
- * genesis self-trust ({@link initializeSeedBootstrap}), config pins (here),
743
- * or runtime enrollment pins ({@link trustOwnerKeys}).
755
+ * out-of-band pinned keys from `config.trustedOwners`. The anchor's BASE comes
756
+ * only from out of band — genesis self-trust ({@link initializeSeedBootstrap}),
757
+ * config pins (here), runtime enrollment pins ({@link trustOwnerKeys}) and a
758
+ * claim; everything else in it is derived from that base along verifiable
759
+ * chains through the replicated `OwnerKey` table ({@link syncOwnerAnchor}),
760
+ * never read from the table directly.
744
761
  *
745
762
  * Idempotent across stop()→start(): the store instance is kept, and
746
763
  * re-seeding config pins is a no-op ({@link TrustedOwnerStore.trust} is
747
764
  * idempotent). An injected store scoped to a different party is a
748
765
  * configuration error (fail closed before any network bring-up).
766
+ *
767
+ * NOTE: a config pin of a key the owners have since REMOVED is re-anchored here at
768
+ * every start and removed again by the first sync (`refreshMembershipGate('start')`,
769
+ * fired without await from {@link start}), so such a pin is honoured for at most one
770
+ * membership refresh per start; dropping the pin from the config ends that.
749
771
  */
750
772
  private initializeTrustedOwnerStore;
773
+ /**
774
+ * Refuse a `config.claim` that sits beside another cold-start trust source. A node
775
+ * has exactly ONE, so `claim` with a non-empty `trustedOwners.pinnedKeys` or a
776
+ * `seedTrustPolicy` is refused here rather than silently letting one of them win.
777
+ * Asserted at this seam, not by a type: the embedders build `CadreNodeConfig` as one
778
+ * object literal with every optional field present, which a discriminated union over
779
+ * the whole config would not express.
780
+ *
781
+ * Runs BEFORE {@link initializeTrustedOwnerStore}, which writes the config pins into
782
+ * the anchor: on a persistent anchor a pin written ahead of this refusal would outlive
783
+ * the failed start, and the node, restarted with the pin dropped from its config and
784
+ * `claim` kept, would read as claimed by that pin and never accept its claimant.
785
+ */
786
+ private validateClaimConfig;
787
+ /**
788
+ * Build {@link claimTrustPolicy} from `config.claim`, once per node (a restart keeps
789
+ * the policy, as {@link initializeTrustedOwnerStore} keeps the anchor it writes).
790
+ * Runs after the anchor exists and before any network bring-up, so a malformed secret
791
+ * fails `start()` closed with nothing to tear down; `parseClaimSecret` names the
792
+ * problem and never echoes the value. The conflicts with the other trust sources are
793
+ * {@link validateClaimConfig}'s, checked before the anchor is written.
794
+ */
795
+ private initializeClaimPolicy;
796
+ /**
797
+ * Is this node waiting to be claimed: started with `config.claim` and owned by nobody
798
+ * yet (empty anchor)? Read live from the anchor every time, as
799
+ * {@link admitControlPeerUnconditionally} reads it per stream: the anchor is the one
800
+ * source of truth for "claimed", and a cached flag would need its own invalidation
801
+ * from the policy's persist, the rollback and a restart. False on a node with no claim
802
+ * secret, on a claimed one (restarted with the secret still set or not), and before
803
+ * {@link start} has built the anchor.
804
+ *
805
+ * While true the node admits strangers' CONNECTIONS (the claim seed has to ride one)
806
+ * and refuses their control-database streams ({@link authorizeInboundControlStream})
807
+ * and relay reservations ({@link admitControlRelayReservation}): an unclaimed node has
808
+ * no siblings to replicate from and must not become a public relay while it waits.
809
+ * cadre-cli reports this on `/status`; `claim:accepted` is the event form.
810
+ */
811
+ isAwaitingClaim(): boolean;
751
812
  /**
752
813
  * Construct (or adopt) the node-local cold-start bootstrap-peer store (see
753
814
  * {@link bootstrapPeerStore}). Synchronous: an injected store has already
@@ -828,35 +889,33 @@ export declare class CadreNode implements SAppIdLookup {
828
889
  getTrustedOwnerStore(): TrustedOwnerStore | null;
829
890
  /**
830
891
  * Persist out-of-band-established owner keys into the node-local anchor —
831
- * the runtime enrollment seam: call with `CadreInvite.ownerKeys` when
832
- * redeeming an invite (BEFORE the first `applySeed`, so the anchor already
833
- * holds the pins when seed trust consults it), or with an operator-supplied
834
- * pin. Idempotent. ('genesis' provenance is reserved for the node's own
835
- * founding key, seeded internally by {@link initializeSeedBootstrap}.)
892
+ * the runtime enrollment seam: {@link redeemCadreInvitation} calls it with the
893
+ * cadre invitation's `ownerKeys` BEFORE dialing a member (so the anchor already
894
+ * holds the pins when the reply and the rows that follow are judged), and an
895
+ * embedder calls it with an operator-supplied pin. Idempotent. ('genesis'
896
+ * provenance is reserved for the node's own founding key, seeded internally by
897
+ * {@link initializeSeedBootstrap}; 'chain' is written only by {@link syncOwnerAnchor}.)
836
898
  *
837
899
  * Validates every key's shape before trusting any of them (all-or-nothing):
838
900
  * a malformed entry anywhere in `keys` rejects the whole call before a
839
- * single key is anchored. For the invite route this means a `CadreInvite`
901
+ * single key is anchored. For the invitation route this means a bundle
840
902
  * carrying one malformed `ownerKeys` entry fails the redemption outright —
841
903
  * consistent with the anchor's existing whole-record-or-nothing policy for
842
904
  * a corrupt persisted entry (see `trusted-owner-store.ts`'s
843
905
  * `unusableEntry: 'discard-all'`) — rather than silently anchoring a subset
844
906
  * and leaving the caller to notice a key went missing.
845
907
  */
846
- trustOwnerKeys(keys: Iterable<string>, source: Exclude<TrustSource, 'genesis'>): Promise<void>;
908
+ trustOwnerKeys(keys: Iterable<string>, source: Exclude<TrustSource, 'genesis' | 'chain'>): Promise<void>;
847
909
  private createControlNode;
848
910
  /**
849
- * The control libp2p node's Optimystic network name — ONE binding for every
850
- * derivation from it (`db-p2p` namespaces all of the node's protocol ids as
851
- * `/optimystic/<networkName>/...`), so the node options and the block-transfer
852
- * protocol prefix the control backfill dials can never drift apart.
853
- *
854
- * NOTE: the party id goes in UNENCODED here, unlike in `controlStorageScope`. Safe
855
- * today because a party id is locally configured rather than replicated in, and both
856
- * ends of a connection derive this string identically — an odd party id yields an odd
857
- * but consistent protocol id, not a mismatch or an escaped name. If a party id ever
858
- * arrives from the network, encode it here as the storage scope key already does.
911
+ * Refuse non-members on every members-only protocol of the control node, at one seam
912
+ * (`control-protocol-guard.ts`). Wake and strand-addr are judged by the live
913
+ * {@link isAuthorizedMember}, bounded by the asker's own attempt deadline at this
914
+ * node's declared link; everything a control-database read depends on is judged by
915
+ * the snapshot ({@link authorizeInboundControlStream}).
859
916
  */
917
+ private installControlProtocolGuard;
918
+ /** This node's control network name ({@link controlNetworkName}, the one binding for it). */
860
919
  private controlNetworkName;
861
920
  /**
862
921
  * Arm the CONTROL network's peer-join block catch-up: push every block in the
@@ -883,12 +942,27 @@ export declare class CadreNode implements SAppIdLookup {
883
942
  * reconnect re-runs identify), and — because the production join order is
884
943
  * connect-then-authorize — on every committed membership change via
885
944
  * {@link refreshAuthorizedControlPeers}'s `scheduleConnectedPeers()` call.
945
+ * The one non-member the gate admits is a device whose invitation
946
+ * redemption this member is admitting ({@link catchUpRedeemingDevice}).
886
947
  *
887
948
  * No-ops (logged) when the embedder configured no control storage or the
888
949
  * node exposes no key network — the backfill would have nothing to read or
889
950
  * no way to dial.
890
951
  */
891
952
  private startControlBackfill;
953
+ /**
954
+ * Push this member's control store to a device whose invitation redemption has verified,
955
+ * and wait for the push, before the redemption seats the invitation's row and writes the
956
+ * device's rows (the handler's `catchUpDevice`; `CadreInviteHandler.catchUpDeviceIfLive`
957
+ * says why those writes need it).
958
+ * The device is admitted at the backfill's gate for the length of the push only.
959
+ *
960
+ * Through {@link PeerJoinBackfill.forceCatchUpPeer}, not `catchUpPeer`: the device's own
961
+ * `peer:identify` scheduled a run a moment earlier, which was denied if it passed the gate
962
+ * before the device was added here, and `catchUpPeer` would answer that run's in-flight
963
+ * status with an empty result.
964
+ */
965
+ private catchUpRedeemingDevice;
892
966
  /**
893
967
  * The control database's cache-wrapped raw storage, resolved from
894
968
  * `config.storage.provider` on first use and then held in {@link controlStorage}
@@ -945,78 +1019,76 @@ export declare class CadreNode implements SAppIdLookup {
945
1019
  */
946
1020
  private logRelayServerSettings;
947
1021
  /**
948
- * Decide whether an inbound CONTROL-network connection from `remotePeerId`
949
- * should be admitted — the policy behind the connection gater wired in
950
- * {@link createControlNode}. Deny only on a positive "unauthorized outsider
951
- * while no stranger path is open" determination; everything ambiguous admits
952
- * and defers to the fail-closed per-stream gates (see
1022
+ * Decide how an inbound CONTROL-network connection from `remotePeerId` is
1023
+ * admitted — the policy behind the connection gater wired in
1024
+ * {@link createControlNode}. Every connection is admitted; the answer decides
1025
+ * only whether it carries the provisional deadline (see
953
1026
  * `membership-connection-gater.ts` for the layer's rationale and the
954
- * stranger-open protocol allowlist).
1027
+ * stranger-open protocol allowlist). Asked once when the connection arrives
1028
+ * and again at a provisional connection's deadline.
955
1029
  *
956
- * Returns `'admit'` when ANY of:
1030
+ * Returns `'admit'` (no deadline) when ANY of:
957
1031
  * 1. a shared-baseline check admits ({@link admitControlPeerUnconditionally}
958
1032
  * — not running / DB torn down, absent-or-empty trusted-owner anchor, or
959
1033
  * configured bootstrap/relay infrastructure);
960
- * 2. an enrollment window is open ({@link openEnrollmentWindow}, opened by
961
- * {@link createInvite}) — the invitee dials in before it is authorized;
962
- * 3. the peer holds a live DELEGATE ADMISSION GRANT — an authorized member
1034
+ * 2. the peer holds a live DELEGATE ADMISSION GRANT — an authorized member
963
1035
  * announced it (over the strand-addr RPC) as the transport peerId of its
964
1036
  * own strand node, so a NAT'd member's strand node can hold a
965
1037
  * circuit-relay reservation here (see `delegate-admission.ts`).
966
1038
  * Connection only: the per-stream gate below never honors a grant;
967
- * 4. the authorized-member set is empty — cold start: the rows that would
1039
+ * 3. the authorized-member set is empty — cold start: the rows that would
968
1040
  * authorize anyone arrive by replication over these very connections;
969
- * 5. the peer IS an authorized member; or
970
- * 6. an open invitation is OUTSTANDING — at least one unexpired,
1041
+ * 4. the peer IS an authorized member; or
1042
+ * 5. an open invitation is OUTSTANDING — at least one unexpired,
971
1043
  * not-fully-consumed invitation this node minted or persisted (see
972
1044
  * `StrandSolicitationService.hasOutstandingInvitation`). A formation
973
1045
  * initiator is another party's peer by design and its token is only
974
1046
  * checkable inside the protocol, so the gate asks the coarser question
975
1047
  * "does this node expect a stranger at all?". REGISTERING the responder
976
- * does not suspend stranger denial: every node registers one at
977
- * {@link start}, and registering mints no invitation.
1048
+ * does not count: every node registers one at {@link start}, and
1049
+ * registering mints no invitation; or
1050
+ * 6. a LIVE cadre invitation exists — a `CadreInvite` row this node holds
1051
+ * that is not withdrawn, unexpired, has uses left and whose issuer is
1052
+ * still an owner (`ControlDatabase.hasLiveCadreInvite`). The device that
1053
+ * redeems it is a stranger until the redemption writes its row, and its
1054
+ * proof of possession is only checkable inside `/sereus/cadre-invite/1.0.0`;
1055
+ * same reasoning as check 5, and the same expectation-of-a-stranger key.
978
1056
  *
979
1057
  * Ordering is semantically free (the checks are OR'd) but decides who pays:
980
- * checks 1-3 are in-memory, 4/5 share one control-DB read, and only a peer
981
- * already on the deny path reaches check 6's invitation lookup.
1058
+ * checks 1-2 are in-memory, 3/4 share one control-DB read, and only a peer
1059
+ * that is otherwise headed for a provisional admission reaches check 5's and
1060
+ * 6's invitation lookups.
982
1061
  *
983
- * Caveats of check 6, both self-healing:
1062
+ * Caveats of check 5:
984
1063
  * - the in-memory mint registry dies with the process, so after a restart
985
1064
  * only invitations persisted as `FormationInvite` rows still hold the
986
- * exemption open (re-mint otherwise — same story as the enrollment window);
1065
+ * window open (re-mint otherwise);
987
1066
  * - a peer holding a token whose `FormationInvite` row has not replicated to
988
- * this node yet is denied even though the formation handler would have
989
- * accepted it, exactly like the unreplicated-membership-row case below.
990
- *
991
- * When every check falls through, the verdict depends on whether this node
992
- * runs the circuit-relay server ({@link relayServer}): without one,
993
- * `'deny'`; with one, `'admit-for-relay'` — a circuit-relay reservation is
994
- * established by the reserving peer DIALING the relay, so a connection deny
995
- * here kills the reservation, and that deny is NOT self-healing: an outbound
996
- * reconcile re-dial re-establishes a data link, but no outbound dial can
997
- * grant the remote peer a reservation, and a relay-only peer has no address
998
- * of its own to dial back — the reservation IS its address. The gater admits
999
- * such a connection, decides the reservation via
1000
- * {@link admitControlRelayReservation}, and drops the connection if no
1001
- * reservation is admitted in time (see `membership-connection-gater.ts` →
1002
- * "The relay-reservation seam").
1003
- *
1004
- * NOTE: check 4/5 runs a control-DB read per inbound connection
1067
+ * this node yet is admitted only provisionally, so its connection is closed
1068
+ * at the deadline unless the row has landed by then.
1069
+ *
1070
+ * When every check falls through: `'admit-provisionally'`, on every node,
1071
+ * relay server or not. The gate sees only a peer id, so it cannot tell an
1072
+ * outsider from a device whose invitation row this node has not received yet
1073
+ * (it redeems here by seating the row from its bundle, after which check 6
1074
+ * admits) or from a sibling whose membership row is still replicating (check 4
1075
+ * admits once it lands; if it has not landed by the deadline, the connection
1076
+ * is closed and either side's next reconcile dial re-forms it, since outbound
1077
+ * dials are never gated). The protocol guard refuses all of them every
1078
+ * members-only protocol meanwhile. A circuit-relay reservation admitted by
1079
+ * {@link admitControlRelayReservation} disarms the deadline (see
1080
+ * `membership-connection-gater.ts` → "The relay-reservation seam").
1081
+ *
1082
+ * NOTE: check 3/4 runs a control-DB read per inbound connection
1005
1083
  * (`listAuthorizedMembers`); connections are rare and cadres small, and this
1006
1084
  * layer is fail-open behind `ADMISSION_DECISION_TIMEOUT_MS`, so the live
1007
1085
  * read is safe here — unlike the per-stream gate, which must consult the
1008
1086
  * materialized {@link authorizedControlPeers} snapshot instead.
1009
- * NOTE: check 6 adds a second control read (`hasOutstandingFormationInvite`) for a
1010
- * stranger with no locally minted invitation in play, on every node now that every
1011
- * node runs the responder — a relay-enabled storage node included. If stranger
1012
- * connections to such a node ever arrive fast enough for that read to show, cache
1013
- * the answer for a few seconds.
1014
- * NOTE: on a relay-DISABLED node, a sibling whose membership row has not yet
1015
- * replicated here is denied until the row converges (typically via the
1016
- * owner); either side's next outbound reconcile dial (outbound is never
1017
- * gated) re-establishes the DATA link — self-healing for data, visible as a
1018
- * transient deny. That self-healing story never covered a reservation, which
1019
- * is exactly why the relay-enabled path above exists.
1087
+ * NOTE: checks 5 and 6 add two more control reads (`hasOutstandingFormationInvite`,
1088
+ * `hasLiveCadreInvite`) for a stranger with no locally minted invitation in play, on
1089
+ * every node, and a provisional connection's deadline runs the whole decision once
1090
+ * more. If stranger connections ever arrive fast enough for those reads — on arrival
1091
+ * or at the deadline — to show, cache the answers for a few seconds.
1020
1092
  */
1021
1093
  private admitInboundControlConnection;
1022
1094
  /**
@@ -1026,7 +1098,13 @@ export declare class CadreNode implements SAppIdLookup {
1026
1098
  * "The relay-reservation seam"); the circuit-relay server consults it per
1027
1099
  * RESERVE request, so it is never called on a node whose relay server is off.
1028
1100
  *
1029
- * Admits when ANY of:
1101
+ * Refuses everyone, ahead of every admission below, while this node is waiting to be
1102
+ * claimed ({@link isAwaitingClaim}): a storage-profile node runs the relay server by
1103
+ * default, and an unclaimed node on a public address must not become anyone's relay.
1104
+ * The empty-anchor admission in check 1 exists for a node that will be enrolled by
1105
+ * its siblings, which an unclaimed node has none of.
1106
+ *
1107
+ * Otherwise admits when ANY of:
1030
1108
  * 1. a shared-baseline check admits ({@link admitControlPeerUnconditionally}
1031
1109
  * — not running / DB torn down, absent-or-empty trusted-owner anchor, or
1032
1110
  * configured bootstrap/relay infrastructure);
@@ -1046,31 +1124,23 @@ export declare class CadreNode implements SAppIdLookup {
1046
1124
  * that boots, reserves, and only then has its row replicate here would
1047
1125
  * otherwise keep that slot spent for the rest of the entry's TTL.
1048
1126
  *
1049
- * The connection gate's stranger carve-outs (an open enrollment window, an
1050
- * outstanding formation invitation) deliberately do NOT extend here: they
1051
- * exist so a stranger's SEED or FORMATION stream can ride a connection, and
1052
- * neither needs relay capacity. A genuine invitee that does need a relay slot
1053
- * takes one from the budget like any other unplaced peer, so an open window
1054
- * never becomes an unbounded grant of this node's forwarding capacity.
1127
+ * The connection gate's stranger carve-outs (an outstanding formation
1128
+ * invitation, a live cadre invitation) deliberately do NOT extend here: they
1129
+ * exist so a stranger's FORMATION or CADRE-INVITE stream can ride a connection,
1130
+ * and neither needs relay capacity. A genuine invitee that does need a relay
1131
+ * slot takes one from the budget like any other unplaced peer, so a live
1132
+ * invitation never becomes an unbounded grant of this node's forwarding capacity.
1055
1133
  */
1056
1134
  private admitControlRelayReservation;
1057
1135
  /** Admit a reservation on the peer's own merits, giving back any budget slot it still holds. */
1058
1136
  private admitReservationUncounted;
1059
- /**
1060
- * Hold the control-network inbound gate open for not-yet-authorized peers
1061
- * until `untilEpochMs` (extends, never shrinks, an already-open window).
1062
- * {@link createInvite} calls this automatically; a host running an
1063
- * out-of-band enrollment flow (e.g. accepting a phone whose invite this node
1064
- * never minted) can open it explicitly before the stranger dials in.
1065
- */
1066
- openEnrollmentWindow(untilEpochMs: number): void;
1067
1137
  /**
1068
1138
  * Record (or refresh) a delegate admission grant: `delegatePeerId` is the
1069
1139
  * transport peerId of `announcerPeerId`'s strand-`strandId` node, admitted
1070
1140
  * at the CONNECTION and RESERVATION levels (all a circuit-relay reservation
1071
1141
  * needs, without spending the unauthorized-reservation budget) for
1072
- * `DELEGATE_GRANT_TTL_MS`. Called by the strand-addr responder after its
1073
- * authorized-membership gate has passed; public so tests can drive the
1142
+ * `DELEGATE_GRANT_TTL_MS`. Called by the strand-addr responder, which the
1143
+ * protocol guard admits only authorized members to; public so tests can drive the
1074
1144
  * admission policy without a full strand launch. A re-announce for the same
1075
1145
  * (announcer, strand) REPLACES the previous delegate rather than
1076
1146
  * accumulating. Never honored by {@link authorizeInboundControlStream}.
@@ -1097,25 +1167,35 @@ export declare class CadreNode implements SAppIdLookup {
1097
1167
  */
1098
1168
  private admitControlPeerUnconditionally;
1099
1169
  /**
1100
- * Per-stream authorization for the four Optimystic control-DB protocols
1101
- * (`/optimystic/control-<party>/{repo,cluster,sync,block-transfer}/…`),
1102
- * wired as `authorizeInboundStream` in {@link createControlNode} — the
1103
- * fail-closed layer behind the fail-open connection gater. Control node
1104
- * ONLY: strand cohort nodes legitimately serve cross-party peers.
1170
+ * Per-stream authorization for the protocol guard's `members-snapshot` class
1171
+ * (`control-protocol-guard.ts`): the four Optimystic control-DB protocols
1172
+ * (`/optimystic/control-<party>/{repo,cluster,sync,block-transfer}/…`), FRET's
1173
+ * five, and any protocol nobody classed. Also wired as Optimystic's
1174
+ * `authorizeInboundStream` in {@link createControlNode} for the four DB
1175
+ * protocols. The fail-closed layer behind the fail-open connection gater.
1176
+ * Control node ONLY: strand cohort nodes legitimately serve cross-party peers.
1105
1177
  *
1106
1178
  * The STRICT SUBSET of {@link admitInboundControlConnection}: the same
1107
1179
  * "no basis to judge" admissions (shared via
1108
1180
  * {@link admitControlPeerUnconditionally}), minus the stranger carve-outs
1109
- * (enrollment window, outstanding open invitation, delegate admission
1110
- * grant). The first two exist so a stranger can reach `/sereus/seed/1.0.0`
1111
- * and `/sereus/formation/1.0.0` — neither is gated here, and admitting a
1112
- * stranger to `repo` during an enrollment window is exactly the hole this
1181
+ * (outstanding open invitation, live cadre invitation, delegate admission
1182
+ * grant). The first two exist so a stranger can reach `/sereus/formation/1.0.0`
1183
+ * and `/sereus/cadre-invite/1.0.0` — neither is gated here, and admitting a
1184
+ * stranger to `repo` while an invitation is live is exactly the hole this
1113
1185
  * gate closes. A DELEGATE-admitted connection (a member's strand node
1114
1186
  * holding a circuit-relay reservation, see `delegate-admission.ts`) is
1115
1187
  * likewise exactly a case this gate must still refuse: the delegate gets
1116
1188
  * the connection and never the control DB.
1117
1189
  *
1118
- * Admits when ANY of:
1190
+ * Refuses everyone, ahead of every admission below, while this node is waiting to be
1191
+ * claimed ({@link isAwaitingClaim}). The empty-anchor and empty-snapshot admissions
1192
+ * exist so a brand-new node can take replication from the siblings that will
1193
+ * authorize it; an unclaimed node has no siblings yet, and the rows that will
1194
+ * authorize anyone arrive only after the claim, over the owner's streams that the
1195
+ * same two admissions then let in. Configured bootstrap infra is refused too: a
1196
+ * relay opens no control-database stream.
1197
+ *
1198
+ * Otherwise admits when ANY of:
1119
1199
  * 1. a shared-baseline check admits (not running / no DB, empty anchor,
1120
1200
  * configured bootstrap infra);
1121
1201
  * 2. the materialized authorized set is empty — cold start, before the
@@ -1152,6 +1232,10 @@ export declare class CadreNode implements SAppIdLookup {
1152
1232
  * read here would sleep its backoff holding the lock and stall every other local
1153
1233
  * writer. Nothing is lost — this refresh already keeps the previous snapshot on
1154
1234
  * failure and is re-driven by the next membership write and by the timed reconcile.
1235
+ *
1236
+ * The trusted-owner anchor sync ({@link syncOwnerAnchor}) runs FIRST, so the snapshot
1237
+ * built here judges `CadrePeer` rows against the anchor as the `OwnerKey` table now
1238
+ * has it. The invitation chain both reads may need is loaded at most once per refresh.
1155
1239
  */
1156
1240
  private refreshAuthorizedControlPeers;
1157
1241
  /**
@@ -1322,6 +1406,18 @@ export declare class CadreNode implements SAppIdLookup {
1322
1406
  * since that key is internal to libp2p and not exposed.
1323
1407
  */
1324
1408
  getIdentityOwnerKey(): Ed25519KeyPair;
1409
+ /**
1410
+ * Publish this node's own address record from the reconcile pass, for a CLAIMED node that
1411
+ * has not published one this session. Its owner writes its `CadrePeer` row only after the
1412
+ * claim is accepted (`claimNode`), so the row reaches this node by replication, after the
1413
+ * boot-time publish and the first-growth drain have both found nothing to publish; without
1414
+ * this the record would wait for the heartbeat ({@link startRecordRefresh}), and the cadre
1415
+ * could not resolve the node's addresses until then. The reconcile pass is where replicated
1416
+ * membership is picked up, so each pass tries until the first publish lands. A try before
1417
+ * the row has arrived is a `skipped` publish: one `CadrePeer` read. A claimed node later
1418
+ * removed from its cadre never publishes again, so it pays that read on every pass.
1419
+ */
1420
+ private publishSelfRecordOnceClaimed;
1325
1421
  /**
1326
1422
  * Collect this node's current dialable addresses for publication, signaling
1327
1423
  * (`/p2p-circuit`) first. Prefers the best invite/NAT-resolved set and folds
@@ -1502,8 +1598,7 @@ export declare class CadreNode implements SAppIdLookup {
1502
1598
  * addresses ({@link dialPeerAddrs}): the reconcile pass's steady-state sibling
1503
1599
  * ({@link dialControlSibling}) and cold-start bootstrap peer
1504
1600
  * ({@link dialBootstrapPeer}) dials, and — handed to every
1505
- * {@link SeedBootstrapService} this node builds — `applySeed`'s owner dials and
1506
- * `dialInvite`.
1601
+ * {@link SeedBootstrapService} this node builds — `applySeed`'s owner dials.
1507
1602
  *
1508
1603
  * See `peer-dial.ts`'s `DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS` for why a peer's dial is
1509
1604
  * bounded as a whole, and `DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS` for why each
@@ -3168,14 +3263,21 @@ export declare class CadreNode implements SAppIdLookup {
3168
3263
  * 1. it is not this node itself — a node publishes its own `CadrePeer` address
3169
3264
  * row so its dialable address rides in seeds, but "self" is not a peer this
3170
3265
  * node authorized;
3171
- * 2. its `CadrePeer` row carries a complete voucher (`StampId`, `VouchOwner`,
3172
- * `VouchSig` all non-null);
3266
+ * 2. its `CadrePeer` row carries a complete proof: EITHER an owner voucher
3267
+ * (`StampId`, `VouchOwner`, `VouchSig` all non-null) OR an invitation admission
3268
+ * (`VouchSig` null, `VouchUsage` naming a `CadreInviteUsage` row);
3173
3269
  * 3. `VouchOwner` is in the NODE-LOCAL trusted-owner anchor
3174
- * ({@link getTrustedOwnerStore}) — never the replicated `OwnerKey` table,
3175
- * which any stranger can genesis-pollute; and
3176
- * 4. `VouchSig` verifies as that owner's signature over the row's voucher
3177
- * digest ({@link verifyCadrePeerVoucher}), so the anchored owner really
3178
- * vouched THIS peer id under THIS row's nonce; and
3270
+ * ({@link getTrustedOwnerStore}), which follows the replicated `OwnerKey` table
3271
+ * only along chains that verify from this node's out-of-band keys
3272
+ * ({@link syncOwnerAnchor}) — never the table itself, which any stranger can
3273
+ * genesis-pollute; and
3274
+ * 4. the proof verifies: for a voucher, `VouchSig` is that owner's signature over
3275
+ * the row's voucher digest ({@link verifyCadrePeerVoucher}), so the anchored
3276
+ * owner really vouched THIS peer id under THIS row's nonce; for an admission,
3277
+ * the chain row → usage → invitation → anchored issuer verifies link by link
3278
+ * ({@link verifyInvitationAdmission} — the issuer is `VouchOwner`, the invitation's
3279
+ * own owner signature, the holder's and the device's signatures, and the stored
3280
+ * peer key really is the key behind the peer id); and
3179
3281
  * 5. the row's `StampId` is NOT retired in `CadreControl.Revocation` — enforced
3180
3282
  * upstream in {@link ControlDatabase.queryCadrePeers}, which drops retired rows
3181
3283
  * before ANY reader (this predicate included) sees them, so a row resurrected
@@ -3184,18 +3286,26 @@ export declare class CadreNode implements SAppIdLookup {
3184
3286
  * local rows) is still inert to every reader that has the tombstone.
3185
3287
  *
3186
3288
  * Fail-closed at every step: a missing anchor (pre-start), an empty anchor (a
3187
- * not-yet-enrolled node authorizes no one), a null/partial voucher, an
3188
- * unanchored `VouchOwner`, or a bad signature all yield "not authorized" —
3189
- * having an address row is NOT membership. The control-network wake and
3190
- * strand-address gates consult this set, NOT the addressable one.
3289
+ * not-yet-enrolled node authorizes no one), a null/partial proof, an
3290
+ * unanchored `VouchOwner`, a bad signature, or a usage or invitation row this node
3291
+ * does not hold all yield "not authorized" — having an address row is NOT
3292
+ * membership. The protocol guard admits wake and strand-address requests by this set,
3293
+ * NOT the addressable one.
3294
+ *
3295
+ * The usage and invitation tables are read only when at least one row needs them,
3296
+ * so a cadre with no invitation-admitted member stays at one read per call.
3191
3297
  *
3192
3298
  * NOTE (rotation): if a party owner rotates keys and only the NEW key is pinned
3193
3299
  * in the anchor, rows the OLD key vouched fail check 3 until re-vouched — a
3194
3300
  * legit member goes un-authorized on readers that only pin the new key. Full
3195
3301
  * rotation handling (re-vouch on rotate) is the
3196
- * `flip-strand-membership-rotation-known-gap` work, not this predicate's.
3302
+ * `flip-strand-membership-rotation-known-gap` work, not this predicate's. The
3303
+ * `OwnerKey` table differs: an owner the rotated-out (or removed) key vouched keeps
3304
+ * its place in every anchor that had derived it ({@link syncOwnerAnchor}: a derived
3305
+ * entry stays while its own row is live), and is only out of reach for a machine that
3306
+ * never held it until a remaining owner re-adds it or an invitation pins it.
3197
3307
  *
3198
- * @param retry - Whether the underlying membership read may retry a transient cluster
3308
+ * @param retry - Whether the underlying membership reads may retry a transient cluster
3199
3309
  * failure. Only {@link refreshAuthorizedControlPeers} passes `false`, because it runs
3200
3310
  * as the control database's membership listener with that database's write lock held;
3201
3311
  * see its comment.
@@ -3204,11 +3314,96 @@ export declare class CadreNode implements SAppIdLookup {
3204
3314
  peerId: string;
3205
3315
  multiaddr: string | null;
3206
3316
  }>>;
3317
+ /**
3318
+ * The body of {@link listAuthorizedMembers}, with the invitation chain supplied as a
3319
+ * source so the membership refresh can share one load with {@link syncOwnerAnchor}.
3320
+ */
3321
+ private collectAuthorizedMembers;
3207
3322
  /**
3208
3323
  * Checks 2–4 of the authorized-membership predicate (see
3209
- * {@link listAuthorizedMembers}) for one `CadrePeer` row: complete voucher,
3210
- * `VouchOwner` in the node-local anchor, signature valid over the row's
3211
- * (PeerId, StampId) voucher digest.
3324
+ * {@link listAuthorizedMembers}) for one `CadrePeer` row, by the kind of proof it
3325
+ * carries: an owner voucher ({@link hasAnchoredVoucher}) or an invitation admission
3326
+ * ({@link verifyInvitationAdmission} over the usage and invitation rows in `chain`,
3327
+ * loaded only when some row needs them). A row with a `VouchSig` is judged as a
3328
+ * voucher even if it also names a usage — the signature is the stronger proof and the
3329
+ * one an owner re-vouch leaves behind.
3330
+ */
3331
+ private hasAnchoredProof;
3332
+ /**
3333
+ * Recompute the node-local trusted-owner anchor from the replicated `OwnerKey` table along
3334
+ * verifiable chains (`owner-anchor-sync.ts` → {@link deriveOwnerAnchor}), as the first step
3335
+ * of every membership refresh ({@link refreshAuthorizedControlPeers}) so the snapshot built
3336
+ * in the same pass judges `CadrePeer` rows against the updated anchor. A key whose live
3337
+ * row's stored proof verifies against a key already anchored enters under `chain` and
3338
+ * stays while its row is live; a key an anchored owner's verifiable tombstone names
3339
+ * leaves, base pins included — which is how a config pin of a removed key, re-anchored at
3340
+ * every start ({@link initializeTrustedOwnerStore}), is removed again. The derivation
3341
+ * refuses removals that would empty the anchor; they are logged here.
3342
+ *
3343
+ * With an EMPTY anchor the sync does nothing: there is no key to derive from, and a node
3344
+ * waiting to be claimed ({@link isAwaitingClaim}: claim secret set, anchor empty) must
3345
+ * stay empty until the claim anchors its claimant — the replicated table must never seat
3346
+ * the first key. The early return below is the seam that rests on.
3347
+ *
3348
+ * Any read failure skips the whole pass and leaves the anchor as it was; the refresh then
3349
+ * builds its snapshot against that anchor, which is also what its previous snapshot was
3350
+ * judged against. Caught here rather than failing the refresh, because the membership read
3351
+ * tolerates an unreachable `Revocation` block on an isolated node
3352
+ * (`ControlDatabase.readRevokedStampRows`) and `queryRevocations` does not: failing the
3353
+ * whole refresh on that would freeze such a node's gate snapshot.
3354
+ * NOTE: on such a node the sync is skipped at every refresh until the block is held; if an
3355
+ * isolated node ever has to derive owner additions while cut off, give `queryRevocations`
3356
+ * the same cohort-unreachable fallback.
3357
+ *
3358
+ * NOTE: cost per membership refresh is one `OwnerKey` read and one `Revocation` read (the
3359
+ * tombstone list, whose `OwnerKey` stamps are handed to `queryOwnerKeyRows` so it does not
3360
+ * read the retired set again), plus the invitation chain when some owner row is
3361
+ * invitation-admitted — shared with the membership read through `loadChain`; the memo
3362
+ * suggestion on {@link loadInvitationChain} covers that part. Before the Revocation ledger
3363
+ * marker is filed that one read is a cohort consult per refresh, which
3364
+ * `control-founding-consult-budget.spec.ts` pins.
3365
+ *
3366
+ * @param retry - forwarded to every read; `false` on the refresh path, for the reason on
3367
+ * {@link refreshAuthorizedControlPeers}.
3368
+ */
3369
+ private syncOwnerAnchor;
3370
+ /**
3371
+ * Apply one pass's result: `chain` entries for the keys it newly derived, removal of the keys
3372
+ * it started with that are outside `target` — base keys a verifiable tombstone named, and
3373
+ * `chain` entries whose row is gone.
3374
+ *
3375
+ * Diffed against `before`, the snapshot the derivation was computed from, NOT the live store:
3376
+ * the anchor is written out of band while the pass's reads are in flight (an invitation pin,
3377
+ * the genesis anchor, a claim, a seed), and such a key is outside `target` only because the
3378
+ * pass never saw it — judging the live store would remove it. Likewise a key removed
3379
+ * meanwhile (a claim rollback) must not come back as `chain`. Both are judged by the next
3380
+ * pass. Each change lands in memory synchronously (`TrustedOwnerStore.trust` / `remove`); a
3381
+ * failed PERSIST is logged and does not stop the pass or fail the refresh, as
3382
+ * `SeedBootstrapService.anchorAcceptedSigner` treats the same failure.
3383
+ */
3384
+ private applyOwnerAnchor;
3385
+ /**
3386
+ * A per-refresh {@link InvitationChainSource}: the first call loads the chain through
3387
+ * {@link loadInvitationChain}, every later call shares that load.
3388
+ */
3389
+ private invitationChainSource;
3390
+ /**
3391
+ * The usage and invitation rows {@link hasAnchoredProof} and {@link syncOwnerAnchor}
3392
+ * resolve an invitation-admitted row through, keyed for lookup. Both reads honour `retry`
3393
+ * for {@link listAuthorizedMembers}' reason.
3394
+ *
3395
+ * NOTE: once invitations are the only admission route (ticket
3396
+ * `cadre-invitations-redeemable-by-any-member`), every cadre with a member not yet
3397
+ * re-vouched pays these two whole-table reads and three ed25519 verifies per row on every
3398
+ * membership refresh. Both tables are append-only and a verified chain never changes (expiry,
3399
+ * use count and withdrawal are not re-checked), so if the gate's admission deadline ever
3400
+ * shows this, memoise verified (row stamp → usage stamp) pairs per node and skip the reads
3401
+ * when every admitted row is already memoised.
3402
+ */
3403
+ private loadInvitationChain;
3404
+ /**
3405
+ * The owner-voucher half of {@link hasAnchoredProof}: complete voucher, `VouchOwner` in
3406
+ * the node-local anchor, signature valid over the row's (PeerId, StampId) voucher digest.
3212
3407
  *
3213
3408
  * NOTE: verifies the ed25519 signature on every call (no memo of already-
3214
3409
  * verified (peerId, stampId, vouchSig) triples). Cadres are a handful of
@@ -3219,8 +3414,8 @@ export declare class CadreNode implements SAppIdLookup {
3219
3414
  private hasAnchoredVoucher;
3220
3415
  /**
3221
3416
  * Probe whether a given peer is an AUTHORIZED party member (see
3222
- * {@link listAuthorizedMembers}) — the gate the control-network wake and
3223
- * strand-address responders consult, NOT {@link isMember} (the addressable
3417
+ * {@link listAuthorizedMembers}) — the check the protocol guard runs ahead of the
3418
+ * control-network wake and strand-address handlers, NOT {@link isMember} (the addressable
3224
3419
  * surface). Deliberately scans the full membership (one `CadrePeer` query)
3225
3420
  * rather than adding a single-row read path: cadres are small, and one code
3226
3421
  * path keeps the predicate impossible to drift from the list.
@@ -3233,20 +3428,23 @@ export declare class CadreNode implements SAppIdLookup {
3233
3428
  * record (via {@link resolvePeerAddrs}, signaling/relay first — so a NAT'd peer
3234
3429
  * is reachable through its circuit-relay address), dials `WAKE_PROTOCOL`, sends
3235
3430
  * the {@link WakeRequest}, and returns the peer's {@link WakeAck}. The receiver
3236
- * gates the request on cadre membership and only resumes a strand it already
3237
- * participates in; it acks once it has decided, before the strand is up. The
3238
- * dial deadlines derive from this node's `network.linkRoundTripMs`.
3431
+ * only resumes a strand it already participates in; it acks once it has decided,
3432
+ * before the strand is up. The dial deadlines derive from this node's
3433
+ * `network.linkRoundTripMs`.
3239
3434
  *
3240
3435
  * @param targetPeerId - The hibernating cadre peer to wake.
3241
3436
  * @param strandId - The strand the caller knows has pending activity.
3242
3437
  * @param reason - Optional cause hint, e.g. `"activity"` or `"manual"`.
3243
- * @throws if the node is not started or the target has no dialable address.
3438
+ * @throws if the node is not started, the target has no dialable address, or no
3439
+ * attempt got an ack — including when the receiver does not count this node as an
3440
+ * authorized member, since its protocol guard resets the stream without a reply.
3244
3441
  */
3245
3442
  pushWake(targetPeerId: string, strandId: string, reason?: string): Promise<WakeAck>;
3246
3443
  /**
3247
3444
  * Enable the seed listener for receiving seeds via the /sereus/seed/1.0.0 protocol.
3248
3445
  * This is for drone nodes that need to receive seeds without being an owner.
3249
- * Does not require an owner key.
3446
+ * Does not require an owner key. Idempotent; a node started with `config.claim`
3447
+ * calls it itself inside {@link start}, so an embedder's own call is a no-op.
3250
3448
  */
3251
3449
  enableSeedListener(): Promise<void>;
3252
3450
  /**
@@ -3278,6 +3476,15 @@ export declare class CadreNode implements SAppIdLookup {
3278
3476
  * vouched for stays denied until the next timed cohort reconcile.
3279
3477
  */
3280
3478
  private seedEventCallbacks;
3479
+ /**
3480
+ * The node-wide default trust policy for INBOUND seeds, the one value every
3481
+ * {@link SeedBootstrapService} this node constructs is given — the owner service,
3482
+ * the listener, and `applySeed`'s temp service — so the three cannot drift. The
3483
+ * claim policy when the node was started with `config.claim` (the two are refused
3484
+ * together at start, so there is nothing to merge), otherwise the configured
3485
+ * `seedTrustPolicy`, and `undefined` for the service's anchored default.
3486
+ */
3487
+ private inboundSeedTrustPolicy;
3281
3488
  /**
3282
3489
  * Re-materialize the authorized-peer snapshot that the fail-closed per-stream
3283
3490
  * control-DB gate ({@link authorizeInboundControlStream}) judges against.
@@ -3359,6 +3566,26 @@ export declare class CadreNode implements SAppIdLookup {
3359
3566
  * @param peerId - The peer ID to remove
3360
3567
  */
3361
3568
  removePeer(peerId: string): Promise<void>;
3569
+ /**
3570
+ * Seat another owner key, signed by this node's owner key (the signed-add branch of
3571
+ * `OwnerKey.Authorized`; the row stores the vouching pair). The write notifies the
3572
+ * membership hub, so the refresh it triggers has run by the time this resolves.
3573
+ *
3574
+ * @param ownerPublicKeyB64 - the new owner's ed25519 public key (base64url).
3575
+ * @returns `true` when the row was seated, `false` when the key is already a live owner.
3576
+ */
3577
+ addOwner(ownerPublicKeyB64: string): Promise<boolean>;
3578
+ /**
3579
+ * Remove an owner key, signed by this node's owner key (which must be a different owner):
3580
+ * the delete and its `Revocation` tombstone commit together. Refused by name when the key
3581
+ * is this node's own or the only live owner. The write notifies the membership hub; the
3582
+ * tombstone rides the committed-delete seam ({@link noteGuardedDelete}), so one committed
3583
+ * while alone is re-issued on cohort growth like every other removal.
3584
+ *
3585
+ * @param ownerPublicKeyB64 - the owner's ed25519 public key (base64url).
3586
+ * @returns `true` when the row was removed, `false` when the key is not a live owner here.
3587
+ */
3588
+ removeOwner(ownerPublicKeyB64: string): Promise<boolean>;
3362
3589
  /**
3363
3590
  * Create a seed from the current control network state.
3364
3591
  * The seed contains peer information and is signed by an owner.
@@ -3368,10 +3595,12 @@ export declare class CadreNode implements SAppIdLookup {
3368
3595
  * Apply a seed to populate the peer cache and enable connections.
3369
3596
  *
3370
3597
  * Validates the seed signature, then evaluates a trust anchor for the signer
3371
- * key (see `SeedTrustPolicy`). An enrollment caller can pass a per-seed
3372
- * `trustPolicy` override — e.g. a `pinnedKeyTrustPolicy` built from a
3373
- * `CadreInvite.ownerKeys` — so a cold-start node can accept its first
3374
- * seed without reconfiguring the service.
3598
+ * key (see `SeedTrustPolicy`). An operator-driven caller can pass a per-seed
3599
+ * `trustPolicy` override — e.g. a `pinnedKeyTrustPolicy` built from a pinned
3600
+ * owner key — so a cold-start node can accept its first seed without
3601
+ * reconfiguring the service. The override wins on a node started with
3602
+ * `config.claim` as well: the claim secret is the node's default inbound policy,
3603
+ * not a lock on its local operator, who already controls the process.
3375
3604
  */
3376
3605
  applySeed(seed: ControlNetworkSeed, options?: {
3377
3606
  trustPolicy?: SeedTrustPolicy;
@@ -3385,11 +3614,17 @@ export declare class CadreNode implements SAppIdLookup {
3385
3614
  private noteAppliedSeed;
3386
3615
  /**
3387
3616
  * Deliver a seed directly to a peer via the /sereus/seed/1.0.0 protocol.
3617
+ *
3618
+ * `target` is a multiaddr string, or a peer id with its addresses (the form a phone
3619
+ * uses for a node it scanned), which are dialed one at a time before the request
3620
+ * (`SeedBootstrapService.deliverSeed`). `options.claimProof` rides beside the seed when
3621
+ * the target is a brand-new node being claimed (`claim-proof.ts`).
3622
+ *
3623
+ * @throws {PeerUnreachableError} for a peer id with addresses, when no connection to it formed.
3388
3624
  */
3389
- deliverSeed(targetMultiaddr: string, seed: ControlNetworkSeed): Promise<{
3390
- accepted: boolean;
3391
- reason?: string;
3392
- }>;
3625
+ deliverSeed(target: SeedDeliveryTarget, seed: ControlNetworkSeed, options?: {
3626
+ claimProof?: string;
3627
+ }): Promise<SeedAckMessage>;
3393
3628
  /**
3394
3629
  * Encode a seed for out-of-band delivery (e.g., QR code, copy/paste).
3395
3630
  */
@@ -3423,32 +3658,138 @@ export declare class CadreNode implements SAppIdLookup {
3423
3658
  */
3424
3659
  addDrone(options: AddDroneOptions): Promise<DroneInitResult>;
3425
3660
  /**
3426
- * Create an invite for a phone to join the cadre.
3427
- * Use when a server wants to invite a NAT'd phone.
3428
- */
3429
- createInvite(token?: string, expiresIn?: number): Promise<InviteResult>;
3430
- /**
3431
- * Accept a phone connection using an invite.
3432
- * Call this when a phone dials in with an invite token.
3433
- */
3434
- acceptPhone(options: AddPhoneOptions, issuedInvite?: CadreInvite): Promise<void>;
3661
+ * Claim a node that was started with a claim secret (`CadreNodeConfig.claim`), from
3662
+ * the peer id, addresses and secret its host showed (the QR code): mint a seed, prove
3663
+ * possession of the secret beside it (`claim-proof.ts`), deliver it, and on acceptance
3664
+ * vouch the node into this cadre and keep its addresses as a dial target. The owner
3665
+ * side of the claim-secret entry in the Seed Delivery Protocol (`docs/architecture.md`).
3666
+ *
3667
+ * Requires the seed service with an owner key ({@link initializeSeedBootstrap}), as
3668
+ * {@link addDrone} does. The order is the REVERSE of `addDrone`'s: seed first, row
3669
+ * second. A refused claim or a failed delivery therefore leaves no `CadrePeer` row and
3670
+ * no retained dial target behind on this node — a phone that typed the wrong secret,
3671
+ * or scanned a node someone else already owns, has nothing to clean up.
3672
+ *
3673
+ * A failure is one of three kinds a caller can tell apart:
3674
+ *
3675
+ * - `ClaimRefusedError` — the node was reached and refused, with its refusal code when
3676
+ * its policy gave one.
3677
+ * - `PeerUnreachableError` — no connection to the node formed from any of its addresses,
3678
+ * so nothing was sent. Each address is dialed in turn on its own limit, so this can
3679
+ * take up to the per-peer dial limit plus the seed request's (114.5 s at the default
3680
+ * declared link) when none answers.
3681
+ * - anything else — the node was reached and the exchange failed, or a local step failed
3682
+ * (a malformed secret or address among them). Repeating the claim with the same secret
3683
+ * is safe (see below).
3684
+ *
3685
+ * Idempotent for the same owner: a second call after a dropped response is accepted
3686
+ * by the node (its anchored branch), the row insert is a no-op on an existing row
3687
+ * (`ControlDatabase.insertCadrePeer`), and the dial target is replaced, not duplicated.
3688
+ *
3689
+ * The first dial is kicked (not awaited) so it happens now rather than on the next
3690
+ * timed reconcile pass; a caller that wants to wait for the connection awaits
3691
+ * {@link reconcileControlCohort} itself, with `addDrone`'s caveat that a pass already
3692
+ * in flight is joined rather than restarted. The claimed node dials this one back too,
3693
+ * from the seed's owner list, when this node has a listen or relay address; a phone
3694
+ * with neither is reached by its own reconcile pass alone.
3695
+ *
3696
+ * @throws {ClaimRefusedError} when the node was reached and refused the claim.
3697
+ * @throws {PeerUnreachableError} when the node was never reached.
3698
+ */
3699
+ claimNode(target: {
3700
+ peerId: string;
3701
+ multiaddrs: string[];
3702
+ secret: string;
3703
+ }): Promise<void>;
3435
3704
  /**
3436
3705
  * Add a phone to the cadre with relay support.
3437
3706
  * Use when both nodes are NAT'd (phone-to-phone).
3438
3707
  */
3439
3708
  addPhoneWithRelay(phonePeerId: string): Promise<DroneInitResult>;
3440
3709
  /**
3441
- * Encode an invite for out-of-band delivery (QR, link, etc.).
3442
- */
3443
- encodeInvite(invite: CadreInvite): string;
3444
- /**
3445
- * Decode an invite from base64url encoding.
3446
- */
3447
- decodeInvite(encoded: string): CadreInvite;
3448
- /**
3449
- * Dial an owner from an invite (for phone joining via invite).
3450
- */
3451
- dialInvite(invite: CadreInvite): Promise<void>;
3710
+ * Mint an owner-signed cadre invitation that a device can redeem at ANY member machine of
3711
+ * this party, the owner offline included ({@link redeemCadreInvitation}). Requires the
3712
+ * owner key ({@link initializeSeedBootstrap}).
3713
+ *
3714
+ * The invitation is an ed25519 keypair: the public half is the `CadreInvite` row's key,
3715
+ * the private half rides in the returned bundle as the proof of possession. Whoever holds
3716
+ * the bundle can redeem it, so an UNTARGETED, OWNER-GRANTING invitation — a bearer
3717
+ * credential for admin rights — defaults to a 15-minute lifetime; every other kind to 24
3718
+ * hours. One use unless `uses` says otherwise. The bundle carries the signed row (so a
3719
+ * member that has not received it by replication seats it from the bundle), this node's
3720
+ * anchored owner keys — every current owner this node has verified, the out-of-band ones
3721
+ * and those derived along verifiable chains ({@link syncOwnerAnchor}) — (the device pins
3722
+ * them and checks the member's reply against them; sourced from the node-local anchor
3723
+ * only, never the raw replicated `OwnerKey` table, because the device anchors whatever
3724
+ * arrives and a stranger's genesis-inserted key must not ride an invitation into a fresh
3725
+ * node's anchor; an empty anchor is refused because a reply could not be checked), and
3726
+ * the addresses of this machine first
3727
+ * ({@link resolveInviteAddresses}, which honours pushed NAT addresses) then up to three
3728
+ * other members ({@link siblingInvitationAddrs}).
3729
+ *
3730
+ * The row commits locally and replicates like any other control write; minted while
3731
+ * alone (a phone with no connection), it reaches the other members through the
3732
+ * peer-join block catch-up on the next connection. The device can redeem at any member it
3733
+ * reaches meanwhile: a member that does not hold the row yet admits the device's connection
3734
+ * provisionally ({@link admitInboundControlConnection}) and seats the row from the bundle.
3735
+ *
3736
+ * @throws when no owner key is wired, the anchor is empty, `uses` is not a positive
3737
+ * integer, or neither this machine nor any other member has an address.
3738
+ */
3739
+ createCadreInvitation(options: CreateCadreInvitationOptions): Promise<CreateCadreInvitationResult>;
3740
+ /**
3741
+ * Every cadre invitation this node holds — minted here or replicated in — with its
3742
+ * standing ({@link CadreInviteStatus}: live, withdrawn, redemptions recorded).
3743
+ */
3744
+ listCadreInvitations(): Promise<CadreInviteStatus[]>;
3745
+ /**
3746
+ * Withdraw a cadre invitation: an owner-signed `Revocation` tombstone over its row, which
3747
+ * stays as the proof of membership for every device it admitted. No further redemption
3748
+ * succeeds anywhere the tombstone reaches, and a member holding it refuses the bundle's
3749
+ * copy of the row. A withdrawal committed while alone is re-issued on cohort growth like
3750
+ * any other tombstone.
3751
+ *
3752
+ * @returns `true` when this call filed the tombstone, `false` when the row is not held
3753
+ * here or was already withdrawn.
3754
+ */
3755
+ withdrawCadreInvitation(key: string): Promise<boolean>;
3756
+ /**
3757
+ * Join the cadre an invitation names by redeeming it at one of the member machines it
3758
+ * lists, the owner that minted it offline or not. Needs a started node with a stable
3759
+ * identity (`keyStore` or `privateKey`): the redemption is signed with the key behind
3760
+ * this node's peer id, and the member checks that the two match.
3761
+ *
3762
+ * Steps: refuse an invitation for another party; pin its `ownerKeys` into the node-local
3763
+ * anchor (`'invite'` provenance — the pin sticks even if the redemption then fails, as it
3764
+ * does for a seed invite, so a later attempt or seed from the same owner is anchored);
3765
+ * sign the request; dial the listed members in order ({@link redeemAtMembers}); on
3766
+ * acceptance verify the reply against the pinned keys, merge its dial hints into the peer
3767
+ * store, retain the answering member as a cold-start dial target, refresh the membership
3768
+ * gate and start a cohort reconcile pass. This node then syncs the control database over
3769
+ * the connection it already holds, the same unified behaviour as after a seed.
3770
+ *
3771
+ * A device that is already a member (a restart with the invitation still configured) is
3772
+ * answered as accepted by the member's idempotent redemption, with no second usage row.
3773
+ *
3774
+ * When the invitation grants ownership, the member seats this node's key as an `OwnerKey`
3775
+ * row by consent. The caller still has to wire that key for signing afterwards —
3776
+ * `initializeSeedBootstrap(ownKey)`, which anchors it; never `ensureOwnerKey`, the row is
3777
+ * already there. Other machines derive this node's key into their anchors from the row's
3778
+ * stored proof on their next membership refresh ({@link syncOwnerAnchor}).
3779
+ *
3780
+ * Limit: the reply check ({@link verifyRedeemReply}) does not authenticate the member. A
3781
+ * forged bundle, or a legitimate one whose addresses were swapped, dials a machine that
3782
+ * can echo the owner-signed row it was just sent; that is what pasting an attacker's
3783
+ * invitation means. What protects this node afterwards is its anchor: every row it then
3784
+ * syncs is judged against the pinned owner keys, so a machine that is not a member of the
3785
+ * cadre the invitation names can admit nobody this node will trust.
3786
+ *
3787
+ * @throws `CadreInviteRejectedError` on a final refusal, `CadreInviteReplyInvalidError` on
3788
+ * an acceptance that fails the reply check, `CadreInviteUnreachableError` when every
3789
+ * listed address was tried without an acceptance, and a plain `Error` for the
3790
+ * preconditions above.
3791
+ */
3792
+ redeemCadreInvitation(invitation: CadreInvitation): Promise<RedeemCadreInvitationResult>;
3452
3793
  /**
3453
3794
  * Replace this node's strand solicitation service — the formation responder every node
3454
3795
  * installs at {@link start}, and the initiator side of {@link formStrand}. Optional: call it
@@ -3501,10 +3842,12 @@ export declare class CadreNode implements SAppIdLookup {
3501
3842
  */
3502
3843
  createOpenInvitation(sAppId: string, expirationMs?: number): Promise<OpenInvitation>;
3503
3844
  /**
3504
- * Addresses of the party's other machines for an invitation's bootstrap list
3505
- * ({@link selectInvitationSiblingAddrs} picks which). The source is each authorized
3506
- * member's signed `CadrePeer` record ({@link resolvePeerRecord}), so a machine whose record
3507
- * is missing, stale or untrusted is left out; live connections only order them.
3845
+ * Addresses of the party's other machines for an invitation's bootstrap list — an open
3846
+ * strand invitation's ({@link createOpenInvitation}) or a cadre invitation's
3847
+ * ({@link createCadreInvitation}); {@link selectInvitationSiblingAddrs} picks which. The
3848
+ * source is each authorized member's signed `CadrePeer` record ({@link resolvePeerRecord}),
3849
+ * so a machine whose record is missing, stale or untrusted is left out; live connections
3850
+ * only order them.
3508
3851
  *
3509
3852
  * Best-effort: a failed read leaves the invitation naming this machine alone, which is what
3510
3853
  * it named before siblings were added.
@@ -3525,7 +3868,7 @@ export declare class CadreNode implements SAppIdLookup {
3525
3868
  * Ask to join through `invitation` and have the party keep trying until the join works, the
3526
3869
  * invitation is used up or expires, or the request is dismissed: across restarts, and from
3527
3870
  * any owner machine of the party (`docs/strands.md` → "Joining while the inviter is
3528
- * offline"). Records a party-wide `PendingJoin` row, runs one attempt on this machine at
3871
+ * offline"). Records a party-wide `JoinRequest` row, runs one attempt on this machine at
3529
3872
  * once, and returns the status after it: `'joined'` when the inviter answered yes, `'waiting'`
3530
3873
  * when it was unreachable or not ready, `'failed'` when it refused for good.
3531
3874
  *
@@ -3555,17 +3898,17 @@ export declare class CadreNode implements SAppIdLookup {
3555
3898
  /** This machine's owner key and signer for an owner-signed control write, or a throw carrying `refusal`. */
3556
3899
  private requireOwnerSigner;
3557
3900
  /**
3558
- * Write a join request's row, or adopt the party's existing row for the same invitation: a
3559
- * pending one as it is, a finished one replaced by `requested` (the user asked again).
3901
+ * Write a join request, or adopt the party's existing request for the same invitation: a
3902
+ * pending one as it is, a finished one rewritten as `requested` (the user asked again).
3560
3903
  */
3561
3904
  private recordJoinRequest;
3562
3905
  /** Build and start the pending-join retry loop over this node (see {@link pendingJoinRunner}). */
3563
3906
  private startPendingJoinRunner;
3564
- /** The invitation a `PendingJoin` row stores. A failure names the row, never the invitation, which is a bearer credential. */
3907
+ /** The invitation a `JoinRequest` row stores. A failure names the row, never the invitation, which is a bearer credential. */
3565
3908
  private pendingJoinInvitation;
3566
3909
  private pendingJoinSAppId;
3567
3910
  /**
3568
- * Stage the membership invitation of each `joined` row this process has not staged yet, so a
3911
+ * Stage the membership invitation of each joined request this process has not staged yet, so a
3569
3912
  * closed strand joined on another owner machine is seated by whichever machine launches it
3570
3913
  * first; the party key it admits is the replicated `StrandPartyKey` row the finishing machine
3571
3914
  * seated. A strand that already has an invitation staged keeps it, for the reconciler to