@serfab/cadre-core 1.13.0 → 1.15.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 (230) hide show
  1. package/README.md +293 -274
  2. package/dist/cadre-invite-protocol.d.ts +383 -0
  3. package/dist/cadre-invite-protocol.js +774 -0
  4. package/dist/cadre-invite-protocol.js.map +1 -0
  5. package/dist/cadre-join-request.d.ts +42 -0
  6. package/dist/cadre-join-request.js +144 -0
  7. package/dist/cadre-join-request.js.map +1 -0
  8. package/dist/cadre-node.d.ts +849 -210
  9. package/dist/cadre-node.js +1574 -374
  10. package/dist/cadre-node.js.map +1 -1
  11. package/dist/claim-proof.d.ts +45 -0
  12. package/dist/claim-proof.js +132 -0
  13. package/dist/claim-proof.js.map +1 -0
  14. package/dist/control-authorization.d.ts +80 -19
  15. package/dist/control-authorization.js +55 -13
  16. package/dist/control-authorization.js.map +1 -1
  17. package/dist/control-cohort.js +1 -1
  18. package/dist/control-cohort.js.map +1 -1
  19. package/dist/control-database.d.ts +605 -108
  20. package/dist/control-database.js +1299 -280
  21. package/dist/control-database.js.map +1 -1
  22. package/dist/control-protocol-guard.d.ts +142 -0
  23. package/dist/control-protocol-guard.js +293 -0
  24. package/dist/control-protocol-guard.js.map +1 -0
  25. package/dist/control-schema.d.ts +1 -1
  26. package/dist/control-schema.js +817 -1067
  27. package/dist/control-schema.js.map +1 -1
  28. package/dist/control-stream.d.ts +4 -0
  29. package/dist/control-stream.js +22 -7
  30. package/dist/control-stream.js.map +1 -1
  31. package/dist/delegate-admission.d.ts +1 -1
  32. package/dist/delegate-admission.js +1 -1
  33. package/dist/device-token.d.ts +11 -9
  34. package/dist/device-token.js +14 -12
  35. package/dist/device-token.js.map +1 -1
  36. package/dist/ed25519-key.d.ts +22 -9
  37. package/dist/ed25519-key.js +47 -10
  38. package/dist/ed25519-key.js.map +1 -1
  39. package/dist/fetch-impl.d.ts +12 -0
  40. package/dist/fetch-impl.js +23 -0
  41. package/dist/fetch-impl.js.map +1 -0
  42. package/dist/file-durable-slot.js +2 -1
  43. package/dist/file-durable-slot.js.map +1 -1
  44. package/dist/file-safe-name.d.ts +25 -0
  45. package/dist/file-safe-name.js +29 -0
  46. package/dist/file-safe-name.js.map +1 -0
  47. package/dist/formation-approval.js +5 -11
  48. package/dist/formation-approval.js.map +1 -1
  49. package/dist/fs-atomic.d.ts +0 -19
  50. package/dist/fs-atomic.js +3 -24
  51. package/dist/fs-atomic.js.map +1 -1
  52. package/dist/index.d.ts +22 -10
  53. package/dist/index.js +54 -11
  54. package/dist/index.js.map +1 -1
  55. package/dist/invitation-bootstrap.d.ts +50 -12
  56. package/dist/invitation-bootstrap.js +76 -13
  57. package/dist/invitation-bootstrap.js.map +1 -1
  58. package/dist/joined-strand-store.d.ts +7 -0
  59. package/dist/joined-strand-store.js +9 -0
  60. package/dist/joined-strand-store.js.map +1 -1
  61. package/dist/key-store-file.js +2 -1
  62. package/dist/key-store-file.js.map +1 -1
  63. package/dist/link-budget.d.ts +25 -33
  64. package/dist/link-budget.js +25 -35
  65. package/dist/link-budget.js.map +1 -1
  66. package/dist/machine-removal.d.ts +78 -0
  67. package/dist/machine-removal.js +166 -0
  68. package/dist/machine-removal.js.map +1 -0
  69. package/dist/membership-connection-gater.d.ts +191 -151
  70. package/dist/membership-connection-gater.js +236 -173
  71. package/dist/membership-connection-gater.js.map +1 -1
  72. package/dist/node-claim-payload.d.ts +74 -0
  73. package/dist/node-claim-payload.js +224 -0
  74. package/dist/node-claim-payload.js.map +1 -0
  75. package/dist/node-default-transports.d.ts +27 -0
  76. package/dist/node-default-transports.js +35 -0
  77. package/dist/node-default-transports.js.map +1 -0
  78. package/dist/node-transports.d.ts +30 -0
  79. package/dist/node-transports.js +40 -0
  80. package/dist/node-transports.js.map +1 -0
  81. package/dist/owner-anchor-sync.d.ts +22 -0
  82. package/dist/owner-anchor-sync.js +143 -0
  83. package/dist/owner-anchor-sync.js.map +1 -0
  84. package/dist/peer-authorization.d.ts +113 -18
  85. package/dist/peer-authorization.js +225 -29
  86. package/dist/peer-authorization.js.map +1 -1
  87. package/dist/peer-dial.d.ts +143 -27
  88. package/dist/peer-dial.js +276 -31
  89. package/dist/peer-dial.js.map +1 -1
  90. package/dist/peer-join-backfill.d.ts +26 -2
  91. package/dist/peer-join-backfill.js +60 -12
  92. package/dist/peer-join-backfill.js.map +1 -1
  93. package/dist/peer-record.d.ts +17 -52
  94. package/dist/peer-record.js +20 -58
  95. package/dist/peer-record.js.map +1 -1
  96. package/dist/pending-join-runner.d.ts +66 -58
  97. package/dist/pending-join-runner.js +136 -145
  98. package/dist/pending-join-runner.js.map +1 -1
  99. package/dist/primary-lan-address.d.ts +8 -0
  100. package/dist/primary-lan-address.js +46 -0
  101. package/dist/primary-lan-address.js.map +1 -0
  102. package/dist/provider/client.d.ts +131 -0
  103. package/dist/provider/client.js +226 -0
  104. package/dist/provider/client.js.map +1 -0
  105. package/dist/provider/descriptor.d.ts +41 -0
  106. package/dist/provider/descriptor.js +103 -0
  107. package/dist/provider/descriptor.js.map +1 -0
  108. package/dist/provider/index.d.ts +9 -0
  109. package/dist/provider/index.js +10 -0
  110. package/dist/provider/index.js.map +1 -0
  111. package/dist/provider/oauth.d.ts +125 -0
  112. package/dist/provider/oauth.js +199 -0
  113. package/dist/provider/oauth.js.map +1 -0
  114. package/dist/provider/rent-node.d.ts +101 -0
  115. package/dist/provider/rent-node.js +220 -0
  116. package/dist/provider/rent-node.js.map +1 -0
  117. package/dist/push-fanout.d.ts +8 -2
  118. package/dist/push-fanout.js +8 -2
  119. package/dist/push-fanout.js.map +1 -1
  120. package/dist/relay-addrs.d.ts +9 -4
  121. package/dist/relay-addrs.js +9 -4
  122. package/dist/relay-addrs.js.map +1 -1
  123. package/dist/relay-reservation.d.ts +51 -16
  124. package/dist/relay-reservation.js +107 -24
  125. package/dist/relay-reservation.js.map +1 -1
  126. package/dist/seed-bootstrap.d.ts +165 -123
  127. package/dist/seed-bootstrap.js +313 -275
  128. package/dist/seed-bootstrap.js.map +1 -1
  129. package/dist/seed-trust-policy.d.ts +98 -37
  130. package/dist/seed-trust-policy.js +188 -2
  131. package/dist/seed-trust-policy.js.map +1 -1
  132. package/dist/strand-addr-protocol.d.ts +24 -26
  133. package/dist/strand-addr-protocol.js +19 -26
  134. package/dist/strand-addr-protocol.js.map +1 -1
  135. package/dist/strand-commit-durability.d.ts +54 -0
  136. package/dist/strand-commit-durability.js +74 -0
  137. package/dist/strand-commit-durability.js.map +1 -0
  138. package/dist/strand-database.d.ts +23 -8
  139. package/dist/strand-database.js +41 -13
  140. package/dist/strand-database.js.map +1 -1
  141. package/dist/strand-first-sync-gate.d.ts +7 -1
  142. package/dist/strand-first-sync-gate.js +7 -1
  143. package/dist/strand-first-sync-gate.js.map +1 -1
  144. package/dist/strand-formation-protocol.js +5 -2
  145. package/dist/strand-formation-protocol.js.map +1 -1
  146. package/dist/strand-fret-addrs.d.ts +19 -1
  147. package/dist/strand-fret-addrs.js +27 -9
  148. package/dist/strand-fret-addrs.js.map +1 -1
  149. package/dist/strand-instance-manager.d.ts +81 -19
  150. package/dist/strand-instance-manager.js +216 -105
  151. package/dist/strand-instance-manager.js.map +1 -1
  152. package/dist/strand-move.d.ts +124 -0
  153. package/dist/strand-move.js +438 -0
  154. package/dist/strand-move.js.map +1 -0
  155. package/dist/strand-network-config.js +7 -6
  156. package/dist/strand-network-config.js.map +1 -1
  157. package/dist/strand-peer-redial.d.ts +113 -0
  158. package/dist/strand-peer-redial.js +236 -0
  159. package/dist/strand-peer-redial.js.map +1 -0
  160. package/dist/strand-revocation-enforcer.d.ts +11 -11
  161. package/dist/strand-revocation-enforcer.js +11 -11
  162. package/dist/strand-wake-protocol.d.ts +13 -16
  163. package/dist/strand-wake-protocol.js +23 -27
  164. package/dist/strand-wake-protocol.js.map +1 -1
  165. package/dist/trusted-owner-store.d.ts +35 -5
  166. package/dist/trusted-owner-store.js +50 -8
  167. package/dist/trusted-owner-store.js.map +1 -1
  168. package/dist/types.d.ts +512 -89
  169. package/dist/types.js +10 -0
  170. package/dist/types.js.map +1 -1
  171. package/package.json +19 -10
  172. package/src/cadre-invite-protocol.ts +1052 -0
  173. package/src/cadre-join-request.ts +167 -0
  174. package/src/cadre-node.ts +1703 -386
  175. package/src/claim-proof.ts +147 -0
  176. package/src/control-authorization.ts +203 -117
  177. package/src/control-cohort.ts +1 -1
  178. package/src/control-database.ts +4663 -3453
  179. package/src/control-protocol-guard.ts +348 -0
  180. package/src/control-schema.ts +828 -1078
  181. package/src/control-stream.ts +26 -7
  182. package/src/delegate-admission.ts +1 -1
  183. package/src/device-token.ts +14 -10
  184. package/src/ed25519-key.ts +47 -10
  185. package/src/fetch-impl.ts +23 -0
  186. package/src/file-durable-slot.ts +64 -63
  187. package/src/file-safe-name.ts +32 -0
  188. package/src/formation-approval.ts +604 -610
  189. package/src/fs-atomic.ts +92 -117
  190. package/src/index.ts +789 -648
  191. package/src/invitation-bootstrap.ts +91 -13
  192. package/src/joined-strand-store.ts +508 -498
  193. package/src/key-store-file.ts +142 -141
  194. package/src/link-budget.ts +25 -36
  195. package/src/machine-removal.ts +216 -0
  196. package/src/membership-connection-gater.ts +259 -191
  197. package/src/node-claim-payload.ts +259 -0
  198. package/src/node-default-transports.ts +38 -0
  199. package/src/node-transports.ts +49 -0
  200. package/src/owner-anchor-sync.ts +171 -0
  201. package/src/peer-authorization.ts +258 -29
  202. package/src/peer-dial.ts +604 -315
  203. package/src/peer-join-backfill.ts +62 -11
  204. package/src/peer-record.ts +159 -197
  205. package/src/pending-join-runner.ts +729 -735
  206. package/src/primary-lan-address.ts +47 -0
  207. package/src/provider/client.ts +326 -0
  208. package/src/provider/descriptor.ts +129 -0
  209. package/src/provider/index.ts +59 -0
  210. package/src/provider/oauth.ts +285 -0
  211. package/src/provider/rent-node.ts +275 -0
  212. package/src/push-fanout.ts +8 -2
  213. package/src/relay-addrs.ts +9 -4
  214. package/src/relay-reservation.ts +1211 -1121
  215. package/src/seed-bootstrap.ts +1502 -1448
  216. package/src/seed-trust-policy.ts +353 -140
  217. package/src/strand-addr-protocol.ts +26 -35
  218. package/src/strand-commit-durability.ts +116 -0
  219. package/src/strand-database.ts +46 -14
  220. package/src/strand-first-sync-gate.ts +7 -1
  221. package/src/strand-formation-protocol.ts +1039 -1036
  222. package/src/strand-fret-addrs.ts +179 -145
  223. package/src/strand-instance-manager.ts +2026 -1883
  224. package/src/strand-move.ts +585 -0
  225. package/src/strand-network-config.ts +7 -6
  226. package/src/strand-peer-redial.ts +307 -0
  227. package/src/strand-revocation-enforcer.ts +11 -11
  228. package/src/strand-wake-protocol.ts +23 -30
  229. package/src/trusted-owner-store.ts +242 -171
  230. package/src/types.ts +2687 -2242
@@ -1,17 +1,19 @@
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, CadreMachine, MachineRemovalPlan, MachineRemovalResult, RemoveMachineOptions, CadreRemoval, CadreInvitationReach, CreateCadreInvitationOptions, CreateCadreInvitationResult, RedeemCadreInvitationOptions, 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';
7
7
  import { type StrandNetworkStateStore } from './strand-network-state.js';
8
8
  import { type EnrolledMachineStore } from './enrolled-machine-store.js';
9
+ import { type JoinedStrandRecord } from './joined-strand-store.js';
9
10
  import { type SAppIdLookup } from './strand-watcher.js';
10
11
  import { type ControlCohortReconcileResult } from './control-cohort.js';
11
12
  import { EnrollmentService } from './enrollment.js';
12
13
  import { ControlDatabase } from './control-database.js';
14
+ import { type CadreInvitation } from './cadre-invite-protocol.js';
13
15
  import { SeedBootstrapService } from './seed-bootstrap.js';
14
- import type { SeedTrustPolicy } from './seed-trust-policy.js';
16
+ import { type SeedTrustPolicy } from './seed-trust-policy.js';
15
17
  import { StrandSolicitationService, type StrandSolicitationServiceOptions } from './strand-solicitation.js';
16
18
  import { type RelayReservationState, type RelayReservationSupervisorOptions } from './relay-reservation.js';
17
19
  import type { WakeAck } from './types.js';
@@ -38,6 +40,15 @@ export declare const STRAND_PEER_ADDR_REFRESH_MS: number;
38
40
  * minute rather than one a tick.
39
41
  */
40
42
  export declare const STRAND_PEER_ADDR_RETRY_MS: number;
43
+ /**
44
+ * Default lifetime of an UNTARGETED, OWNER-GRANTING cadre invitation
45
+ * ({@link CadreNode.createCadreInvitation}): whoever holds the bundle becomes an owner, so
46
+ * it is a bearer credential for admin rights and lives only long enough to be pasted or
47
+ * scanned by the device it was minted for.
48
+ */
49
+ export declare const OWNER_INVITATION_DEFAULT_TTL_MS: number;
50
+ /** Default lifetime of every other cadre invitation: targeted, or granting membership only. */
51
+ export declare const CADRE_INVITATION_DEFAULT_TTL_MS: number;
41
52
  type EventHandler<T> = (data: T) => void;
42
53
  /**
43
54
  * CadreNode is the main entry point for a cadre member.
@@ -78,11 +89,26 @@ export declare class CadreNode implements SAppIdLookup {
78
89
  * start() cycle rebuilds it with a fresh caught-up-peer memo.
79
90
  */
80
91
  private controlBackfill;
92
+ /**
93
+ * Devices whose invitation redemption this member has verified and is admitting right now,
94
+ * each with its count of overlapping redemptions. The control backfill's gate and the
95
+ * control cohort choice (Optimystic's `isNetworkMember`) count them as members, for that
96
+ * window only ({@link admitRedeemingDevice}); the per-stream gate never does.
97
+ */
98
+ private readonly admittingDevices;
81
99
  private strandWatcher;
82
100
  private strandManager;
83
101
  private hibernationManager;
84
102
  private enrollmentService;
85
103
  private seedBootstrapService;
104
+ /**
105
+ * The claim-secret trust policy of a node started with `config.claim`, built once by
106
+ * {@link initializeClaimPolicy} over {@link trustedOwnerStore} and handed to every
107
+ * seed service this node constructs ({@link inboundSeedTrustPolicy}). Kept across
108
+ * stop()→start() like the anchor it writes: its in-process latch and failure window
109
+ * are per node, not per seed service. Null when the node has no claim secret.
110
+ */
111
+ private claimTrustPolicy;
86
112
  private strandSolicitationService;
87
113
  /**
88
114
  * The service whose formation handler is registered on the control node. Trails
@@ -101,6 +127,13 @@ export declare class CadreNode implements SAppIdLookup {
101
127
  * stops conflating control addresses with strand seeding.
102
128
  */
103
129
  private strandAddrService;
130
+ /**
131
+ * Cadre invitation redemption handler (`/sereus/cadre-invite/1.0.0`): a device holding an
132
+ * invitation dials this machine and proves possession; the handler seats the row and
133
+ * admits the device against this node's control database. Registered by {@link start}
134
+ * once that database is up, on every node.
135
+ */
136
+ private cadreInviteHandler;
104
137
  /**
105
138
  * Server-side push-wake fan-out. Constructed by {@link start} only when
106
139
  * `config.push` (an injected `PushNotifier` + policy) is present — without it
@@ -110,6 +143,8 @@ export declare class CadreNode implements SAppIdLookup {
110
143
  private pushFanoutService;
111
144
  /** Backing field for the {@link running} / {@link isRunning} getters. */
112
145
  private _running;
146
+ /** The {@link stop} in progress, or null; a concurrent `stop()` awaits it. */
147
+ private stopInFlight;
113
148
  /**
114
149
  * The control database's own raw storage (cache-wrapped), resolved once per
115
150
  * `start()` and released in {@link cleanup}. Owning it is what keeps the
@@ -205,24 +240,10 @@ export declare class CadreNode implements SAppIdLookup {
205
240
  /**
206
241
  * Most-recently pushed invite addresses (see {@link setInviteAddresses}).
207
242
  * 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.
243
+ * minting cadre invitations — the host pushes NAT-resolved addresses here so
244
+ * the control-network node never needs to dial back to the manager.
210
245
  */
211
246
  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
247
  /**
227
248
  * Lazily-parsed PeerIds of `controlNetwork.bootstrapNodes` (see
228
249
  * {@link getBootstrapPeerIds}) — infrastructure the inbound gate always admits.
@@ -230,13 +251,15 @@ export declare class CadreNode implements SAppIdLookup {
230
251
  private bootstrapPeerIds;
231
252
  /**
232
253
  * Materialized in-memory snapshot of the AUTHORIZED member peer ids (the
233
- * {@link listAuthorizedMembers} result), consulted by the per-stream
234
- * control-DB gate ({@link authorizeInboundControlStream}). That predicate
235
- * runs on EVERY inbound Optimystic control-DB stream and must never await a
236
- * control-DB read: those reads pull blocks over the very protocols the
237
- * predicate gates, which closes a circular wait that ends in mutual denial
238
- * (the upstream gate is fail-closed on timeout, unlike the fail-open
239
- * connection gater). So the set is refreshed OUT OF BAND instead — after
254
+ * {@link listAuthorizedMembers} result), consulted through
255
+ * {@link isControlNetworkMember} by the per-stream control-DB gate
256
+ * ({@link authorizeInboundControlStream}) and by Optimystic's cohort choice
257
+ * (`isNetworkMember`). The stream gate runs on EVERY inbound Optimystic
258
+ * control-DB stream and must never await a control-DB read: those reads pull
259
+ * blocks over the very protocols the predicate gates, which closes a circular
260
+ * wait that ends in mutual denial (the upstream gate is fail-closed on
261
+ * timeout, unlike the fail-open connection gater). Optimystic's predicate is
262
+ * synchronous by contract. So the set is refreshed OUT OF BAND instead — after
240
263
  * start, on every {@link reconcileControlCohort} pass (15s cadence), and
241
264
  * immediately after every LOCAL membership mutation so a just-vouched peer
242
265
  * is admitted without waiting for the timer. That last refresh is AUTOMATIC:
@@ -245,7 +268,11 @@ export declare class CadreNode implements SAppIdLookup {
245
268
  * writer has to remember. A change that arrives by REPLICATION is picked up
246
269
  * on the next timed refresh — bounded staleness, acceptable because this gate
247
270
  * is defense in depth: rows an unadmitted peer manages to write are still
248
- * disbelieved at read time by the voucher-anchored predicate.
271
+ * disbelieved at read time by the voucher-anchored predicate. For cohort
272
+ * choice the same window means a member whose row arrived by replication is
273
+ * left out of this node's control-block cohorts until that refresh.
274
+ * Optimystic reads the predicate live on every lookup, so replacing this set
275
+ * is the whole re-evaluation; there is no per-peer reclassify call to make.
249
276
  */
250
277
  private authorizedControlPeers;
251
278
  /**
@@ -257,6 +284,17 @@ export declare class CadreNode implements SAppIdLookup {
257
284
  private membershipGateDirty;
258
285
  private membershipGateDrain;
259
286
  private membershipGateDeferDepth;
287
+ /**
288
+ * This node's own `CadrePeer` row as a membership refresh last read it live, or null until
289
+ * one has. Reporting a removal needs it ({@link noteOwnRemoval}). Kept across
290
+ * stop()→start(), like {@link removal}.
291
+ */
292
+ private ownLiveRow;
293
+ /**
294
+ * The removal this node learned of ({@link removedFromCadre}), or null. Set once and never
295
+ * cleared, so `cadre:removed` fires once per node object.
296
+ */
297
+ private removal;
260
298
  /**
261
299
  * In-memory delegate admission grants (see `delegate-admission.ts`): the
262
300
  * strand-node transport peerIds this party's members have announced over the
@@ -284,10 +322,8 @@ export declare class CadreNode implements SAppIdLookup {
284
322
  * Does this node's CONTROL libp2p run the circuit-relay server, and with which
285
323
  * init? Resolved once from `network` and `profile` by the same function every
286
324
  * 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).
325
+ * {@link buildControlNodeOptions} (which configures the server from it) and
326
+ * the unauthorized budget above (whose TTL is the server's).
291
327
  */
292
328
  private readonly relayServer;
293
329
  /**
@@ -393,7 +429,7 @@ export declare class CadreNode implements SAppIdLookup {
393
429
  private readonly pendingMembershipInvites;
394
430
  /**
395
431
  * Invite keys of the membership invitations this process has staged, by
396
- * {@link adoptFormationMembershipInvite} or from a `joined` `PendingJoin` row
432
+ * {@link adoptFormationMembershipInvite} or from a `JoinSuccess` row
397
433
  * ({@link stageMembershipInvitesFromPendingJoins}), so one the reconciler has settled is not
398
434
  * staged again by the next pending-join pass. Same lifetime as
399
435
  * {@link pendingMembershipInvites}: a restarted process stages each live one once more, and the
@@ -539,9 +575,11 @@ export declare class CadreNode implements SAppIdLookup {
539
575
  * When this node last PUBLISHED its own `CadrePeer` address record (inserted or
540
576
  * refreshed), or null if it never has in this session. Stamped by
541
577
  * {@link noteSelfRecordPublished} on every successful publish, wherever it was driven
542
- * from — the boot pass, the heartbeat, an address change, a drain or an explicit call.
578
+ * from — the boot pass, the heartbeat, an address change, a drain, a reconcile pass or an
579
+ * explicit call.
543
580
  *
544
- * Read by {@link escalateIfSelfRecordStale} for the one consequence worth an operator's
581
+ * Read by {@link publishFirstSelfRecord}, which stops trying once it is set, and by
582
+ * {@link escalateIfSelfRecordStale} for the one consequence worth an operator's
545
583
  * attention: once this gap exceeds {@link DEFAULT_PEER_RECORD_MAX_AGE_MS}, every other
546
584
  * machine in the party is already discarding this node's address as stale.
547
585
  */
@@ -709,6 +747,7 @@ export declare class CadreNode implements SAppIdLookup {
709
747
  * Stop the cadre node
710
748
  */
711
749
  stop(): Promise<void>;
750
+ private stopRunning;
712
751
  /**
713
752
  * Subscribe to events
714
753
  */
@@ -737,17 +776,70 @@ export declare class CadreNode implements SAppIdLookup {
737
776
  private resolveIdentityKey;
738
777
  /**
739
778
  * 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}).
779
+ * out-of-band pinned keys from `config.trustedOwners`. The anchor's BASE comes
780
+ * only from out of band — genesis self-trust ({@link initializeSeedBootstrap}),
781
+ * config pins (here), runtime enrollment pins ({@link trustOwnerKeys}) and a
782
+ * claim; everything else in it is derived from that base along verifiable
783
+ * chains through the replicated `OwnerKey` table ({@link syncOwnerAnchor}),
784
+ * never read from the table directly.
744
785
  *
745
786
  * Idempotent across stop()→start(): the store instance is kept, and
746
787
  * re-seeding config pins is a no-op ({@link TrustedOwnerStore.trust} is
747
788
  * idempotent). An injected store scoped to a different party is a
748
789
  * configuration error (fail closed before any network bring-up).
790
+ *
791
+ * NOTE: a config pin of a key the owners have since REMOVED is re-anchored here at
792
+ * every start and removed again by the first sync (`refreshMembershipGate('start')`,
793
+ * fired without await from {@link start}), so such a pin is honoured for at most one
794
+ * membership refresh per start; dropping the pin from the config ends that.
749
795
  */
750
796
  private initializeTrustedOwnerStore;
797
+ /**
798
+ * Refuse a `config.claim` that sits beside another cold-start trust source. A node
799
+ * has exactly ONE, so `claim` with a non-empty `trustedOwners.pinnedKeys` or a
800
+ * `seedTrustPolicy` is refused here rather than silently letting one of them win.
801
+ * Asserted at this seam, not by a type: the embedders build `CadreNodeConfig` as one
802
+ * object literal with every optional field present, which a discriminated union over
803
+ * the whole config would not express.
804
+ *
805
+ * Runs BEFORE {@link initializeTrustedOwnerStore}, which writes the config pins into
806
+ * the anchor: on a persistent anchor a pin written ahead of this refusal would outlive
807
+ * the failed start, and the node, restarted with the pin dropped from its config and
808
+ * `claim` kept, would read as claimed by that pin and never accept its claimant.
809
+ */
810
+ private validateClaimConfig;
811
+ /**
812
+ * Build {@link claimTrustPolicy} from `config.claim`, once per node (a restart keeps
813
+ * the policy, as {@link initializeTrustedOwnerStore} keeps the anchor it writes).
814
+ * Runs after the anchor exists and before any network bring-up, so a malformed secret
815
+ * fails `start()` closed with nothing to tear down; `parseClaimSecret` names the
816
+ * problem and never echoes the value. The conflicts with the other trust sources are
817
+ * {@link validateClaimConfig}'s, checked before the anchor is written.
818
+ */
819
+ private initializeClaimPolicy;
820
+ /**
821
+ * Is this node waiting to be claimed: started with `config.claim` and owned by nobody
822
+ * yet (empty anchor)? Read live from the anchor every time, as
823
+ * {@link admitControlPeerUnconditionally} reads it per stream: the anchor is the one
824
+ * source of truth for "claimed", and a cached flag would need its own invalidation
825
+ * from the policy's persist, the rollback and a restart. False on a node with no claim
826
+ * secret, on a claimed one (restarted with the secret still set or not), and before
827
+ * {@link start} has built the anchor.
828
+ *
829
+ * While true the node admits strangers' CONNECTIONS (the claim seed has to ride one)
830
+ * and refuses them control-database streams and cohort places ({@link isControlNetworkMember})
831
+ * and relay reservations ({@link admitControlRelayReservation}): an unclaimed node has
832
+ * no siblings to replicate from and must not become a public relay while it waits.
833
+ * cadre-cli reports this on `/status`; `claim:accepted` is the event form.
834
+ */
835
+ isAwaitingClaim(): boolean;
836
+ /**
837
+ * The removal from the cadre this node has learned of, or null: the query form of
838
+ * `cadre:removed`, for an embedder that subscribed after it fired. Kept once set, across
839
+ * stop()→start() and a later re-admission, because the embedder acts on it once (stops this
840
+ * node, deletes the cadre's data); a re-added machine joins again from a fresh node.
841
+ */
842
+ removedFromCadre(): CadreRemoval | null;
751
843
  /**
752
844
  * Construct (or adopt) the node-local cold-start bootstrap-peer store (see
753
845
  * {@link bootstrapPeerStore}). Synchronous: an injected store has already
@@ -820,6 +912,29 @@ export declare class CadreNode implements SAppIdLookup {
820
912
  * strand restart with?".
821
913
  */
822
914
  getStrandNetworkStateStore(): StrandNetworkStateStore | null;
915
+ /**
916
+ * The strands this machine joined from another party and has not published party-wide
917
+ * (see {@link CadreNodeConfig.joinedStrands}). Publication needs a control connection,
918
+ * so a one-node party's joins live here until it holds one; `exportStrandBundle` reads
919
+ * it beside the `JoinedStrand` table.
920
+ *
921
+ * @throws when the node is not started.
922
+ */
923
+ listUnpublishedJoinedStrands(): Promise<JoinedStrandRecord[]>;
924
+ /**
925
+ * The strand-network addresses a formation carried for `strandId` (see
926
+ * {@link formationStrandAddrs}), as kept: attributed per peer. Empty when none were
927
+ * carried or the strand's peers were forgotten. A copy; the node keeps its own list.
928
+ */
929
+ getCarriedStrandAddrs(strandId: string): string[];
930
+ /**
931
+ * Seat `addrs` as the addresses a formation carried for `strandId`, exactly as a
932
+ * formation of this node's own would ({@link recordFormationStrandAddrs}): a strand
933
+ * move seats the exporting node's list here so the first launch has a cross-party
934
+ * seed. The usual rules apply: a list naming a peer replaces the strand's earlier
935
+ * one, and a list naming none records nothing.
936
+ */
937
+ recordCarriedStrandAddrs(strandId: string, addrs: readonly string[]): Promise<void>;
823
938
  /**
824
939
  * The node-local trusted-owner anchor (null before {@link start}). This is
825
940
  * the set the authorized-membership predicate and seed-trust anchor consult —
@@ -828,35 +943,33 @@ export declare class CadreNode implements SAppIdLookup {
828
943
  getTrustedOwnerStore(): TrustedOwnerStore | null;
829
944
  /**
830
945
  * 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}.)
946
+ * the runtime enrollment seam: {@link redeemCadreInvitation} calls it with the
947
+ * cadre invitation's `ownerKeys` BEFORE dialing a member (so the anchor already
948
+ * holds the pins when the reply and the rows that follow are judged), and an
949
+ * embedder calls it with an operator-supplied pin. Idempotent. ('genesis'
950
+ * provenance is reserved for the node's own founding key, seeded internally by
951
+ * {@link initializeSeedBootstrap}; 'chain' is written only by {@link syncOwnerAnchor}.)
836
952
  *
837
953
  * Validates every key's shape before trusting any of them (all-or-nothing):
838
954
  * 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`
955
+ * single key is anchored. For the invitation route this means a bundle
840
956
  * carrying one malformed `ownerKeys` entry fails the redemption outright —
841
957
  * consistent with the anchor's existing whole-record-or-nothing policy for
842
958
  * a corrupt persisted entry (see `trusted-owner-store.ts`'s
843
959
  * `unusableEntry: 'discard-all'`) — rather than silently anchoring a subset
844
960
  * and leaving the caller to notice a key went missing.
845
961
  */
846
- trustOwnerKeys(keys: Iterable<string>, source: Exclude<TrustSource, 'genesis'>): Promise<void>;
962
+ trustOwnerKeys(keys: Iterable<string>, source: Exclude<TrustSource, 'genesis' | 'chain'>): Promise<void>;
847
963
  private createControlNode;
848
964
  /**
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.
965
+ * Refuse non-members on every members-only protocol of the control node, at one seam
966
+ * (`control-protocol-guard.ts`). Wake and strand-addr are judged by the live
967
+ * {@link isAuthorizedMember}, bounded by the asker's own attempt deadline at this
968
+ * node's declared link; everything a control-database read depends on is judged by
969
+ * the snapshot ({@link authorizeInboundControlStream}).
859
970
  */
971
+ private installControlProtocolGuard;
972
+ /** This node's control network name ({@link controlNetworkName}, the one binding for it). */
860
973
  private controlNetworkName;
861
974
  /**
862
975
  * Arm the CONTROL network's peer-join block catch-up: push every block in the
@@ -883,12 +996,32 @@ export declare class CadreNode implements SAppIdLookup {
883
996
  * reconnect re-runs identify), and — because the production join order is
884
997
  * connect-then-authorize — on every committed membership change via
885
998
  * {@link refreshAuthorizedControlPeers}'s `scheduleConnectedPeers()` call.
999
+ * The one non-member the gate admits is a device whose invitation
1000
+ * redemption this member is admitting ({@link admitRedeemingDevice}).
886
1001
  *
887
1002
  * No-ops (logged) when the embedder configured no control storage or the
888
1003
  * node exposes no key network — the backfill would have nothing to read or
889
1004
  * no way to dial.
890
1005
  */
891
1006
  private startControlBackfill;
1007
+ /**
1008
+ * Admit a device whose invitation redemption has verified to this member's control
1009
+ * replication until the returned release is called (the handler's `admitDevice`;
1010
+ * `CadreInviteHandler.admitDeviceIfLive` says why the redemption's writes need it): push it
1011
+ * this member's control store and wait for the push, with the device counted as a member by
1012
+ * the backfill's gate and by the control cohort choice ({@link admittingDevices}), so the
1013
+ * seat and the admission that follow are written to the device too. A failed push releases
1014
+ * at once and rethrows: in the cohort without the store, the device would refuse the
1015
+ * commits and tear the writes. With no backfill on this node nothing is admitted.
1016
+ *
1017
+ * Through {@link PeerJoinBackfill.forceCatchUpPeer}, not `catchUpPeer`: the device's own
1018
+ * `peer:identify` scheduled a run a moment earlier, which was denied if it passed the gate
1019
+ * before the device was added here, and `catchUpPeer` would answer that run's in-flight
1020
+ * status with an empty result.
1021
+ */
1022
+ private admitRedeemingDevice;
1023
+ /** Count one more redemption admitting `peerId`; the returned release undoes it, once. */
1024
+ private countAdmittingDevice;
892
1025
  /**
893
1026
  * The control database's cache-wrapped raw storage, resolved from
894
1027
  * `config.storage.provider` on first use and then held in {@link controlStorage}
@@ -945,78 +1078,76 @@ export declare class CadreNode implements SAppIdLookup {
945
1078
  */
946
1079
  private logRelayServerSettings;
947
1080
  /**
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
1081
+ * Decide how an inbound CONTROL-network connection from `remotePeerId` is
1082
+ * admitted — the policy behind the connection gater wired in
1083
+ * {@link createControlNode}. Every connection is admitted; the answer decides
1084
+ * only whether it carries the provisional deadline (see
953
1085
  * `membership-connection-gater.ts` for the layer's rationale and the
954
- * stranger-open protocol allowlist).
1086
+ * stranger-open protocol allowlist). Asked once when the connection arrives
1087
+ * and again at a provisional connection's deadline.
955
1088
  *
956
- * Returns `'admit'` when ANY of:
1089
+ * Returns `'admit'` (no deadline) when ANY of:
957
1090
  * 1. a shared-baseline check admits ({@link admitControlPeerUnconditionally}
958
1091
  * — not running / DB torn down, absent-or-empty trusted-owner anchor, or
959
1092
  * 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
1093
+ * 2. the peer holds a live DELEGATE ADMISSION GRANT — an authorized member
963
1094
  * announced it (over the strand-addr RPC) as the transport peerId of its
964
1095
  * own strand node, so a NAT'd member's strand node can hold a
965
1096
  * circuit-relay reservation here (see `delegate-admission.ts`).
966
1097
  * 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
1098
+ * 3. the authorized-member set is empty — cold start: the rows that would
968
1099
  * 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,
1100
+ * 4. the peer IS an authorized member; or
1101
+ * 5. an open invitation is OUTSTANDING — at least one unexpired,
971
1102
  * not-fully-consumed invitation this node minted or persisted (see
972
1103
  * `StrandSolicitationService.hasOutstandingInvitation`). A formation
973
1104
  * initiator is another party's peer by design and its token is only
974
1105
  * checkable inside the protocol, so the gate asks the coarser question
975
1106
  * "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.
1107
+ * does not count: every node registers one at {@link start}, and
1108
+ * registering mints no invitation; or
1109
+ * 6. a LIVE cadre invitation exists — a `CadreInvite` row this node holds
1110
+ * that is not withdrawn, unexpired, has uses left and whose issuer is
1111
+ * still an owner (`ControlDatabase.hasLiveCadreInvite`). The device that
1112
+ * redeems it is a stranger until the redemption writes its row, and its
1113
+ * proof of possession is only checkable inside `/sereus/cadre-invite/1.0.0`;
1114
+ * same reasoning as check 5, and the same expectation-of-a-stranger key.
978
1115
  *
979
1116
  * 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.
1117
+ * checks 1-2 are in-memory, 3/4 share one control-DB read, and only a peer
1118
+ * that is otherwise headed for a provisional admission reaches check 5's and
1119
+ * 6's invitation lookups.
982
1120
  *
983
- * Caveats of check 6, both self-healing:
1121
+ * Caveats of check 5:
984
1122
  * - the in-memory mint registry dies with the process, so after a restart
985
1123
  * only invitations persisted as `FormationInvite` rows still hold the
986
- * exemption open (re-mint otherwise — same story as the enrollment window);
1124
+ * window open (re-mint otherwise);
987
1125
  * - 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
1126
+ * this node yet is admitted only provisionally, so its connection is closed
1127
+ * at the deadline unless the row has landed by then.
1128
+ *
1129
+ * When every check falls through: `'admit-provisionally'`, on every node,
1130
+ * relay server or not. The gate sees only a peer id, so it cannot tell an
1131
+ * outsider from a device whose invitation row this node has not received yet
1132
+ * (it redeems here by seating the row from its bundle, after which check 6
1133
+ * admits) or from a sibling whose membership row is still replicating (check 4
1134
+ * admits once it lands; if it has not landed by the deadline, the connection
1135
+ * is closed and either side's next reconcile dial re-forms it, since outbound
1136
+ * dials are never gated). The protocol guard refuses all of them every
1137
+ * members-only protocol meanwhile. A circuit-relay reservation admitted by
1138
+ * {@link admitControlRelayReservation} disarms the deadline (see
1139
+ * `membership-connection-gater.ts` → "The relay-reservation seam").
1140
+ *
1141
+ * NOTE: check 3/4 runs a control-DB read per inbound connection
1005
1142
  * (`listAuthorizedMembers`); connections are rare and cadres small, and this
1006
1143
  * layer is fail-open behind `ADMISSION_DECISION_TIMEOUT_MS`, so the live
1007
1144
  * read is safe here — unlike the per-stream gate, which must consult the
1008
1145
  * 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.
1146
+ * NOTE: checks 5 and 6 add two more control reads (`hasOutstandingFormationInvite`,
1147
+ * `hasLiveCadreInvite`) for a stranger with no locally minted invitation in play, on
1148
+ * every node, and a provisional connection's deadline runs the whole decision once
1149
+ * more. If stranger connections ever arrive fast enough for those reads — on arrival
1150
+ * or at the deadline — to show, cache the answers for a few seconds.
1020
1151
  */
1021
1152
  private admitInboundControlConnection;
1022
1153
  /**
@@ -1026,7 +1157,13 @@ export declare class CadreNode implements SAppIdLookup {
1026
1157
  * "The relay-reservation seam"); the circuit-relay server consults it per
1027
1158
  * RESERVE request, so it is never called on a node whose relay server is off.
1028
1159
  *
1029
- * Admits when ANY of:
1160
+ * Refuses everyone, ahead of every admission below, while this node is waiting to be
1161
+ * claimed ({@link isAwaitingClaim}): a storage-profile node runs the relay server by
1162
+ * default, and an unclaimed node on a public address must not become anyone's relay.
1163
+ * The empty-anchor admission in check 1 exists for a node that will be enrolled by
1164
+ * its siblings, which an unclaimed node has none of.
1165
+ *
1166
+ * Otherwise admits when ANY of:
1030
1167
  * 1. a shared-baseline check admits ({@link admitControlPeerUnconditionally}
1031
1168
  * — not running / DB torn down, absent-or-empty trusted-owner anchor, or
1032
1169
  * configured bootstrap/relay infrastructure);
@@ -1046,31 +1183,23 @@ export declare class CadreNode implements SAppIdLookup {
1046
1183
  * that boots, reserves, and only then has its row replicate here would
1047
1184
  * otherwise keep that slot spent for the rest of the entry's TTL.
1048
1185
  *
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.
1186
+ * The connection gate's stranger carve-outs (an outstanding formation
1187
+ * invitation, a live cadre invitation) deliberately do NOT extend here: they
1188
+ * exist so a stranger's FORMATION or CADRE-INVITE stream can ride a connection,
1189
+ * and neither needs relay capacity. A genuine invitee that does need a relay
1190
+ * slot takes one from the budget like any other unplaced peer, so a live
1191
+ * invitation never becomes an unbounded grant of this node's forwarding capacity.
1055
1192
  */
1056
1193
  private admitControlRelayReservation;
1057
1194
  /** Admit a reservation on the peer's own merits, giving back any budget slot it still holds. */
1058
1195
  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
1196
  /**
1068
1197
  * Record (or refresh) a delegate admission grant: `delegatePeerId` is the
1069
1198
  * transport peerId of `announcerPeerId`'s strand-`strandId` node, admitted
1070
1199
  * at the CONNECTION and RESERVATION levels (all a circuit-relay reservation
1071
1200
  * 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
1201
+ * `DELEGATE_GRANT_TTL_MS`. Called by the strand-addr responder, which the
1202
+ * protocol guard admits only authorized members to; public so tests can drive the
1074
1203
  * admission policy without a full strand launch. A re-announce for the same
1075
1204
  * (announcer, strand) REPLACES the previous delegate rather than
1076
1205
  * accumulating. Never honored by {@link authorizeInboundControlStream}.
@@ -1097,45 +1226,73 @@ export declare class CadreNode implements SAppIdLookup {
1097
1226
  */
1098
1227
  private admitControlPeerUnconditionally;
1099
1228
  /**
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.
1229
+ * The membership rule this control node applies to a remote peer, without logging. Two
1230
+ * consumers: the per-stream gate ({@link authorizeInboundControlStream}, who may SEND to
1231
+ * this node on a members-only protocol) and Optimystic's `isNetworkMember` (who this node
1232
+ * sends control writes TO — which peers can be a control block's cohort member or
1233
+ * coordinator), which adds only a device whose verified invitation redemption is being
1234
+ * written ({@link admittingDevices}). One rule for both keeps the outbound choice as
1235
+ * permissive as the inbound acceptance: a joining node's cohort must include the inviter
1236
+ * its empty anchor or empty snapshot lets replicate to it, or it would see a self-only
1237
+ * cohort, rule an existing collection absent and found a second copy. Sereus builds the
1238
+ * control node with `bootstrapNodes: []`, so Optimystic's refused-bootstrap-peer guard
1239
+ * does not cover that.
1105
1240
  *
1106
1241
  * The STRICT SUBSET of {@link admitInboundControlConnection}: the same
1107
1242
  * "no basis to judge" admissions (shared via
1108
1243
  * {@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
1113
- * gate closes. A DELEGATE-admitted connection (a member's strand node
1244
+ * (outstanding open invitation, live cadre invitation, delegate admission
1245
+ * grant). The first two exist so a stranger can reach `/sereus/formation/1.0.0`
1246
+ * and `/sereus/cadre-invite/1.0.0` — neither is gated here, and admitting a
1247
+ * stranger to `repo` while an invitation is live is exactly the hole this
1248
+ * rule closes. A DELEGATE-admitted connection (a member's strand node
1114
1249
  * holding a circuit-relay reservation, see `delegate-admission.ts`) is
1115
- * likewise exactly a case this gate must still refuse: the delegate gets
1116
- * the connection and never the control DB.
1117
- *
1118
- * Admits when ANY of:
1250
+ * likewise refused: the delegate gets the connection and never the control DB.
1251
+ *
1252
+ * Refuses everyone, ahead of every admission below, while this node is waiting to be
1253
+ * claimed ({@link isAwaitingClaim}). The empty-anchor and empty-snapshot admissions
1254
+ * exist so a brand-new node can take replication from the siblings that will
1255
+ * authorize it; an unclaimed node has no siblings yet, and the rows that will
1256
+ * authorize anyone arrive only after the claim, over the owner's streams that the
1257
+ * same two admissions then let in. Its cohort is itself alone meanwhile, which costs
1258
+ * nothing: it built its control database with zero connections. Configured bootstrap
1259
+ * infra is refused too: a relay opens no control-database stream.
1260
+ *
1261
+ * Otherwise admits when ANY of:
1119
1262
  * 1. a shared-baseline check admits (not running / no DB, empty anchor,
1120
- * configured bootstrap infra);
1263
+ * configured bootstrap infra — which Optimystic still classifies `foreign`,
1264
+ * since a relay serves none of the party's database protocols);
1121
1265
  * 2. the materialized authorized set is empty — cold start, before the
1122
1266
  * rows that would authorize anyone have replicated in;
1123
1267
  * 3. the peer IS in the materialized authorized set.
1124
1268
  *
1125
1269
  * Deliberately SYNCHRONOUS and pure in-memory — see
1126
1270
  * {@link authorizedControlPeers} for why a control-DB read here would
1127
- * deadlock into mutual denial. With a sync predicate the upstream deadline
1128
- * (`authorizeInboundStreamTimeoutMs`) never trips, so it stays at its
1129
- * default.
1271
+ * deadlock into mutual denial.
1130
1272
  *
1131
1273
  * NOTE: the snapshot keys on PEER ID, so a node with no persistent identity
1132
1274
  * key never lands a `CadrePeer` row ({@link registerSelf} skips) and its
1133
- * siblings deny its control-DB streams once their own snapshot is non-empty —
1134
- * owner status is a KEY, not a peer id, so being the owner does not exempt it.
1135
- * Harmless today (a real deployment persists its identity; the ephemeral-owner
1136
- * case appears only in tests, which configure the owner as bootstrap infra —
1137
- * `push-wake-e2e.integration.ts` design note 3). If an ephemeral-identity node
1138
- * ever becomes a supported deployment, this gate needs a key-based admission.
1275
+ * siblings deny its control-DB streams, and leave it out of their cohorts, once
1276
+ * their own snapshot is non-empty — owner status is a KEY, not a peer id, so
1277
+ * being the owner does not exempt it. Harmless today (a real deployment persists
1278
+ * its identity; the ephemeral-owner case appears only in tests, which configure
1279
+ * the owner as bootstrap infra — `push-wake-e2e.integration.ts` design note 3).
1280
+ * If an ephemeral-identity node ever becomes a supported deployment, this rule
1281
+ * needs a key-based admission.
1282
+ */
1283
+ private isControlNetworkMember;
1284
+ /**
1285
+ * Per-stream authorization for the protocol guard's `members-snapshot` class
1286
+ * (`control-protocol-guard.ts`): the four Optimystic control-DB protocols
1287
+ * (`/optimystic/control-<party>/{repo,cluster,sync,block-transfer}/…`), FRET's
1288
+ * five, and any protocol nobody classed. Also wired as Optimystic's
1289
+ * `authorizeInboundStream` in {@link createControlNode} for the four DB
1290
+ * protocols. The fail-closed layer behind the fail-open connection gater.
1291
+ * Control node ONLY: strand cohort nodes legitimately serve cross-party peers.
1292
+ *
1293
+ * Admits exactly the peers {@link isControlNetworkMember} does, and logs why it
1294
+ * refused one. With a sync predicate the upstream deadline
1295
+ * (`authorizeInboundStreamTimeoutMs`) never trips, so it stays at its default.
1139
1296
  */
1140
1297
  private authorizeInboundControlStream;
1141
1298
  /**
@@ -1152,8 +1309,53 @@ export declare class CadreNode implements SAppIdLookup {
1152
1309
  * read here would sleep its backoff holding the lock and stall every other local
1153
1310
  * writer. Nothing is lost — this refresh already keeps the previous snapshot on
1154
1311
  * failure and is re-driven by the next membership write and by the timed reconcile.
1312
+ *
1313
+ * The trusted-owner anchor sync ({@link syncOwnerAnchor}) runs FIRST, so the snapshot
1314
+ * built here judges `CadrePeer` rows against the anchor as the `OwnerKey` table now
1315
+ * has it. The invitation chain both reads may need, and the tombstone list the sync and
1316
+ * the two end-of-refresh checks ({@link hangUpRevokedControlPeers}, then
1317
+ * {@link noteOwnRemoval}) read, are each loaded at most once per refresh.
1155
1318
  */
1156
1319
  private refreshAuthorizedControlPeers;
1320
+ /**
1321
+ * Hang up this node's control connections to every peer that a `CadrePeer` tombstone names
1322
+ * and that has no live `CadrePeer` row: a machine removed from the cadre, whose streams the
1323
+ * protocol guard already refuses but whose open connection would otherwise stay up until it
1324
+ * dropped on its own. Runs at the end of every membership refresh: on the remover from the
1325
+ * removal's own membership notification, so the connection is closed before
1326
+ * {@link removeMachine} returns; on every other machine from the refresh that follows the
1327
+ * replicated tombstone.
1328
+ *
1329
+ * Denies only on evidence held, the strand enforcer's rule (`strand-revocation-enforcer.ts`):
1330
+ * a peer that merely dropped out of the authorized snapshot (an anchor read that failed, a
1331
+ * voucher whose owner is gone) keeps its connection, self is never hung up, and a peer with a
1332
+ * tombstone AND a live row under a fresh stamp was re-admitted and keeps its connection too.
1333
+ * The tombstones are the refresh's own read (`loadTombstones`, shared with
1334
+ * {@link syncOwnerAnchor}), taken only when some connected peer has no live row, so a cadre
1335
+ * with every connection accounted for costs nothing here. A failed `hangUp` is logged and
1336
+ * retried by the next refresh. Never rejects.
1337
+ */
1338
+ private hangUpRevokedControlPeers;
1339
+ /**
1340
+ * Learn that an owner removed this node from the cadre, at the end of every membership
1341
+ * refresh: a `CadrePeer` tombstone names this node's peer id, `rows` (the refresh's own
1342
+ * retired-stamp-filtered read) has no live row for it, and an earlier refresh of this process
1343
+ * saw one ({@link ownLiveRow}). Then it records {@link removal} and emits `cadre:removed`,
1344
+ * once. Nothing is stopped here: stopping and deleting are the embedder's, whose storage,
1345
+ * saved starts and process lifetime they are.
1346
+ *
1347
+ * The "seen live" condition keeps a machine joining again, whose fresh store can sync the old
1348
+ * tombstone before its new row, from reporting itself removed. A re-admission under a fresh
1349
+ * stamp is a live row, so a tombstone plus a live row is never a removal. The tombstones are
1350
+ * the refresh's shared read, taken only once the own row is missing. Never rejects.
1351
+ */
1352
+ private noteOwnRemoval;
1353
+ /**
1354
+ * The tombstone's signer when its signature verifies and the key is in this node's
1355
+ * trusted-owner anchor, else null. A tombstone that fails this is still a removal (every
1356
+ * other machine honours it, and this node's own row is gone), so it is logged, not refused.
1357
+ */
1358
+ private anchoredRevocationSigner;
1157
1359
  /**
1158
1360
  * Add the configured `controlNetwork.bootstrapNodes` to the control node's address book
1159
1361
  * and dial each peer once — what `@libp2p/bootstrap` would have done had the node been
@@ -1186,6 +1388,18 @@ export declare class CadreNode implements SAppIdLookup {
1186
1388
  * reference apps), and a write it signed would only be refused.
1187
1389
  */
1188
1390
  private enrolledOwnerSigningKey;
1391
+ /**
1392
+ * Whether this node can act as an owner of its cadre: its identity key is wired as its
1393
+ * owner signing key ({@link initializeSeedBootstrap}) and enrolled in the live `OwnerKey`
1394
+ * set ({@link enrolledOwnerSigningKey}). What the owner-only flows (`createCadreInvitation`,
1395
+ * `claimNode`, `rentNode`) need; false before `start()`, and on a machine that joined as a
1396
+ * member. The reference apps' "is this device an owner" check.
1397
+ */
1398
+ isOwner(): Promise<boolean>;
1399
+ /** {@link enrolledOwnerSigningKey} judged against an owner set the caller has already read. */
1400
+ private ownerSigningKeyEnrolledIn;
1401
+ /** This machine's identity key as an owner signing key, when its seed bootstrap is wired to sign; enrollment not yet checked. */
1402
+ private ownerSigningKeyCandidate;
1189
1403
  /**
1190
1404
  * Schedule the node's initial self-record publish + ongoing refresh shortly
1191
1405
  * after start (non-blocking). {@link registerSelf} is idempotent and safely
@@ -1322,6 +1536,31 @@ export declare class CadreNode implements SAppIdLookup {
1322
1536
  * since that key is internal to libp2p and not exposed.
1323
1537
  */
1324
1538
  getIdentityOwnerKey(): Ed25519KeyPair;
1539
+ /**
1540
+ * Publish this node's own address record from the reconcile pass until its first publish of
1541
+ * this session lands. A node with no owner key — seeded (`addDrone`, `cadre enroll add`),
1542
+ * claimed (`claimNode`) or otherwise authorized after it booted — cannot insert its own
1543
+ * `CadrePeer` row; the row an owner wrote for it reaches it by replication once the two
1544
+ * connect, usually after the boot-time publish and the first-growth drain have both found
1545
+ * nothing to publish. Without this the record would wait for the heartbeat
1546
+ * ({@link startRecordRefresh}), and the cadre could not resolve the node's addresses until
1547
+ * then. The reconcile pass is where replicated membership is picked up, so each pass tries.
1548
+ * It also retries an owner's boot-time publish that failed.
1549
+ *
1550
+ * A try before the row has arrived is a `skipped` publish: one `CadrePeer` read. Not tried
1551
+ * while the node waits to be claimed (it has no row and no party yet) or once it has learned
1552
+ * it was removed (nothing to publish). A removed node that never saw its own row live in this
1553
+ * process does not learn it was removed ({@link noteOwnRemoval}), and pays that read on every
1554
+ * pass.
1555
+ *
1556
+ * Connected-only: a row written elsewhere arrives over a connection, and an owner's publish
1557
+ * committed alone is local-only and forks its own history — a restarted owner whose embedder
1558
+ * runs a pass before it reconnects would write its row on the stale side of the fork, and
1559
+ * then never see rows written while it was down (`cadre-invite-any-member` runs that pass and
1560
+ * fails every time without this condition). The boot-time publish and the first-growth drain already try at the node's lone and
1561
+ * first-connection moments; this retries on the passes after.
1562
+ */
1563
+ private publishFirstSelfRecord;
1325
1564
  /**
1326
1565
  * Collect this node's current dialable addresses for publication, signaling
1327
1566
  * (`/p2p-circuit`) first. Prefers the best invite/NAT-resolved set and folds
@@ -1384,8 +1623,9 @@ export declare class CadreNode implements SAppIdLookup {
1384
1623
  * ({@link bootstrapDialAddrs}).
1385
1624
  *
1386
1625
  * An address naming a DIFFERENT trailing peer id is dropped: it does not reach
1387
- * `peerId`, so it does not belong in this peer's candidate list (libp2p's own
1388
- * dial queue filters the same shape out one layer lower).
1626
+ * `peerId`, so it does not belong in this peer's candidate list (`dialPeerAddrs`
1627
+ * binds every address to the peer it is given and drops the same shape one
1628
+ * layer lower; here it is dropped before it can reach the address book).
1389
1629
  */
1390
1630
  private normalizeDialAddrs;
1391
1631
  /**
@@ -1502,8 +1742,7 @@ export declare class CadreNode implements SAppIdLookup {
1502
1742
  * addresses ({@link dialPeerAddrs}): the reconcile pass's steady-state sibling
1503
1743
  * ({@link dialControlSibling}) and cold-start bootstrap peer
1504
1744
  * ({@link dialBootstrapPeer}) dials, and — handed to every
1505
- * {@link SeedBootstrapService} this node builds — `applySeed`'s owner dials and
1506
- * `dialInvite`.
1745
+ * {@link SeedBootstrapService} this node builds — `applySeed`'s owner dials.
1507
1746
  *
1508
1747
  * See `peer-dial.ts`'s `DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS` for why a peer's dial is
1509
1748
  * bounded as a whole, and `DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS` for why each
@@ -1559,9 +1798,9 @@ export declare class CadreNode implements SAppIdLookup {
1559
1798
  *
1560
1799
  * The list may name transports this node cannot dial (a lent node reports TCP
1561
1800
  * and `/ws` addresses to a phone that dials WebSockets only). That needs no
1562
- * filtering here: libp2p's dial queue (`calculateMultiaddrs`, libp2p 3.3.11)
1563
- * rejects an address no transport can dial before touching the network, so
1564
- * such an address costs a log line.
1801
+ * filtering here: libp2p's transport manager rejects an address no transport
1802
+ * can dial before touching the network (`TransportUnavailableError`), so such
1803
+ * an address costs a log line.
1565
1804
  *
1566
1805
  * The list is NOT handed to one `dial()`. libp2p tries a multi-address dial's
1567
1806
  * addresses one after another under a single deadline, and sorts loopback
@@ -3052,6 +3291,15 @@ export declare class CadreNode implements SAppIdLookup {
3052
3291
  *
3053
3292
  * Passing an empty list stops the supervisor and resets the posture to `none`
3054
3293
  * without dialing or waiting.
3294
+ *
3295
+ * NOTE: on a node with no `network.relayAddrs` (a browser tab with a hand-written
3296
+ * bare `/p2p-circuit`), libp2p's relay discovery stays on, because it was the only
3297
+ * thing filling that slot before this call. Such a tab connected to some other
3298
+ * relay-capable peer it identifies (a party member running the relay server) can
3299
+ * lose its one slot to that peer before this supervisor's request runs, the theft
3300
+ * `nodeTransportsOption` prevents on configured nodes (`relay-reservation.ts`). If a
3301
+ * tab is ever seen reserved through a relay it was not asked to use, build that
3302
+ * node through `withoutRelayDiscovery` too, or have the tab configure `relayAddrs`.
3055
3303
  */
3056
3304
  reserveRelays(addrs: string[], opts?: RelayReservationSupervisorOptions): Promise<RelayReservationState>;
3057
3305
  /**
@@ -3143,6 +3391,11 @@ export declare class CadreNode implements SAppIdLookup {
3143
3391
  * {@link isMember}). Rows whose stamp is retired in `CadreControl.Revocation`
3144
3392
  * never reach here: {@link ControlDatabase.queryCadrePeers} excludes them, so a
3145
3393
  * revoked peer is not addressable either.
3394
+ *
3395
+ * NOTE: answers the whole `CadrePeerRow`, past the two fields declared; the integration
3396
+ * harness (`owner-cli-node.ts`, `OwnerCliMemberRow.sig`) reads `sig` off it through the admin
3397
+ * channel to tell a row another member seated from one the subject published itself. Narrow
3398
+ * this to the declared fields only together with that reader.
3146
3399
  */
3147
3400
  listMembers(): Promise<Array<{
3148
3401
  peerId: string;
@@ -3168,14 +3421,21 @@ export declare class CadreNode implements SAppIdLookup {
3168
3421
  * 1. it is not this node itself — a node publishes its own `CadrePeer` address
3169
3422
  * row so its dialable address rides in seeds, but "self" is not a peer this
3170
3423
  * node authorized;
3171
- * 2. its `CadrePeer` row carries a complete voucher (`StampId`, `VouchOwner`,
3172
- * `VouchSig` all non-null);
3424
+ * 2. its `CadrePeer` row carries a complete proof: EITHER an owner voucher
3425
+ * (`StampId`, `VouchOwner`, `VouchSig` all non-null) OR an invitation admission
3426
+ * (`VouchSig` null, `VouchUsage` naming a `CadreInviteUsage` row);
3173
3427
  * 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
3428
+ * ({@link getTrustedOwnerStore}), which follows the replicated `OwnerKey` table
3429
+ * only along chains that verify from this node's out-of-band keys
3430
+ * ({@link syncOwnerAnchor}) — never the table itself, which any stranger can
3431
+ * genesis-pollute; and
3432
+ * 4. the proof verifies: for a voucher, `VouchSig` is that owner's signature over
3433
+ * the row's voucher digest ({@link verifyCadrePeerVoucher}), so the anchored
3434
+ * owner really vouched THIS peer id under THIS row's nonce; for an admission,
3435
+ * the chain row → usage → invitation → anchored issuer verifies link by link
3436
+ * ({@link verifyInvitationAdmission} — the issuer is `VouchOwner`, the invitation's
3437
+ * own owner signature, the holder's and the device's signatures, and the stored
3438
+ * peer key really is the key behind the peer id); and
3179
3439
  * 5. the row's `StampId` is NOT retired in `CadreControl.Revocation` — enforced
3180
3440
  * upstream in {@link ControlDatabase.queryCadrePeers}, which drops retired rows
3181
3441
  * before ANY reader (this predicate included) sees them, so a row resurrected
@@ -3184,18 +3444,26 @@ export declare class CadreNode implements SAppIdLookup {
3184
3444
  * local rows) is still inert to every reader that has the tombstone.
3185
3445
  *
3186
3446
  * 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.
3447
+ * not-yet-enrolled node authorizes no one), a null/partial proof, an
3448
+ * unanchored `VouchOwner`, a bad signature, or a usage or invitation row this node
3449
+ * does not hold all yield "not authorized" — having an address row is NOT
3450
+ * membership. The protocol guard admits wake and strand-address requests by this set,
3451
+ * NOT the addressable one.
3452
+ *
3453
+ * The usage and invitation tables are read only when at least one row needs them,
3454
+ * so a cadre with no invitation-admitted member stays at one read per call.
3191
3455
  *
3192
3456
  * NOTE (rotation): if a party owner rotates keys and only the NEW key is pinned
3193
3457
  * in the anchor, rows the OLD key vouched fail check 3 until re-vouched — a
3194
3458
  * legit member goes un-authorized on readers that only pin the new key. Full
3195
3459
  * rotation handling (re-vouch on rotate) is the
3196
- * `flip-strand-membership-rotation-known-gap` work, not this predicate's.
3460
+ * `flip-strand-membership-rotation-known-gap` work, not this predicate's. The
3461
+ * `OwnerKey` table differs: an owner the rotated-out (or removed) key vouched keeps
3462
+ * its place in every anchor that had derived it ({@link syncOwnerAnchor}: a derived
3463
+ * entry stays while its own row is live), and is only out of reach for a machine that
3464
+ * never held it until a remaining owner re-adds it or an invitation pins it.
3197
3465
  *
3198
- * @param retry - Whether the underlying membership read may retry a transient cluster
3466
+ * @param retry - Whether the underlying membership reads may retry a transient cluster
3199
3467
  * failure. Only {@link refreshAuthorizedControlPeers} passes `false`, because it runs
3200
3468
  * as the control database's membership listener with that database's write lock held;
3201
3469
  * see its comment.
@@ -3204,11 +3472,108 @@ export declare class CadreNode implements SAppIdLookup {
3204
3472
  peerId: string;
3205
3473
  multiaddr: string | null;
3206
3474
  }>>;
3475
+ /**
3476
+ * The body of {@link listAuthorizedMembers}, with the invitation chain supplied as a
3477
+ * source so the membership refresh can share one load with {@link syncOwnerAnchor}.
3478
+ */
3479
+ private collectAuthorizedMembers;
3480
+ /** The invitation chain, loaded only when some row in `rows` is invitation-admitted. */
3481
+ private chainFor;
3482
+ /**
3483
+ * The authorized members among `rows`, already read: checks 1 to 4 of
3484
+ * {@link listAuthorizedMembers}' predicate (check 5 is `queryCadrePeers`'s), so the membership
3485
+ * refresh can judge the rows it has to read anyway for the hang-up of removed peers.
3486
+ */
3487
+ private authorizedAmong;
3207
3488
  /**
3208
3489
  * 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.
3490
+ * {@link listAuthorizedMembers}) for one `CadrePeer` row, by the kind of proof it
3491
+ * carries: an owner voucher ({@link hasAnchoredVoucher}) or an invitation admission
3492
+ * ({@link verifyInvitationAdmission} over the usage and invitation rows in `chain`,
3493
+ * loaded only when some row needs them). A row with a `VouchSig` is judged as a
3494
+ * voucher even if it also names a usage — the signature is the stronger proof and the
3495
+ * one an owner re-vouch leaves behind.
3496
+ */
3497
+ private hasAnchoredProof;
3498
+ /**
3499
+ * Recompute the node-local trusted-owner anchor from the replicated `OwnerKey` table along
3500
+ * verifiable chains (`owner-anchor-sync.ts` → {@link deriveOwnerAnchor}), as the first step
3501
+ * of every membership refresh ({@link refreshAuthorizedControlPeers}) so the snapshot built
3502
+ * in the same pass judges `CadrePeer` rows against the updated anchor. A key whose live
3503
+ * row's stored proof verifies against a key already anchored enters under `chain` and
3504
+ * stays while its row is live; a key an anchored owner's verifiable tombstone names
3505
+ * leaves, base pins included — which is how a config pin of a removed key, re-anchored at
3506
+ * every start ({@link initializeTrustedOwnerStore}), is removed again. The derivation
3507
+ * refuses removals that would empty the anchor; they are logged here.
3508
+ *
3509
+ * With an EMPTY anchor the sync does nothing: there is no key to derive from, and a node
3510
+ * waiting to be claimed ({@link isAwaitingClaim}: claim secret set, anchor empty) must
3511
+ * stay empty until the claim anchors its claimant — the replicated table must never seat
3512
+ * the first key. The early return below is the seam that rests on.
3513
+ *
3514
+ * Any read failure skips the whole pass and leaves the anchor as it was; the refresh then
3515
+ * builds its snapshot against that anchor, which is also what its previous snapshot was
3516
+ * judged against. Caught here rather than failing the refresh, because the membership read
3517
+ * tolerates an unreachable `Revocation` block on an isolated node
3518
+ * (`ControlDatabase.readRevokedStampRows`) and `queryRevocations` does not: failing the
3519
+ * whole refresh on that would freeze such a node's gate snapshot.
3520
+ * NOTE: on such a node the sync is skipped at every refresh until the block is held; if an
3521
+ * isolated node ever has to derive owner additions while cut off, give `queryRevocations`
3522
+ * the same cohort-unreachable fallback.
3523
+ *
3524
+ * NOTE: cost per membership refresh is one `OwnerKey` read and one `Revocation` read (the
3525
+ * tombstone list, whose `OwnerKey` stamps are handed to `queryOwnerKeyRows` so it does not
3526
+ * read the retired set again, and which the hang-up of removed peers shares through
3527
+ * `loadTombstones`), plus the invitation chain when some owner row is
3528
+ * invitation-admitted — shared with the membership read through `loadChain`; the memo
3529
+ * suggestion on {@link loadInvitationChain} covers that part. Before the Revocation ledger
3530
+ * marker is filed that one read is a cohort consult per refresh, which
3531
+ * `control-founding-consult-budget.spec.ts` pins.
3532
+ *
3533
+ * @param retry - forwarded to every read; `false` on the refresh path, for the reason on
3534
+ * {@link refreshAuthorizedControlPeers}.
3535
+ * @param loadTombstones - the refresh's shared tombstone read ({@link revocationSource}).
3536
+ */
3537
+ private syncOwnerAnchor;
3538
+ /**
3539
+ * Apply one pass's result: `chain` entries for the keys it newly derived, removal of the keys
3540
+ * it started with that are outside `target` — base keys a verifiable tombstone named, and
3541
+ * `chain` entries whose row is gone.
3542
+ *
3543
+ * Diffed against `before`, the snapshot the derivation was computed from, NOT the live store:
3544
+ * the anchor is written out of band while the pass's reads are in flight (an invitation pin,
3545
+ * the genesis anchor, a claim, a seed), and such a key is outside `target` only because the
3546
+ * pass never saw it — judging the live store would remove it. Likewise a key removed
3547
+ * meanwhile (a claim rollback) must not come back as `chain`. Both are judged by the next
3548
+ * pass. Each change lands in memory synchronously (`TrustedOwnerStore.trust` / `remove`); a
3549
+ * failed PERSIST is logged and does not stop the pass or fail the refresh, as
3550
+ * `SeedBootstrapService.anchorAcceptedSigner` treats the same failure.
3551
+ */
3552
+ private applyOwnerAnchor;
3553
+ /**
3554
+ * A per-refresh {@link InvitationChainSource}: the first call loads the chain through
3555
+ * {@link loadInvitationChain}, every later call shares that load.
3556
+ */
3557
+ private invitationChainSource;
3558
+ /** A per-refresh {@link RevocationSource}: one `queryRevocations` read, shared by every caller in the pass. */
3559
+ private revocationSource;
3560
+ /**
3561
+ * The usage and invitation rows {@link hasAnchoredProof} and {@link syncOwnerAnchor}
3562
+ * resolve an invitation-admitted row through, keyed for lookup. Both reads honour `retry`
3563
+ * for {@link listAuthorizedMembers}' reason.
3564
+ *
3565
+ * NOTE: once invitations are the only admission route (ticket
3566
+ * `cadre-invitations-redeemable-by-any-member`), every cadre with a member not yet
3567
+ * re-vouched pays these two whole-table reads and three ed25519 verifies per row on every
3568
+ * membership refresh. Both tables are append-only and a verified chain never changes (expiry,
3569
+ * use count and withdrawal are not re-checked), so if the gate's admission deadline ever
3570
+ * shows this, memoise verified (row stamp → usage stamp) pairs per node and skip the reads
3571
+ * when every admitted row is already memoised.
3572
+ */
3573
+ private loadInvitationChain;
3574
+ /**
3575
+ * The owner-voucher half of {@link hasAnchoredProof}: complete voucher, `VouchOwner` in
3576
+ * the node-local anchor, signature valid over the row's (PeerId, StampId) voucher digest.
3212
3577
  *
3213
3578
  * NOTE: verifies the ed25519 signature on every call (no memo of already-
3214
3579
  * verified (peerId, stampId, vouchSig) triples). Cadres are a handful of
@@ -3219,8 +3584,8 @@ export declare class CadreNode implements SAppIdLookup {
3219
3584
  private hasAnchoredVoucher;
3220
3585
  /**
3221
3586
  * 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
3587
+ * {@link listAuthorizedMembers}) — the check the protocol guard runs ahead of the
3588
+ * control-network wake and strand-address handlers, NOT {@link isMember} (the addressable
3224
3589
  * surface). Deliberately scans the full membership (one `CadrePeer` query)
3225
3590
  * rather than adding a single-row read path: cadres are small, and one code
3226
3591
  * path keeps the predicate impossible to drift from the list.
@@ -3233,20 +3598,23 @@ export declare class CadreNode implements SAppIdLookup {
3233
3598
  * record (via {@link resolvePeerAddrs}, signaling/relay first — so a NAT'd peer
3234
3599
  * is reachable through its circuit-relay address), dials `WAKE_PROTOCOL`, sends
3235
3600
  * 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`.
3601
+ * only resumes a strand it already participates in; it acks once it has decided,
3602
+ * before the strand is up. The dial deadlines derive from this node's
3603
+ * `network.linkRoundTripMs`.
3239
3604
  *
3240
3605
  * @param targetPeerId - The hibernating cadre peer to wake.
3241
3606
  * @param strandId - The strand the caller knows has pending activity.
3242
3607
  * @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.
3608
+ * @throws if the node is not started, the target has no dialable address, or no
3609
+ * attempt got an ack — including when the receiver does not count this node as an
3610
+ * authorized member, since its protocol guard resets the stream without a reply.
3244
3611
  */
3245
3612
  pushWake(targetPeerId: string, strandId: string, reason?: string): Promise<WakeAck>;
3246
3613
  /**
3247
3614
  * Enable the seed listener for receiving seeds via the /sereus/seed/1.0.0 protocol.
3248
3615
  * This is for drone nodes that need to receive seeds without being an owner.
3249
- * Does not require an owner key.
3616
+ * Does not require an owner key. Idempotent; a node started with `config.claim`
3617
+ * calls it itself inside {@link start}, so an embedder's own call is a no-op.
3250
3618
  */
3251
3619
  enableSeedListener(): Promise<void>;
3252
3620
  /**
@@ -3278,6 +3646,15 @@ export declare class CadreNode implements SAppIdLookup {
3278
3646
  * vouched for stays denied until the next timed cohort reconcile.
3279
3647
  */
3280
3648
  private seedEventCallbacks;
3649
+ /**
3650
+ * The node-wide default trust policy for INBOUND seeds, the one value every
3651
+ * {@link SeedBootstrapService} this node constructs is given — the owner service,
3652
+ * the listener, and `applySeed`'s temp service — so the three cannot drift. The
3653
+ * claim policy when the node was started with `config.claim` (the two are refused
3654
+ * together at start, so there is nothing to merge), otherwise the configured
3655
+ * `seedTrustPolicy`, and `undefined` for the service's anchored default.
3656
+ */
3657
+ private inboundSeedTrustPolicy;
3281
3658
  /**
3282
3659
  * Re-materialize the authorized-peer snapshot that the fail-closed per-stream
3283
3660
  * control-DB gate ({@link authorizeInboundControlStream}) judges against.
@@ -3356,9 +3733,105 @@ export declare class CadreNode implements SAppIdLookup {
3356
3733
  * Remove a previously-authorized peer from the cadre.
3357
3734
  * Signs the peer ID with the owner key and deletes the CadrePeer row.
3358
3735
  *
3736
+ * Removes the `CadrePeer` row only. The call that removes a machine completely (its owner
3737
+ * key, its push token and the invitations it could come back through, in one transaction)
3738
+ * is {@link removeMachine}.
3739
+ *
3359
3740
  * @param peerId - The peer ID to remove
3360
3741
  */
3361
3742
  removePeer(peerId: string): Promise<void>;
3743
+ /**
3744
+ * Seat another owner key, signed by this node's owner key (the signed-add branch of
3745
+ * `OwnerKey.Authorized`; the row stores the vouching pair). The write notifies the
3746
+ * membership hub, so the refresh it triggers has run by the time this resolves.
3747
+ *
3748
+ * @param ownerPublicKeyB64 - the new owner's ed25519 public key (base64url).
3749
+ * @returns `true` when the row was seated, `false` when the key is already a live owner.
3750
+ */
3751
+ addOwner(ownerPublicKeyB64: string): Promise<boolean>;
3752
+ /**
3753
+ * Remove an owner key, signed by this node's owner key (which must be a different owner):
3754
+ * the delete and its `Revocation` tombstone commit together. Refused by name when the key
3755
+ * is this node's own or the only live owner. The write notifies the membership hub; the
3756
+ * tombstone rides the committed-delete seam ({@link noteGuardedDelete}), so one committed
3757
+ * while alone is re-issued on cohort growth like every other removal.
3758
+ *
3759
+ * Removes the `OwnerKey` row only. The call that removes a machine completely, its owner key
3760
+ * included, is {@link removeMachine}.
3761
+ *
3762
+ * @param ownerPublicKeyB64 - the owner's ed25519 public key (base64url).
3763
+ * @returns `true` when the row was removed, `false` when the key is not a live owner here.
3764
+ */
3765
+ removeOwner(ownerPublicKeyB64: string): Promise<boolean>;
3766
+ /**
3767
+ * The cadre's machines as an app lists them ({@link CadreMachine}): one entry per live
3768
+ * `CadrePeer` row, judged by this node's membership predicate, plus one per live `OwnerKey`
3769
+ * whose key names a machine with no row (`describeMachines` in `machine-removal.ts`).
3770
+ * Unlocked reads with the default retry.
3771
+ */
3772
+ listMachines(): Promise<CadreMachine[]>;
3773
+ /**
3774
+ * What {@link removeMachine} would do for `peerId`, read without writing, for an app's
3775
+ * confirmation step ({@link MachineRemovalPlan}): whether this node may remove it, whether its
3776
+ * owner key goes with it, which rows that key vouched (so the app can offer to keep them by
3777
+ * re-vouching), which invitations it could come back through, and whether this node's own
3778
+ * rows depend on it (`planMachineRemoval` in `machine-removal.ts`).
3779
+ */
3780
+ planMachineRemoval(peerId: string): Promise<MachineRemovalPlan>;
3781
+ /**
3782
+ * Remove a machine from the cadre completely, signed by this node's owner key: its
3783
+ * `CadrePeer` row, its `OwnerKey` row when its key is an owner, its `DeviceToken` row (a
3784
+ * removed phone must not be push-woken) and a withdrawal of every live invitation it could
3785
+ * redeem to come back, in ONE transaction ({@link ControlDatabase.removeMachine}), with a
3786
+ * `Revocation` tombstone per row so the removal holds on every machine
3787
+ * ("Deletes made while alone" in docs/architecture.md).
3788
+ *
3789
+ * Steps: re-plan ({@link planMachineRemoval}) and throw {@link MachineRemovalRefusedError} on a
3790
+ * refusal; check `options.revouch` against the plan's dependents and throw before any write
3791
+ * when it names anything else; **re-vouch first**, under this node's key, the dependents the
3792
+ * caller chose to keep (`ControlDatabase.revouchOwnerKey`, `reauthorizePeer`), so they are
3793
+ * never untrusted in between, and stop before the removal if one fails (re-vouches already
3794
+ * made stay, harmlessly); then the removal transaction. Afterwards this node forgets the
3795
+ * machine as a dial target, and the refresh the write notifies hangs up its control
3796
+ * connection ({@link hangUpRevokedControlPeers}) before this resolves.
3797
+ *
3798
+ * Not undone by this call: the machine keeps every row and strand block it already synced
3799
+ * and the party's strand member keys (`tickets/blocked/decide-removed-machine-strand-access`).
3800
+ * A fresh invitation re-admits it later under fresh stamps.
3801
+ *
3802
+ * @returns what was written ({@link MachineRemovalResult}); all false when no live row names
3803
+ * the machine here (already removed elsewhere).
3804
+ * @throws {MachineRemovalRefusedError} `'not-an-owner'`, `'self'` or `'last-owner'`.
3805
+ */
3806
+ removeMachine(peerId: string, options?: RemoveMachineOptions): Promise<MachineRemovalResult>;
3807
+ /**
3808
+ * The reads behind {@link listMachines}: the live `CadrePeer` and `OwnerKey` rows and the
3809
+ * machines projected from them, with this node's membership predicate as the `trusted` test.
3810
+ */
3811
+ private readMachines;
3812
+ /** {@link planMachineRemoval} with the rows it was judged from, which {@link removeMachine} re-vouches dependents by. */
3813
+ private planMachineRemovalWith;
3814
+ /**
3815
+ * The re-vouch step of {@link removeMachine}: every chosen dependent row is rebound to this
3816
+ * node's key BEFORE the removal, so no reader ever judges it against an owner that is gone.
3817
+ * Owners through `ControlDatabase.revouchOwnerKey` (a delete-and-re-add under a fresh stamp,
3818
+ * the only rebinding `OwnerKey.NoUpdate` allows); peers through the owner re-touch
3819
+ * (`reauthorizePeer`), which rewrites `VouchOwner`/`VouchSig` and clears `VouchUsage`.
3820
+ *
3821
+ * The peer re-touch keeps the row's `UpdatedAt` where it is. The owner branch of
3822
+ * `CadrePeer.AuthorizedUpdate` does not require a bump, and bumping a self-published row
3823
+ * would leave its `Sig` unverifiable, so unresolvable and unlisted as self-published, until
3824
+ * the machine's next heartbeat (the NOTE on that constraint); the write-while-alone drain
3825
+ * bumps because it re-issues an otherwise unchanged row, whereas here the voucher itself
3826
+ * changes. A row never self-published carries no stamp to keep and gets the clock.
3827
+ *
3828
+ * A failure rethrows and stops the call before the removal; re-vouches already made stay
3829
+ * (the row is only re-signed by a current owner) and the caller retries. An owner re-vouch
3830
+ * committed alone is not re-issued on growth (the add half has no re-issue path,
3831
+ * `tickets/backlog/debt-owner-key-add-while-alone-has-no-reissue`), so it is logged as
3832
+ * {@link addOwner} logs it; the peer re-touches are re-issued by the existing drain.
3833
+ */
3834
+ private revouchDependents;
3362
3835
  /**
3363
3836
  * Create a seed from the current control network state.
3364
3837
  * The seed contains peer information and is signed by an owner.
@@ -3368,10 +3841,12 @@ export declare class CadreNode implements SAppIdLookup {
3368
3841
  * Apply a seed to populate the peer cache and enable connections.
3369
3842
  *
3370
3843
  * 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.
3844
+ * key (see `SeedTrustPolicy`). An operator-driven caller can pass a per-seed
3845
+ * `trustPolicy` override — e.g. a `pinnedKeyTrustPolicy` built from a pinned
3846
+ * owner key — so a cold-start node can accept its first seed without
3847
+ * reconfiguring the service. The override wins on a node started with
3848
+ * `config.claim` as well: the claim secret is the node's default inbound policy,
3849
+ * not a lock on its local operator, who already controls the process.
3375
3850
  */
3376
3851
  applySeed(seed: ControlNetworkSeed, options?: {
3377
3852
  trustPolicy?: SeedTrustPolicy;
@@ -3385,11 +3860,17 @@ export declare class CadreNode implements SAppIdLookup {
3385
3860
  private noteAppliedSeed;
3386
3861
  /**
3387
3862
  * Deliver a seed directly to a peer via the /sereus/seed/1.0.0 protocol.
3863
+ *
3864
+ * `target` is a multiaddr string, or a peer id with its addresses (the form a phone
3865
+ * uses for a node it scanned), which are dialed one at a time before the request
3866
+ * (`SeedBootstrapService.deliverSeed`). `options.claimProof` rides beside the seed when
3867
+ * the target is a brand-new node being claimed (`claim-proof.ts`).
3868
+ *
3869
+ * @throws {PeerUnreachableError} for a peer id with addresses, when no connection to it formed.
3388
3870
  */
3389
- deliverSeed(targetMultiaddr: string, seed: ControlNetworkSeed): Promise<{
3390
- accepted: boolean;
3391
- reason?: string;
3392
- }>;
3871
+ deliverSeed(target: SeedDeliveryTarget, seed: ControlNetworkSeed, options?: {
3872
+ claimProof?: string;
3873
+ }): Promise<SeedAckMessage>;
3393
3874
  /**
3394
3875
  * Encode a seed for out-of-band delivery (e.g., QR code, copy/paste).
3395
3876
  */
@@ -3423,32 +3904,184 @@ export declare class CadreNode implements SAppIdLookup {
3423
3904
  */
3424
3905
  addDrone(options: AddDroneOptions): Promise<DroneInitResult>;
3425
3906
  /**
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>;
3907
+ * Claim a node that was started with a claim secret (`CadreNodeConfig.claim`), from
3908
+ * the peer id, addresses and secret its host showed (the QR code): mint a seed, prove
3909
+ * possession of the secret beside it (`claim-proof.ts`), deliver it, and on acceptance
3910
+ * vouch the node into this cadre and keep its addresses as a dial target. The owner
3911
+ * side of the claim-secret entry in the Seed Delivery Protocol (`docs/architecture.md`).
3912
+ *
3913
+ * Requires the seed service with an owner key ({@link initializeSeedBootstrap}), as
3914
+ * {@link addDrone} does. The order is the REVERSE of `addDrone`'s: seed first, row
3915
+ * second. A refused claim or a failed delivery therefore leaves no `CadrePeer` row and
3916
+ * no retained dial target behind on this node — a phone that typed the wrong secret,
3917
+ * or scanned a node someone else already owns, has nothing to clean up.
3918
+ *
3919
+ * A failure is one of three kinds a caller can tell apart:
3920
+ *
3921
+ * - `ClaimRefusedError` — the node was reached and refused, with its refusal code when
3922
+ * its policy gave one.
3923
+ * - `PeerUnreachableError` — no connection to the node formed from any of its addresses,
3924
+ * so nothing was sent. Each address is dialed in turn on its own limit, so this can
3925
+ * take up to the per-peer dial limit plus the seed request's (114.5 s at the default
3926
+ * declared link) when none answers.
3927
+ * - anything else — the node was reached and the exchange failed, or a local step failed
3928
+ * (a malformed secret or address among them). Repeating the claim with the same secret
3929
+ * is safe (see below).
3930
+ *
3931
+ * Idempotent for the same owner: a second call after a dropped response is accepted
3932
+ * by the node (its anchored branch), the row insert is a no-op on an existing row
3933
+ * (`ControlDatabase.insertCadrePeer`), and the dial target is replaced, not duplicated.
3934
+ *
3935
+ * The first dial is kicked (not awaited) so it happens now rather than on the next
3936
+ * timed reconcile pass; a caller that wants to wait for the connection awaits
3937
+ * {@link reconcileControlCohort} itself, with `addDrone`'s caveat that a pass already
3938
+ * in flight is joined rather than restarted. The claimed node dials this one back too,
3939
+ * from the seed's owner list, when this node has a listen or relay address; a phone
3940
+ * with neither is reached by its own reconcile pass alone.
3941
+ *
3942
+ * @throws {ClaimRefusedError} when the node was reached and refused the claim.
3943
+ * @throws {PeerUnreachableError} when the node was never reached.
3944
+ */
3945
+ claimNode(target: {
3946
+ peerId: string;
3947
+ multiaddrs: string[];
3948
+ secret: string;
3949
+ }): Promise<void>;
3435
3950
  /**
3436
3951
  * Add a phone to the cadre with relay support.
3437
3952
  * Use when both nodes are NAT'd (phone-to-phone).
3438
3953
  */
3439
3954
  addPhoneWithRelay(phonePeerId: string): Promise<DroneInitResult>;
3440
3955
  /**
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>;
3956
+ * Where a device could redeem a cadre invitation minted now from, once this machine is gone
3957
+ * ({@link CadreInvitationReach}): the same member selection {@link createCadreInvitation}
3958
+ * makes, judged without minting anything. `rentNode` reads it to choose between inviting a
3959
+ * provider's container and claiming it; `local-network` counts as not reachable there, since
3960
+ * the container is never on a member's LAN. Requires a started node.
3961
+ */
3962
+ invitationReach(): Promise<CadreInvitationReach>;
3963
+ /**
3964
+ * The `members` a cadre invitation minted now would name, and how far they reach: one
3965
+ * selection behind both {@link createCadreInvitation} and {@link invitationReach}, so the
3966
+ * two cannot disagree.
3967
+ */
3968
+ private selectInvitationMembers;
3969
+ /**
3970
+ * Mint an owner-signed cadre invitation that a device can redeem at ANY member machine of
3971
+ * this party, the owner offline included ({@link redeemCadreInvitation}). Requires the
3972
+ * owner key ({@link initializeSeedBootstrap}).
3973
+ *
3974
+ * The invitation is an ed25519 keypair: the public half is the `CadreInvite` row's key,
3975
+ * the private half rides in the returned bundle as the proof of possession. Whoever holds
3976
+ * the bundle can redeem it, so an UNTARGETED, OWNER-GRANTING invitation — a bearer
3977
+ * credential for admin rights — defaults to a 15-minute lifetime; every other kind to 24
3978
+ * hours. One use unless `uses` says otherwise. The bundle carries the signed row (so a
3979
+ * member that has not received it by replication seats it from the bundle), this node's
3980
+ * anchored owner keys — every current owner this node has verified, the out-of-band ones
3981
+ * and those derived along verifiable chains ({@link syncOwnerAnchor}) — (the device pins
3982
+ * them and checks the member's reply against them; sourced from the node-local anchor
3983
+ * only, never the raw replicated `OwnerKey` table, because the device anchors whatever
3984
+ * arrives and a stranger's genesis-inserted key must not ride an invitation into a fresh
3985
+ * node's anchor; an empty anchor is refused because a reply could not be checked), and
3986
+ * at most six addresses: this machine's first ({@link resolveInviteAddresses}, which
3987
+ * honours pushed NAT addresses), then two each of up to three other members
3988
+ * (`selectCadreInvitationMembers`), so the encoded text fits a QR code.
3989
+ *
3990
+ * The result's `reach` says where a device could redeem it from once this machine is gone
3991
+ * (`cadreInvitationReach`, judged on the other members' addresses only), so an app can warn
3992
+ * before handing over an invitation only it could answer.
3993
+ *
3994
+ * The row commits locally and replicates like any other control write; minted while
3995
+ * alone (a phone with no connection), it reaches the other members through the
3996
+ * peer-join block catch-up on the next connection. The device can redeem at any member it
3997
+ * reaches meanwhile: a member that does not hold the row yet admits the device's connection
3998
+ * provisionally ({@link admitInboundControlConnection}) and seats the row from the bundle.
3999
+ *
4000
+ * @throws when no owner key is wired, the anchor is empty, `uses` is not a positive
4001
+ * integer, or neither this machine nor any other member has an address.
4002
+ */
4003
+ createCadreInvitation(options: CreateCadreInvitationOptions): Promise<CreateCadreInvitationResult>;
4004
+ /**
4005
+ * Every cadre invitation this node holds — minted here or replicated in — with its
4006
+ * standing ({@link CadreInviteStatus}: live, withdrawn, redemptions recorded).
4007
+ */
4008
+ listCadreInvitations(): Promise<CadreInviteStatus[]>;
4009
+ /**
4010
+ * Withdraw a cadre invitation: an owner-signed `Revocation` tombstone over its row, which
4011
+ * stays as the proof of membership for every device it admitted. No further redemption
4012
+ * succeeds anywhere the tombstone reaches, and a member holding it refuses the bundle's
4013
+ * copy of the row. A withdrawal committed while alone is re-issued on cohort growth like
4014
+ * any other tombstone.
4015
+ *
4016
+ * @returns `true` when this call filed the tombstone, `false` when the row is not held
4017
+ * here or was already withdrawn.
4018
+ */
4019
+ withdrawCadreInvitation(key: string): Promise<boolean>;
4020
+ /**
4021
+ * Join the cadre an invitation names by redeeming it at one of the member machines it
4022
+ * lists, the owner that minted it offline or not. Needs a started node with a stable
4023
+ * identity (`keyStore` or `privateKey`): the redemption is signed with the key behind
4024
+ * this node's peer id, and the member checks that the two match.
4025
+ *
4026
+ * Steps: refuse an invitation for another party; pin its `ownerKeys` into the node-local
4027
+ * anchor (`'invite'` provenance — the pin sticks even if the redemption then fails, as it
4028
+ * does for a seed invite, so a later attempt or seed from the same owner is anchored);
4029
+ * sign the request; dial the listed members in order (`redeemAtMembers`); on
4030
+ * acceptance verify the reply against the pinned keys, merge its dial hints into the peer
4031
+ * store, retain the answering member as a cold-start dial target, refresh the membership
4032
+ * gate and start a cohort reconcile pass. This node then syncs the control database over
4033
+ * the connection it already holds, the same unified behaviour as after a seed.
4034
+ *
4035
+ * A device that is already a member (a restart with the invitation still configured) is
4036
+ * answered as accepted by the member's idempotent redemption, with no second usage row.
4037
+ *
4038
+ * One pass over the listed members by default. With `options.waitForMember` the call keeps
4039
+ * making passes while every answer is retryable — for a device redeeming while no
4040
+ * always-on member is up — until a member accepts, one refuses finally, the invitation
4041
+ * expires (thrown as the `invite-spent` refusal) or `options.signal` aborts
4042
+ * (`redeemInPasses`). The request is signed once, before the first pass, and every pass
4043
+ * sends it: its `usageStampId` keys the usage row, so a member that admitted this node on a
4044
+ * pass whose reply was lost answers the next as already a member. `options.onProgress`
4045
+ * reports each pass and pause.
4046
+ *
4047
+ * When the invitation grants ownership, the member seats this node's key as an `OwnerKey`
4048
+ * row by consent. The caller still has to wire that key for signing afterwards, with
4049
+ * {@link wireOwnKeyIfOwner}; never `ensureOwnerKey`, the row is already there. Other
4050
+ * machines derive this node's key into their anchors from the row's stored proof on their
4051
+ * next membership refresh ({@link syncOwnerAnchor}).
4052
+ *
4053
+ * Limit: the reply check ({@link verifyRedeemReply}) does not authenticate the member. A
4054
+ * forged bundle, or a legitimate one whose addresses were swapped, dials a machine that
4055
+ * can echo the owner-signed row it was just sent; that is what pasting an attacker's
4056
+ * invitation means. What protects this node afterwards is its anchor: every row it then
4057
+ * syncs is judged against the pinned owner keys, so a machine that is not a member of the
4058
+ * cadre the invitation names can admit nobody this node will trust.
4059
+ *
4060
+ * @throws `CadreInviteRejectedError` on a final refusal, `CadreInviteReplyInvalidError` on
4061
+ * an acceptance that fails the reply check, `CadreInviteUnreachableError` when every
4062
+ * listed address was tried without an acceptance (one pass only), the signal's reason on
4063
+ * an abort, and a plain `Error` for the preconditions above.
4064
+ */
4065
+ redeemCadreInvitation(invitation: CadreInvitation, options?: RedeemCadreInvitationOptions): Promise<RedeemCadreInvitationResult>;
4066
+ /**
4067
+ * If this node's identity key is a live `OwnerKey` here, wire it for signing
4068
+ * ({@link initializeSeedBootstrap}) and return true; otherwise change nothing and return
4069
+ * false. For a machine that joined someone else's cadre by invitation: after an
4070
+ * owner-granting redemption, and on every later start. A redemption seats the request's
4071
+ * `peerKey`, the key behind this node's peer id, so the identity key is the one to look for.
4072
+ *
4073
+ * Unlike `ensureOwnerKey` it never inserts an `OwnerKey` row, so a machine that joined a
4074
+ * cadre can never genesis-insert itself into it, and a non-owner never anchors its own key.
4075
+ * An owner whose key is already wired (an earlier call) is answered true with nothing redone.
4076
+ *
4077
+ * NOTE: a node running the listener-only seed service ({@link enableSeedListener}) cannot be
4078
+ * wired this way: libp2p takes one seed handler, and {@link initializeSeedBootstrap} would
4079
+ * register a second. Nothing that joins by invitation enables the listener today; if one
4080
+ * does, replace the listener here rather than beside it.
4081
+ *
4082
+ * @throws when the node is not started.
4083
+ */
4084
+ wireOwnKeyIfOwner(): Promise<boolean>;
3452
4085
  /**
3453
4086
  * Replace this node's strand solicitation service — the formation responder every node
3454
4087
  * installs at {@link start}, and the initiator side of {@link formStrand}. Optional: call it
@@ -3501,15 +4134,21 @@ export declare class CadreNode implements SAppIdLookup {
3501
4134
  */
3502
4135
  createOpenInvitation(sAppId: string, expirationMs?: number): Promise<OpenInvitation>;
3503
4136
  /**
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.
4137
+ * Addresses of the party's other machines for an open strand invitation's bootstrap list
4138
+ * ({@link createOpenInvitation}); {@link selectInvitationSiblingAddrs} picks which.
4139
+ */
4140
+ private siblingInvitationAddrs;
4141
+ /**
4142
+ * The party's other machines an invitation may name — an open strand invitation
4143
+ * ({@link siblingInvitationAddrs}) or a cadre invitation ({@link createCadreInvitation}) —
4144
+ * and which of them this node is connected to. The source is each authorized member's
4145
+ * signed `CadrePeer` record ({@link resolvePeerRecord}), so a machine whose record is
4146
+ * missing, stale or untrusted is left out; live connections only order them.
3508
4147
  *
3509
4148
  * Best-effort: a failed read leaves the invitation naming this machine alone, which is what
3510
4149
  * it named before siblings were added.
3511
4150
  */
3512
- private siblingInvitationAddrs;
4151
+ private resolveInvitationSiblings;
3513
4152
  /**
3514
4153
  * Form a strand with a responder via an open invitation: one attempt, from this machine, with
3515
4154
  * nothing recorded party-wide. {@link requestJoin} is the path that keeps trying.
@@ -3525,7 +4164,7 @@ export declare class CadreNode implements SAppIdLookup {
3525
4164
  * Ask to join through `invitation` and have the party keep trying until the join works, the
3526
4165
  * invitation is used up or expires, or the request is dismissed: across restarts, and from
3527
4166
  * 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
4167
+ * offline"). Records a party-wide `JoinRequest` row, runs one attempt on this machine at
3529
4168
  * once, and returns the status after it: `'joined'` when the inviter answered yes, `'waiting'`
3530
4169
  * when it was unreachable or not ready, `'failed'` when it refused for good.
3531
4170
  *
@@ -3555,17 +4194,17 @@ export declare class CadreNode implements SAppIdLookup {
3555
4194
  /** This machine's owner key and signer for an owner-signed control write, or a throw carrying `refusal`. */
3556
4195
  private requireOwnerSigner;
3557
4196
  /**
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).
4197
+ * Write a join request, or adopt the party's existing request for the same invitation: a
4198
+ * pending one as it is, a finished one rewritten as `requested` (the user asked again).
3560
4199
  */
3561
4200
  private recordJoinRequest;
3562
4201
  /** Build and start the pending-join retry loop over this node (see {@link pendingJoinRunner}). */
3563
4202
  private startPendingJoinRunner;
3564
- /** The invitation a `PendingJoin` row stores. A failure names the row, never the invitation, which is a bearer credential. */
4203
+ /** The invitation a `JoinRequest` row stores. A failure names the row, never the invitation, which is a bearer credential. */
3565
4204
  private pendingJoinInvitation;
3566
4205
  private pendingJoinSAppId;
3567
4206
  /**
3568
- * Stage the membership invitation of each `joined` row this process has not staged yet, so a
4207
+ * Stage the membership invitation of each joined request this process has not staged yet, so a
3569
4208
  * closed strand joined on another owner machine is seated by whichever machine launches it
3570
4209
  * first; the party key it admits is the replicated `StrandPartyKey` row the finishing machine
3571
4210
  * seated. A strand that already has an invitation staged keeps it, for the reconciler to