@serfab/cadre-core 1.16.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/README.md +4 -3
  2. package/dist/bootstrap-peer-store-file.js +1 -1
  3. package/dist/bootstrap-peer-store.d.ts +1 -1
  4. package/dist/bootstrap-peer-store.js +4 -4
  5. package/dist/cadre-invite-protocol.d.ts +81 -142
  6. package/dist/cadre-invite-protocol.js +168 -458
  7. package/dist/cadre-invite-protocol.js.map +1 -1
  8. package/dist/cadre-join-request.d.ts +15 -2
  9. package/dist/cadre-join-request.js +22 -3
  10. package/dist/cadre-join-request.js.map +1 -1
  11. package/dist/cadre-node.d.ts +1089 -475
  12. package/dist/cadre-node.js +2170 -1037
  13. package/dist/cadre-node.js.map +1 -1
  14. package/dist/control-authorization.d.ts +36 -14
  15. package/dist/control-authorization.js +20 -3
  16. package/dist/control-authorization.js.map +1 -1
  17. package/dist/control-cohort.d.ts +88 -32
  18. package/dist/control-cohort.js +91 -33
  19. package/dist/control-cohort.js.map +1 -1
  20. package/dist/control-database.d.ts +856 -318
  21. package/dist/control-database.js +1422 -701
  22. package/dist/control-database.js.map +1 -1
  23. package/dist/control-formation-recorder.d.ts +46 -43
  24. package/dist/control-formation-recorder.js +77 -78
  25. package/dist/control-formation-recorder.js.map +1 -1
  26. package/dist/control-protocol-guard.d.ts +11 -4
  27. package/dist/control-protocol-guard.js +20 -5
  28. package/dist/control-protocol-guard.js.map +1 -1
  29. package/dist/control-schema-version.d.ts +97 -0
  30. package/dist/control-schema-version.js +225 -0
  31. package/dist/control-schema-version.js.map +1 -0
  32. package/dist/control-schema.d.ts +45 -5
  33. package/dist/control-schema.js +262 -78
  34. package/dist/control-schema.js.map +1 -1
  35. package/dist/control-write-retry.d.ts +45 -10
  36. package/dist/control-write-retry.js +92 -22
  37. package/dist/control-write-retry.js.map +1 -1
  38. package/dist/delegate-admission.d.ts +13 -0
  39. package/dist/delegate-admission.js +27 -0
  40. package/dist/delegate-admission.js.map +1 -1
  41. package/dist/ed25519-key.d.ts +10 -7
  42. package/dist/ed25519-key.js +23 -20
  43. package/dist/ed25519-key.js.map +1 -1
  44. package/dist/edge-node/config.d.ts +122 -0
  45. package/dist/edge-node/config.js +199 -0
  46. package/dist/edge-node/config.js.map +1 -0
  47. package/dist/edge-node/dissolve.d.ts +95 -0
  48. package/dist/edge-node/dissolve.js +184 -0
  49. package/dist/edge-node/dissolve.js.map +1 -0
  50. package/dist/edge-node/edge-join.d.ts +44 -0
  51. package/dist/edge-node/edge-join.js +227 -0
  52. package/dist/edge-node/edge-join.js.map +1 -0
  53. package/dist/edge-node/edge-node.d.ts +295 -0
  54. package/dist/edge-node/edge-node.js +704 -0
  55. package/dist/edge-node/edge-node.js.map +1 -0
  56. package/dist/edge-node/index.d.ts +18 -0
  57. package/dist/edge-node/index.js +18 -0
  58. package/dist/edge-node/index.js.map +1 -0
  59. package/dist/edge-node/join-credential.d.ts +11 -0
  60. package/dist/edge-node/join-credential.js +11 -0
  61. package/dist/edge-node/join-credential.js.map +1 -0
  62. package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
  63. package/dist/edge-node/lazy-raw-storage.js +134 -0
  64. package/dist/edge-node/lazy-raw-storage.js.map +1 -0
  65. package/dist/edge-node/node-local.d.ts +60 -0
  66. package/dist/edge-node/node-local.js +58 -0
  67. package/dist/edge-node/node-local.js.map +1 -0
  68. package/dist/edge-node/options.d.ts +83 -0
  69. package/dist/edge-node/options.js +110 -0
  70. package/dist/edge-node/options.js.map +1 -0
  71. package/dist/edge-node/owner-grant.d.ts +41 -0
  72. package/dist/edge-node/owner-grant.js +38 -0
  73. package/dist/edge-node/owner-grant.js.map +1 -0
  74. package/dist/edge-node/platform.d.ts +140 -0
  75. package/dist/edge-node/platform.js +12 -0
  76. package/dist/edge-node/platform.js.map +1 -0
  77. package/dist/edge-node/strand-move.d.ts +115 -0
  78. package/dist/edge-node/strand-move.js +223 -0
  79. package/dist/edge-node/strand-move.js.map +1 -0
  80. package/dist/edge-node/strands.d.ts +28 -0
  81. package/dist/edge-node/strands.js +60 -0
  82. package/dist/edge-node/strands.js.map +1 -0
  83. package/dist/enrolled-machine-store.d.ts +3 -3
  84. package/dist/enrolled-machine-store.js +3 -3
  85. package/dist/formation-token.d.ts +48 -0
  86. package/dist/formation-token.js +26 -0
  87. package/dist/formation-token.js.map +1 -0
  88. package/dist/index.d.ts +27 -17
  89. package/dist/index.js +39 -17
  90. package/dist/index.js.map +1 -1
  91. package/dist/joined-strand-store.d.ts +29 -12
  92. package/dist/joined-strand-store.js +66 -27
  93. package/dist/joined-strand-store.js.map +1 -1
  94. package/dist/key-store.d.ts +9 -4
  95. package/dist/key-store.js +9 -4
  96. package/dist/key-store.js.map +1 -1
  97. package/dist/link-budget.d.ts +9 -7
  98. package/dist/link-budget.js +9 -7
  99. package/dist/link-budget.js.map +1 -1
  100. package/dist/machine-removal.d.ts +19 -39
  101. package/dist/machine-removal.js +24 -98
  102. package/dist/machine-removal.js.map +1 -1
  103. package/dist/member-redemption.d.ts +256 -0
  104. package/dist/member-redemption.js +490 -0
  105. package/dist/member-redemption.js.map +1 -0
  106. package/dist/membership-view.d.ts +91 -0
  107. package/dist/membership-view.js +110 -0
  108. package/dist/membership-view.js.map +1 -0
  109. package/dist/owner-anchor-sync.d.ts +4 -4
  110. package/dist/owner-anchor-sync.js +16 -15
  111. package/dist/owner-anchor-sync.js.map +1 -1
  112. package/dist/owner-grant-protocol.d.ts +251 -0
  113. package/dist/owner-grant-protocol.js +344 -0
  114. package/dist/owner-grant-protocol.js.map +1 -0
  115. package/dist/owner-key-export.d.ts +70 -0
  116. package/dist/owner-key-export.js +279 -0
  117. package/dist/owner-key-export.js.map +1 -0
  118. package/dist/owner-key-flows.d.ts +55 -0
  119. package/dist/owner-key-flows.js +60 -0
  120. package/dist/owner-key-flows.js.map +1 -0
  121. package/dist/owner-key-store.d.ts +31 -0
  122. package/dist/owner-key-store.js +81 -0
  123. package/dist/owner-key-store.js.map +1 -0
  124. package/dist/owner-keys.d.ts +78 -0
  125. package/dist/owner-keys.js +133 -0
  126. package/dist/owner-keys.js.map +1 -0
  127. package/dist/owner-signer.d.ts +54 -0
  128. package/dist/owner-signer.js +51 -0
  129. package/dist/owner-signer.js.map +1 -0
  130. package/dist/peer-addr-book.d.ts +10 -1
  131. package/dist/peer-addr-book.js +4 -3
  132. package/dist/peer-addr-book.js.map +1 -1
  133. package/dist/peer-authorization.d.ts +83 -18
  134. package/dist/peer-authorization.js +123 -53
  135. package/dist/peer-authorization.js.map +1 -1
  136. package/dist/peer-join-backfill.d.ts +89 -20
  137. package/dist/peer-join-backfill.js +265 -108
  138. package/dist/peer-join-backfill.js.map +1 -1
  139. package/dist/pending-join-runner.d.ts +48 -18
  140. package/dist/pending-join-runner.js +74 -56
  141. package/dist/pending-join-runner.js.map +1 -1
  142. package/dist/provider/rent-node.js +1 -1
  143. package/dist/provider/rent-node.js.map +1 -1
  144. package/dist/seed-bootstrap.d.ts +162 -133
  145. package/dist/seed-bootstrap.js +300 -283
  146. package/dist/seed-bootstrap.js.map +1 -1
  147. package/dist/seed-trust-policy.js +4 -4
  148. package/dist/strand-addr-protocol.d.ts +4 -1
  149. package/dist/strand-addr-protocol.js +8 -2
  150. package/dist/strand-addr-protocol.js.map +1 -1
  151. package/dist/strand-cohort.d.ts +3 -0
  152. package/dist/strand-cohort.js.map +1 -1
  153. package/dist/strand-database.d.ts +24 -2
  154. package/dist/strand-database.js +29 -6
  155. package/dist/strand-database.js.map +1 -1
  156. package/dist/strand-first-sync-gate.d.ts +176 -88
  157. package/dist/strand-first-sync-gate.js +288 -121
  158. package/dist/strand-first-sync-gate.js.map +1 -1
  159. package/dist/strand-formation-deadlines.d.ts +9 -9
  160. package/dist/strand-formation-deadlines.js +9 -9
  161. package/dist/strand-formation-manager.d.ts +23 -17
  162. package/dist/strand-formation-manager.js +46 -44
  163. package/dist/strand-formation-manager.js.map +1 -1
  164. package/dist/strand-formation-protocol.d.ts +5 -8
  165. package/dist/strand-formation-protocol.js +101 -16
  166. package/dist/strand-formation-protocol.js.map +1 -1
  167. package/dist/strand-instance-manager.d.ts +27 -18
  168. package/dist/strand-instance-manager.js +119 -50
  169. package/dist/strand-instance-manager.js.map +1 -1
  170. package/dist/strand-membership-reconciler.d.ts +16 -13
  171. package/dist/strand-membership-reconciler.js +30 -22
  172. package/dist/strand-membership-reconciler.js.map +1 -1
  173. package/dist/strand-membership-writer.d.ts +45 -22
  174. package/dist/strand-membership-writer.js +226 -131
  175. package/dist/strand-membership-writer.js.map +1 -1
  176. package/dist/strand-move.d.ts +6 -5
  177. package/dist/strand-move.js +36 -32
  178. package/dist/strand-move.js.map +1 -1
  179. package/dist/strand-peer-redial.js +2 -3
  180. package/dist/strand-peer-redial.js.map +1 -1
  181. package/dist/strand-revocation-enforcer.d.ts +134 -60
  182. package/dist/strand-revocation-enforcer.js +144 -71
  183. package/dist/strand-revocation-enforcer.js.map +1 -1
  184. package/dist/strand-solicitation.d.ts +50 -39
  185. package/dist/strand-solicitation.js +10 -3
  186. package/dist/strand-solicitation.js.map +1 -1
  187. package/dist/strand-watcher.d.ts +80 -13
  188. package/dist/strand-watcher.js +152 -48
  189. package/dist/strand-watcher.js.map +1 -1
  190. package/dist/table-change-watch.d.ts +63 -0
  191. package/dist/table-change-watch.js +209 -0
  192. package/dist/table-change-watch.js.map +1 -0
  193. package/dist/timeout-scheduler.d.ts +4 -3
  194. package/dist/timeout-scheduler.js +4 -3
  195. package/dist/timeout-scheduler.js.map +1 -1
  196. package/dist/types.d.ts +374 -150
  197. package/dist/types.js +4 -4
  198. package/dist/types.js.map +1 -1
  199. package/package.json +17 -8
  200. package/src/bootstrap-peer-store-file.ts +44 -44
  201. package/src/bootstrap-peer-store.ts +214 -214
  202. package/src/cadre-invite-protocol.ts +740 -1052
  203. package/src/cadre-join-request.ts +24 -3
  204. package/src/cadre-node.ts +2402 -1079
  205. package/src/control-authorization.ts +47 -14
  206. package/src/control-cohort.ts +204 -128
  207. package/src/control-database.ts +1749 -820
  208. package/src/control-formation-recorder.ts +359 -353
  209. package/src/control-protocol-guard.ts +23 -6
  210. package/src/control-schema-version.ts +301 -0
  211. package/src/control-schema.ts +273 -78
  212. package/src/control-write-retry.ts +641 -567
  213. package/src/delegate-admission.ts +351 -314
  214. package/src/ed25519-key.ts +174 -171
  215. package/src/edge-node/config.ts +277 -0
  216. package/src/edge-node/dissolve.ts +251 -0
  217. package/src/edge-node/edge-join.ts +319 -0
  218. package/src/edge-node/edge-node.ts +1045 -0
  219. package/src/edge-node/index.ts +59 -0
  220. package/src/edge-node/join-credential.ts +16 -0
  221. package/src/edge-node/lazy-raw-storage.ts +178 -0
  222. package/src/edge-node/node-local.ts +84 -0
  223. package/src/edge-node/options.ts +179 -0
  224. package/src/edge-node/owner-grant.ts +63 -0
  225. package/src/edge-node/platform.ts +151 -0
  226. package/src/edge-node/strand-move.ts +270 -0
  227. package/src/edge-node/strands.ts +72 -0
  228. package/src/enrolled-machine-store.ts +3 -3
  229. package/src/formation-token.ts +63 -0
  230. package/src/index.ts +899 -789
  231. package/src/joined-strand-store.ts +73 -28
  232. package/src/key-store.ts +10 -4
  233. package/src/link-budget.ts +9 -7
  234. package/src/machine-removal.ts +26 -111
  235. package/src/member-redemption.ts +649 -0
  236. package/src/membership-view.ts +144 -0
  237. package/src/owner-anchor-sync.ts +19 -18
  238. package/src/owner-grant-protocol.ts +547 -0
  239. package/src/owner-key-export.ts +348 -0
  240. package/src/owner-key-flows.ts +109 -0
  241. package/src/owner-key-store.ts +85 -0
  242. package/src/owner-keys.ts +178 -0
  243. package/src/owner-signer.ts +94 -0
  244. package/src/peer-addr-book.ts +4 -3
  245. package/src/peer-authorization.ts +558 -488
  246. package/src/peer-join-backfill.ts +971 -761
  247. package/src/pending-join-runner.ts +76 -53
  248. package/src/provider/rent-node.ts +1 -1
  249. package/src/seed-bootstrap.ts +343 -349
  250. package/src/seed-trust-policy.ts +4 -4
  251. package/src/strand-addr-protocol.ts +547 -538
  252. package/src/strand-cohort.ts +4 -0
  253. package/src/strand-database.ts +352 -319
  254. package/src/strand-first-sync-gate.ts +334 -133
  255. package/src/strand-formation-deadlines.ts +227 -227
  256. package/src/strand-formation-manager.ts +731 -725
  257. package/src/strand-formation-protocol.ts +122 -24
  258. package/src/strand-instance-manager.ts +2092 -2026
  259. package/src/strand-membership-reconciler.ts +33 -25
  260. package/src/strand-membership-writer.ts +2093 -1974
  261. package/src/strand-move.ts +50 -38
  262. package/src/strand-peer-redial.ts +2 -3
  263. package/src/strand-revocation-enforcer.ts +740 -654
  264. package/src/strand-solicitation.ts +61 -34
  265. package/src/strand-watcher.ts +175 -56
  266. package/src/table-change-watch.ts +285 -0
  267. package/src/timeout-scheduler.ts +29 -28
  268. package/src/types.ts +391 -152
@@ -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, 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
- import { type Ed25519KeyPair } from './ed25519-key.js';
3
+ import type { CadreNodeConfig, StrandInstance, StrandRow, StrandConfig, FoundStrandConfig, FoundStrandResult, FoundStrandOptions, FoundStrandBackgroundResult, SAppConfig, CadreNodeEvents, ControlNetworkSeed, ApplySeedResult, SeedAckMessage, SeedDeliveryTarget, AddDroneOptions, DroneInitResult, OpenInvitation, FormStrandResult, StrandFormationDisclosure, StrandMembershipInvite, ResolveOpts, SelfRegistrationOutcome, ServiceWakeResult, PushPlatform, DeviceTokenRecord, CadreInviteStatus, CadreMachine, CadreOwnerKey, MachineRemovalPlan, MachineRemovalResult, OwnerKeyRemovalPlan, OwnerKeyRemovalResult, RemoveOwnerOptions, CadreRemoval, CadreInvitationReach, CreateCadreInvitationOptions, CreateCadreInvitationResult, RedeemCadreInvitationOptions, RedeemCadreInvitationResult, CreateOwnerGrantOptions, CreateOwnerGrantResult, OwnerGrantStatus, RedeemOwnerGrantOptions, RedeemOwnerGrantResult, JoinWithOwnerGrantResult, ResolveDeviceTokenOpts, PendingJoinStatus } from './types.js';
5
4
  import { type TrustedOwnerStore, type TrustSource } from './trusted-owner-store.js';
5
+ import { type OwnerSign, type OwnerSigner } from './owner-signer.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
9
  import { type JoinedStrandRecord } from './joined-strand-store.js';
10
10
  import { type SAppIdLookup } from './strand-watcher.js';
11
+ import type { CohortPeerRow } from './strand-cohort.js';
11
12
  import { type ControlCohortReconcileResult } from './control-cohort.js';
12
13
  import { EnrollmentService } from './enrollment.js';
13
14
  import { ControlDatabase } from './control-database.js';
14
15
  import { type CadreInvitation } from './cadre-invite-protocol.js';
16
+ import { type OwnerGrantBundle } from './owner-grant-protocol.js';
15
17
  import { SeedBootstrapService } from './seed-bootstrap.js';
16
18
  import { type SeedTrustPolicy } from './seed-trust-policy.js';
17
19
  import { StrandSolicitationService, type StrandSolicitationServiceOptions } from './strand-solicitation.js';
@@ -26,8 +28,8 @@ import { type ConnectionPathSummary } from './diagnostics/connection-path.js';
26
28
  *
27
29
  * Ten minutes sits comfortably inside the peerStore's one-hour address expiry
28
30
  * (`MAX_ADDRESS_AGE`, see `peer-addr-book.ts`) with headroom for several missed
29
- * passes, and far above the 15 s reconcile cadence the refresh rides on, so the
30
- * strand-addr RPC fan-out stays cheap.
31
+ * passes, and far above the reconcile pass's 2-minute backstop the refresh rides
32
+ * on, so the strand-addr RPC fan-out stays cheap.
31
33
  */
32
34
  export declare const STRAND_PEER_ADDR_REFRESH_MS: number;
33
35
  /**
@@ -36,18 +38,19 @@ export declare const STRAND_PEER_ADDR_REFRESH_MS: number;
36
38
  * phone asking before its `CadrePeer` row has replicated to the sibling is refused
37
39
  * and its delegate grant goes unrecorded, and waiting the full
38
40
  * {@link STRAND_PEER_ADDR_REFRESH_MS} for the next try is the delay this bounds.
39
- * Four reconcile ticks, so a sibling that keeps failing costs one timed-out RPC a
40
- * minute rather than one a tick.
41
+ * A pass arms its own wake for the earliest due time it leaves behind
42
+ * (`nextReconcileWakeMs`), so a sibling that keeps failing costs one timed-out
43
+ * RPC a minute, with no pass in between.
41
44
  */
42
45
  export declare const STRAND_PEER_ADDR_RETRY_MS: number;
43
46
  /**
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.
47
+ * Default lifetime of an owner grant ({@link CadreNode.createOwnerGrant}): whoever holds the
48
+ * bundle can become an owner, so it is a bearer credential for admin rights and lives only
49
+ * long enough to be pasted or scanned by the device it was minted for. A backup grant is minted
50
+ * with no expiry, by explicit choice.
48
51
  */
49
- export declare const OWNER_INVITATION_DEFAULT_TTL_MS: number;
50
- /** Default lifetime of every other cadre invitation: targeted, or granting membership only. */
52
+ export declare const OWNER_GRANT_DEFAULT_TTL_MS: number;
53
+ /** Default lifetime of a cadre invitation ({@link CadreNode.createCadreInvitation}): it admits a machine and grants nothing else, so it need not be short. */
51
54
  export declare const CADRE_INVITATION_DEFAULT_TTL_MS: number;
52
55
  type EventHandler<T> = (data: T) => void;
53
56
  /**
@@ -134,6 +137,13 @@ export declare class CadreNode implements SAppIdLookup {
134
137
  * once that database is up, on every node.
135
138
  */
136
139
  private cadreInviteHandler;
140
+ /**
141
+ * Owner grant redemption handler (`/sereus/owner-grant/1.0.0`): a holder of a grant dials
142
+ * this machine and proves possession of the grant and of the key it seats; the handler seats
143
+ * the row and writes the `OwnerKey` row against this node's control database. Registered by
144
+ * {@link start} beside the invitation handler, on every node.
145
+ */
146
+ private ownerGrantHandler;
137
147
  /**
138
148
  * Server-side push-wake fan-out. Constructed by {@link start} only when
139
149
  * `config.push` (an injected `PushNotifier` + policy) is present — without it
@@ -231,8 +241,8 @@ export declare class CadreNode implements SAppIdLookup {
231
241
  *
232
242
  * Cleared by {@link cleanup} only, deliberately NOT by {@link detachStrand}: the watcher's
233
243
  * filter rejection stops a replica through that very method, and forgetting the id there
234
- * would have the next poll find it unknown again, re-admit the strand provisionally and
235
- * relaunch the replica — once every other poll, forever. A strand id's sApp never changes,
244
+ * would have the next watcher run find it unknown again, re-admit the strand provisionally
245
+ * and relaunch the replica — on every other run, forever. A strand id's sApp never changes,
236
246
  * and the map is bounded by the strands this party has published, like
237
247
  * {@link discoveredStrands}.
238
248
  */
@@ -260,19 +270,24 @@ export declare class CadreNode implements SAppIdLookup {
260
270
  * wait that ends in mutual denial (the upstream gate is fail-closed on
261
271
  * timeout, unlike the fail-open connection gater). Optimystic's predicate is
262
272
  * synchronous by contract. So the set is refreshed OUT OF BAND instead — after
263
- * start, on every {@link reconcileControlCohort} pass (15s cadence), and
264
- * immediately after every LOCAL membership mutation so a just-vouched peer
265
- * is admitted without waiting for the timer. That last refresh is AUTOMATIC:
266
- * the control database notifies {@link refreshMembershipGate} after every
273
+ * start, at the head of every {@link reconcileControlCohort} pass, whenever a
274
+ * membership table changes (the gate watch {@link start} takes through
275
+ * `ControlDatabase.watchMembershipTables`, which hears a local commit and a change
276
+ * replicated into this node's storage alike, within a 50 ms window), and immediately
277
+ * after every LOCAL membership mutation so a just-vouched peer is admitted
278
+ * before the writer's call returns. That last refresh is AUTOMATIC: the
279
+ * control database notifies {@link refreshMembershipGate} after every
267
280
  * committed `CadrePeer` write (see `ControlDatabase.mutateCadrePeer`), so no
268
- * writer has to remember. A change that arrives by REPLICATION is picked up
269
- * on the next timed refresh — bounded staleness, acceptable because this gate
270
- * is defense in depth: rows an unadmitted peer manages to write are still
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.
281
+ * writer has to remember. A change this node's storage never hears of — a
282
+ * machine outside the block's cohort on a party larger than the replication
283
+ * breadth — is picked up at the pass's 2-minute backstop: bounded staleness,
284
+ * acceptable because this gate is defense in depth: rows an unadmitted peer
285
+ * manages to write are still disbelieved at read time by the voucher-anchored
286
+ * predicate. For cohort choice the same window means a member whose row
287
+ * arrived by replication is left out of this node's control-block cohorts
288
+ * until that refresh. Optimystic reads the predicate live on every lookup, so
289
+ * replacing this set is the whole re-evaluation; there is no per-peer
290
+ * reclassify call to make.
276
291
  */
277
292
  private authorizedControlPeers;
278
293
  /**
@@ -330,22 +345,42 @@ export declare class CadreNode implements SAppIdLookup {
330
345
  * When this node last ANNOUNCED a delegate, keyed `<targetPeerId>\n<strandId>`
331
346
  * (the peer announced TO, not the delegate). Throttles
332
347
  * {@link refreshDelegateGrants} to once per `DELEGATE_GRANT_TTL_MS / 2` per
333
- * (relay, strand) so the 15 s reconcile tick never becomes per-tick RPC
334
- * chatter; the launch/resume announce passes record here too. Keys whose
335
- * strand is no longer running are pruned on each reconcile pass.
348
+ * (relay, strand) so a run of change-driven reconcile passes never becomes
349
+ * per-pass RPC chatter; the launch/resume announce passes record here too.
350
+ * Keys whose strand is no longer running are pruned on each reconcile pass.
336
351
  */
337
352
  private readonly delegateAnnounceAt;
353
+ /**
354
+ * Relay peerIds whose protocol negotiation refused the delegate announcement
355
+ * (strand-addr outcome `unsupported`), recorded and cleared by
356
+ * {@link recordDelegateAnnounces} from every announce pass's outcomes. Only a
357
+ * party-run relay has a membership gate that needs a grant, and it serves
358
+ * strand-addr, so a refusal marks a dedicated relay: {@link circuitRelayTargets}
359
+ * leaves it out of the founding/resume and periodic announcements, which would
360
+ * otherwise spend a protocol negotiation on it every time (gotchoices/sereus#49). Not
361
+ * pruned: one entry per relay this node ever named.
362
+ *
363
+ * {@link announceDelegateToRelay} still asks every relay, remembered or not, and
364
+ * its reply is what clears an entry. A party-run relay registers its strand-addr
365
+ * handler late in its own `start()`, after its libp2p node and relay server are
366
+ * up, so an announcement landing in that window is refused by a relay that serves
367
+ * the protocol a moment later. If the re-drive hook skipped remembered relays too,
368
+ * that relay would stay skipped until this node restarts and its gate would deny
369
+ * every reservation through it.
370
+ */
371
+ private readonly relaysRefusingStrandAddr;
338
372
  /**
339
373
  * When each connected sibling is next due a strand-addr RPC for each running
340
374
  * strand, keyed `peerStrandKey(siblingControlPeerId, strandId)` → epoch ms; a
341
375
  * missing key is due. Bounds {@link refreshStrandPeerAddrs}'s fan-out per
342
376
  * (sibling, strand) rather than per strand, so a sibling that connects after a
343
- * pass is asked on the next 15 s tick instead of waiting out a stamp another
344
- * sibling's answer set. An answer (even an empty one) makes the sibling due again
345
- * in {@link STRAND_PEER_ADDR_REFRESH_MS}; no answer, in
346
- * {@link STRAND_PEER_ADDR_RETRY_MS}. Keys whose strand stopped running or whose
347
- * sibling is no longer connected are pruned every pass, so a resumed strand and a
348
- * reconnected sibling are both asked at once.
377
+ * pass is asked on the pass its connection starts instead of waiting out a stamp
378
+ * another sibling's answer set. An answer (even an empty one) makes the sibling
379
+ * due again in {@link STRAND_PEER_ADDR_REFRESH_MS}; no answer, in
380
+ * {@link STRAND_PEER_ADDR_RETRY_MS}, and each pass arms its own wake for the
381
+ * earliest due time it leaves behind (`nextReconcileWakeMs`). Keys whose strand
382
+ * stopped running or whose sibling is no longer connected are pruned every pass,
383
+ * so a resumed strand and a reconnected sibling are both asked at once.
349
384
  */
350
385
  private readonly strandAddrAskDueAt;
351
386
  /**
@@ -427,6 +462,18 @@ export declare class CadreNode implements SAppIdLookup {
427
462
  * `unpublishStrand` / `forgetJoinedStrand` as {@link formationStrandAddrs} is.
428
463
  */
429
464
  private readonly pendingMembershipInvites;
465
+ /**
466
+ * The party key {@link adoptFormationMembershipInvite} just seated or read, by strand id,
467
+ * so the launch that follows a join does not read the `StrandPartyKey` row again. Taken
468
+ * (deleted) by the {@link resolveStrandPartyKey} that uses it, so any later launch reads
469
+ * the row; dropped by {@link detachStrand}. The joiner's counterpart of the founder's
470
+ * `StrandConfig.partyMemberPrivateKey`, without the app carrying the secret.
471
+ *
472
+ * NOTE: an entry is not dropped when the row is removed, because no `CadreNode` path removes
473
+ * it; an embedder calling `getControlDatabase().deleteStrandPartyKey` between a join and its
474
+ * launch would get the deleted key. If a node-level rotation is added, drop the entry there.
475
+ */
476
+ private readonly formedPartyKeys;
430
477
  /**
431
478
  * Invite keys of the membership invitations this process has staged, by
432
479
  * {@link adoptFormationMembershipInvite} or from a `JoinSuccess` row
@@ -460,7 +507,7 @@ export declare class CadreNode implements SAppIdLookup {
460
507
  /**
461
508
  * Dial targets learned out of band, keyed by peer id, valued by the multiaddr
462
509
  * strings this node was handed (parsed lazily, at dial time). Two writers:
463
- * {@link recordSeedBootstrapPeers} retains the owner-flagged peers of every seed
510
+ * {@link recordSeedBootstrapPeers} retains the preferred peers of every seed
464
511
  * this node applies, and {@link addDrone} retains the addresses of every node
465
512
  * this node adds. Two readers: {@link dialColdStartBootstrap} dials every entry
466
513
  * while the control database still has no siblings, and
@@ -520,7 +567,7 @@ export declare class CadreNode implements SAppIdLookup {
520
567
  private joinedStrandStore;
521
568
  /**
522
569
  * This session's joined strands: {@link joinedStrandStore} and the party-wide
523
- * `JoinedStrand` table, unioned with the control rows each watcher poll, published and
570
+ * `JoinedStrand` table, unioned with the control rows each watcher run, published and
524
571
  * removed by the reconcile pass. Per SESSION, unlike the store — rebuilt at every
525
572
  * {@link start}, which is what ends a revoked join's "keep offering until this session
526
573
  * ends" (see `JoinedStrandSession.forgetAfterThisSession`).
@@ -551,17 +598,41 @@ export declare class CadreNode implements SAppIdLookup {
551
598
  /** Listener that re-publishes the self record when reachable addresses change. */
552
599
  private selfPeerUpdateHandler;
553
600
  /**
554
- * Recurring proactive control-cohort dial cadence (see
555
- * {@link reconcileControlCohort}). Wired alongside {@link recordRefreshTimer}
556
- * in {@link startRecordRefresh} and torn down symmetrically in
557
- * {@link stopRecordRefresh}; `.unref()`'d so it never keeps the loop alive.
558
- */
559
- private controlCohortReconcileTimer;
560
- /**
561
- * Single-flight guard for {@link reconcileControlCohort}. The eager start pass,
562
- * the recurring interval, and the `self:peer:update` trigger can fire close
563
- * together; collapsing concurrent passes into one in-flight run prevents two
564
- * passes from double-dialing the same siblings (mirrors {@link registerSelfInFlight}).
601
+ * The reconcile pass's schedule (see {@link reconcileControlCohort}): single-flight
602
+ * runs on `table-change-watch.ts`'s rule, with no table subscription of its own — the
603
+ * gate watch ({@link membershipGateWatch}), the control connection listeners,
604
+ * `self:peer:update`, and the pass's own wake ({@link armReconcileWake}) `trigger` it,
605
+ * and its backstop (`network.controlCohort.reconcileMs`, default
606
+ * `DEFAULT_CONTROL_COHORT_RECONCILE_MS`) runs it when nothing has. Built alongside
607
+ * {@link recordRefreshTimer} in {@link startRecordRefresh}, about 1 s after start, and
608
+ * disposed in {@link stopRecordRefresh}; its timers never keep the loop alive.
609
+ */
610
+ private controlReconcileRuns;
611
+ /**
612
+ * The subscription on the membership tables (`ControlDatabase.watchMembershipTables`) that
613
+ * re-materializes the membership gate and triggers {@link controlReconcileRuns} on every
614
+ * change ({@link onMembershipGateTablesChanged}).
615
+ * Taken in {@link start} right after the membership listener is wired, disposed in
616
+ * {@link cleanup} before the control database closes.
617
+ */
618
+ private membershipGateWatch;
619
+ /** The second membership-view drop after a sibling connects ({@link armMembershipViewReload}); cleared in {@link stopRecordRefresh}. */
620
+ private membershipViewReloadTimer;
621
+ /**
622
+ * Consecutive passes whose dial of a selected sibling or a cold-start bootstrap peer
623
+ * failed (`ControlCohortPassOutcome.unconnected`), for the redial ladder
624
+ * ({@link armReconcileWake}); 0 after a pass none of whose dials did. Per session:
625
+ * reset in {@link stopRecordRefresh}.
626
+ */
627
+ private reconcileRedialStreak;
628
+ /** Peers a {@link redialClaimedNode} loop is running for, so a repeated claim starts no second one. */
629
+ private readonly claimRedialPeers;
630
+ /**
631
+ * Single-flight guard for {@link reconcileControlCohort}. An explicit call, the
632
+ * `self:peer:update` trigger and a scheduled run can fire close together; collapsing
633
+ * concurrent passes into one in-flight run prevents two passes from double-dialing the
634
+ * same siblings (mirrors {@link registerSelfInFlight}). A scheduled run waits for this
635
+ * pass and then starts its own rather than joining it.
565
636
  */
566
637
  private reconcileControlCohortInFlight;
567
638
  /**
@@ -584,18 +655,26 @@ export declare class CadreNode implements SAppIdLookup {
584
655
  * machine in the party is already discarding this node's address as stale.
585
656
  */
586
657
  private lastSelfRecordPublishAt;
658
+ /**
659
+ * The newest record this process signed for its own `CadrePeer` row: written by either path
660
+ * of {@link publishSelfRecord}, or handed to a member to write by {@link redeemCadreInvitation}.
661
+ * What {@link publishSelfRecordUnlessCurrent} compares the row against. Per session, cleared
662
+ * with {@link lastSelfRecordPublishAt}: a row carried over from an earlier process is never
663
+ * taken as current.
664
+ */
665
+ private lastSignedSelfRecord;
587
666
  /** Say-once latch for {@link escalateIfSelfRecordStale}; re-armed by the next publish. */
588
667
  private selfRecordStaleWarned;
589
668
  /**
590
669
  * Re-replication queue for owner `authorizePeer` writes that committed while
591
- * this node was alone — `controlNode.getConnections().length === 0` at write
592
- * time means the Optimystic commit was local-only (the block's cluster was ≤1)
593
- * and never broadcast. Maps the affected subject peerId → 'authorize', to
670
+ * this node was alone ({@link committedAlone}) — the Optimystic commit was
671
+ * local-only (the block's cohort was this node) and never broadcast. Maps the
672
+ * affected subject peerId → 'authorize', to
594
673
  * re-issue once the cohort grows. Removals are NOT tracked here — a delete
595
674
  * leaves a `Revocation` tombstone, tracked by {@link pendingRevocations}; a
596
675
  * removePeer also clears any queued authorize for the same subject (the row is
597
- * gone, so re-issuing the insert would be wrong). Drained on the 0→≥1
598
- * control-connection transition by {@link drainPendingControlReplication}.
676
+ * gone, so re-issuing the insert would be wrong). Drained when a sibling becomes
677
+ * reachable ({@link updateControlSiblingReachability}) by {@link drainPendingControlReplication}.
599
678
  */
600
679
  private pendingPeerWrites;
601
680
  /**
@@ -619,14 +698,6 @@ export declare class CadreNode implements SAppIdLookup {
619
698
  * exactly a "removals from before this lifetime" case).
620
699
  */
621
700
  private reissuedHeldRevocations;
622
- /**
623
- * Whether this process has seen the singleton `Revocation` ledger marker filed — its own
624
- * `'opened'` or an `'already-open'` answer, in {@link openRevocationLedgerIfDue}. A
625
- * record of work done, not a cached authorization answer: the marker can never be
626
- * deleted (`NoDelete`), so this can only go stale if the database itself is replaced,
627
- * which is why stop() clears it with the other per-process replication state.
628
- */
629
- private revocationLedgerOpened;
630
701
  /** This node's own `CadrePeer` self-write committed local-only (re-touched on growth). */
631
702
  private pendingSelfPeerWrite;
632
703
  /** This node's own `DeviceToken` self-write committed local-only (re-touched on growth). */
@@ -641,14 +712,24 @@ export declare class CadreNode implements SAppIdLookup {
641
712
  /** Single-flight guard for the re-replication drain (mirrors {@link registerSelfInFlight}). */
642
713
  private drainControlReplicationInFlight;
643
714
  /**
644
- * Tracks the control-connection presence edge so the drain fires only on the
645
- * 0→≥1 transition (the earliest point a re-issue can broadcast), not on every
646
- * subsequent `connection:open`. Re-armed to false once connections return to 0.
715
+ * Connected control peers whose identify says they serve the control database
716
+ * ({@link onControlPeerIdentified}): the peers Optimystic can count into a control block's
717
+ * cohort, before its membership check. A peer joins on its first `peer:identify` of a
718
+ * connected period, not on `connection:open`: until it identifies, every control block's
719
+ * cohort is this node alone. It leaves when its last connection closes. Read through
720
+ * {@link hasReachableControlSibling}, which applies the membership check.
647
721
  */
648
- private hasControlConnection;
649
- /** `connection:open` listener driving the growth-triggered drain (teardown in {@link stopRecordRefresh}). */
650
- private controlConnectionOpenHandler;
651
- /** `connection:close` listener re-arming the growth edge (teardown in {@link stopRecordRefresh}). */
722
+ private readonly identifiedControlPeers;
723
+ /**
724
+ * {@link hasReachableControlSibling} at its last evaluation, so the drain fires on the
725
+ * false→true edge only (the earliest point a re-issue reaches the cohort), not on every later
726
+ * identify. Re-armed to false when no reachable sibling remains
727
+ * ({@link updateControlSiblingReachability}).
728
+ */
729
+ private controlSiblingReachable;
730
+ /** `peer:identify` listener: {@link onControlPeerIdentified} (teardown in {@link stopRecordRefresh}). */
731
+ private controlPeerIdentifyHandler;
732
+ /** `connection:close` listener: {@link onControlConnectionClosed} (teardown in {@link stopRecordRefresh}). */
652
733
  private controlConnectionCloseHandler;
653
734
  constructor(config: CadreNodeConfig);
654
735
  /**
@@ -777,7 +858,7 @@ export declare class CadreNode implements SAppIdLookup {
777
858
  /**
778
859
  * Construct (or adopt) the node-local trusted-owner anchor and seed the
779
860
  * out-of-band pinned keys from `config.trustedOwners`. The anchor's BASE comes
780
- * only from out of band — genesis self-trust ({@link initializeSeedBootstrap}),
861
+ * only from out of band — genesis self-trust ({@link useOwnerSigner}),
781
862
  * config pins (here), runtime enrollment pins ({@link trustOwnerKeys}) and a
782
863
  * claim; everything else in it is derived from that base along verifiable
783
864
  * chains through the replicated `OwnerKey` table ({@link syncOwnerAnchor}),
@@ -943,12 +1024,13 @@ export declare class CadreNode implements SAppIdLookup {
943
1024
  getTrustedOwnerStore(): TrustedOwnerStore | null;
944
1025
  /**
945
1026
  * Persist out-of-band-established owner keys into the node-local anchor —
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
1027
+ * the runtime enrollment seam: {@link redeemCadreInvitation} and
1028
+ * {@link redeemOwnerGrant} call it with their bundle's `ownerKeys` BEFORE dialing
1029
+ * a member (so the anchor already holds the pins when the reply and the rows
1030
+ * that follow are judged), and an
949
1031
  * embedder calls it with an operator-supplied pin. Idempotent. ('genesis'
950
1032
  * provenance is reserved for the node's own founding key, seeded internally by
951
- * {@link initializeSeedBootstrap}; 'chain' is written only by {@link syncOwnerAnchor}.)
1033
+ * {@link useOwnerSigner}; 'chain' is written only by {@link syncOwnerAnchor}.)
952
1034
  *
953
1035
  * Validates every key's shape before trusting any of them (all-or-nothing):
954
1036
  * a malformed entry anywhere in `keys` rejects the whole call before a
@@ -973,8 +1055,8 @@ export declare class CadreNode implements SAppIdLookup {
973
1055
  private controlNetworkName;
974
1056
  /**
975
1057
  * Arm the CONTROL network's peer-join block catch-up: push every block in the
976
- * control database's own raw store to each newly connected AUTHORIZED member
977
- * this runtime has not yet caught up. This is what physically replicates
1058
+ * control database's own raw store to each AUTHORIZED member on each
1059
+ * (re)connection, skipping what it is known to hold. This is what physically replicates
978
1060
  * control blocks committed while the writer was alone — the named
979
1061
  * collection-header blocks written once at genesis above all, whose revision
980
1062
  * never moves again, so no later commit ever carries them to a member that
@@ -983,7 +1065,9 @@ export declare class CadreNode implements SAppIdLookup {
983
1065
  * for peers it knew about before the restart).
984
1066
  *
985
1067
  * Unlike the per-strand instances (see `strand-instance-manager.ts`), pushes
986
- * are gated on {@link isAuthorizedMember}, judged at push time: the control
1068
+ * are gated on the authorized-member snapshot ({@link authorizedControlPeers},
1069
+ * which {@link refreshAuthorizedControlPeers} materializes from the same rows and
1070
+ * rule {@link isAuthorizedMember} queries), judged at push time: the control
987
1071
  * network's inbound connection gate deliberately admits non-members in
988
1072
  * several states (seed delivery to an un-enrolled node, an open enrollment
989
1073
  * window, an outstanding invitation, configured bootstrap/relay peers), and
@@ -999,9 +1083,9 @@ export declare class CadreNode implements SAppIdLookup {
999
1083
  * The one non-member the gate admits is a device whose invitation
1000
1084
  * redemption this member is admitting ({@link admitRedeemingDevice}).
1001
1085
  *
1002
- * No-ops (logged) when the embedder configured no control storage or the
1003
- * node exposes no key network — the backfill would have nothing to read or
1004
- * no way to dial.
1086
+ * No-ops (logged) when {@link controlBackfillStorage} answers nothing (disabled,
1087
+ * or no control storage configured) or the node exposes no key network — the
1088
+ * backfill would have nothing to read or no way to dial.
1005
1089
  */
1006
1090
  private startControlBackfill;
1007
1091
  /**
@@ -1041,6 +1125,17 @@ export declare class CadreNode implements SAppIdLookup {
1041
1125
  * operation count.
1042
1126
  */
1043
1127
  private resolveControlStorage;
1128
+ /**
1129
+ * The store the control network's peer-join block catch-up reads, or `undefined` when this
1130
+ * runtime does not arm it: `controlBackfill.enabled: false`, or no control storage. Both of
1131
+ * its readers ask here so they cannot disagree: {@link startControlBackfill} arms on it, and
1132
+ * {@link buildControlNodeOptions} turns Optimystic's rebalance reaction off on it.
1133
+ *
1134
+ * Through the resolver, not the {@link controlStorage} memo field: the resolver is
1135
+ * idempotent, so the catch-up cannot be silently disarmed by a future reordering that moves
1136
+ * a read ahead of the one that populated the field.
1137
+ */
1138
+ private controlBackfillStorage;
1044
1139
  /**
1045
1140
  * Warn the operator when `network.announceAddrs` will silently cost this node the
1046
1141
  * relay reachability it also configured.
@@ -1095,10 +1190,18 @@ export declare class CadreNode implements SAppIdLookup {
1095
1190
  * own strand node, so a NAT'd member's strand node can hold a
1096
1191
  * circuit-relay reservation here (see `delegate-admission.ts`).
1097
1192
  * Connection only: the per-stream gate below never honors a grant;
1098
- * 3. the authorized-member set is empty — cold start: the rows that would
1193
+ * 3. the peer is a device whose invitation or owner-grant redemption this
1194
+ * node has verified and is writing ({@link admittingDevices}): its
1195
+ * connection carries the exchange, which must not be cut at the
1196
+ * provisional deadline. A LIVE OWNER GRANT does not count as expecting a
1197
+ * stranger the way check 7's invitation does: a backup grant has no
1198
+ * expiry, and counting it would admit every stranger outright for the
1199
+ * party's life, so the holder rides the provisional admission until its
1200
+ * redemption verifies, then this;
1201
+ * 4. the authorized-member set is empty — cold start: the rows that would
1099
1202
  * authorize anyone arrive by replication over these very connections;
1100
- * 4. the peer IS an authorized member; or
1101
- * 5. an open invitation is OUTSTANDING — at least one unexpired,
1203
+ * 5. the peer IS an authorized member; or
1204
+ * 6. an open invitation is OUTSTANDING — at least one unexpired,
1102
1205
  * not-fully-consumed invitation this node minted or persisted (see
1103
1206
  * `StrandSolicitationService.hasOutstandingInvitation`). A formation
1104
1207
  * initiator is another party's peer by design and its token is only
@@ -1106,19 +1209,19 @@ export declare class CadreNode implements SAppIdLookup {
1106
1209
  * "does this node expect a stranger at all?". REGISTERING the responder
1107
1210
  * does not count: every node registers one at {@link start}, and
1108
1211
  * registering mints no invitation; or
1109
- * 6. a LIVE cadre invitation exists — a `CadreInvite` row this node holds
1212
+ * 7. a LIVE cadre invitation exists — a `CadreInvite` row this node holds
1110
1213
  * that is not withdrawn, unexpired, has uses left and whose issuer is
1111
1214
  * still an owner (`ControlDatabase.hasLiveCadreInvite`). The device that
1112
1215
  * redeems it is a stranger until the redemption writes its row, and its
1113
1216
  * 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.
1217
+ * same reasoning as check 6, and the same expectation-of-a-stranger key.
1115
1218
  *
1116
1219
  * Ordering is semantically free (the checks are OR'd) but decides who pays:
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.
1220
+ * checks 1-3 are in-memory, 4/5 share one control-DB read, and only a peer
1221
+ * that is otherwise headed for a provisional admission reaches check 6's and
1222
+ * 7's invitation lookups.
1120
1223
  *
1121
- * Caveats of check 5:
1224
+ * Caveats of check 6:
1122
1225
  * - the in-memory mint registry dies with the process, so after a restart
1123
1226
  * only invitations persisted as `FormationInvite` rows still hold the
1124
1227
  * window open (re-mint otherwise);
@@ -1129,21 +1232,21 @@ export declare class CadreNode implements SAppIdLookup {
1129
1232
  * When every check falls through: `'admit-provisionally'`, on every node,
1130
1233
  * relay server or not. The gate sees only a peer id, so it cannot tell an
1131
1234
  * 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
1235
+ * (it redeems here by seating the row from its bundle, after which check 7
1236
+ * admits) or from a sibling whose membership row is still replicating (check 5
1134
1237
  * 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
1238
+ * is closed and the reconcile pass the close starts on either side re-forms it,
1239
+ * since outbound dials are never gated). The protocol guard refuses all of them every
1137
1240
  * members-only protocol meanwhile. A circuit-relay reservation admitted by
1138
1241
  * {@link admitControlRelayReservation} disarms the deadline (see
1139
1242
  * `membership-connection-gater.ts` → "The relay-reservation seam").
1140
1243
  *
1141
- * NOTE: check 3/4 runs a control-DB read per inbound connection
1244
+ * NOTE: check 4/5 runs a control-DB read per inbound connection
1142
1245
  * (`listAuthorizedMembers`); connections are rare and cadres small, and this
1143
1246
  * layer is fail-open behind `ADMISSION_DECISION_TIMEOUT_MS`, so the live
1144
1247
  * read is safe here — unlike the per-stream gate, which must consult the
1145
1248
  * materialized {@link authorizedControlPeers} snapshot instead.
1146
- * NOTE: checks 5 and 6 add two more control reads (`hasOutstandingFormationInvite`,
1249
+ * NOTE: checks 6 and 7 add two more control reads (`hasOutstandingFormationInvite`,
1147
1250
  * `hasLiveCadreInvite`) for a stranger with no locally minted invitation in play, on
1148
1251
  * every node, and a provisional connection's deadline runs the whole decision once
1149
1252
  * more. If stranger connections ever arrive fast enough for those reads — on arrival
@@ -1281,6 +1384,15 @@ export declare class CadreNode implements SAppIdLookup {
1281
1384
  * needs a key-based admission.
1282
1385
  */
1283
1386
  private isControlNetworkMember;
1387
+ /**
1388
+ * Whether this node sends control writes to `remotePeerId`: Optimystic's `isNetworkMember`
1389
+ * for the control node (cohort and coordinator choice), and the membership half of
1390
+ * {@link hasReachableControlSibling}. The stream gate's rule ({@link isControlNetworkMember}),
1391
+ * unlogged, because Optimystic asks several times per peer per cohort assembly and counts
1392
+ * refusals on its own debug line; plus a device whose verified redemption is being written
1393
+ * ({@link admittingDevices}), which must hold the rows that admit it.
1394
+ */
1395
+ private isControlCohortMember;
1284
1396
  /**
1285
1397
  * Per-stream authorization for the protocol guard's `members-snapshot` class
1286
1398
  * (`control-protocol-guard.ts`): the four Optimystic control-DB protocols
@@ -1308,13 +1420,14 @@ export declare class CadreNode implements SAppIdLookup {
1308
1420
  * this listener with its write lock HELD (`notifyMembershipChanged`), so a retrying
1309
1421
  * read here would sleep its backoff holding the lock and stall every other local
1310
1422
  * writer. Nothing is lost — this refresh already keeps the previous snapshot on
1311
- * failure and is re-driven by the next membership write and by the timed reconcile.
1423
+ * failure and is re-driven by the next membership change (local or replicated, through
1424
+ * the gate watch) and by the reconcile pass's backstop.
1312
1425
  *
1313
1426
  * The trusted-owner anchor sync ({@link syncOwnerAnchor}) runs FIRST, so the snapshot
1314
1427
  * 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.
1428
+ * has it. The tombstone list the sync and the two end-of-refresh checks
1429
+ * ({@link hangUpRevokedControlPeers}, then {@link noteOwnRemoval}) read is loaded at most
1430
+ * once per refresh, as is the invitation chain the membership read may need.
1318
1431
  */
1319
1432
  private refreshAuthorizedControlPeers;
1320
1433
  /**
@@ -1370,9 +1483,10 @@ export declare class CadreNode implements SAppIdLookup {
1370
1483
  */
1371
1484
  private getBootstrapPeerIds;
1372
1485
  /**
1373
- * What the strand watcher polls: this party's control rows plus a row per cross-party
1486
+ * What the strand watcher reads: this party's control rows plus a row per cross-party
1374
1487
  * join (see {@link joinedStrands}), so a joined strand is offered and relaunched on
1375
- * the same path as the party's own.
1488
+ * the same path as the party's own. It runs on a change to either table; the
1489
+ * machine-local joins wake it through {@link JoinedStrandSession}'s change callback.
1376
1490
  */
1377
1491
  private createStrandQueryable;
1378
1492
  /**
@@ -1381,30 +1495,40 @@ export declare class CadreNode implements SAppIdLookup {
1381
1495
  */
1382
1496
  private createPartyJoinedStrandLedger;
1383
1497
  /**
1384
- * This machine's owner signing key when it can make owner-gated control writes: its seed
1385
- * bootstrap holds an owner key (`SeedBootstrapService.canAuthorize`) and the party's
1386
- * `OwnerKey` table enrolls it. A phone that ran self-genesis after another machine
1387
- * founded the party passes the first test and not the second (`runOwnerGenesis` in the
1388
- * reference apps), and a write it signed would only be refused.
1498
+ * This machine's owner signer when it can make owner-gated control writes: one is wired
1499
+ * ({@link useOwnerSigner}) and the party's `OwnerKey` table enrolls its key. A phone that
1500
+ * ran self-genesis after another machine founded the party passes the first test and not
1501
+ * the second (`runOwnerGenesis` in the reference apps), and a write it signed would only
1502
+ * be refused.
1389
1503
  */
1390
- private enrolledOwnerSigningKey;
1504
+ private enrolledOwnerSigner;
1391
1505
  /**
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.
1506
+ * Whether this node can act as an owner of its cadre: an owner signer is wired
1507
+ * ({@link useOwnerSigner}) and its key is in the live `OwnerKey` set. What the owner-only
1508
+ * flows (`createCadreInvitation`, `claimNode`, `rentNode`) need; false before `start()`,
1509
+ * and on a machine that joined as a member. The reference apps' "is this device an owner"
1510
+ * check.
1397
1511
  */
1398
1512
  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;
1513
+ /** {@link enrolledOwnerSigner} judged against an owner set the caller has already read. */
1514
+ private ownerSignerEnrolledIn;
1515
+ /**
1516
+ * The owner signer {@link useOwnerSigner} wired, or null: every owner signature this node
1517
+ * makes goes through it. Enrollment is not checked ({@link isOwner} does). A caller that
1518
+ * makes several owner writes as one action opens one `approve` on it and hands the
1519
+ * resulting `sign` to the methods that take one (`publishStrand`, `ensureStrandPartyKey`,
1520
+ * `publishFormationInvite`), as `adoptStrandBundle` does.
1521
+ */
1522
+ getOwnerSigner(): OwnerSigner | null;
1403
1523
  /**
1404
1524
  * Schedule the node's initial self-record publish + ongoing refresh shortly
1405
1525
  * after start (non-blocking). {@link registerSelf} is idempotent and safely
1406
1526
  * no-ops when it cannot yet sign/insert (e.g. owner key not installed),
1407
1527
  * so the timer is harmless even when registration only becomes possible later.
1528
+ * A record this process already put in place that is still current (a founding owner's
1529
+ * explicit publish, a redeemed invitation's admission, a restart's first-growth re-touch) is
1530
+ * not written again ({@link publishSelfRecordUnlessCurrent}). Whether the node is connected
1531
+ * yet does not matter: a device that redeems within a second of starting is.
1408
1532
  */
1409
1533
  private scheduleSelfRegistration;
1410
1534
  /**
@@ -1436,11 +1560,42 @@ export declare class CadreNode implements SAppIdLookup {
1436
1560
  /** The body of {@link registerSelf}; serialised by its single-flight guard. */
1437
1561
  private publishSelfRecord;
1438
1562
  /**
1439
- * Record that this node's own address record just landed, and re-arm the stale-record
1440
- * warning. Called from BOTH publishing paths of {@link publishSelfRecord} and nowhere
1441
- * else — a `skipped` publish wrote nothing, so it must not refresh the stamp.
1563
+ * Bookkeeping for a committed write of this node's own `CadrePeer` row by {@link publishSelfRecord}:
1564
+ * `record` is the one the row holds (for the update path, the one its body signed on the
1565
+ * attempt that committed, not the first it signed). Queued for the drain's re-touch
1566
+ * ({@link pendingSelfPeerWrite}) when no sibling was reachable when the write started or when
1567
+ * it ended: a sibling that identifies while the write is out may do so after the write's cohort
1568
+ * was chosen, and the first-growth drain skips a row holding {@link lastSignedSelfRecord} unless
1569
+ * this flag is set. Both fields are set here together so no write can leave the record without
1570
+ * the flag.
1571
+ */
1572
+ private noteSelfPeerWritten;
1573
+ /**
1574
+ * Record that this node's own address record is in place as of `at`, and re-arm the
1575
+ * stale-record warning. Called for BOTH publishing paths of {@link publishSelfRecord}
1576
+ * ({@link noteSelfPeerWritten}), and by {@link publishSelfRecordUnlessCurrent} with the stamp
1577
+ * of a record already in place — a `skipped` publish wrote nothing and found nothing, so it
1578
+ * must not refresh the stamp.
1442
1579
  */
1443
1580
  private noteSelfRecordPublished;
1581
+ /**
1582
+ * {@link registerSelf}, unless the own row already holds the record this process last signed
1583
+ * ({@link lastSignedSelfRecord}) and that record is current ({@link isCurrentSelfRecord});
1584
+ * then nothing is written and the record counts as published at its own stamp. For a publish
1585
+ * that puts a record in place (the boot timer, the reconcile pass's first publish, the
1586
+ * first-growth drain), never the heartbeat, which exists to re-stamp freshness. `'current'`
1587
+ * is this method's own answer; {@link registerSelf} always writes.
1588
+ */
1589
+ private publishSelfRecordUnlessCurrent;
1590
+ /**
1591
+ * Is `record` still the one to publish: signed less than half a heartbeat ago, with this
1592
+ * node's profile, over the addresses it would publish now, and the signature the own row
1593
+ * holds? The age bound keeps a record that is not re-written fresh for resolvers: at most half
1594
+ * a heartbeat old here and re-stamped by the heartbeat within one more, so never older than
1595
+ * 1.5 heartbeats (11.25 min) against {@link DEFAULT_PEER_RECORD_MAX_AGE_MS}. Cheap checks
1596
+ * first: the row comes from the membership view, the addresses from the app's resolver.
1597
+ */
1598
+ private isCurrentSelfRecord;
1444
1599
  /**
1445
1600
  * Report an abandoned control write to the embedding app.
1446
1601
  *
@@ -1501,41 +1656,15 @@ export declare class CadreNode implements SAppIdLookup {
1501
1656
  */
1502
1657
  private getSelfSigningKey;
1503
1658
  /**
1504
- * Does the `Strand` row name THIS machine as its founder — i.e. is its
1505
- * `FounderOwnerKey` this node's own owner key (the key behind its PeerId)?
1506
- * A null/absent column (a consent-seated strand, or a hand-built row) and a
1507
- * node with no owner key both derive `false`: without a positive match this
1508
- * machine must attach, never bootstrap. Pure key derivation, no I/O — cheap
1509
- * enough for {@link launchStrand}'s tracked-instance early return.
1510
- *
1511
- * NOTE: reads "one owner key per machine" — the reference model, where the owner
1512
- * key IS the key behind the PeerId. Two machines running the SAME identity key
1513
- * would both derive `true` and each bootstrap on its own replica, which is the
1514
- * double-`Header` hazard this derivation exists to avoid. Unreachable today: that
1515
- * configuration also gives both machines one PeerId, which already breaks control
1516
- * networking well before any strand launches. Revisit if machines ever share an
1517
- * owner key while holding distinct transport identities — the derivation would
1518
- * then need a per-machine discriminator (the `CadrePeer` PeerId) on the row.
1659
+ * Does the `Strand` row name THIS machine as its founder — is its `FounderPeerId` this
1660
+ * node's peer id? A null column (a consent-seated strand, or a hand-built row) derives
1661
+ * `false`: without a positive match this machine must attach, never bootstrap. A peer id
1662
+ * rather than an owner key, so two machines holding one owner key never both bootstrap
1663
+ * (the double-`Header` hazard), and a node derives it before any owner signer is wired.
1664
+ * Pure comparison, no I/O — cheap enough for {@link launchStrand}'s tracked-instance
1665
+ * early return.
1519
1666
  */
1520
1667
  private isSelfFoundedRow;
1521
- /**
1522
- * The owner keypair (base64url Ed25519) derived from this node's resolved
1523
- * identity key. In the single-key reference model the owner signing key is
1524
- * *derived from* the node identity (see {@link ed25519KeyPairFromLibp2p}), so the
1525
- * same key material protected in a secure enclave backs both.
1526
- *
1527
- * Exposed so the hosting app retains control of owner genesis: cadre-core
1528
- * resolves + protects the identity, then the app sources this pair to drive
1529
- * `ensureOwnerKey(pub)` + `initializeSeedBootstrap(priv)` itself — cadre-core
1530
- * never silently runs genesis. A future separate-owner slot would return a
1531
- * distinct key here instead of the identity-derived one.
1532
- *
1533
- * @returns The base64url seed/public-key owner pair.
1534
- * @throws If called before {@link start} has resolved the identity, or when the
1535
- * node runs on an ephemeral libp2p key (no `keyStore`/`privateKey` configured),
1536
- * since that key is internal to libp2p and not exposed.
1537
- */
1538
- getIdentityOwnerKey(): Ed25519KeyPair;
1539
1668
  /**
1540
1669
  * Publish this node's own address record from the reconcile pass until its first publish of
1541
1670
  * this session lands. A node with no owner key — seeded (`addDrone`, `cadre enroll add`),
@@ -1553,12 +1682,17 @@ export declare class CadreNode implements SAppIdLookup {
1553
1682
  * process does not learn it was removed ({@link noteOwnRemoval}), and pays that read on every
1554
1683
  * pass.
1555
1684
  *
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.
1685
+ * Only while a sibling is reachable ({@link hasReachableControlSibling}): a row written
1686
+ * elsewhere arrives from a sibling, and an owner's publish committed alone is local-only and
1687
+ * forks its own history — a restarted owner whose embedder runs a pass before it reconnects
1688
+ * would write its row on the stale side of the fork, and then never see rows written while it
1689
+ * was down (`cadre-invite-any-member` runs that pass and fails every time without this
1690
+ * condition). A connection is not enough: until the sibling identifies, the write's cohort is
1691
+ * this node alone. The boot-time publish and the first-growth drain already try at the
1692
+ * node's lone and first-sibling moments; this retries on the passes after.
1693
+ *
1694
+ * A machine admitted by invitation finds its row already holding the record its redemption
1695
+ * carried, and writes nothing ({@link publishSelfRecordUnlessCurrent}).
1562
1696
  */
1563
1697
  private publishFirstSelfRecord;
1564
1698
  /**
@@ -1575,7 +1709,21 @@ export declare class CadreNode implements SAppIdLookup {
1575
1709
  * stack listeners/timers.
1576
1710
  */
1577
1711
  private startRecordRefresh;
1578
- /** Tear down the self-record refresh timers + listener (see {@link cleanup}). */
1712
+ /** The reconcile schedule's backstop: `network.controlCohort.reconcileMs`, default `DEFAULT_CONTROL_COHORT_RECONCILE_MS`. */
1713
+ private controlReconcileBackstopMs;
1714
+ /**
1715
+ * The gate watch's run ({@link membershipGateWatch}): re-materialize the gate, so a
1716
+ * replicated revocation hangs up the removed machine now (`hangUpRevokedControlPeers`
1717
+ * ends every refresh), then start a reconcile pass for whatever else the change means
1718
+ * (a new sibling to dial, a tombstoned row to reap). A local membership write refreshes
1719
+ * the gate twice — once from the awaited membership listener, so the writer sees its
1720
+ * change before returning, and once here, 50 ms later. The listener's refresh runs under
1721
+ * the write lock, so its reads take the committed-read path and the view keeps nothing
1722
+ * from them; this refresh is the read that refills the view after the write, which some
1723
+ * later caller would otherwise pay. Accepted: one reload per change, not two.
1724
+ */
1725
+ private onMembershipGateTablesChanged;
1726
+ /** Tear down the self-record refresh timers, the reconcile schedule and the listeners (see {@link cleanup}). */
1579
1727
  private stopRecordRefresh;
1580
1728
  /**
1581
1729
  * Resolve a peer's current, signed, trust-checkable multiaddrs from only its
@@ -1649,37 +1797,49 @@ export declare class CadreNode implements SAppIdLookup {
1649
1797
  * Run one proactive control-cohort dial pass to keep this node connected to its
1650
1798
  * cadre siblings (so the `CadreControl` collections form a replicating cohort).
1651
1799
  * Public so the cohort-growth-driven re-replication path
1652
- * (`control-write-ensure-replicated`) and tests can trigger a pass on demand;
1653
- * normally driven by the eager start pass, the recurring interval, and
1654
- * `self:peer:update` (all wired in {@link startRecordRefresh}).
1800
+ * (`control-write-ensure-replicated`), `addDrone`'s callers, `claimNode`,
1801
+ * `redeemCadreInvitation` and tests can run a pass on demand. On its own a pass
1802
+ * runs when something it acts on changes, and otherwise at a backstop
1803
+ * ({@link controlReconcileRuns}): once after start; on `self:peer:update`; when a
1804
+ * membership table changes, local commit or replicated ({@link membershipGateWatch});
1805
+ * when a control connection to an authorized sibling opens or closes
1806
+ * ({@link onControlSiblingConnectionChange}); on the redial and strand-address wakes
1807
+ * a pass arms for itself ({@link armReconcileWake}); and at the backstop,
1808
+ * `network.controlCohort.reconcileMs` (default `DEFAULT_CONTROL_COHORT_RECONCILE_MS`,
1809
+ * 2 minutes) after the last pass settled. A connection to a relay, a bootstrap node or
1810
+ * another party's machine starts no pass: the pass acts on none of them, beyond
1811
+ * counting them as "connected" for its write steps, which therefore run at the next
1812
+ * membership change or backstop on a one-machine party whose only connection is a
1813
+ * relay.
1655
1814
  *
1656
1815
  * Concurrent triggers collapse into a single in-flight pass
1657
1816
  * (see {@link reconcileControlCohortInFlight}) so two passes never double-dial;
1658
- * a call that joins an in-flight pass resolves to THAT pass's result.
1817
+ * a call that joins an in-flight pass resolves to THAT pass's result (a scheduled
1818
+ * run waits for it instead, so the change that woke the run is seen).
1659
1819
  * Best-effort throughout: a failure to resolve/dial any one sibling is logged
1660
1820
  * and the pass continues; the whole pass is a no-op when the node is alone.
1661
1821
  *
1662
1822
  * Resolves to the peers the pass dialled ({@link ControlCohortReconcileResult}),
1663
1823
  * so a caller can tell a link this pass opened from one something else opened —
1664
- * the pass skips a peer that is already connected. The timer and event triggers
1665
- * discard it.
1824
+ * the pass skips a peer that is already connected. The scheduled runs discard it.
1666
1825
  */
1667
1826
  reconcileControlCohort(): Promise<ControlCohortReconcileResult>;
1827
+ /** One pass, then the wake it earns ({@link armReconcileWake}); the public result is the pass's `dialed`. */
1828
+ private runReconcileControlCohortAndArmWake;
1829
+ /**
1830
+ * After every pass, explicit or scheduled: advance or reset the redial streak and
1831
+ * `trigger` the schedule for the earliest of the pass's own reasons to run again
1832
+ * (`nextReconcileWakeMs`): a redial for a sibling or bootstrap peer it left
1833
+ * unconnected, or a strand-address ask falling due before the backstop. Nothing else
1834
+ * the pass does needs a wake sooner than the backstop: delegate grants re-announce
1835
+ * every 15 minutes inside a 30-minute TTL, the FRET and formation address re-merges
1836
+ * guard a one-hour expiry, and the self record has its own heartbeat. A pass that
1837
+ * runs before the schedule exists (an explicit call during the first second after
1838
+ * start) still counts for the streak.
1839
+ */
1840
+ private armReconcileWake;
1668
1841
  /** Body of {@link reconcileControlCohort}; serialised by its single-flight guard. */
1669
1842
  private runReconcileControlCohort;
1670
- /**
1671
- * The owner-only half of the reconcile pass's ledger-marker step: file the singleton
1672
- * `Revocation` marker ({@link SeedBootstrapService.openRevocationLedger}) unless this
1673
- * process has already seen it filed. The connectivity gate is the caller's
1674
- * ({@link runReconcileControlCohort}); why the marker exists is on
1675
- * {@link ControlDatabase.openRevocationLedger}.
1676
- *
1677
- * Sets {@link revocationLedgerOpened} on `'opened'` or `'already-open'`. A node that
1678
- * cannot sign as an owner does nothing: it picks the owner's marker up the first time it
1679
- * reads the table, like any other replicated row. Best-effort like every step of the
1680
- * pass — a failure is logged and leaves the flag clear, so the next connected pass retries.
1681
- */
1682
- private openRevocationLedgerIfDue;
1683
1843
  /**
1684
1844
  * Resolve every sibling's signed address record once and merge what resolves
1685
1845
  * into the control node's libp2p **address book** (peerStore). Returns the
@@ -1703,8 +1863,9 @@ export declare class CadreNode implements SAppIdLookup {
1703
1863
  * current ({@link refreshDialHint}).
1704
1864
  *
1705
1865
  * NOTE: this resolves EVERY sibling serially before the dial loop below runs,
1706
- * so it costs one record query per sibling per reconcile pass (~15s) and each
1707
- * one delays the pass's first dial. A cadre is a handful of devices, so today
1866
+ * so it costs one record query per sibling per reconcile pass (served from the
1867
+ * membership rows view while it is current) and each one delays the pass's first
1868
+ * dial. A cadre is a handful of devices, so today
1708
1869
  * that is a few extra local reads; if cadres ever grow large, batch the records
1709
1870
  * into one query, merge only on change, or move the warm pass after the dials.
1710
1871
  */
@@ -1766,16 +1927,18 @@ export declare class CadreNode implements SAppIdLookup {
1766
1927
  */
1767
1928
  private collectSiblingStrandAddrs;
1768
1929
  /**
1769
- * Dial one sibling from its already-resolved addresses, best-effort. Returns
1770
- * whether the dial resolved (false when no address resolves or the dial fails).
1930
+ * Dial one sibling from its already-resolved addresses, best-effort.
1771
1931
  *
1772
1932
  * A per-peer failure (NAT, offline, relay down, connection-gater denial, or
1773
1933
  * the {@link controlDialBudget} expiring) is logged and swallowed so one
1774
1934
  * unreachable sibling never aborts the pass — exactly like
1775
1935
  * {@link SeedBootstrapService.applySeed}'s owner-dial loop. A failed dial is
1776
- * simply retried on the next pass. A sibling whose every address relays
1777
- * through this node ({@link SelfRelayOnlyError}) is not dialed at all and gets
1778
- * a one-line log instead: only the sibling can reconnect.
1936
+ * retried on the pass's redial ladder ({@link armReconcileWake}: 15 s, 30 s, 60 s,
1937
+ * then every backstop) and on any pass a change starts sooner. A sibling with no
1938
+ * dialable address, or whose every address relays through this node
1939
+ * ({@link SelfRelayOnlyError}), is not dialed at all and is `undialable`: it gets a
1940
+ * one-line log and no redial, since only a change — its record replicating in, or
1941
+ * the sibling connecting — can make it dialable, and each of those starts a pass.
1779
1942
  */
1780
1943
  private dialControlSibling;
1781
1944
  /**
@@ -1819,7 +1982,7 @@ export declare class CadreNode implements SAppIdLookup {
1819
1982
  * These addresses are never merged into the libp2p address book: they are
1820
1983
  * unverified, and the address book takes only verified ones (see
1821
1984
  * `mergePeerAddrs`). Layers that dial by bare peer id use the connection this
1822
- * dial opens, and after it drops the next reconcile pass dials again.
1985
+ * dial opens, and after it drops the reconcile pass the close starts dials again.
1823
1986
  *
1824
1987
  * NOTE: an entry is consulted here only for a current sibling, so a peer revoked
1825
1988
  * by another owner is not dialed from it, even though the entry stays (only this
@@ -1847,22 +2010,22 @@ export declare class CadreNode implements SAppIdLookup {
1847
2010
  */
1848
2011
  private peerStoreAddrs;
1849
2012
  /**
1850
- * Retain a just-applied seed's owner-flagged peers as cold-start bootstrap
1851
- * dial targets (see {@link bootstrapPeerStore}).
2013
+ * Retain a just-applied seed's preferred peers (`SeedPeer.preferred`) as cold-start
2014
+ * bootstrap dial targets (see {@link bootstrapPeerStore}).
1852
2015
  *
1853
2016
  * Called for every seed this node accepts, on BOTH intake paths — the
1854
2017
  * {@link applySeed} wrapper (which may run on a throwaway service, so the
1855
2018
  * service itself is the wrong place to keep this) and the inbound
1856
- * `/sereus/seed/1.0.0` handler via `onSeedApplied`. Peers with no address are
1857
- * skipped: there is nothing to dial.
2019
+ * `/sereus/seed/1.0.0` handler via `onSeedApplied`.
1858
2020
  *
1859
- * `isOwner` is the seed's own claim about its peers, exactly as
2021
+ * `preferred` is the seed's own claim about its peers, exactly as
1860
2022
  * `SeedBootstrapService.applySeed` uses it to choose its dial targets. That is
1861
2023
  * sound here for the same reason it is sound there — the whole seed is
1862
2024
  * signature-checked against a trust-anchored signer before this runs, and the
1863
2025
  * flag only *selects a dial target*; a dial grants no authority.
1864
2026
  *
1865
- * Addressless peers and self are skipped by {@link retainDialTarget}.
2027
+ * The peers retained are `seedDialTargets` (addressless peers and self dropped), the same
2028
+ * ones the service dials; they are returned for the `seed:applied` payload.
1866
2029
  */
1867
2030
  private recordSeedBootstrapPeers;
1868
2031
  /**
@@ -1877,34 +2040,35 @@ export declare class CadreNode implements SAppIdLookup {
1877
2040
  * failing a seed or an add that has already been accepted.
1878
2041
  *
1879
2042
  * Two peers are never retained. One with no address: there is nothing to dial.
1880
- * And self: `createSeed` projects EVERY `CadrePeer` row, so an owner that applies
1881
- * a seed minted after it joined finds itself in the owner list, and `addDrone`
1882
- * could be handed this node's own id. Retaining self would make the cold-start
2043
+ * And self: `addDrone` could be handed this node's own id (seed intake arrives
2044
+ * already filtered by `seedDialTargets`). Retaining self would make the cold-start
1883
2045
  * pass dial this node forever (the steady-state pass filters self out of its
1884
2046
  * sibling list for the same reason).
1885
2047
  */
1886
2048
  private retainDialTarget;
1887
2049
  /**
1888
2050
  * Cold-start branch of the reconcile pass: re-dial every retained out-of-band
1889
- * dial target (see {@link bootstrapPeerStore}) — in practice the owner peers of
1890
- * the seeds this node applied — while the control database still has no
2051
+ * dial target (see {@link bootstrapPeerStore}) — in practice the preferred peers
2052
+ * of the seeds this node applied — while the control database still has no
1891
2053
  * siblings to dial.
1892
2054
  *
1893
- * `SeedBootstrapService.applySeed` dials those owners exactly ONCE, best-effort.
1894
- * When that single dial fails — owner momentarily down, relay reservation not
2055
+ * `SeedBootstrapService.applySeed` dials those peers exactly ONCE, best-effort.
2056
+ * When that single dial fails — peer momentarily down, relay reservation not
1895
2057
  * up yet, NAT traversal lost the race — the joining node has an empty
1896
2058
  * `CadrePeer` table and no connection, and the steady-state pass above cannot
1897
2059
  * help: it dials only siblings enumerated from that very table. Retrying here
1898
2060
  * is the only way back in.
1899
2061
  *
1900
- * Unbounded, at the reconcile cadence, with no backoff: the branch is already
1901
- * gated by "the control database has no siblings", so it stops the moment the
1902
- * node is actually in the party, and a node that is stranded MUST keep trying —
1903
- * a give-up rule would turn a transient outage into a permanent one. The steady
1904
- * cost is one dial per bootstrap peer per pass (seeds nominate one or a few
1905
- * owners), each failing fast against an unreachable address.
1906
- * NOTE: if seeds ever carry many owner peers, or the reconcile cadence tightens
1907
- * well below its 15 s default, add per-peer backoff here.
2062
+ * Unbounded: the branch is already gated by "the control database has no
2063
+ * siblings", so it stops the moment the node is actually in the party, and a node
2064
+ * that is stranded MUST keep trying — a give-up rule would turn a transient outage
2065
+ * into a permanent one. A failed dial earns the pass a redial wake on the ladder
2066
+ * {@link armReconcileWake} runs (15 s, 30 s, 60 s, then every backstop, 2 minutes
2067
+ * by default), so the steady cost is one dial per bootstrap peer per backstop (a
2068
+ * seed prefers its minter and the party's always-on machines, a few at most), each
2069
+ * failing fast against an unreachable address.
2070
+ * NOTE: the ladder is per pass, not per peer; if seeds ever carry many preferred
2071
+ * peers, add per-peer backoff here.
1908
2072
  *
1909
2073
  * Best-effort per peer, exactly like {@link dialControlSibling}: one dead
1910
2074
  * address never aborts the pass.
@@ -1923,20 +2087,26 @@ export declare class CadreNode implements SAppIdLookup {
1923
2087
  * encapsulated; an unencapsulatable one is dropped rather than dialed bare.
1924
2088
  */
1925
2089
  private bootstrapDialAddrs;
1926
- /** Dial one cold-start bootstrap peer, best-effort. Returns whether it connected. */
2090
+ /**
2091
+ * Dial one peer from its retained dial target's addresses, best-effort: a cold-start bootstrap
2092
+ * peer, or a just-claimed node ({@link redialClaimedNode}), which `context` names in the logs.
2093
+ * Returns whether it connected.
2094
+ */
1927
2095
  private dialBootstrapPeer;
1928
2096
  /**
1929
- * Whether a control write happening now would commit local-only. A sound lower
1930
- * bound is "no connected control peers": 0 connections ⇒ the block's cluster is
1931
- * ≤1 ⇒ Optimystic commits without broadcasting. This is the pragmatic proxy for
1932
- * the precise signal (the block's `getClusterSize`); it over-approximates safely
1933
- * — a connected-but-not-in-this-block's-cluster write may still be local-only and
1934
- * is caught by the cohort's periodic pull-on-read instead. Re-issuing an already-
1935
- * replicated row is harmless (an idempotent monotonic bump), so the coarse proxy
1936
- * is acceptable as the agreed first cut.
2097
+ * Whether a control write happening now would commit local-only: no sibling is reachable
2098
+ * ({@link hasReachableControlSibling}, the public form, so the two cannot drift). A relay, a
2099
+ * bootstrap node or a peer that has not identified yet holds a connection without being in the
2100
+ * write's cohort, so a write made with only those connected is queued too. One made alone at
2101
+ * the revision the cohort reached meanwhile is a fork no later consult sees, because a consult
2102
+ * compares revision numbers only; the re-issue on the growth edge is what conflicts with the
2103
+ * cohort's copy and pulls it in.
1937
2104
  *
1938
- * Reads {@link getControlConnectionCount} — the public, embedder-facing form of
1939
- * the same sample — so the two cannot drift.
2105
+ * Still a proxy for the precise signal (the block's own cohort): a reachable sibling outside
2106
+ * this block's cohort leaves the write local-only, caught by the cohort's pull-on-read. On the
2107
+ * control network every block's cohort is the whole party, so that gap is a party larger than
2108
+ * `CONTROL_REPLICATION_BREADTH`. Re-issuing a replicated row is harmless (an idempotent
2109
+ * monotonic bump).
1940
2110
  */
1941
2111
  private committedAlone;
1942
2112
  /**
@@ -1966,7 +2136,7 @@ export declare class CadreNode implements SAppIdLookup {
1966
2136
  /**
1967
2137
  * Drain the write-while-alone re-replication queue: re-issue the writes that
1968
2138
  * committed local-only now that the cohort can broadcast them. Public so the
1969
- * 0→≥1 growth trigger ({@link handleControlConnectionChange}) and tests can drive
2139
+ * growth trigger ({@link updateControlSiblingReachability}) and tests can drive
1970
2140
  * it; concurrent calls collapse into one in-flight drain
1971
2141
  * ({@link drainControlReplicationInFlight}) so two growth signals never
1972
2142
  * double-issue. Best-effort throughout — a per-row failure leaves that entry
@@ -2035,8 +2205,11 @@ export declare class CadreNode implements SAppIdLookup {
2035
2205
  private reconstructAuthoredMembership;
2036
2206
  /**
2037
2207
  * Body of {@link reconstructAuthoredMembership}'s sweep: re-issue each candidate
2038
- * owner-authored row, returning how many were re-touched. A per-row failure is
2039
- * logged and skipped; a teardown mid-sweep stops it.
2208
+ * owner-authored row, returning how many were re-touched. A row the sweep already holds as
2209
+ * self-published is skipped without a call; the re-touch itself decides again over the row
2210
+ * it reads under the lock, so a row that vanished or was self-published since the sweep's
2211
+ * read is reported, not written. A per-row failure is logged and skipped; a teardown
2212
+ * mid-sweep stops it.
2040
2213
  */
2041
2214
  private reissueAuthoredMembershipRows;
2042
2215
  /**
@@ -2052,9 +2225,10 @@ export declare class CadreNode implements SAppIdLookup {
2052
2225
  */
2053
2226
  private drainPendingPeerWrites;
2054
2227
  /**
2055
- * Body of {@link drainPendingPeerWrites}' loop. An entry is cleared only on
2056
- * success (or when there is nothing left to re-issue); a failure is logged and
2057
- * the entry stays queued for the next cohort growth.
2228
+ * Body of {@link drainPendingPeerWrites}' loop. An entry is cleared on every outcome the
2229
+ * re-touch reports — re-touched, or nothing left to re-issue because the row was removed
2230
+ * since or the peer self-published; the re-touch decides which over the row it reads under
2231
+ * the lock. A failure is logged and the entry stays queued for the next cohort growth.
2058
2232
  */
2059
2233
  private reissuePendingPeerWrites;
2060
2234
  /**
@@ -2062,23 +2236,84 @@ export declare class CadreNode implements SAppIdLookup {
2062
2236
  * `UpdatedAt` strictly above the stored value (and the wall clock) and re-sign via
2063
2237
  * the owner branch of `CadrePeer.AuthorizedUpdate`. The row already exists
2064
2238
  * locally (it committed there, just local-only), so this is an UPDATE, not the
2065
- * original INSERT.
2239
+ * original INSERT. The bump, and whether the row is still the owner's to re-issue, are
2240
+ * decided over the row as read inside the write ({@link SeedBootstrapService.reauthorizePeer}'s
2241
+ * re-broadcast mode).
2066
2242
  */
2067
2243
  private reissuePeerAuthorize;
2068
2244
  /**
2069
- * On a control connection opening, detect the 0→≥1 transition and drain the
2070
- * write-while-alone re-replication queue (single-flight; fires only on the edge,
2071
- * not on every subsequent `connection:open`). Best-effort — a drain failure is
2072
- * logged, not thrown.
2073
- */
2074
- private handleControlConnectionChange;
2075
- /** Re-arm the growth edge once all control connections drop, so a later reconnect re-drains. */
2076
- private handleControlConnectionClose;
2077
- /**
2078
- * Wire the control-connection growth listeners that drive write-while-alone
2079
- * re-replication. Wired in {@link start} (not the delayed refresh path) so no
2080
- * early `connection:open` is missed; torn down in {@link stopRecordRefresh}.
2081
- * Idempotent.
2245
+ * Re-evaluate {@link hasReachableControlSibling} and act on its edges: on false→true, drain
2246
+ * the write-while-alone re-replication queue (single-flight; best-effort, a drain failure is
2247
+ * logged, not thrown); on true→false, re-arm, so the next reachable sibling drains again.
2248
+ * Called when a peer identifies, when a connection closes, and after every membership refresh,
2249
+ * where a sibling that connected before this node authorized it (the production join order)
2250
+ * becomes reachable. A device counted in for its admission ({@link admittingDevices}) is not
2251
+ * a trigger: the admission's own writes are in flight, and its row landing refreshes
2252
+ * membership.
2253
+ */
2254
+ private updateControlSiblingReachability;
2255
+ /**
2256
+ * A connected peer identified as serving the control database: count it
2257
+ * ({@link identifiedControlPeers}), re-evaluate the growth edge, and run the sibling-open
2258
+ * reactions, once per connected period. libp2p re-emits `peer:identify` on every
2259
+ * identify-push from a connected peer, and those repeats start nothing.
2260
+ */
2261
+ private onControlPeerIdentified;
2262
+ /**
2263
+ * A control connection closed: `remotePeer` stops counting once it has none left, and the
2264
+ * growth edge re-arms if no reachable sibling remains. The sibling-close reaction runs on every
2265
+ * close. libp2p's connection manager drops the closed connection from `getConnections` before
2266
+ * this listener runs: libp2p dispatches each event on its internal bus, where the connection
2267
+ * manager listens, before re-dispatching it to the node's own listeners.
2268
+ */
2269
+ private onControlConnectionClosed;
2270
+ /**
2271
+ * Count the peers that identified before {@link wireControlConnectionListeners} ran, whose
2272
+ * `peer:identify` has already fired, from their peer-store records. Updates the growth edge
2273
+ * but runs no sibling-open reaction, as a connection that predates the listeners never did. A
2274
+ * peer not yet in the peer store is still identifying, and its own event counts it.
2275
+ */
2276
+ private noteIdentifiedControlPeers;
2277
+ /**
2278
+ * An AUTHORIZED sibling became reachable (its identify, once per connected period:
2279
+ * {@link onControlPeerIdentified}) or one of its connections closed: start a reconcile
2280
+ * pass. On open the pass asks the new sibling for strand addresses and re-arms the
2281
+ * delegate grants; on close it re-dials. First, on open, drop the membership rows
2282
+ * views: a machine that was offline while a sibling committed learns of the commit
2283
+ * only when it next reads the block from storage, and dropping the views makes the
2284
+ * pass's reads do that now rather than after the views' maximum age. Those reads can
2285
+ * fall inside the read-repair window, so the open also arms a second drop past it
2286
+ * ({@link armMembershipViewReload}). Any other
2287
+ * peer — a relay, a bootstrap node, another party's machine — starts nothing.
2288
+ *
2289
+ * On identify rather than `connection:open` because, until the sibling identifies,
2290
+ * Optimystic's cohort for every control block is this node alone: a pass started at the
2291
+ * open writes (the first self-record publish) and reads on a cohort of one.
2292
+ */
2293
+ private onControlSiblingConnectionChange;
2294
+ /**
2295
+ * Drop the membership views again {@link MEMBERSHIP_VIEW_RECONNECT_RELOAD_MS} after a
2296
+ * sibling connects, and start a pass whose reads reload them. This node's reads before
2297
+ * the connection stamped Optimystic's read-repair window on the control blocks, so the
2298
+ * reload the open itself starts is served from the local copy with no cohort consult,
2299
+ * and the view would keep rows that predate the sibling's commits until its maximum age.
2300
+ * The reload past the window starts the consults; one that brings a newer revision
2301
+ * raises the table change that drops the view and refreshes the gate. Re-armed by every
2302
+ * open, so it runs one window after the latest.
2303
+ *
2304
+ * NOTE: a consult Optimystic defers (16 already in flight) starts nothing and leaves the block
2305
+ * unstamped, so the view keeps its rows until the first read past its maximum age, which on an
2306
+ * idle node is the reconcile backstop. If `cluster-tx:read-repair-deferred` lines show at a
2307
+ * reconnect, re-arm this reload once more instead of waiting for the backstop.
2308
+ */
2309
+ private armMembershipViewReload;
2310
+ /**
2311
+ * Wire the control-connection listeners, which keep {@link identifiedControlPeers}: the
2312
+ * growth edge that drives write-while-alone re-replication, and the sibling reconcile
2313
+ * trigger ({@link onControlSiblingConnectionChange}). Wired in {@link start} (not the
2314
+ * delayed refresh path) so no early identify is missed, and reads back the peers that
2315
+ * identified before it ran ({@link noteIdentifiedControlPeers}); torn down in
2316
+ * {@link stopRecordRefresh}. Idempotent.
2082
2317
  */
2083
2318
  private wireControlConnectionListeners;
2084
2319
  /**
@@ -2322,7 +2557,7 @@ export declare class CadreNode implements SAppIdLookup {
2322
2557
  * `Strand` row. A joiner (the row arrived over the control network) wants exactly this; a
2323
2558
  * FOUNDER wants {@link foundStrand}, which publishes and attaches in one resumable call.
2324
2559
  * With no explicit `founder` flag, founder-ness is derived from the row's
2325
- * `FounderOwnerKey` (see {@link StrandConfig.founder}) — so attaching a row THIS machine
2560
+ * `FounderPeerId` (see {@link StrandConfig.founder}) — so attaching a row THIS machine
2326
2561
  * published (e.g. re-attaching its own orphan after a restart) founds it, and attaching
2327
2562
  * anyone else's row joins, without the caller needing to know which it is.
2328
2563
  *
@@ -2335,8 +2570,9 @@ export declare class CadreNode implements SAppIdLookup {
2335
2570
  * registered, deliberately: both an explicit retry and the {@link StrandWatcher}'s
2336
2571
  * automatic relaunch need it. A failed launch here hands the strand back to that
2337
2572
  * relaunch — `StrandWatcher.forgetStrand` drops the id from the watcher's
2338
- * `knownStrands` and records the backoff, so a later poll re-attempts it on the same
2339
- * `pollInterval * 2^(failures-1)` ladder a watcher-driven failure gets. Because the
2573
+ * `knownStrands`, records the backoff and wakes the watcher when it expires, so the
2574
+ * strand is re-attempted on the same ladder a watcher-driven failure gets
2575
+ * (`min(strandWatchInterval, 5 s) * 2^(failures-1)`, capped at 5 minutes). Because the
2340
2576
  * config stays registered, the retry takes `handleStrandAdded`'s auto-launch branch:
2341
2577
  * what the app sees is `strand:error` per failed retry and `strand:started` when one
2342
2578
  * succeeds, never a second `strand:discovered`. {@link detachStrand} (reached via
@@ -2393,11 +2629,12 @@ export declare class CadreNode implements SAppIdLookup {
2393
2629
  * fresh publish also clears it for an OPEN strand, but is destructive for a closed one
2394
2630
  * (the key exists nowhere else). No app-data wipe is needed for either.
2395
2631
  *
2396
- * The insert is signed with the ed25519 key behind this node's PeerId — which
2397
- * {@link ed25519KeyPairFromLibp2p} also exposes as the node's owner keypair,
2398
- * so peer identity and owner key are one and the same. That key is also persisted on the
2399
- * row as `FounderOwnerKey` (the schema pins the column to the verified signer), which is
2400
- * what later lets any launch derive "this machine is the founder" from the row alone.
2632
+ * The insert is signed through this node's owner signer ({@link getOwnerSigner}), in one
2633
+ * approval and one control transaction with a closed strand's party key
2634
+ * ({@link ControlDatabase.insertFoundingRows}). The signer's key is persisted on the row
2635
+ * as `FounderOwnerKey` (the schema pins the column to the verified signer), and this node's
2636
+ * peer id as `FounderPeerId` (bound into the signature), which is what later lets any
2637
+ * launch derive "this machine is the founder" from the row alone.
2401
2638
  * The key must be enrolled in `OwnerKey` (e.g. via {@link ControlDatabase.ensureOwnerKey}
2402
2639
  * at genesis) or the schema's `Strand.AuthorizedInsert` constraint rejects the write.
2403
2640
  * Failing loudly here is intentional: a silently-unpublished strand would run
@@ -2405,7 +2642,8 @@ export declare class CadreNode implements SAppIdLookup {
2405
2642
  *
2406
2643
  * A CLOSED strand's publish also seats this party's own membership identity key —
2407
2644
  * the `StrandPartyKey` row {@link ensureStrandPartyKey} documents — which is what the
2408
- * founder bootstrap derives `Member.Key`/`Manager.MemberKey` from. The
2645
+ * founder bootstrap derives `Member.Key`/`Manager.MemberKey` from; a key the party already
2646
+ * holds for the id is kept. The
2409
2647
  * `memberPrivateKey` ARGUMENT stays the strand-wide read secret formation hands to
2410
2648
  * joiners; it derives nobody's identity.
2411
2649
  *
@@ -2413,20 +2651,67 @@ export declare class CadreNode implements SAppIdLookup {
2413
2651
  * {@link addStrand}).
2414
2652
  * @param type - `'o'` for open (default) or `'c'` for closed.
2415
2653
  * @param memberPrivateKey - Optional shared membership (read) key for a closed strand.
2654
+ * @param sign - The `sign` of an owner approval the caller already holds
2655
+ * ({@link getOwnerSigner}), so a larger action asks once; omitted, this asks for its own.
2416
2656
  * @returns The live `Strand` row — the one just inserted, or the matching one already
2417
2657
  * there. A closed strand's caller should carry THIS row's `MemberPrivateKey` forward:
2418
2658
  * on a repeat it is the stored key, not the argument.
2419
- * @throws if the node is not started, exposes no owner signing key, the id is blank or
2659
+ * @throws if the node is not started, has no owner signer wired, the id is blank or
2420
2660
  * unusable as a storage scope key (`InvalidStrandIdError` — see `storage-scope.ts`), a
2421
2661
  * row with the same id holds different content, or the control DB rejects the
2422
2662
  * (unauthorized) insert.
2423
2663
  */
2424
- publishStrand(strandId: string, type?: 'o' | 'c', memberPrivateKey?: string): Promise<StrandRow>;
2664
+ publishStrand(strandId: string, type?: 'o' | 'c', memberPrivateKey?: string, sign?: OwnerSign): Promise<StrandRow>;
2665
+ /**
2666
+ * The `Strand` row this machine publishes for `strandId`, as {@link publishStrand} and
2667
+ * {@link foundStrand} write it: the id trimmed and checked, this node's owner key and peer id
2668
+ * as its founder.
2669
+ */
2670
+ private founderStrandRow;
2671
+ /**
2672
+ * The write half of {@link publishStrand} and {@link foundStrand}: under one owner approval,
2673
+ * insert `desired`, a closed strand's party key and the requested invitation in one control
2674
+ * transaction ({@link ControlDatabase.insertFoundingRows}), adopting a matching row a
2675
+ * concurrent founder landed first. Reads nothing first: the caller has just read the
2676
+ * `Strand` row, and a stored key or row is resolved from the insert's own rejection.
2677
+ */
2678
+ private writeFoundingRows;
2679
+ /** {@link writeFoundingRows} inside its approval. */
2680
+ private insertOrAdoptFounding;
2681
+ /**
2682
+ * Insert `desired`'s founding rows, minting a closed strand's party key — this party's own
2683
+ * membership identity, which the founder bootstrap derives `Member`/`Manager` from, distinct
2684
+ * from the row's shared `MemberPrivateKey` read secret. A key already stored for the id with
2685
+ * no strand row (one {@link ensureStrandPartyKey} seated, which it would adopt too) refuses
2686
+ * that transaction whole; the rows are then written again without a key.
2687
+ *
2688
+ * @returns The party key this seated; undefined when it seated none, so the launch reads the stored one.
2689
+ */
2690
+ private seatFoundingRows;
2691
+ /**
2692
+ * Resolve a founding refused on `Strand.Id`: re-read the row that landed and adopt it when it
2693
+ * matches `desired` — a concurrent founder, such as two machines of one party on one id —
2694
+ * or throw naming the columns that differ.
2695
+ */
2696
+ private adoptConcurrentFounding;
2697
+ /**
2698
+ * Publish a founding's invitation on its own, bound to `strandId`, for a founding that adopted
2699
+ * a stored row rather than writing its own — unless the invitation is already stored, bound to
2700
+ * this strand, as a repeat of a founding whose transaction committed leaves it.
2701
+ *
2702
+ * @param sign - The open approval's `sign`; omitted, the publish asks for its own.
2703
+ */
2704
+ private publishFoundingInviteIfAbsent;
2425
2705
  /**
2426
2706
  * Found a strand — publish its row cadre-wide AND start the local instance as its
2427
2707
  * founder — in one resumable call. The single entry point for creating a strand; a
2428
2708
  * caller joining one someone else founded uses {@link addStrand} alone.
2429
2709
  *
2710
+ * The publish is one control transaction under one owner approval: the `Strand` row, a
2711
+ * closed strand's `StrandPartyKey` row, and the invitation in
2712
+ * {@link FoundStrandConfig.formationInvite} when one is given. The start then uses the party
2713
+ * key the publish seated rather than reading it back.
2714
+ *
2430
2715
  * Safe to re-run from any point of interruption, which hand-rolling
2431
2716
  * {@link publishStrand} + {@link addStrand} is not. Either half can already have
2432
2717
  * happened:
@@ -2438,7 +2723,7 @@ export declare class CadreNode implements SAppIdLookup {
2438
2723
  * attempt (as the reference apps do) would otherwise present a key that does not match
2439
2724
  * the membership already seated in the strand.
2440
2725
  * - **instance already running** → the tracked instance is returned, and — because the
2441
- * `Strand` row records its publishing machine (`FounderOwnerKey`) — a founder request
2726
+ * `Strand` row records its founding machine (`FounderPeerId`) — a founder request
2442
2727
  * against an instance that was first launched as a joiner now runs the (idempotent,
2443
2728
  * insert-if-absent) founder bootstrap on it rather than silently skipping it
2444
2729
  * ({@link StrandInstanceManager.foundExistingStrand}). So a strand that something else
@@ -2447,7 +2732,7 @@ export declare class CadreNode implements SAppIdLookup {
2447
2732
  * `resolveCohortSeed` window — still ends up with its `Strand.Header` written.
2448
2733
  *
2449
2734
  * Founder-ness is resolved FROM THE ROW, not assumed: this machine founds iff the
2450
- * resolved row's `FounderOwnerKey` is its own owner key. A fresh publish records this
2735
+ * resolved row's `FounderPeerId` is its own peer id. A fresh publish records this
2451
2736
  * machine, so the common path founds; a machine that lost a concurrent founding race to
2452
2737
  * a sibling adopts the sibling's row and ATTACHES instead — deliberately, since two
2453
2738
  * machines bootstrapping the same strand on separate replicas is the double-`Header`
@@ -2456,12 +2741,51 @@ export declare class CadreNode implements SAppIdLookup {
2456
2741
  * `Type` is compared, not adopted: a stored row of the other type means the caller and
2457
2742
  * the control plane disagree about what this strand IS, so it throws.
2458
2743
  *
2744
+ * **Returning after the publish.** `foundStrand(config, { background: true })` resolves
2745
+ * once the row is published and continues the local start in
2746
+ * {@link FoundStrandBackgroundResult.ready}, for a caller that needs only the row (an
2747
+ * invitation reads just its id and membership key). Publish errors still reject the call
2748
+ * itself. A joiner that redeems an invitation to a closed strand before its start finishes
2749
+ * is refused with the retryable `host-strand-unavailable` and the token stays unspent:
2750
+ * {@link requestJoin} retries that on its backoff, while {@link formStrand} throws it to its
2751
+ * caller once every machine the invitation names has refused.
2752
+ *
2459
2753
  * @returns The instance AND the row the strand actually runs under — read the membership
2460
2754
  * key from the returned row, not from a freshly minted one ({@link FoundStrandResult}).
2461
2755
  * @throws if the node is not started, the id is blank, a published row of the same id has
2462
2756
  * a different `Type`, or either half rejects.
2463
2757
  */
2464
2758
  foundStrand(config: FoundStrandConfig): Promise<FoundStrandResult>;
2759
+ /**
2760
+ * Publish the strand's row as {@link foundStrand} does, and resolve once it is published;
2761
+ * the local start continues in {@link FoundStrandBackgroundResult.ready}.
2762
+ *
2763
+ * @throws as the default form does for the publish half; the start's own failure rejects
2764
+ * `ready` instead.
2765
+ */
2766
+ foundStrand(config: FoundStrandConfig, options: FoundStrandOptions): Promise<FoundStrandBackgroundResult>;
2767
+ /**
2768
+ * The publish half of {@link foundStrand}: adopt the stored row or publish a new one, and
2769
+ * derive whether this machine founds it. Every check that can refuse the founding is here,
2770
+ * so both forms of `foundStrand` reject on it before returning.
2771
+ */
2772
+ private publishFoundingRow;
2773
+ /** {@link publishFoundingRow} when no row is stored: write the founding rows under this node's owner. */
2774
+ private publishFoundingStrand;
2775
+ /**
2776
+ * {@link publishFoundingRow} over a stored row: adopt it ({@link adoptPublishedStrand}), then
2777
+ * publish the requested invitation on its own, under its own approval.
2778
+ */
2779
+ private resumeFounding;
2780
+ /**
2781
+ * The start half of {@link foundStrand}. Its timing lines (`addStrand: start`, then the
2782
+ * duration or `failed after`) are logged when the start settles, also in the background
2783
+ * form, so a start that hangs still shows as a start line with no end line.
2784
+ *
2785
+ * A party key the publish half just seated is handed to the launch, which would otherwise
2786
+ * read back the row the same founding wrote.
2787
+ */
2788
+ private attachFoundingRow;
2465
2789
  /**
2466
2790
  * Resume onto an already-published `Strand` row: keep its stored content, reject a type
2467
2791
  * disagreement. The stored `MemberPrivateKey` is authoritative — see {@link foundStrand}
@@ -2475,9 +2799,10 @@ export declare class CadreNode implements SAppIdLookup {
2475
2799
  * inverse of {@link publishStrand}.
2476
2800
  *
2477
2801
  * Party-wide, unlike {@link stopStrand}, which only stops the strand on THIS node: every
2478
- * cadre node watching the table sees the row vanish on its next poll (default 5 s) and
2479
- * stops its own instance. This node converges immediately — the method forces a watcher
2480
- * poll and stops any still-running local instance before resolving. Removing the row
2802
+ * cadre node watching the table hears the row's removal and stops its own instance (a
2803
+ * machine outside the `Strand` block's cohort, on a cadre larger than 16, only at its
2804
+ * watcher's backstop read, default 5 minutes). This node converges immediately — the
2805
+ * method forces a watcher run and stops any still-running local instance before resolving. Removing the row
2481
2806
  * removes OUR party's participation only: other parties in the strand keep their own
2482
2807
  * rows and the strand network carries on, so no cross-party sign-off is involved — this
2483
2808
  * is an owner-signed control-plane write like every other, not "destroy the network".
@@ -2521,7 +2846,7 @@ export declare class CadreNode implements SAppIdLookup {
2521
2846
  * step, not a widening of this method.
2522
2847
  *
2523
2848
  * @param strandId - The `Strand` row id to remove (as passed to {@link publishStrand}).
2524
- * @throws if the node is not started, exposes no owner signing key, the id is blank, or
2849
+ * @throws if the node is not started, has no owner signer wired, the id is blank, or
2525
2850
  * the signer is not an enrolled owner (the schema's `Strand.AuthorizedDelete` rejects
2526
2851
  * the write and the row survives). A rejection does NOT imply the row survived: the
2527
2852
  * local stop runs after the control-plane delete has already committed, so a failure
@@ -2542,9 +2867,11 @@ export declare class CadreNode implements SAppIdLookup {
2542
2867
  * as `MemberPrivateKey`; docs/strands.md → "Closed-Strand Member Key Handling") and is
2543
2868
  * never put on the formation wire.
2544
2869
  *
2545
- * Called internally by {@link publishStrand} (closed strands mint at publish) and by a
2546
- * founder launch that finds no row (a publish interrupted before its mint); public so a
2547
- * test harness — or a joiner flow that persists a formation-issued identity — can seat
2870
+ * A founding seats the key in the same transaction as the strand row, not through here.
2871
+ * Called internally by {@link publishStrand}'s repeat of a published closed strand and by a
2872
+ * founder launch that finds no row, which heal a closed strand row stored without its key
2873
+ * (one seated by `ControlDatabase.insertStrand` directly, or whose key was removed); public
2874
+ * so a test harness — or a joiner flow that persists a formation-issued identity — can seat
2548
2875
  * a specific key deliberately.
2549
2876
  *
2550
2877
  * @param strandId - The strand the key is this party's identity for.
@@ -2552,15 +2879,17 @@ export declare class CadreNode implements SAppIdLookup {
2552
2879
  * `generateStrandMemberKey` mints). When a row already holds a DIFFERENT key this
2553
2880
  * throws — a party has one identity per strand; rotation is a deliberate
2554
2881
  * remove-then-insert, not a silent swap.
2882
+ * @param sign - The `sign` of an owner approval the caller already holds; omitted, a
2883
+ * mint asks for its own (a stored key asks for none).
2555
2884
  * @returns The live party key: the one just seated, or the stored one.
2556
2885
  */
2557
- ensureStrandPartyKey(strandId: string, partyMemberPrivateKey?: string): Promise<string>;
2886
+ ensureStrandPartyKey(strandId: string, partyMemberPrivateKey?: string, sign?: OwnerSign): Promise<string>;
2558
2887
  /**
2559
2888
  * Resolve the party membership key a closed strand's launch threads into the founder
2560
2889
  * bootstrap: the explicit attach-time key when given, else the party's persisted
2561
- * `StrandPartyKey` row, else — on the one machine whose owner key the row names as
2562
- * founder — a freshly minted-and-persisted key, which seats the identity for a publish
2563
- * that was interrupted before its mint. Everyone else resolves undefined: non-founding
2890
+ * `StrandPartyKey` row, else — on the one machine the row names as founder (`FounderPeerId`)
2891
+ * — a freshly minted-and-persisted key, which seats the identity for a closed row stored
2892
+ * without one (see {@link ensureStrandPartyKey}). Everyone else resolves undefined: non-founding
2564
2893
  * machines never mint (no mint race between a party's machines), and only a founder
2565
2894
  * bootstrap needs the key at all.
2566
2895
  *
@@ -2585,26 +2914,35 @@ export declare class CadreNode implements SAppIdLookup {
2585
2914
  * Counterpart to {@link createOpenInvitation}, which only mints the
2586
2915
  * out-of-band {@link OpenInvitation} envelope: persisting the matching
2587
2916
  * `FormationInvite` row is what makes the token *redeemable* — the host's
2588
- * {@link ControlFormationUsageRecorder} answers `isTokenValid`/`isTokenUsed`
2589
- * from this row. A host minting a closed-strand invite does both (mint +
2917
+ * {@link ControlFormationUsageRecorder} reads it (`readToken`) to check a
2918
+ * redemption. A host minting a closed-strand invite does both (mint +
2590
2919
  * publish), exactly as the integration harness's `createInvitation` does.
2591
2920
  *
2592
- * Signs with the same self-owner key as {@link publishStrand} (the ed25519
2593
- * key behind this node's PeerId, which must be an enrolled `OwnerKey`).
2594
- * Throws loudly if the node isn't started or exposes no signing key.
2921
+ * Signs through the same owner signer as {@link publishStrand}, whose key must be an
2922
+ * enrolled `OwnerKey`. Throws loudly if the node isn't started or has no owner signer wired.
2595
2923
  *
2596
2924
  * @param token - Invitation token (the `FormationInvite` primary key); use the
2597
2925
  * `token` of the {@link OpenInvitation} from {@link createOpenInvitation}.
2598
2926
  * @param sAppId - The sApp a redeemed strand will use.
2599
2927
  * @param options - Optional `expiresAtMs` (epoch ms), `totalUses`, `validationUrl`,
2600
2928
  * `strandId` (bind a closed/pre-existing host strand for provision-then-record).
2929
+ * @param sign - The `sign` of an owner approval the caller already holds; omitted, this
2930
+ * asks for its own.
2601
2931
  */
2602
2932
  publishFormationInvite(token: string, sAppId: string, options?: {
2603
2933
  expiresAtMs?: number;
2604
2934
  totalUses?: number;
2605
2935
  validationUrl?: string;
2606
2936
  strandId?: string;
2607
- }): Promise<void>;
2937
+ }, sign?: OwnerSign): Promise<void>;
2938
+ /**
2939
+ * Register an invitation this node just published with the strand solicitation service. A
2940
+ * host may publish one whose token was minted elsewhere; registering it locally opens the
2941
+ * connection gate's formation exemption immediately rather than waiting for the durable row
2942
+ * to become readable. A token {@link createOpenInvitation} minted here is already registered,
2943
+ * and this records the same expiry again.
2944
+ */
2945
+ private registerPublishedInvitation;
2608
2946
  /**
2609
2947
  * Enroll an approver public key allowed to sign off on `ValidationUrl` redemptions.
2610
2948
  *
@@ -2613,10 +2951,10 @@ export declare class CadreNode implements SAppIdLookup {
2613
2951
  * — this is how a party says which outside approver it trusts. Without an enrollment,
2614
2952
  * every such invitation is unredeemable.
2615
2953
  *
2616
- * Signs with the same self-owner key as {@link publishStrand} (the ed25519 key behind
2617
- * this node's PeerId, which must be an enrolled `OwnerKey`). Throws loudly if the node
2618
- * isn't started, exposes no signing key, or the key is blank — a blank key would reach
2619
- * the database and fail the signature CHECK as an opaque constraint error.
2954
+ * Signs through the same owner signer as {@link publishStrand}, whose key must be an
2955
+ * enrolled `OwnerKey`. Throws loudly if the node isn't started, has no owner signer wired,
2956
+ * or the key is blank — a blank key would reach the database and fail the signature CHECK
2957
+ * as an opaque constraint error.
2620
2958
  *
2621
2959
  * Enrolling this node's OWN owner key is permitted and harmless: the domain/action tags
2622
2960
  * baked into each authorization digest mean an owner-key signature can never satisfy the
@@ -2650,22 +2988,22 @@ export declare class CadreNode implements SAppIdLookup {
2650
2988
  removeValidationKey(key: string): Promise<void>;
2651
2989
  /**
2652
2990
  * The approver keys currently enrolled, sorted. Read-only, so unlike
2653
- * {@link enrollValidationKey} / {@link removeValidationKey} it needs no owner signing
2654
- * key — only a started node.
2991
+ * {@link enrollValidationKey} / {@link removeValidationKey} it needs no owner signer —
2992
+ * only a started node.
2655
2993
  */
2656
2994
  listValidationKeys(): Promise<string[]>;
2657
2995
  /**
2658
- * Resolve the owner keypair every owner-signed control write needs — {@link publishStrand},
2659
- * {@link unpublishStrand}, {@link publishFormationInvite}, {@link enrollValidationKey},
2660
- * {@link removeValidationKey}
2661
- * — failing loudly in one two-part shape (not started / no signing key, naming owner
2662
- * genesis as the fix). Narrows `controlDatabase` for the caller: a non-null return means
2996
+ * The owner signer every owner-signed strand write needs — {@link publishStrand},
2997
+ * {@link unpublishStrand}, {@link ensureStrandPartyKey}, {@link publishFormationInvite},
2998
+ * {@link enrollValidationKey}, {@link removeValidationKey} — failing loudly in one two-part
2999
+ * shape (not started / no signer wired, naming the wiring call as the fix). Enrollment is the
3000
+ * schema's to judge. Narrows `controlDatabase` for the caller: a return means
2663
3001
  * `this.controlDatabase` is non-null too.
2664
3002
  *
2665
3003
  * @param action - Infinitive phrase naming the attempted write, e.g. `'enroll a
2666
3004
  * validation key'`; interpolated into both messages.
2667
3005
  */
2668
- private requireOwnerSigningKey;
3006
+ private requireOwnerSigner;
2669
3007
  /**
2670
3008
  * Shared strand launch path for both the explicit (`addStrand`) and the
2671
3009
  * control-discovered (`handleStrandAdded`) entry points. Resolves the cohort
@@ -2673,10 +3011,10 @@ export declare class CadreNode implements SAppIdLookup {
2673
3011
  * before emitting `strand:started`.
2674
3012
  *
2675
3013
  * Founder-ness: an explicit `founder` argument wins (the formation/responder flows pass
2676
- * one deliberately — their consent-seated rows carry a null `FounderOwnerKey`); when
3014
+ * one deliberately — their consent-seated rows carry a null `FounderPeerId`); when
2677
3015
  * unset it is DERIVED from the row, so the control-discovered path
2678
3016
  * (`handleStrandAdded`) and a restart's re-attach found this machine's own strands
2679
- * without the caller having to know. The derivation is pure key comparison — no I/O.
3017
+ * without the caller having to know. The derivation is a pure peer-id comparison — no I/O.
2680
3018
  *
2681
3019
  * Idempotent when the strand manager already tracks `strand.Id` — the watcher
2682
3020
  * rediscovers a row this node already started and calls this again via
@@ -2686,7 +3024,7 @@ export declare class CadreNode implements SAppIdLookup {
2686
3024
  * fan-out, so a rediscovery costs nothing beyond the map lookup plus the (pure)
2687
3025
  * founder derivation. NOT a silent no-op any more when the launch resolves as a
2688
3026
  * FOUNDER: the tracked instance may have been launched first as a joiner (an app
2689
- * attach, or this node's own watcher poll winning the `resolveCohortSeed` window
3027
+ * attach, or this node's own watcher run winning the `resolveCohortSeed` window
2690
3028
  * below), which used to drop the founder request and leave the strand headerless —
2691
3029
  * now the tracked instance is founded in place
2692
3030
  * ({@link StrandInstanceManager.foundExistingStrand}), waking it first if quiesced
@@ -2698,9 +3036,9 @@ export declare class CadreNode implements SAppIdLookup {
2698
3036
  *
2699
3037
  * An absent `sAppConfig` launches a storage replica ({@link CadreNodeConfig.hostUnclaimedStrands}),
2700
3038
  * which is ALWAYS a joiner whatever the row says: the founder bootstrap writes the sApp
2701
- * into `Strand.Header`, and a replica has none. So a self-founded row whose watcher poll
3039
+ * into `Strand.Header`, and a replica has none. So a self-founded row whose watcher run
2702
3040
  * wins the race against its app's `addStrand` after a restart comes up as a replica — and
2703
- * so does a `foundStrand` whose publish a watcher poll saw before its attach. A claim
3041
+ * so does a `foundStrand` whose publish a watcher run saw before its attach. A claim
2704
3042
  * that finds a tracked replica upgrades it in place, over the same node and store
2705
3043
  * ({@link StrandInstanceManager.attachSApp}), BEFORE any founding: the founder bootstrap
2706
3044
  * needs the sApp the attach supplies.
@@ -2745,10 +3083,11 @@ export declare class CadreNode implements SAppIdLookup {
2745
3083
  * receiver records an admission grant BEFORE `libp2p.start()` (re-)dials the
2746
3084
  * relay reservation — the gate denial there is fatal, not degraded. A
2747
3085
  * party-member relay answers the RPC (its control node admits us as a
2748
- * member); a dedicated ops/ relay does not speak the protocol and the
2749
- * per-peer failure comes back `unreachable` — harmless, it has no membership gate and
2750
- * needs no grant. The relay's direct addr rides along as the dial fallback
2751
- * for a relay we are not yet connected to.
3086
+ * member); a dedicated ops/ relay does not speak the protocol, comes back
3087
+ * `unsupported`, and is left out of later passes
3088
+ * ({@link relaysRefusingStrandAddr}) — it has no membership gate and needs no
3089
+ * grant. The relay's direct addr rides along as the dial fallback for a relay
3090
+ * we are not yet connected to.
2752
3091
  */
2753
3092
  private resolveCohortSeed;
2754
3093
  /**
@@ -2811,20 +3150,25 @@ export declare class CadreNode implements SAppIdLookup {
2811
3150
  * `libp2p.start()`. Kept because it costs one spread and this is the one place
2812
3151
  * that would silently stop announcing if that rejection is ever relaxed.
2813
3152
  *
3153
+ * Less the relays remembered as refusing the strand-addr protocol
3154
+ * ({@link relaysRefusingStrandAddr}): both callers are announcement passes, and
3155
+ * those relays hold no grant.
3156
+ *
2814
3157
  * NOTE: a relay the STRAND node discovers on its own (autorelay — in neither
2815
3158
  * source) gets no announcement, and a membership-gated one will deny it; fine
2816
3159
  * now, every realistic topology feeds one of the two sources.
2817
3160
  */
2818
3161
  private circuitRelayTargets;
2819
3162
  /**
2820
- * Record the announce timestamps {@link refreshDelegateGrants} throttles on,
2821
- * for every relay a delegate-carrying announce pass dialed.
3163
+ * Record what a delegate-carrying announce pass learned about the relays it
3164
+ * dialed: the announce timestamps {@link refreshDelegateGrants} throttles on,
3165
+ * and from `outcomes` which relays refuse the strand-addr protocol
3166
+ * ({@link relaysRefusingStrandAddr}). Every announce site records through here.
2822
3167
  *
2823
- * Recorded OPTIMISTICALLY at announce time, whatever `collectStrandAddrs`
2824
- * reports per peer: a dedicated `ops/` relay never speaks the strand-addr
2825
- * protocol, so recording only on success would re-announce to it on every
2826
- * reconcile tick, one wasted protocol negotiation each. A failed INITIAL announce
2827
- * costs the strand supervisor its first attempt only (the relay denies the
3168
+ * Timestamps are recorded OPTIMISTICALLY, whatever `collectStrandAddrs` reports
3169
+ * per peer: recording only on success would re-announce to an unreachable relay
3170
+ * on every reconcile pass, one dial timeout each. A failed INITIAL announce costs
3171
+ * the strand supervisor its first attempt only (the relay denies the
2828
3172
  * reservation; every re-drive re-announces first through
2829
3173
  * {@link announceDelegateToRelay}); a failed REFRESH retries within
2830
3174
  * `DELEGATE_GRANT_TTL_MS / 2` (15 min), still inside the 30 min TTL.
@@ -2860,11 +3204,12 @@ export declare class CadreNode implements SAppIdLookup {
2860
3204
  * The throttle map is updated afterwards, so the periodic pass does not announce
2861
3205
  * again right away.
2862
3206
  *
2863
- * Against a dedicated ops relay (no strand-addr RPC) the request fails per-peer
2864
- * and `collectStrandAddrs` reports it `unreachable`: one wasted protocol negotiation per
2865
- * re-drive attempt, bounded by the supervisor's backoff. Never throws on that
2866
- * path; a relay addr that names no peer id is logged and skipped (the hook's
2867
- * caller runs the drive regardless).
3207
+ * Unlike the other announce passes, this asks a relay remembered in
3208
+ * {@link relaysRefusingStrandAddr} too, and its reply clears the entry — see
3209
+ * there for why. Against a dedicated ops relay (no strand-addr RPC) that costs
3210
+ * one refused protocol negotiation (`unsupported`) per re-drive attempt, bounded
3211
+ * by the supervisor's backoff. Never throws on that path; a relay addr that names
3212
+ * no peer id is logged and skipped (the hook's caller runs the drive regardless).
2868
3213
  *
2869
3214
  * NOTE: against a relay that is DOWN this hook costs up to two strand-addr
2870
3215
  * timeouts (dial by peer id, then by addr; 28.5 s each at the default declared link round trip)
@@ -2896,9 +3241,11 @@ export declare class CadreNode implements SAppIdLookup {
2896
3241
  *
2897
3242
  * Due times are per (sibling, strand) and set from each sibling's own outcome, so
2898
3243
  * a sibling that connects late — a phone joining after the party's always-on
2899
- * machines — is asked on the next tick, and one that could not answer or refused
2900
- * (its view of the membership may not include us yet) is retried within
2901
- * {@link STRAND_PEER_ADDR_RETRY_MS} rather than {@link STRAND_PEER_ADDR_REFRESH_MS}.
3244
+ * machines — is asked on the pass its connection starts, and one that could not
3245
+ * answer or refused (its view of the membership may not include us yet) is retried
3246
+ * within {@link STRAND_PEER_ADDR_RETRY_MS} rather than
3247
+ * {@link STRAND_PEER_ADDR_REFRESH_MS}; the pass that stamped the retry arms a wake
3248
+ * for it ({@link armReconcileWake}).
2902
3249
  *
2903
3250
  * Distinct from {@link refreshDelegateGrants}, deliberately: that pass covers
2904
3251
  * RELAYS on a `DELEGATE_GRANT_TTL_MS / 2` throttle to keep circuit-relay
@@ -2918,15 +3265,17 @@ export declare class CadreNode implements SAppIdLookup {
2918
3265
  *
2919
3266
  * `connectedSiblingTargets`' membership read is unbounded, and with zero
2920
3267
  * connections its answer is empty whatever the table holds, so decide it from the
2921
- * connection list instead of paying for the read once a tick.
3268
+ * connection list instead of paying for the read once a pass.
2922
3269
  *
2923
- * NOTE: otherwise the read runs on EVERY tick with a running strand, not only when
3270
+ * NOTE: otherwise the read runs on EVERY pass with a running strand, not only when
2924
3271
  * some (sibling, strand) is due: pruning a departed sibling needs the current
2925
3272
  * target set, and whether anyone is due cannot be told without it (a connected
2926
- * non-member never gets a due time to compare). One more `CadrePeer` read per 15 s
2927
- * tick, on top of the two `runReconcileControlCohort` already makes; if those reads
2928
- * get costly, share one row-set across the pass (see the NOTE there) rather than
2929
- * skipping ticks here.
3273
+ * non-member never gets a due time to compare). One more `CadrePeer` read per
3274
+ * pass, on top of the two `runReconcileControlCohort` already makes, all served
3275
+ * from the membership rows view: an in-memory scan while the view is current,
3276
+ * storage only after the table changed or the view's maximum age passed. Sharing
3277
+ * one row set across the pass would save no storage read and was declined (see
3278
+ * the NOTE there).
2930
3279
  *
2931
3280
  * NOTE: one strand-addr RPC per (running strand × due sibling) — each a tiny
2932
3281
  * request/response on an already-open control connection. If a node ever runs
@@ -2971,8 +3320,8 @@ export declare class CadreNode implements SAppIdLookup {
2971
3320
  *
2972
3321
  * NOTE: every pass serialises the whole FRET table (`exportTable()`), verifies one
2973
3322
  * signature per record and makes one `peerStore.merge` per peer, per running strand
2974
- * per 15 s tick. Fine at strand table sizes, and an unchanged address set writes
2975
- * nothing in libp2p's persistent peer store. If many strands or large tables make it
3323
+ * per pass — every change-driven pass and the 2-minute backstop. Fine at strand table
3324
+ * sizes, and an unchanged address set writes nothing in libp2p's persistent peer store. If many strands or large tables make it
2976
3325
  * show up, read only the entries whose address stamp is near the one-hour mark.
2977
3326
  *
2978
3327
  * @returns the ids of the peers the table holds a usable record for.
@@ -3129,8 +3478,8 @@ export declare class CadreNode implements SAppIdLookup {
3129
3478
  * The stop + emit are skipped when the strand manager holds no instance for `strandId` —
3130
3479
  * e.g. a party owner that published a strand's row but never ran it locally, or an
3131
3480
  * explicit {@link stopStrand} for an id this node never started. `hibernationManager`
3132
- * untrack and the `sAppConfigs` / {@link strandLaunchRefusals} / {@link discoveredStrands}
3133
- * deletes stay unconditional (all no-ops when there is nothing to remove), so a launch
3481
+ * untrack and the `sAppConfigs` / {@link strandLaunchRefusals} / {@link discoveredStrands} /
3482
+ * {@link formedPartyKeys} deletes stay unconditional (all no-ops when there is nothing to remove), so a launch
3134
3483
  * that failed before an instance was ever tracked still gets its stray entries cleared.
3135
3484
  *
3136
3485
  * Dropping the {@link discoveredStrands} entry here covers both callers, and both
@@ -3321,18 +3670,26 @@ export declare class CadreNode implements SAppIdLookup {
3321
3670
  */
3322
3671
  getControlDatabase(): ControlDatabase | null;
3323
3672
  /**
3324
- * Open control-network connections right now. A lower-bound proxy for replication
3325
- * reach: 0 connections ⇒ a control write commits local-only. The private
3326
- * `committedAlone` write-while-alone seam is defined in terms of this.
3327
- *
3328
- * It approximates the precise signal (the block's cluster size), and a caller that
3329
- * samples it AFTER a write samples a slightly wider window than `committedAlone`
3330
- * does inside the write itself. It exists so an embedder can warn an owner before a
3331
- * control write that this machine currently sees none of its siblings, and report
3332
- * after one that the write may not have travelled — a warning shown unconditionally
3333
- * is a warning people learn to ignore.
3673
+ * Open control-network connections right now, of every kind: siblings, relays, bootstrap
3674
+ * nodes and peers still identifying. A report of connectivity, not of replication reach: a
3675
+ * control write can commit local-only with connections open, and whether one would is
3676
+ * {@link hasReachableControlSibling}.
3334
3677
  */
3335
3678
  getControlConnectionCount(): number;
3679
+ /**
3680
+ * Whether a control write made now reaches another machine: a connected peer has identified
3681
+ * as serving the control database and is one this node sends control writes to (an
3682
+ * authorized member, or a device whose admission it is writing). Optimystic counts a peer
3683
+ * into a control block's cohort on those same terms; with none, a write commits on this node
3684
+ * alone and is queued for re-issue once one is reachable (docs/architecture.md → "Writes made
3685
+ * while alone").
3686
+ *
3687
+ * It exists so an embedder can warn an owner before a control write that this machine
3688
+ * currently reaches none of its siblings, and report after one that the write may not have
3689
+ * travelled; a warning shown unconditionally is a warning people learn to ignore. A caller
3690
+ * that samples it AFTER a write samples a slightly wider window than the write did.
3691
+ */
3692
+ hasReachableControlSibling(): boolean;
3336
3693
  /**
3337
3694
  * Force a poll of the strand watcher (for testing)
3338
3695
  */
@@ -3342,10 +3699,26 @@ export declare class CadreNode implements SAppIdLookup {
3342
3699
  */
3343
3700
  getSAppConfig(strandId: string): SAppConfig | undefined;
3344
3701
  /**
3345
- * Initialize the seed bootstrap service with an owner key.
3346
- * Must be called before using seed-related methods that require signing.
3702
+ * Make `signer` this node's owner signer (`owner-signer.ts`): every owner signature the node
3703
+ * produces from now on goes through it — seeds, vouches, removals, invitations, strand and
3704
+ * join rows, and the background re-issues. Builds the owner-capable seed bootstrap service
3705
+ * around it, so this must come before any of those. One signer per node; a node started
3706
+ * with `claim` is refused (its claimant owns it).
3707
+ *
3708
+ * Anchors the signer's key in the node-local trusted-owner store as `genesis`: wiring a key
3709
+ * declares it an authority of this party.
3347
3710
  *
3348
- * @param ownerPrivateKey - The owner's private key (base64url encoded)
3711
+ * @throws when the node is not started, was started with `claim`, or already holds the
3712
+ * seed protocol handler (a second wiring, or {@link enableSeedListener} first).
3713
+ */
3714
+ useOwnerSigner(signer: OwnerSigner): Promise<void>;
3715
+ /**
3716
+ * {@link useOwnerSigner} with a software signer over `ownerPrivateKey` (`softwareOwnerSigner`):
3717
+ * the owner key held in this process's memory. Kept as a convenience because most tests and
3718
+ * the embedders that hold the key themselves wire it this way; one that keeps the key
3719
+ * elsewhere wires its own signer.
3720
+ *
3721
+ * @param ownerPrivateKey - The owner key's 32-byte Ed25519 seed, base64url.
3349
3722
  */
3350
3723
  initializeSeedBootstrap(ownerPrivateKey: string): Promise<void>;
3351
3724
  /**
@@ -3392,15 +3765,12 @@ export declare class CadreNode implements SAppIdLookup {
3392
3765
  * never reach here: {@link ControlDatabase.queryCadrePeers} excludes them, so a
3393
3766
  * revoked peer is not addressable either.
3394
3767
  *
3395
- * NOTE: answers the whole `CadrePeerRow`, past the two fields declared; the integration
3768
+ * NOTE: answers the whole `CadrePeerRow`, past the fields declared; the integration
3396
3769
  * harness (`owner-cli-node.ts`, `OwnerCliMemberRow.sig`) reads `sig` off it through the admin
3397
3770
  * channel to tell a row another member seated from one the subject published itself. Narrow
3398
3771
  * this to the declared fields only together with that reader.
3399
3772
  */
3400
- listMembers(): Promise<Array<{
3401
- peerId: string;
3402
- multiaddr: string | null;
3403
- }>>;
3773
+ listMembers(): Promise<CohortPeerRow[]>;
3404
3774
  /**
3405
3775
  * Probe whether a given peer is a `CadrePeer` member.
3406
3776
  *
@@ -3515,7 +3885,7 @@ export declare class CadreNode implements SAppIdLookup {
3515
3885
  * builds its snapshot against that anchor, which is also what its previous snapshot was
3516
3886
  * judged against. Caught here rather than failing the refresh, because the membership read
3517
3887
  * tolerates an unreachable `Revocation` block on an isolated node
3518
- * (`ControlDatabase.readRevokedStampRows`) and `queryRevocations` does not: failing the
3888
+ * (`ControlDatabase.readRevocationRowsOrNone`) and `queryRevocations` does not: failing the
3519
3889
  * whole refresh on that would freeze such a node's gate snapshot.
3520
3890
  * NOTE: on such a node the sync is skipped at every refresh until the block is held; if an
3521
3891
  * isolated node ever has to derive owner additions while cut off, give `queryRevocations`
@@ -3524,11 +3894,12 @@ export declare class CadreNode implements SAppIdLookup {
3524
3894
  * NOTE: cost per membership refresh is one `OwnerKey` read and one `Revocation` read (the
3525
3895
  * tombstone list, whose `OwnerKey` stamps are handed to `queryOwnerKeyRows` so it does not
3526
3896
  * 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.
3897
+ * `loadTombstones`), plus the owner-grant chain's two reads when some owner row is
3898
+ * grant-seated. All are served from the control database's membership views, so they reach
3899
+ * storage only after their table changed or the view's maximum age passed
3900
+ * (`control-founding-consult-budget.spec.ts` pins the counts). On a party founded before
3901
+ * genesis created every control collection, whose `Revocation` table has never been
3902
+ * written, each `Revocation` load is a cohort consult until the first tombstone.
3532
3903
  *
3533
3904
  * @param retry - forwarded to every read; `false` on the refresh path, for the reason on
3534
3905
  * {@link refreshAuthorizedControlPeers}.
@@ -3558,19 +3929,18 @@ export declare class CadreNode implements SAppIdLookup {
3558
3929
  /** A per-refresh {@link RevocationSource}: one `queryRevocations` read, shared by every caller in the pass. */
3559
3930
  private revocationSource;
3560
3931
  /**
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.
3932
+ * The usage and invitation rows {@link hasAnchoredProof} resolves an invitation-admitted
3933
+ * `CadrePeer` row through, keyed for lookup. Both reads honour `retry` for
3934
+ * {@link listAuthorizedMembers}' reason. The rows come from the membership views, so they
3935
+ * reach storage only after the table changed or the view's maximum age passed.
3572
3936
  */
3573
3937
  private loadInvitationChain;
3938
+ /**
3939
+ * The usage and grant rows {@link syncOwnerAnchor} resolves a grant-seated `OwnerKey` row
3940
+ * through, keyed for lookup: the owner-grant twin of {@link loadInvitationChain}, read only by
3941
+ * the anchor sync, since a grant admits no machine.
3942
+ */
3943
+ private loadOwnerGrantChain;
3574
3944
  /**
3575
3945
  * The owner-voucher half of {@link hasAnchoredProof}: complete voucher, `VouchOwner` in
3576
3946
  * the node-local anchor, signature valid over the row's (PeerId, StampId) voucher digest.
@@ -3635,15 +4005,15 @@ export declare class CadreNode implements SAppIdLookup {
3635
4005
  private installSeedBootstrapService;
3636
4006
  /**
3637
4007
  * The event callbacks every {@link SeedBootstrapService} this node owns is
3638
- * wired with — shared by the owner-capable service ({@link initializeSeedBootstrap})
4008
+ * wired with — shared by the owner-capable service ({@link useOwnerSigner})
3639
4009
  * and the listener-only one ({@link enableSeedListener}) so the two cannot drift.
3640
4010
  *
3641
4011
  * A seed applied by the INBOUND protocol handler writes no `CadrePeer` row of
3642
- * its own (it merges the libp2p peer store and dials owners), so the automatic
4012
+ * its own (it merges the libp2p peer store and dials the seed's preferred peers), so the automatic
3643
4013
  * write-driven refresh never fires for it — yet applying it can ANCHOR a new
3644
4014
  * owner key, which flips rows already present from unauthorized to authorized.
3645
4015
  * Hence the explicit refresh here; without it a peer the freshly anchored owner
3646
- * vouched for stays denied until the next timed cohort reconcile.
4016
+ * vouched for stays denied until the next membership change or the backstop pass.
3647
4017
  */
3648
4018
  private seedEventCallbacks;
3649
4019
  /**
@@ -3662,13 +4032,14 @@ export declare class CadreNode implements SAppIdLookup {
3662
4032
  * NO `CadrePeer` writer needs to call this: the control database notifies it
3663
4033
  * after every committed member-row write (`ControlDatabase.mutateCadrePeer`,
3664
4034
  * wired in {@link start}), which is exactly what makes the refresh automatic
3665
- * rather than a caller obligation. It stays public for the changes that write
3666
- * NO row locally and so raise no notification:
3667
- *
3668
- * - a membership row that arrived by REPLICATION (otherwise picked up on the
3669
- * next timed cohort reconcile — bounded staleness by design), and
3670
- * - a newly anchored trusted owner key ({@link applySeed}), which flips rows
3671
- * ALREADY present from unauthorized to authorized without touching them.
4035
+ * rather than a caller obligation. A membership row that arrived by REPLICATION
4036
+ * refreshes it too, through the gate watch {@link start} takes on the membership
4037
+ * tables (within its 50 ms window; a change this node's storage never hears of
4038
+ * waits for the reconcile pass's backstop). It stays public for the one change
4039
+ * that writes no row and raises no table event: a newly anchored trusted owner
4040
+ * key ({@link applySeed}), which flips rows ALREADY present from unauthorized to
4041
+ * authorized without touching them. A caller may also await it to observe a
4042
+ * refresh that began after its own change.
3672
4043
  *
3673
4044
  * Coalescing: marks the snapshot stale and resolves once a refresh that began
3674
4045
  * after this call has completed, so a caller that awaits it always observes
@@ -3681,13 +4052,10 @@ export declare class CadreNode implements SAppIdLookup {
3681
4052
  * connected peer it newly covers — the strand-side counterpart to
3682
4053
  * {@link refreshMembershipGate}.
3683
4054
  *
3684
- * Unlike that one, NOTHING calls this automatically: strand membership is
3685
- * written through the strand's own `Database` handle
3686
- * (`strand-membership-writer.ts`), which raises no notification this runtime
3687
- * can hook, and a revocation that arrives by REPLICATION raises none either.
3688
- * The gate therefore polls (default 30 s). An app that has just called
3689
- * `revokeMember` or `leaveStrand` should follow the write with this call so
3690
- * the cut is immediate rather than up to one poll interval late.
4055
+ * The gate already refreshes on its own when the strand's `Member` or
4056
+ * `MemberPeer` table changes: at once for a `revokeMember` or `leaveStrand`
4057
+ * written on this machine, and when a replicated removal reaches this node's
4058
+ * storage. Call this to await the cut — it resolves after the sweep.
3691
4059
  *
3692
4060
  * Quiet no-op for a strand that is not running, is quiesced, is open, or has
3693
4061
  * the gate disabled. Never rejects; resolves once the sweep has finished.
@@ -3717,8 +4085,8 @@ export declare class CadreNode implements SAppIdLookup {
3717
4085
  * CONCURRENT external write's refresh for the life of the scope: that writer's
3718
4086
  * promise resolves before its peer is admitted, and admission lands at scope
3719
4087
  * exit instead. Acceptable — scope exit is milliseconds away (bounded by the
3720
- * drain), versus the ~15 s reconcile interval that was the alternative before
3721
- * any of this existed — but it is the reason these scopes stay short and rare.
4088
+ * drain), versus the reconcile pass that was the alternative before any of this
4089
+ * existed — but it is the reason these scopes stay short and rare.
3722
4090
  */
3723
4091
  private deferMembershipGateRefresh;
3724
4092
  /**
@@ -3729,13 +4097,15 @@ export declare class CadreNode implements SAppIdLookup {
3729
4097
  * @param multiaddrs - Optional multiaddrs for the peer
3730
4098
  */
3731
4099
  authorizePeer(peerId: string, multiaddrs?: string[]): Promise<void>;
4100
+ /** {@link authorizePeer}'s body, signing inside the caller's owner approval when it passes `sign` ({@link claimNode}). */
4101
+ private vouchPeer;
3732
4102
  /**
3733
4103
  * Remove a previously-authorized peer from the cadre.
3734
4104
  * Signs the peer ID with the owner key and deletes the CadrePeer row.
3735
4105
  *
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}.
4106
+ * Removes the `CadrePeer` row only. The call that removes a machine completely (its push
4107
+ * token and the invitations it could come back through too, in one transaction) is
4108
+ * {@link removeMachine}.
3739
4109
  *
3740
4110
  * @param peerId - The peer ID to remove
3741
4111
  */
@@ -3745,55 +4115,123 @@ export declare class CadreNode implements SAppIdLookup {
3745
4115
  * `OwnerKey.Authorized`; the row stores the vouching pair). The write notifies the
3746
4116
  * membership hub, so the refresh it triggers has run by the time this resolves.
3747
4117
  *
4118
+ * Refused while alone ({@link ownerKeyWriteAlone}): an `OwnerKey` add has no re-issue path,
4119
+ * so one committed with no control connection would stay on this node.
4120
+ *
3748
4121
  * @param ownerPublicKeyB64 - the new owner's ed25519 public key (base64url).
4122
+ * @param options.label - the user's name for the key ("phone", "paper backup"), bound into
4123
+ * the signature; trimmed, at most 64 characters, blank meaning none.
3749
4124
  * @returns `true` when the row was seated, `false` when the key is already a live owner.
4125
+ * @throws OwnerKeyAddRefusedError `alone`, before any write.
4126
+ */
4127
+ addOwner(ownerPublicKeyB64: string, options?: {
4128
+ label?: string;
4129
+ }): Promise<boolean>;
4130
+ /**
4131
+ * Whether an `OwnerKey` add made now ({@link addOwner}, or the re-add of
4132
+ * {@link removeOwner}'s re-vouch) is refused `alone`: this node holds no control connection
4133
+ * while the party has other members. An `OwnerKey` row has no update to re-touch, so nothing
4134
+ * re-issues it on cohort growth, and a row written now would fork this node's history. A
4135
+ * party with no other members cannot fork, so a solo founder adding an owner key is not
4136
+ * refused.
4137
+ *
4138
+ * Waits out an in-flight membership refresh first: until `start`'s first one lands the
4139
+ * member snapshot is empty, which would read as a party with no other members.
4140
+ */
4141
+ private ownerKeyWriteAlone;
4142
+ /**
4143
+ * Every live owner key with its label, who vouched it, whether this node's owner signer
4144
+ * holds it and whether this node's trusted-owner anchor trusts it ({@link CadreOwnerKey}),
4145
+ * for an app's owner-key screen.
4146
+ */
4147
+ listOwnerKeys(): Promise<CadreOwnerKey[]>;
4148
+ /**
4149
+ * What {@link removeOwner} would do for `ownerPublicKeyB64`, read without writing, for an
4150
+ * app's confirmation step ({@link OwnerKeyRemovalPlan}): whether this node may remove it,
4151
+ * which rows the key vouched (so the app can offer to keep them by re-vouching), and how this
4152
+ * node's own rows depend on it (`planOwnerKeyRemoval` in `owner-keys.ts`).
4153
+ */
4154
+ planOwnerKeyRemoval(ownerPublicKeyB64: string): Promise<OwnerKeyRemovalPlan>;
4155
+ /**
4156
+ * Remove an owner key, signed by this node's owner key (which must be a different owner): the
4157
+ * `OwnerKey` row and its `Revocation` tombstone commit together, and the tombstone rides the
4158
+ * committed-delete seam ({@link noteGuardedDelete}), so one committed while alone is re-issued
4159
+ * on cohort growth like every other removal. The machines the key was used on stay members;
4160
+ * removing one is {@link removeMachine}. The owner grants the key issued stop being redeemable
4161
+ * (the plan's `grants` names the live ones) and are not withdrawn: a tombstone over a grant
4162
+ * nobody can redeem would add nothing.
4163
+ *
4164
+ * Steps: re-plan ({@link planOwnerKeyRemoval}) and throw {@link OwnerKeyRemovalRefusedError}
4165
+ * on a refusal; check `options.revouch` against the plan's dependents and throw before any
4166
+ * write when it names anything else; refuse `alone` ({@link ownerKeyWriteAlone}) when an
4167
+ * `OwnerKey` row is to be re-vouched, since that re-add has no re-issue path; **re-vouch
4168
+ * first**, under this node's key, the dependents the caller chose to keep and this node's own
4169
+ * row when the key vouched it (`ControlDatabase.revouchOwnerKey`, `reauthorizePeer`), so they
4170
+ * are never untrusted in between, and stop before the removal if one fails (re-vouches
4171
+ * already made stay, harmlessly); then the removal. The re-vouches and the removal are one owner approval. The
4172
+ * write notifies the membership hub, so the refresh it triggers has run by the time this
4173
+ * resolves.
4174
+ *
4175
+ * @param ownerPublicKeyB64 - the owner's ed25519 public key (base64url).
4176
+ * @returns what was written ({@link OwnerKeyRemovalResult}).
4177
+ * @throws {OwnerKeyRemovalRefusedError} `'not-an-owner'`, `'unknown'`, `'last-owner'`,
4178
+ * `'self'` or `'alone'`, before any write.
3750
4179
  */
3751
- addOwner(ownerPublicKeyB64: string): Promise<boolean>;
4180
+ removeOwner(ownerPublicKeyB64: string, options?: RemoveOwnerOptions): Promise<OwnerKeyRemovalResult>;
4181
+ /** {@link planOwnerKeyRemoval} over `controlDatabase`, for a caller that already holds it. */
4182
+ private planOwnerKeyRemovalWith;
3752
4183
  /**
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.
4184
+ * The re-vouch step of {@link removeOwner}: every chosen dependent row is rebound to this
4185
+ * node's key BEFORE the removal, so no reader ever judges it against an owner that is gone.
4186
+ * Owners through `ControlDatabase.revouchOwnerKey` (a delete-and-re-add under a fresh stamp,
4187
+ * the only rebinding `OwnerKey.NoUpdate` allows); peers through the owner re-touch
4188
+ * (`reauthorizePeer`), which rewrites `VouchOwner`/`VouchSig` and clears `VouchUsage`.
3758
4189
  *
3759
- * Removes the `OwnerKey` row only. The call that removes a machine completely, its owner key
3760
- * included, is {@link removeMachine}.
4190
+ * The peer re-touch is the re-vouch mode of `reauthorizePeer`, which leaves the row's
4191
+ * `UpdatedAt` untouched and writes whether or not the machine has self-published. The owner
4192
+ * branch of `CadrePeer.AuthorizedUpdate` does not require a bump, and bumping a self-published
4193
+ * row would leave its `Sig` unverifiable, so unresolvable and unlisted as self-published, until
4194
+ * the machine's next heartbeat (the NOTE on that constraint); the write-while-alone drain's
4195
+ * re-broadcast mode bumps because it re-issues an otherwise unchanged row, whereas here the
4196
+ * voucher itself changes. A dependent whose row vanished since the plan is reported absent and
4197
+ * skipped.
3761
4198
  *
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.
4199
+ * A failure rethrows and stops the call before the removal; re-vouches already made stay
4200
+ * (the row is only re-signed by a current owner) and the caller retries. An owner re-vouch
4201
+ * is refused alone before this runs; one whose last connection drops while it runs is logged,
4202
+ * as {@link addOwner}'s add is.
4203
+ *
4204
+ * NOTE: a peer re-touch committed alone is not queued, and the drain skips self-published
4205
+ * rows anyway, so the other machines may get the removal's tombstone without the new voucher
4206
+ * (`tickets/backlog/bug-owner-key-removal-revouch-made-alone-is-not-reissued`).
3764
4207
  */
3765
- removeOwner(ownerPublicKeyB64: string): Promise<boolean>;
4208
+ private revouchDependents;
3766
4209
  /**
3767
4210
  * 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`).
4211
+ * `CadrePeer` row, judged by this node's membership predicate (`describeMachines` in
4212
+ * `machine-removal.ts`). Owner keys are not machines; {@link listOwnerKeys} lists them.
3770
4213
  * Unlocked reads with the default retry.
3771
4214
  */
3772
4215
  listMachines(): Promise<CadreMachine[]>;
3773
4216
  /**
3774
4217
  * 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`).
4218
+ * confirmation step ({@link MachineRemovalPlan}): whether this node may remove it and which
4219
+ * invitations it could come back through (`planMachineRemoval` in `machine-removal.ts`).
3779
4220
  */
3780
4221
  planMachineRemoval(peerId: string): Promise<MachineRemovalPlan>;
3781
4222
  /**
3782
4223
  * 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.
4224
+ * `CadrePeer` row, its `DeviceToken` row (a removed phone must not be push-woken) and a
4225
+ * withdrawal of every live invitation it could redeem to come back, in ONE transaction
4226
+ * ({@link ControlDatabase.removeMachine}), with a `Revocation` tombstone per row so the
4227
+ * removal holds on every machine ("Deletes made while alone" in docs/architecture.md), under
4228
+ * one owner approval. Afterwards this node forgets the machine as a dial target, and the
4229
+ * refresh the write notifies hangs up its control connection ({@link hangUpRevokedControlPeers})
4230
+ * before this resolves.
4231
+ *
4232
+ * Owner keys stay: they are the person's, not the machine's. A lost phone that held one can
4233
+ * still sign, a fresh machine invitation it could redeem included, so removing that key
4234
+ * ({@link removeOwner}) is the step that locks it out.
3797
4235
  *
3798
4236
  * Not undone by this call: the machine keeps every row and strand block it already synced
3799
4237
  * and the party's strand member keys (`tickets/blocked/decide-removed-machine-strand-access`).
@@ -3801,37 +4239,14 @@ export declare class CadreNode implements SAppIdLookup {
3801
4239
  *
3802
4240
  * @returns what was written ({@link MachineRemovalResult}); all false when no live row names
3803
4241
  * the machine here (already removed elsewhere).
3804
- * @throws {MachineRemovalRefusedError} `'not-an-owner'`, `'self'` or `'last-owner'`.
4242
+ * @throws {MachineRemovalRefusedError} `'not-an-owner'` or `'self'`, before any write.
3805
4243
  */
3806
- removeMachine(peerId: string, options?: RemoveMachineOptions): Promise<MachineRemovalResult>;
4244
+ removeMachine(peerId: string): Promise<MachineRemovalResult>;
3807
4245
  /**
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.
4246
+ * The read behind {@link listMachines}: the live `CadrePeer` rows projected as machines, with
4247
+ * this node's membership predicate as the `trusted` test.
3810
4248
  */
3811
4249
  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;
3835
4250
  /**
3836
4251
  * Create a seed from the current control network state.
3837
4252
  * The seed contains peer information and is signed by an owner.
@@ -3852,9 +4267,9 @@ export declare class CadreNode implements SAppIdLookup {
3852
4267
  trustPolicy?: SeedTrustPolicy;
3853
4268
  }): Promise<ApplySeedResult>;
3854
4269
  /**
3855
- * Post-process an {@link applySeed} outcome: retain the seed's owner peers as
4270
+ * Post-process an {@link applySeed} outcome: retain the seed's preferred peers as
3856
4271
  * cold-start bootstrap targets, and surface a seed that was accepted but whose
3857
- * every owner dial failed — the node is now seeded yet unconnected, and the
4272
+ * every dial failed — the node is now seeded yet unconnected, and the
3858
4273
  * cold-start reconcile branch is what gets it out of that state.
3859
4274
  */
3860
4275
  private noteAppliedSeed;
@@ -3895,12 +4310,13 @@ export declare class CadreNode implements SAppIdLookup {
3895
4310
  * that connection from the addresses it was handed, on this launch or a later
3896
4311
  * one.
3897
4312
  *
3898
- * Nothing is dialed here: the drone has not received the seed yet. After
3899
- * delivering it, call {@link reconcileControlCohort} to dial straight away;
3900
- * otherwise the next timed reconcile pass does. A pass already in flight is
3901
- * joined rather than restarted, and one that listed siblings before this add
3902
- * does not dial the drone, so a caller waiting for the connection should allow
3903
- * for one more timed pass.
4313
+ * Nothing is dialed here: the drone has not received the seed yet. Its `CadrePeer`
4314
+ * insert is a membership change, so a reconcile pass dials the retained addresses
4315
+ * within about 50 ms of this call returning, whether or not the caller runs one, and
4316
+ * a failed dial is retried on the redial ladder (15 s, 30 s, 60 s, then every
4317
+ * backstop). A caller that wants to await the connection calls
4318
+ * {@link reconcileControlCohort} itself, twice in a row: a call made while a pass is
4319
+ * in flight joins it, and that pass may have listed siblings before this add.
3904
4320
  */
3905
4321
  addDrone(options: AddDroneOptions): Promise<DroneInitResult>;
3906
4322
  /**
@@ -3910,9 +4326,10 @@ export declare class CadreNode implements SAppIdLookup {
3910
4326
  * vouch the node into this cadre and keep its addresses as a dial target. The owner
3911
4327
  * side of the claim-secret entry in the Seed Delivery Protocol (`docs/architecture.md`).
3912
4328
  *
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
4329
+ * Requires the seed service with an owner key ({@link useOwnerSigner}), as
4330
+ * {@link addDrone} does, and like it is one owner approval (the seed and the vouch). The
4331
+ * order is the REVERSE of `addDrone`'s: seed first, row second. A refused claim or a
4332
+ * failed delivery therefore leaves no `CadrePeer` row and
3916
4333
  * no retained dial target behind on this node — a phone that typed the wrong secret,
3917
4334
  * or scanned a node someone else already owns, has nothing to clean up.
3918
4335
  *
@@ -3932,12 +4349,19 @@ export declare class CadreNode implements SAppIdLookup {
3932
4349
  * by the node (its anchored branch), the row insert is a no-op on an existing row
3933
4350
  * (`ControlDatabase.insertCadrePeer`), and the dial target is replaced, not duplicated.
3934
4351
  *
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.
4352
+ * Once it has acknowledged, the node restarts into this party (cadre-cli's
4353
+ * `restartIntoClaimedParty`), closing the connection the claim used. The dial target puts
4354
+ * the address that connection was dialed on first, so every later dial tries the address
4355
+ * that just worked before the others (a QR code lists public addresses before LAN ones, and
4356
+ * a home router often drops a connection to its own public address from inside). A
4357
+ * reconcile pass is kicked (not awaited) now, and the vouch's `CadrePeer` insert starts
4358
+ * another about 50 ms later; for the first {@link CONTROL_COHORT_REDIAL_MS} after the
4359
+ * acknowledgement this node also retries the node every second while it restarts
4360
+ * ({@link redialClaimedNode}). A caller that wants to wait for the connection awaits
4361
+ * {@link reconcileControlCohort} itself, with `addDrone`'s caveat that a pass already in
4362
+ * flight is joined rather than restarted. The claimed node dials this one back too, as the
4363
+ * seed's minter, when this node has a listen or relay address; a phone with neither is
4364
+ * reached by its own dials alone.
3941
4365
  *
3942
4366
  * @throws {ClaimRefusedError} when the node was reached and refused the claim.
3943
4367
  * @throws {PeerUnreachableError} when the node was never reached.
@@ -3947,6 +4371,33 @@ export declare class CadreNode implements SAppIdLookup {
3947
4371
  multiaddrs: string[];
3948
4372
  secret: string;
3949
4373
  }): Promise<void>;
4374
+ /**
4375
+ * `addrs` with the address this node's open connection to `peerId` was dialed on moved to
4376
+ * the front, when it is one of them; `addrs` unchanged otherwise. Compared without a
4377
+ * trailing `/p2p/<peerId>`, which a QR code's address may or may not carry ({@link addrKey}).
4378
+ */
4379
+ private connectedAddrFirst;
4380
+ /**
4381
+ * Retry a node this node has just claimed ({@link claimNode}) once every
4382
+ * {@link CLAIM_RESTART_REDIAL_STEP_MS} for {@link CONTROL_COHORT_REDIAL_MS} after it accepted.
4383
+ * The node restarts into this party right after it acknowledges, and while it does, a
4384
+ * reconcile pass's dial either fails or opens a connection the restarting node closes at
4385
+ * once. The first failure puts the pass's next redial 15 s out ({@link armReconcileWake}),
4386
+ * and an owner that cannot be dialed has no other way back to the node.
4387
+ *
4388
+ * Each step dials once from the retained dial target, unless a connection to the node is
4389
+ * open: the one the claim used until the restart closes it, or one to the restarted node
4390
+ * that has not identified yet. A dial that connects does not end the loop, because the
4391
+ * restarting node may still close it. The loop ends once the node has identified as serving
4392
+ * this party's control database ({@link identifiedControlPeers}, which the placeholder node
4393
+ * it ran before the claim never joins), at the window's end, when this node stops, or when
4394
+ * the target is forgotten ({@link removePeer}). Steps run one after another, so a dial
4395
+ * finishes on its own budget first. The redial streak is left alone: the ladder carries on
4396
+ * after the window. One loop per peer: a repeated claim while one runs starts none.
4397
+ */
4398
+ private redialClaimedNode;
4399
+ /** One step of {@link redialClaimedNode}; false ends the loop. */
4400
+ private redialClaimedNodeStep;
3950
4401
  /**
3951
4402
  * Add a phone to the cadre with relay support.
3952
4403
  * Use when both nodes are NAT'd (phone-to-phone).
@@ -3969,13 +4420,13 @@ export declare class CadreNode implements SAppIdLookup {
3969
4420
  /**
3970
4421
  * Mint an owner-signed cadre invitation that a device can redeem at ANY member machine of
3971
4422
  * this party, the owner offline included ({@link redeemCadreInvitation}). Requires the
3972
- * owner key ({@link initializeSeedBootstrap}).
4423
+ * owner key ({@link useOwnerSigner}).
3973
4424
  *
3974
4425
  * 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
4426
+ * the private half rides in the returned bundle as the proof of possession. It admits a
4427
+ * machine and grants nothing else (ownership is {@link createOwnerGrant}'s), so it defaults
4428
+ * to a 24-hour lifetime ({@link CADRE_INVITATION_DEFAULT_TTL_MS}) and one use unless `uses`
4429
+ * says otherwise. The bundle carries the signed row (so a
3979
4430
  * member that has not received it by replication seats it from the bundle), this node's
3980
4431
  * anchored owner keys — every current owner this node has verified, the out-of-band ones
3981
4432
  * and those derived along verifiable chains ({@link syncOwnerAnchor}) — (the device pins
@@ -4000,7 +4451,15 @@ export declare class CadreNode implements SAppIdLookup {
4000
4451
  * @throws when no owner key is wired, the anchor is empty, `uses` is not a positive
4001
4452
  * integer, or neither this machine nor any other member has an address.
4002
4453
  */
4003
- createCadreInvitation(options: CreateCadreInvitationOptions): Promise<CreateCadreInvitationResult>;
4454
+ createCadreInvitation(options?: CreateCadreInvitationOptions): Promise<CreateCadreInvitationResult>;
4455
+ /**
4456
+ * What the two credentials an owner mints share ({@link createCadreInvitation},
4457
+ * {@link createOwnerGrant}), checked in this order: the owner-capable service; the anchor's
4458
+ * keys the holder pins (an empty anchor is refused rather than falling back to the table, since
4459
+ * a reply could not be checked against it); the use count; and the member addresses with their
4460
+ * reach, refused when neither this machine nor any other member has one.
4461
+ */
4462
+ private issuedBundleParts;
4004
4463
  /**
4005
4464
  * Every cadre invitation this node holds — minted here or replicated in — with its
4006
4465
  * standing ({@link CadreInviteStatus}: live, withdrawn, redemptions recorded).
@@ -4026,11 +4485,13 @@ export declare class CadreNode implements SAppIdLookup {
4026
4485
  * Steps: refuse an invitation for another party; pin its `ownerKeys` into the node-local
4027
4486
  * anchor (`'invite'` provenance — the pin sticks even if the redemption then fails, as it
4028
4487
  * 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.
4488
+ * sign the request, which carries this node's own signed address record; dial the listed
4489
+ * members in order (`redeemAtMembers`); on acceptance verify the reply against the pinned
4490
+ * keys, merge its dial hints into the peer store, retain the answering member as a
4491
+ * cold-start dial target and start a cohort reconcile pass. This node then syncs the control
4492
+ * database over the connection it already holds, the same unified behaviour as after a seed.
4493
+ * The member writes the record on this node's `CadrePeer` row when its signature verifies, so
4494
+ * the row resolves to this node's addresses from its admission.
4034
4495
  *
4035
4496
  * A device that is already a member (a restart with the invitation still configured) is
4036
4497
  * answered as accepted by the member's idempotent redemption, with no second usage row.
@@ -4044,11 +4505,9 @@ export declare class CadreNode implements SAppIdLookup {
4044
4505
  * pass whose reply was lost answers the next as already a member. `options.onProgress`
4045
4506
  * reports each pass and pause.
4046
4507
  *
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}).
4508
+ * The invitation admits this node as a machine and nothing else: ownership is an owner
4509
+ * grant ({@link redeemOwnerGrant}, {@link joinWithOwnerGrant}). A key of this node's that is
4510
+ * already an owner here is wired afterwards with {@link wireOwnKeyIfOwner}(signer).
4052
4511
  *
4053
4512
  * Limit: the reply check ({@link verifyRedeemReply}) does not authenticate the member. A
4054
4513
  * forged bundle, or a legitimate one whose addresses were swapped, dials a machine that
@@ -4064,24 +4523,175 @@ export declare class CadreNode implements SAppIdLookup {
4064
4523
  */
4065
4524
  redeemCadreInvitation(invitation: CadreInvitation, options?: RedeemCadreInvitationOptions): Promise<RedeemCadreInvitationResult>;
4066
4525
  /**
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.
4526
+ * If `signer`'s key is a live `OwnerKey` here, wire it ({@link useOwnerSigner}) and return
4527
+ * true; otherwise change nothing and return false. For a machine that joined someone else's
4528
+ * cadre: after an owner grant seated its key, and on every later start.
4529
+ *
4530
+ * Without `signer` the candidate is the signer already wired, so the call answers whether
4531
+ * that key is still a live owner; with nothing wired it is false. The node's identity key is
4532
+ * never a candidate: a machine's network identity is never an owner key.
4072
4533
  *
4073
4534
  * Unlike `ensureOwnerKey` it never inserts an `OwnerKey` row, so a machine that joined a
4074
4535
  * cadre can never genesis-insert itself into it, and a non-owner never anchors its own key.
4075
4536
  * An owner whose key is already wired (an earlier call) is answered true with nothing redone.
4076
4537
  *
4077
4538
  * 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>;
4539
+ * wired this way: libp2p takes one seed handler, and {@link useOwnerSigner} would register a
4540
+ * second. Nothing that joins by invitation enables the listener today; if one does, replace
4541
+ * the listener here rather than beside it.
4542
+ *
4543
+ * @throws when the node is not started, or when `signer` names a different key from the
4544
+ * signer already wired (one signer per node).
4545
+ */
4546
+ wireOwnKeyIfOwner(signer?: OwnerSigner): Promise<boolean>;
4547
+ /**
4548
+ * Mint an owner-signed owner grant: whoever holds the returned bundle can seat an owner key of
4549
+ * their own through ANY member machine of this party ({@link redeemOwnerGrant}), or join the
4550
+ * party as a machine and an owner at once ({@link joinWithOwnerGrant}), the owner that minted
4551
+ * it offline or not. Requires the owner key ({@link useOwnerSigner}); one owner approval.
4552
+ *
4553
+ * The grant is an ed25519 keypair: the public half is the `OwnerGrant` row's key, the private
4554
+ * half rides in the bundle as the proof of possession. It admits no machine and names no peer
4555
+ * id. Whoever holds the bundle can become an owner, so it defaults to a 15-minute lifetime
4556
+ * ({@link OWNER_GRANT_DEFAULT_TTL_MS}) and one use; `expiresInMs: null` mints a BACKUP grant
4557
+ * with no expiry, which must be asked for explicitly and kept as carefully as an owner key.
4558
+ * The label is the issuer's choice and goes on every key the grant seats ({@link addOwner}'s
4559
+ * rule: trimmed, at most 64 characters, blank meaning none). The bundle carries the signed
4560
+ * row, this node's anchored owner keys and at most six member addresses, chosen as
4561
+ * {@link createCadreInvitation} chooses them and for the same reasons; `reach` says where a
4562
+ * holder could redeem it once this machine is gone. A backup's addresses are those at mint
4563
+ * time, so it can go stale if every member's address changes.
4564
+ *
4565
+ * @throws when no owner key is wired, the anchor is empty, `uses` is not a positive integer,
4566
+ * the label is over 64 characters, or neither this machine nor any other member has an address.
4567
+ */
4568
+ createOwnerGrant(options?: CreateOwnerGrantOptions): Promise<CreateOwnerGrantResult>;
4569
+ /**
4570
+ * Every owner grant this node holds — minted here or replicated in — with its standing
4571
+ * ({@link OwnerGrantStatus}: live, withdrawn, redemptions recorded).
4572
+ */
4573
+ listOwnerGrants(): Promise<OwnerGrantStatus[]>;
4574
+ /**
4575
+ * Withdraw an owner grant: an owner-signed `Revocation` tombstone over its row, which stays as
4576
+ * the proof behind every key it seated; those keys stay owners (removing one is
4577
+ * {@link removeOwner}). No further redemption succeeds anywhere the tombstone reaches, and a
4578
+ * member holding it refuses the bundle's copy of the row. A withdrawal committed while alone is
4579
+ * re-issued on cohort growth like any other tombstone.
4580
+ *
4581
+ * @returns `true` when this call filed the tombstone, `false` when the row is not held here or
4582
+ * was already withdrawn.
4583
+ */
4584
+ withdrawOwnerGrant(key: string): Promise<boolean>;
4585
+ /**
4586
+ * Become an owner of this node's party with an owner grant: seat `signer`'s key as an
4587
+ * `OwnerKey` row by consent, with no owner online. `signer` holds the key to seat; its one
4588
+ * `approve` names the party and says the key becomes an owner of it, and signs the `'consent'`
4589
+ * digest. The grant's `'redeem'` signature is made with the bundle's private key in memory.
4590
+ * A bundle for another party is refused before anything is pinned, dialed or written.
4591
+ *
4592
+ * Two paths, chosen by whether this node is a live member of the party ({@link isLiveMemberSelf})
4593
+ * or an enrolled owner of it ({@link isOwner}):
4594
+ *
4595
+ * - **Local**, on a member machine: the grant row is seated from the bundle when absent (its
4596
+ * own transaction) and the redemption written in this node's own control database
4597
+ * (`ControlDatabase.redeemOwnerGrant`), replicating like any other write. Refused `alone`
4598
+ * under {@link addOwner}'s rule, before the approval: an `OwnerKey` row has no re-issue
4599
+ * path, so one committed with no control connection while the party has other members would
4600
+ * stay here; connect, then try again. A grant that is not live here (withdrawn, expired,
4601
+ * spent, its issuer no longer an owner) is refused before the approval too, unless the key
4602
+ * is already an owner. The bundle's `ownerKeys` are NOT pinned: this node judges the issuer
4603
+ * by its own `OwnerKey` table, and its anchor must not be widened by pasted text.
4604
+ * - **Protocol**, otherwise (a device with no machine in the cadre; a node started in the
4605
+ * party on an empty store): the bundle's `ownerKeys` are pinned into the node-local anchor
4606
+ * (`'invite'` provenance, the pin a credential the owner handed out carries, as
4607
+ * {@link redeemCadreInvitation} pins), then the members the bundle names are dialed over
4608
+ * `/sereus/owner-grant/1.0.0` (`redeemOwnerGrantInPasses`), one signed request for every
4609
+ * address and every pass as {@link redeemCadreInvitation} dials, so a retry after a lost
4610
+ * reply is answered `alreadyOwner`. The member that accepts pushes its control store here
4611
+ * and writes the row with this node counted in its write cohort, so a member alone with
4612
+ * this node still commits. `options` are the invitation's (`waitForMember`, `signal`,
4613
+ * `onProgress`).
4614
+ *
4615
+ * Neither path wires the signer: call {@link wireOwnKeyIfOwner}(signer) afterwards — at once
4616
+ * on the local path, and on the protocol path once the row has synced here. A key that is
4617
+ * already a live owner is answered `alreadyOwner` with no use spent, on both paths. To join
4618
+ * the party as a machine too, use {@link joinWithOwnerGrant}.
4619
+ *
4620
+ * @throws a plain `Error` for a node not started or a bundle naming another party;
4621
+ * {@link OwnerKeyAddRefusedError} `alone` on the local path, `OwnerGrantIssuerUnknownError`
4622
+ * when the issuer is not an owner here, `OwnerGrantRejectedError` `grant-spent` for a
4623
+ * grant no longer live here, and `OwnerGrantRejectedError` `conflict` (retryable) when the
4624
+ * cadre holds the grant row and this node has not synced it yet; on the protocol path
4625
+ * `OwnerGrantRejectedError` on a final refusal, `OwnerGrantReplyInvalidError` on an
4626
+ * acceptance that fails the reply check,
4627
+ * `OwnerGrantUnreachableError` when every address was tried (one pass only), and the
4628
+ * signal's reason on an abort.
4629
+ */
4630
+ redeemOwnerGrant(bundle: OwnerGrantBundle, signer: OwnerSigner, options?: RedeemOwnerGrantOptions): Promise<RedeemOwnerGrantResult>;
4631
+ /**
4632
+ * {@link redeemOwnerGrant}'s local path: the alone refusal, the seat (only when this node holds
4633
+ * no grant row), the liveness refusal, then one approval around the consent signature and the
4634
+ * write. Every refusal comes before the approval, so a prompting signer never asks for a
4635
+ * redemption this node would refuse. The held row is read once and carried into the liveness
4636
+ * check and the write, as the member handler carries it (`MemberRedemptionHandler`).
4637
+ */
4638
+ private redeemOwnerGrantLocally;
4639
+ /**
4640
+ * {@link redeemOwnerGrant}'s protocol path, given the signed request: dial the bundle's members
4641
+ * in passes, then refresh the membership gate, since the rows the member pushed and wrote are
4642
+ * judged against the pins and the anchor derives this node's key from them once its row is here.
4643
+ */
4644
+ private redeemOwnerGrantAtMembers;
4645
+ /** The signed request for `bundle` seating `signer`'s key, under an open approval's `sign`; the nonce is minted here, by the holder. */
4646
+ private signOwnerGrantRequest;
4647
+ /**
4648
+ * Join this node's party as a machine AND an owner with one owner grant, for a node started in
4649
+ * the grant's party that is not a member: a fresh install recovering with a backup grant, an
4650
+ * app joining as an owner. Needs a started node with a stable identity, as
4651
+ * {@link redeemCadreInvitation} does, and a bundle for this party.
4652
+ *
4653
+ * Steps, the two signatures made under ONE approval of `signer` before any dial, so a
4654
+ * prompting signer asks once and its prompt does not span the network:
4655
+ *
4656
+ * 1. {@link redeemOwnerGrant}'s protocol path: the member that accepts seats `signer`'s key.
4657
+ * 2. A **self-invitation**: a `CadreInvite` row targeted at this node's peer id, one use, a
4658
+ * short expiry ({@link SELF_INVITATION_TTL_MS}), signed by `signer`
4659
+ * (`ControlDatabase.signCadreInvite`; never inserted here, since this store holds no
4660
+ * `OwnerKey` row for the issuer yet). Its bundle carries the grant's owner keys plus the
4661
+ * new key (the device's reply check wants the row's issuer among the pins) and the grant's
4662
+ * members with the member that accepted the grant first, since that member holds the new
4663
+ * key as a live owner and seats the row from the bundle; one the row has not reached yet
4664
+ * answers `issuer-unknown`, which is retryable.
4665
+ * 3. {@link redeemCadreInvitation} with it: the member admits this node.
4666
+ * 4. {@link wireOwnKeyIfOwner}(signer) once the `OwnerKey` row has synced here, asked every
4667
+ * {@link OWNER_ROW_SYNC_RECHECK_MS} up to {@link OWNER_ROW_SYNC_TIMEOUT_MS}; the result's
4668
+ * `ownerWired` says whether it did in time, and a later call wires it otherwise.
4669
+ *
4670
+ * @throws as {@link redeemOwnerGrant} (protocol path) and {@link redeemCadreInvitation} throw,
4671
+ * and a plain `Error` for an ephemeral identity.
4672
+ */
4673
+ joinWithOwnerGrant(bundle: OwnerGrantBundle, signer: OwnerSigner, options?: RedeemOwnerGrantOptions): Promise<JoinWithOwnerGrantResult>;
4674
+ /**
4675
+ * {@link wireOwnKeyIfOwner}(signer) once `signer`'s `OwnerKey` row has reached this node, asked
4676
+ * again every {@link OWNER_ROW_SYNC_RECHECK_MS} until it has, the node stops, or
4677
+ * {@link OWNER_ROW_SYNC_TIMEOUT_MS} passes: false then, and a later call wires it. Usually
4678
+ * immediate: the member pushed its whole store, the row included, before the admission write.
4679
+ */
4680
+ private wireOwnKeyOnceSynced;
4681
+ /**
4682
+ * The started node's handles for a grant call, the bundle's party checked first: a grant for
4683
+ * another party is refused before any pin, dial or write.
4684
+ */
4685
+ private requireStartedForGrant;
4686
+ /**
4687
+ * Is this node a live member of its party: its own `CadrePeer` row is held here and passes the
4688
+ * membership predicate ({@link hasAnchoredProof}, judged as {@link listAuthorizedMembers}
4689
+ * judges every other row; that listing leaves this node out)? What decides
4690
+ * {@link redeemOwnerGrant}'s path. A machine's own row is written by its enrollment (or its
4691
+ * first self-record, for an owner's machine), so a node on an empty store, or one holding only
4692
+ * a store a member pushed it, is not one.
4693
+ */
4694
+ private isLiveMemberSelf;
4085
4695
  /**
4086
4696
  * Replace this node's strand solicitation service — the formation responder every node
4087
4697
  * installs at {@link start}, and the initiator side of {@link formStrand}. Optional: call it
@@ -4191,8 +4801,8 @@ export declare class CadreNode implements SAppIdLookup {
4191
4801
  */
4192
4802
  dismissPendingJoin(id: string): Promise<boolean>;
4193
4803
  private requirePendingJoins;
4194
- /** This machine's owner key and signer for an owner-signed control write, or a throw carrying `refusal`. */
4195
- private requireOwnerSigner;
4804
+ /** This machine's enrolled owner signer ({@link enrolledOwnerSigner}), or a throw carrying `refusal`. */
4805
+ private requireEnrolledOwnerSigner;
4196
4806
  /**
4197
4807
  * Write a join request, or adopt the party's existing request for the same invitation: a
4198
4808
  * pending one as it is, a finished one rewritten as `requested` (the user asked again).
@@ -4270,6 +4880,10 @@ export declare class CadreNode implements SAppIdLookup {
4270
4880
  * implements): mint a single-use `Strand.Invite` against the LIVE host strand so a
4271
4881
  * validated joiner can seat its own `Strand.Member` row.
4272
4882
  *
4883
+ * `host` is the strand as the formation session resolved it from its `Strand` row, read
4884
+ * once there; this method reads no `Strand` row of its own. A row removed in between is
4885
+ * refused when the consent row is written (the `StrandExists` CHECK).
4886
+ *
4273
4887
  * - Open host strand → `null` (no members, nothing to invite into).
4274
4888
  * - Closed host strand whose founder launch was refused as pre-split
4275
4889
  * ({@link strandLaunchRefusals}) → rethrow the recorded `PreSplitStrandIdentityError`;