@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
package/src/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ConnectionGater, Libp2p, PeerId, PrivateKey } from '@libp2p/interface';
2
2
  import type { CircuitRelayServerInit } from '@libp2p/circuit-relay-v2';
3
3
  import type { IRawStorage, Libp2pConnectionMonitorInit, Libp2pTransports, NoiseCryptoInterface } from '@optimystic/db-p2p';
4
- import type { IPeerNetwork, IRepo } from '@optimystic/db-core';
4
+ import type { CollectionChangeListener, IPeerNetwork, IRepo } from '@optimystic/db-core';
5
5
  import type { PeerJoinBackfillConfig } from './peer-join-backfill.js';
6
6
  import type { StrandReactivityConfig } from './strand-reactivity.js';
7
7
  import type { StrandRevocationEnforcementConfig } from './strand-revocation-enforcer.js';
@@ -17,8 +17,11 @@ import type { StrandNetworkStateStore } from './strand-network-state.js';
17
17
  import type { EnrolledMachineStore } from './enrolled-machine-store.js';
18
18
  import type { JoinedStrandStore } from './joined-strand-store.js';
19
19
  import type { PushNotifier } from './push-notifier.js';
20
- import type { CadreInviteSignedFields, RevocableTable } from './control-authorization.js';
21
- import type { CadreInvitation, CadreInviteAddressOutcome } from './cadre-invite-protocol.js';
20
+ import type { CadreInviteSignedFields, OwnerGrantSignedFields, RevocableTable } from './control-authorization.js';
21
+ import type { FoundingInvite } from './control-database.js';
22
+ import type { CadreInvitation } from './cadre-invite-protocol.js';
23
+ import type { OwnerGrantBundle } from './owner-grant-protocol.js';
24
+ import type { RedemptionAddressOutcome } from './member-redemption.js';
22
25
  import type { ControlRetryAbandonment } from './control-retry.js';
23
26
  import type { FormationRejectionCode } from './strand-formation-rejection.js';
24
27
  import type { ListenTransportOptions } from './relay-addrs.js';
@@ -37,6 +40,21 @@ export interface Libp2pNodeWithRepo extends Libp2p {
37
40
  * backfill (`peer-join-backfill.ts`) logs once and stays inert when it is absent.
38
41
  */
39
42
  keyNetwork?: IPeerNetwork;
43
+ /**
44
+ * The node's block store, attached by the same factory (Optimystic's
45
+ * `OptimysticNodeAttachments.storageRepo`), narrowed to the storage-wide change feed: it
46
+ * fires for every block that lands in this node's store, a push or a commit. Optional for
47
+ * the same reason as `keyNetwork`; the first-sync gate (`strand-first-sync-gate.ts`) wakes
48
+ * on it, and logs once and probes on its backstop alone when it is absent.
49
+ */
50
+ storageRepo?: { onAnyCollectionChange(listener: CollectionChangeListener): () => void };
51
+ /**
52
+ * Tells the node a peer holds a block at a revision or later (Optimystic's
53
+ * `OptimysticNodeAttachments.reportBlockHeld`), so its under-replication drain and rebalance
54
+ * monitor do not push that peer the same block again. Optional for the same reason as
55
+ * `keyNetwork`; the join catch-up (`peer-join-backfill.ts`) reports each block a peer accepted.
56
+ */
57
+ reportBlockHeld?: (blockId: string, rev: number, peerId: string) => Promise<void>;
40
58
  }
41
59
 
42
60
  /**
@@ -576,8 +594,13 @@ export interface NetworkConfig {
576
594
  */
577
595
  targetDegree?: number;
578
596
  /**
579
- * Recurring reconcile cadence in ms. Defaults to
580
- * {@link DEFAULT_CONTROL_COHORT_RECONCILE_MS}.
597
+ * The reconcile pass's backstop in ms: a pass runs this long after the last one
598
+ * settled when nothing has started one sooner (a membership table change, a sibling
599
+ * connecting or disconnecting, an address change, a redial or strand-address wake).
600
+ * It also caps the redial ladder. Defaults to
601
+ * {@link DEFAULT_CONTROL_COHORT_RECONCILE_MS} (2 minutes). The membership rows view's
602
+ * maximum age (`MEMBERSHIP_VIEW_MAX_AGE_MS`, 15 s) does not follow this field: a backstop
603
+ * longer than it reads storage on every backstop pass, a shorter one every 15 s at most.
581
604
  */
582
605
  reconcileMs?: number;
583
606
  /**
@@ -646,10 +669,10 @@ export interface NetworkConfig {
646
669
  * pinging node's monitor sees the reset and aborts the connection exactly as it would on its
647
670
  * own timeout. So this deadline is only real up to the handler timeout of the node being
648
671
  * pinged, which has to stay above it. The deployed relay sets its handler to 60 s for that
649
- * reason (`ops/docker/libp2p-infra/src/main.ts`). Every cadre node's handler is Optimystic's
650
- * bare `ping()`, `@libp2p/ping`'s 10 s, and nothing here can move it
651
- * (`tickets/blocked/optimystic-ping-handler-timeout-below-the-monitor-deadline.md`): between
652
- * two cadre nodes, a ping whose echo takes longer than 10 s still drops the connection.
672
+ * reason (`ops/docker/libp2p-infra/src/main.ts`). Every cadre node's handler is Optimystic's,
673
+ * which derives it from this object: twice `pingTimeout.maxTimeout`, so 60 s
674
+ * (`NodeOptions.pingHandlerTimeoutMs`, unset here, since Optimystic 1.16.0). An app that
675
+ * replaces this object without a `pingTimeout` gets `@libp2p/ping`'s 10 s handler back.
653
676
  *
654
677
  * WHAT THE LINK NEEDS OF IT. One ping opens a fresh stream and echoes over it: a protocol
655
678
  * negotiation plus the echo, two link round trips by `link-budget.ts`'s counts (a `newStream`
@@ -776,11 +799,10 @@ export interface CadreNodeConfig {
776
799
  * form). Mutually exclusive with {@link privateKey}. Absent ⇒ use `privateKey`
777
800
  * when set, else libp2p generates an ephemeral key.
778
801
  *
779
- * In the single-key reference model the owner signing key is *derived from*
780
- * the node identity (see {@link CadreNode.getIdentityOwnerKey}), so
781
- * protecting the identity in a secure enclave protects the owner key too.
782
- * A future separate-owner slot (`ownerKeyId`) is anticipated but not
783
- * built here.
802
+ * The node signs owner writes through whatever owner signer the embedder wires
803
+ * (`CadreNode.useOwnerSigner`). An owner key has its own slot
804
+ * (`owner-key-store.ts`), which may be in this same store; this identity is never
805
+ * an owner key.
784
806
  */
785
807
  keyStore?: KeyStore;
786
808
 
@@ -844,10 +866,11 @@ export interface CadreNodeConfig {
844
866
 
845
867
  /**
846
868
  * Tuning for the strand peer-join block catch-up (see `peer-join-backfill.ts`):
847
- * when a strand's libp2p node connects to a peer this runtime has not yet
848
- * caught up, every block in the strand's own raw store is pushed to that peer,
849
- * so a machine that joined the strand after blocks were committed still ends
850
- * up physically holding them. Applies to every strand this node starts.
869
+ * when a strand's libp2p node connects (or reconnects) to a peer, every
870
+ * block in the strand's own raw store that the peer is not
871
+ * known to hold is pushed to it, each block once per peer, so a machine that
872
+ * joined the strand after blocks were committed still ends up physically
873
+ * holding them. Applies to every strand this node starts.
851
874
  * Omit for the defaults (`DEFAULT_PEER_JOIN_BACKFILL`); `{ enabled: false }`
852
875
  * restores the no-backfill behaviour. The control network runs its own,
853
876
  * separately tuned catch-up — see {@link controlBackfill}.
@@ -876,13 +899,14 @@ export interface CadreNodeConfig {
876
899
  /**
877
900
  * Tuning for the CONTROL network's peer-join block catch-up (same module,
878
901
  * `peer-join-backfill.ts`, wired in `CadreNode.start`): when the control
879
- * libp2p node connects to an AUTHORIZED party member this runtime has not yet
880
- * caught up, every block in the control database's own raw store is pushed to
881
- * it. This is what makes a control block committed while the writer was alone
882
- * (collection headers written at genesis above all) physically reach members
883
- * that joined later — without it, such a member that restarts offline reads
884
- * the affected tables as empty. Pushes are gated on
885
- * `CadreNode.isAuthorizedMember`, checked at push time, because the control
902
+ * libp2p node connects (or reconnects) to an AUTHORIZED party member, every
903
+ * block in the control database's own raw store that the
904
+ * member is not known to hold is pushed to it. This is what makes a control
905
+ * block committed while the writer was alone (collection headers written at
906
+ * genesis above all) physically reach members that joined later — without it,
907
+ * such a member that restarts offline reads the affected tables as empty.
908
+ * Pushes are gated on the node's authorized-member snapshot (the rule
909
+ * `CadreNode.isAuthorizedMember` reads), checked at push time, because the control
886
910
  * network's inbound gate deliberately admits non-members in several states
887
911
  * (seed delivery, enrollment, bootstrap/relay peers). Omit for the defaults
888
912
  * (`DEFAULT_PEER_JOIN_BACKFILL`); `{ enabled: false }` disables it.
@@ -895,9 +919,10 @@ export interface CadreNodeConfig {
895
919
  * starts: each strand node materializes the peer ids bound to REMOVED members
896
920
  * (orphaned `Strand.MemberPeer` rows) and refuses them at the stream and
897
921
  * connection layers, so removing a party cuts the network to its machines
898
- * rather than only deleting its row. Open strands never arm it. Omit for the
899
- * defaults (`DEFAULT_REVOCATION_POLL_INTERVAL_MS` refresh cadence);
900
- * `{ enabled: false }` restores the pre-existing behaviour.
922
+ * rather than only deleting its row. Open strands never arm it. Each strand
923
+ * re-reads its membership when `Strand.Member` or `Strand.MemberPeer` changes,
924
+ * and at the latest every `DEFAULT_REVOCATION_BACKSTOP_MS`. Omit for the
925
+ * defaults; `{ enabled: false }` restores the pre-existing behaviour.
901
926
  */
902
927
  strandRevocationEnforcement?: StrandRevocationEnforcementConfig;
903
928
 
@@ -910,9 +935,8 @@ export interface CadreNodeConfig {
910
935
  * background without blocking bring-up. `pollIntervalMs` is the IDLE cadence
911
936
  * (how often a machine nobody has admitted re-checks) and the cap of the
912
937
  * unfinished-join retry ladder, NOT the retry cadence itself — an unfinished
913
- * join retries from 1 s, doubling. Omit for the defaults (it mirrors
914
- * {@link strandRevocationEnforcement}'s `pollIntervalMs`);
915
- * `{ enabled: false }` disarms the loop — for test fixtures that hand-drive
938
+ * join retries from 1 s, doubling. Omit for the defaults
939
+ * (`DEFAULT_MEMBERSHIP_RECONCILE_INTERVAL_MS`); `{ enabled: false }` disarms the loop — for test fixtures that hand-drive
916
940
  * the membership writers and assert exact row sets.
917
941
  */
918
942
  strandMembershipReconciliation?: StrandMembershipReconciliationConfig;
@@ -922,15 +946,17 @@ export interface CadreNodeConfig {
922
946
  * (`strand-first-sync-gate.ts`), applied to every strand this node launches as a
923
947
  * non-founder: the strand's database is withheld from the app (`StrandInstance.database`
924
948
  * unset, status `'syncing'`) until the strand's `Strand.Header` has been received from
925
- * another member and every `App` table has been read once, because a machine that writes
926
- * to a table it has never fetched creates a private copy of it that never merges.
949
+ * another member and every `Strand` and `App` table has been read once, because a
950
+ * machine that writes to a table it has never fetched creates a private copy of it that
951
+ * never merges.
927
952
  * `timeoutMs` bounds how long {@link CadreNode.addStrand} waits before rejecting with
928
- * `StrandAwaitingFirstSyncError` (retryable — the launch stays up and keeps probing);
929
- * `pollIntervalMs` is the probe cadence. Omit for the defaults (`DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS`,
953
+ * `StrandAwaitingFirstSyncError` (retryable — the launch stays up and keeps probing).
954
+ * While gated the machine probes whenever data lands in the strand's store, and
955
+ * `backstopMs` after its last probe when none has. Omit for the defaults (`DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS`,
930
956
  * sized for a machine re-attaching through a relay on a slow link, the slowest attach
931
957
  * measured; that constant's doc comment carries the number, the measurement behind it and
932
- * what the budget costs, and is the only copy of them; `DEFAULT_STRAND_FIRST_SYNC_POLL_MS`,
933
- * 500 ms). There is deliberately no way to disable the gate: a machine whose store already
958
+ * what the budget costs, and is the only copy of them; `DEFAULT_STRAND_FIRST_SYNC_BACKSTOP_MS`,
959
+ * 2 s). There is deliberately no way to disable the gate: a machine whose store already
934
960
  * holds the Header is never gated, so nothing that works today is blocked by it.
935
961
  */
936
962
  strandFirstSync?: StrandFirstSyncConfig;
@@ -938,7 +964,11 @@ export interface CadreNodeConfig {
938
964
  /** Hibernation configuration */
939
965
  hibernation?: HibernationConfig;
940
966
 
941
- /** Polling interval for strand watcher in ms (default: 5000) */
967
+ /**
968
+ * The strand watcher's backstop in ms (default 300000): the longest it goes without
969
+ * re-reading `Strand` and `JoinedStrand` when no change to them is heard. It otherwise runs
970
+ * on a change. Also caps the first step of its retry ladders, which is 5 s otherwise.
971
+ */
942
972
  strandWatchInterval?: number;
943
973
 
944
974
  /**
@@ -972,7 +1002,7 @@ export interface CadreNodeConfig {
972
1002
  *
973
1003
  * A node has exactly one cold-start trust source, so `claim` beside a non-empty
974
1004
  * `trustedOwners.pinnedKeys` or a `seedTrustPolicy` fails `start()` closed, and
975
- * `initializeSeedBootstrap` (an owner key) refuses on such a node: a hosted node never
1005
+ * `useOwnerSigner` (an owner key) refuses on such a node: a hosted node never
976
1006
  * holds an owner key. A claimed node restarted with this still set ignores it (its
977
1007
  * anchor is non-empty), so the operator need not remove it.
978
1008
  *
@@ -1025,7 +1055,7 @@ export interface CadreNodeConfig {
1025
1055
  /**
1026
1056
  * Provenance recorded for {@link pinnedKeys}. Default: 'operator'.
1027
1057
  * ('genesis' is reserved for the node's own founding key, seeded
1028
- * internally by `initializeSeedBootstrap`; 'chain' is written only by the
1058
+ * internally by `useOwnerSigner`; 'chain' is written only by the
1029
1059
  * anchor sync, `CadreNode.syncOwnerAnchor`.)
1030
1060
  */
1031
1061
  pinnedSource?: Exclude<TrustSource, 'genesis' | 'chain'>;
@@ -1272,16 +1302,21 @@ export interface StrandRow {
1272
1302
  MemberPrivateKey: string | null;
1273
1303
  Type: 'o' | 'c';
1274
1304
  /**
1275
- * ed25519 (base64url) owner key of the MACHINE that published this row — the
1276
- * signer of the owner-signed `Strand` insert (`== context.OwnerKey`, pinned by
1277
- * the schema's `AuthorizedInsert`). Identifies the founding machine, so a
1278
- * launch with no explicit `founder` flag derives "am I the founder?" by
1279
- * comparing it to the node's own owner key (see `CadreNode.launchStrand`).
1280
- * `null` on a consent-seated strand (the unsigned `FormationUsage` branch
1281
- * records no trustworthy signer). Provenance, not content —
1282
- * `strandRowMismatches` excludes it from the identical-content comparison.
1305
+ * ed25519 (base64url) owner key that signed the owner-signed `Strand` insert
1306
+ * (`== context.OwnerKey`, pinned by the schema's `AuthorizedInsert`). `null` on a
1307
+ * consent-seated strand (the unsigned `FormationUsage` branch records no
1308
+ * signer). Provenance, not content — `strandRowMismatches` excludes it from the
1309
+ * identical-content comparison.
1283
1310
  */
1284
1311
  FounderOwnerKey: string | null;
1312
+ /**
1313
+ * Peer id of the machine that runs the founder bootstrap, named by the owner
1314
+ * signature on the insert (`CadreNode.publishStrand` writes its own). A launch
1315
+ * with no explicit `founder` flag founds iff this is its own peer id (see
1316
+ * `CadreNode.launchStrand`), so two machines holding one owner key never both
1317
+ * found. `null` on a consent-seated strand. Provenance, as {@link FounderOwnerKey}.
1318
+ */
1319
+ FounderPeerId: string | null;
1285
1320
  }
1286
1321
 
1287
1322
  /**
@@ -1404,7 +1439,7 @@ export interface StrandConfig {
1404
1439
  * unset and writes nothing — it receives those rows via Optimystic sync.
1405
1440
  *
1406
1441
  * When unset, founder-ness is DERIVED from the row: this node founds iff
1407
- * {@link StrandRow.FounderOwnerKey} equals its own owner key. An explicit
1442
+ * {@link StrandRow.FounderPeerId} is its own peer id. An explicit
1408
1443
  * `true`/`false` wins over the derivation — the formation/responder flows pass
1409
1444
  * it deliberately, since a consent-seated row carries a null column.
1410
1445
  */
@@ -1457,6 +1492,14 @@ export interface FoundStrandConfig {
1457
1492
  memberPrivateKey?: string;
1458
1493
  /** sApp configuration the hosting application provides, as for {@link StrandConfig}. */
1459
1494
  sAppConfig: SAppConfig;
1495
+ /**
1496
+ * A formation invitation to publish bound to this strand, for the app `sAppConfig.id`, in the
1497
+ * same control transaction and owner approval as the strand row. Mint it first with
1498
+ * `createOpenInvitation` (its `expiration` is `expiresAtMs`). When the strand is already
1499
+ * published the invitation is published on its own, under its own approval, as
1500
+ * `publishFormationInvite` would; one already stored under `token` for this strand is kept.
1501
+ */
1502
+ formationInvite?: Omit<FoundingInvite, 'sAppId'>;
1460
1503
  /**
1461
1504
  * Forwarded to the attach ({@link StrandConfig.awaitFirstSync}). Matters only when the
1462
1505
  * call ATTACHES rather than founds — the stored row was published by a sibling machine
@@ -1492,6 +1535,37 @@ export interface FoundStrandResult {
1492
1535
  founded: boolean;
1493
1536
  }
1494
1537
 
1538
+ /**
1539
+ * Options for {@link CadreNode.foundStrand}. Passing them selects the form that returns once
1540
+ * the row is published ({@link FoundStrandBackgroundResult}), for a caller that needs only the
1541
+ * row: minting an invitation reads just its id and membership key.
1542
+ *
1543
+ * An invitation redeemed before the start finishes is refused, retryably: a closed host
1544
+ * strand answers `host-strand-unavailable` and leaves the token unspent. For an open host
1545
+ * strand the approval is sent, but carries no strand addresses, because the strand node does
1546
+ * not exist yet.
1547
+ */
1548
+ export interface FoundStrandOptions {
1549
+ /** Return once the row is published; the start continues in {@link FoundStrandBackgroundResult.ready}. */
1550
+ background: true;
1551
+ }
1552
+
1553
+ /** What {@link CadreNode.foundStrand} returns with `{ background: true }`: the row is published; the local start continues. */
1554
+ export interface FoundStrandBackgroundResult {
1555
+ /** The live control-plane row: freshly published, or the one already there. Same as {@link FoundStrandResult.strandRow}. */
1556
+ strandRow: StrandRow;
1557
+ /** Same as {@link FoundStrandResult.founded}. */
1558
+ founded: boolean;
1559
+ /**
1560
+ * Settles when the local start does. Resolves with the instance `addStrand` returns.
1561
+ * Rejects with the start's own error. After a launch failure the strand stays published
1562
+ * and the strand watcher keeps retrying it. After a first-sync timeout (only an adopted,
1563
+ * not-founded row waits for one) the instance stays up and still syncing, so wait with
1564
+ * {@link CadreNode.whenStrandWritable}.
1565
+ */
1566
+ ready: Promise<StrandInstance>;
1567
+ }
1568
+
1495
1569
  /**
1496
1570
  * Result of creating a new cadre peer
1497
1571
  */
@@ -1742,14 +1816,14 @@ export interface CadreNodeEvents {
1742
1816
  * Also emitted for a strand this node joined from ANOTHER party, which no control
1743
1817
  * row names: the node remembers every such join (see
1744
1818
  * {@link CadreNodeConfig.joinedStrands}) and offers it here on each start, as a row
1745
- * with `FounderOwnerKey: null` carrying the closed strand's `MemberPrivateKey`. That
1819
+ * with null founder columns carrying the closed strand's `MemberPrivateKey`. That
1746
1820
  * row is the product of the formation's consent, so claiming it needs no second one.
1747
1821
  *
1748
1822
  * **Fired once per strand per session, and it can fire before your listener is
1749
- * attached.** The strand watcher's first poll runs inside `CadreNode.start()`
1823
+ * attached.** The strand watcher's first run is inside `CadreNode.start()`
1750
1824
  * (100 ms after the watcher starts), so every strand already stored for this
1751
1825
  * party is normally offered while the embedding app is still inside its own
1752
- * `start()` await. The watcher then records the strand as seen and no later poll
1826
+ * `start()` await. The watcher then records the strand as seen and no later run
1753
1827
  * re-offers it, so a listener attached a moment late misses those strands for the
1754
1828
  * life of the process.
1755
1829
  *
@@ -1804,8 +1878,14 @@ export interface CadreNodeEvents {
1804
1878
  'pendingJoin:changed': PendingJoinStatus;
1805
1879
  /** Emitted when a seed is received via the seed protocol */
1806
1880
  'seed:received': { partyId: string; peerId: string };
1807
- /** Emitted when a seed is successfully applied */
1808
- 'seed:applied': { partyId: string; peersAdded: number };
1881
+ /**
1882
+ * Emitted when a seed delivered over the seed protocol has been verified, merged and
1883
+ * acknowledged to its sender. The dials of its preferred peers follow this event, so it
1884
+ * waits on no connection. `dialTargets` are those peers (`seedDialTargets`): an embedder
1885
+ * that restarts into another party's stores carries them there, since this node retained
1886
+ * them in its own.
1887
+ */
1888
+ 'seed:applied': { partyId: string; peersAdded: number; dialTargets: SeedDialTarget[] };
1809
1889
  /** Emitted when seed application fails */
1810
1890
  'seed:error': { partyId: string; error: string };
1811
1891
  /**
@@ -1813,10 +1893,10 @@ export interface CadreNodeEvents {
1813
1893
  * valid claim proof has been recorded (`claim.record`) and has durably anchored its
1814
1894
  * signer as this node's owner. Carries the claim's party beside the owner key: the
1815
1895
  * party this node should serve from now on, which its own config did not know. Fires
1816
- * BEFORE the seed is acknowledged to the claimant, its peers merged and its owners
1817
- * dialed, so it precedes that seed's `seed:applied`; an embedder that restarts into
1818
- * the claimed party on this event should wait for that `seed:applied` (or `seed:error`)
1819
- * so the acknowledgement is not lost with the node. `CadreNode.isAwaitingClaim` is the
1896
+ * BEFORE the seed is acknowledged to the claimant and its peers merged, so it precedes
1897
+ * that seed's `seed:applied`; an embedder that restarts into the claimed party on this
1898
+ * event should wait for that `seed:applied` (or `seed:error`) so the acknowledgement is
1899
+ * not lost with the node. `CadreNode.isAwaitingClaim` is the
1820
1900
  * matching predicate for a reader that subscribed too late to see the event.
1821
1901
  */
1822
1902
  'claim:accepted': NodeClaimRecord;
@@ -1915,25 +1995,27 @@ export type CadrePeerVoucherFields = Pick<CadrePeerRow, 'peerId' | 'stampId' | '
1915
1995
 
1916
1996
  /**
1917
1997
  * One live `CadreControl.OwnerKey` row as `ControlDatabase.queryOwnerKeyRows` reads it: the
1918
- * key and the persisted proof of how it was seated. The three proof shapes are the schema's
1919
- * (`OwnerKey.Authorized`): the founding row (all three null), an owner-signed add (`vouchOwner`
1920
- * and `vouchSig`, the signer's signature over `ownerKeyAddDigest(key, stampId)`), or an
1921
- * owner-granting invitation redeemed by consent (`vouchOwner` the issuer, `vouchSig` null,
1922
- * `vouchUsage` the `CadreInviteUsage` row). Rows whose stamp is retired in `Revocation`, and
1923
- * the stampless row the schema permits but no writer produces, never reach a reader, so
1924
- * `stampId` is non-null here. The anchor sync (`deriveOwnerAnchor`) judges
1925
- * the proof against the keys it already trusts, never against the table itself.
1998
+ * key, its label and the persisted proof of how it was seated. The three proof shapes are the
1999
+ * schema's (`OwnerKey.Authorized`): the founding row (all three null), an owner-signed add
2000
+ * (`vouchOwner` and `vouchSig`, the signer's signature over `ownerKeyAddDigest(key, label, stampId)`), or an
2001
+ * owner grant redeemed by consent (`vouchOwner` the issuer, `vouchSig` null, `vouchUsage` the
2002
+ * `OwnerGrantUsage` row). Rows whose stamp is
2003
+ * retired in `Revocation`, and the stampless row the schema permits but no writer produces,
2004
+ * never reach a reader, so `stampId` is non-null here. The anchor sync (`deriveOwnerAnchor`)
2005
+ * judges the proof against the keys it already trusts, never against the table itself.
1926
2006
  */
1927
2007
  export interface OwnerKeyRow {
1928
2008
  /** ed25519 public key (base64url) — the row key. */
1929
2009
  key: string;
2010
+ /** The user's name for the key, bound into an owner-signed add or into the grant that seated it; null when none was given. */
2011
+ label: string | null;
1930
2012
  /** Single-use anti-replay nonce the voucher signature, or the usage row, is bound to. */
1931
2013
  stampId: string;
1932
- /** The owner that signed the add, or that issued the invitation that admitted it; null on the founding row. */
2014
+ /** The owner that signed the add, or that issued the grant that seated it; null on the founding row. */
1933
2015
  vouchOwner: string | null;
1934
- /** That owner's signature over the row's `'add'` digest, base64url; null on the founding row and on an invitation-admitted row. */
2016
+ /** That owner's signature over the row's `'add'` digest, base64url; null on the founding row and on a consent-seated row. */
1935
2017
  vouchSig: string | null;
1936
- /** The `CadreInviteUsage.UsageStampId` that admitted this row; null otherwise. */
2018
+ /** The `OwnerGrantUsage.UsageStampId` that seated this row; null otherwise. */
1937
2019
  vouchUsage: string | null;
1938
2020
  }
1939
2021
 
@@ -1967,10 +2049,8 @@ export interface CadreInviteUsageRow {
1967
2049
  peerId: string;
1968
2050
  /** The admitted device's ed25519 public key (base64url). */
1969
2051
  peerKey: string;
1970
- /** The `CadrePeer.StampId` this redemption seated (or found seated). */
2052
+ /** The `CadrePeer.StampId` this redemption seated. */
1971
2053
  peerStampId: string;
1972
- /** The `OwnerKey.StampId` this redemption seated, when the invitation grants ownership; else null. */
1973
- ownerStampId: string | null;
1974
2054
  /** The invitation private key's signature over the `'redeem'` digest, base64url. */
1975
2055
  inviteSig: string;
1976
2056
  /** The device's signature over the `'consent'` digest, base64url. */
@@ -1978,11 +2058,9 @@ export interface CadreInviteUsageRow {
1978
2058
  }
1979
2059
 
1980
2060
  /**
1981
- * The usage and invitation rows an invitation-admitted row is verified through, keyed by
1982
- * `UsageStampId` and `CadreInvite.Key`: a `CadrePeer` row through `verifyInvitationAdmission`
1983
- * (`CadreNode.listAuthorizedMembers`) and an `OwnerKey` row through
1984
- * `verifyInvitationOwnerAdmission` (`deriveOwnerAnchor`). Loaded once per membership refresh
1985
- * (`CadreNode.loadInvitationChain`) and shared by both readers.
2061
+ * The usage and invitation rows an invitation-admitted `CadrePeer` row is verified through
2062
+ * (`verifyInvitationAdmission`, `CadreNode.listAuthorizedMembers`), keyed by `UsageStampId`
2063
+ * and `CadreInvite.Key`. Loaded once per membership refresh (`CadreNode.loadInvitationChain`).
1986
2064
  */
1987
2065
  export interface InvitationChain {
1988
2066
  usages: Map<string, CadreInviteUsageRow>;
@@ -1990,9 +2068,56 @@ export interface InvitationChain {
1990
2068
  }
1991
2069
 
1992
2070
  /**
1993
- * One machine of the cadre as `CadreNode.listMachines` reports it: a live `CadrePeer` row,
1994
- * or a live `OwnerKey` whose key derives a peer id with no `CadrePeer` row (an owner key
1995
- * seated for a machine not yet admitted, or a founder that never self-published).
2071
+ * One `CadreControl.OwnerGrant` row: an owner-signed grant of ownership to whoever holds its
2072
+ * private half, as `ControlDatabase.insertOwnerGrant` returns it and `queryOwnerGrant` reads
2073
+ * it. Every column is here because the row travels, as a {@link CadreInviteRow} does: the
2074
+ * holder carries it inside the grant bundle and a member re-seats it verbatim
2075
+ * (`seatOwnerGrant`), so `expiresAt` is the engine-canonical `datetime` string the row
2076
+ * stores. The signed subset is `OwnerGrantSignedFields` (`control-authorization.ts`); the
2077
+ * grant names no machine and no peer id.
2078
+ */
2079
+ export interface OwnerGrantRow extends OwnerGrantSignedFields {
2080
+ /** The owner that signed the row (`IssuerKey`, base64url ed25519); a reader checks it against its anchor. */
2081
+ issuerKey: string;
2082
+ /** That owner's `'add'` signature over the row (`IssuerSig`, base64url). */
2083
+ issuerSig: string;
2084
+ }
2085
+
2086
+ /**
2087
+ * One `CadreControl.OwnerGrantUsage` row: a redemption of an {@link OwnerGrantRow}, as
2088
+ * `ControlDatabase.queryOwnerGrantUsages` reads it. Both signatures are stored so any reader
2089
+ * can re-verify the chain grant → usage → `OwnerKey` row (`verifyOwnerGrantAdmission`).
2090
+ */
2091
+ export interface OwnerGrantUsageRow {
2092
+ /** The redemption's single-use nonce (the row key), signed into both signatures. */
2093
+ usageStampId: string;
2094
+ /** The `OwnerGrant.Key` redeemed. */
2095
+ grantKey: string;
2096
+ /** The owner key seated (base64url ed25519): the holder's, fresh or already held. */
2097
+ seatedKey: string;
2098
+ /** The `OwnerKey.StampId` this redemption seated. */
2099
+ seatedStampId: string;
2100
+ /** The grant private key's signature over the `'redeem'` digest, base64url. */
2101
+ grantSig: string;
2102
+ /** The seated key's signature over the `'consent'` digest, base64url. */
2103
+ ownerSig: string;
2104
+ }
2105
+
2106
+ /**
2107
+ * The usage and grant rows a grant-seated `OwnerKey` row is verified through, keyed by
2108
+ * `UsageStampId` and `OwnerGrant.Key` (`verifyOwnerGrantAdmission`, from `deriveOwnerAnchor`).
2109
+ * The grant twin of {@link InvitationChain}, loaded beside it by `CadreNode.syncOwnerAnchor`
2110
+ * when some owner row is consent-seated; a row's `vouchUsage` is looked up in this chain
2111
+ * first and in the invitation chain otherwise.
2112
+ */
2113
+ export interface OwnerGrantChain {
2114
+ usages: Map<string, OwnerGrantUsageRow>;
2115
+ grants: Map<string, OwnerGrantRow>;
2116
+ }
2117
+
2118
+ /**
2119
+ * One machine of the cadre as `CadreNode.listMachines` reports it: a live `CadrePeer` row. An
2120
+ * owner key is not a machine; `CadreNode.listOwnerKeys` lists those.
1996
2121
  *
1997
2122
  * `profile` and `lastPublishedAt` are the machine's own claim only when the row's
1998
2123
  * self-signature verifies; both are null otherwise, so an owner re-vouch that bumped
@@ -2001,58 +2126,108 @@ export interface InvitationChain {
2001
2126
  */
2002
2127
  export interface CadreMachine {
2003
2128
  peerId: string;
2004
- /** ed25519 public key (base64url) behind `peerId`; null when the id is not ed25519. */
2129
+ /** The machine's ed25519 identity key (base64url) behind `peerId`; null when the id is not ed25519. Never an owner key (`listOwnerKeys` lists those). */
2005
2130
  publicKey: string | null;
2006
2131
  /** This node itself. */
2007
2132
  isSelf: boolean;
2008
- /** `publicKey` is a live `OwnerKey` here. */
2009
- isOwner: boolean;
2010
- /** Has a live `CadrePeer` row; false for an owner key with no machine row. */
2011
- isMember: boolean;
2012
2133
  /** Passes this node's authorized-membership predicate now (`CadreNode.listAuthorizedMembers`; always false for self, which that set excludes). */
2013
2134
  trusted: boolean;
2014
2135
  /** The machine's self-published node profile; null until it publishes, or when its signature does not verify. */
2015
2136
  profile: NodeProfile | null;
2016
2137
  /** `UpdatedAt` of its self-signed record, re-stamped while it runs; null on the same terms as `profile`. */
2017
2138
  lastPublishedAt: number | null;
2018
- /** `CadrePeer.VouchOwner` (or `OwnerKey.VouchOwner` for an owner-only entry): the owner that vouched it, or the issuer of the invitation that admitted it. */
2139
+ /** `CadrePeer.VouchOwner`: the owner key that vouched it, or the issuer of the invitation that admitted it. */
2140
+ vouchedBy: string | null;
2141
+ }
2142
+
2143
+ /** One live owner key as `CadreNode.listOwnerKeys` lists it, for an app's owner-key screen. */
2144
+ export interface CadreOwnerKey {
2145
+ /** ed25519 public key (base64url). */
2146
+ key: string;
2147
+ /** The user's name for the key ("phone", "paper backup"); null when none was given. */
2148
+ label: string | null;
2149
+ /** `OwnerKey.VouchOwner`: the owner that signed its add, or that issued the owner grant that seated it; null on the founding key. */
2019
2150
  vouchedBy: string | null;
2151
+ /** This node's owner signer holds it. */
2152
+ heldHere: boolean;
2153
+ /** In this node's trusted-owner anchor, so this node accepts what it signs. */
2154
+ trusted: boolean;
2020
2155
  }
2021
2156
 
2157
+ /** Why `CadreNode.addOwner` refuses by name (`OwnerKeyAddRefusedError.code`). */
2158
+ export type OwnerKeyAddRefusal =
2159
+ /** No sibling reachable (`CadreNode.hasReachableControlSibling`) while the party has other members: the row would stay on this node, and nothing re-issues an `OwnerKey` add. */
2160
+ | 'alone';
2161
+
2022
2162
  /** Why `CadreNode.removeMachine` refuses by name (`MachineRemovalRefusedError.code`). */
2023
2163
  export type MachineRemovalRefusal =
2024
2164
  /** This node holds no enrolled owner key, so it cannot sign the removal. */
2025
2165
  | 'not-an-owner'
2026
- /** The machine is this node: an owner cannot sign its own `OwnerKey` removal, so ask another owner. */
2027
- | 'self'
2028
- /** The machine holds the only live owner key here (`MinOneOwner`). */
2029
- | 'last-owner';
2166
+ /** The machine is this node: leaving a cadre is the embedder's flow (stop the node, delete its data), not a removal. */
2167
+ | 'self';
2030
2168
 
2031
2169
  /**
2032
2170
  * What `CadreNode.removeMachine` would do for one machine, read without writing
2033
- * (`CadreNode.planMachineRemoval`), for an app's confirmation step.
2171
+ * (`CadreNode.planMachineRemoval`), for an app's confirmation step. A machine vouches nothing
2172
+ * (a `VouchOwner` is an owner key), so removing one strands no other row.
2034
2173
  */
2035
2174
  export interface MachineRemovalPlan {
2036
2175
  /** The machine as listed, or null when no live row names it here (already removed). */
2037
2176
  machine: CadreMachine | null;
2038
2177
  refusal: MachineRemovalRefusal | null;
2039
- /** The machine's key is a live owner key, so the removal also retires it. */
2040
- removesOwnerKey: boolean;
2041
- /** Rows the machine's owner key vouched, which stop passing the membership predicate everywhere once it is removed unless re-vouched first. Empty unless `removesOwnerKey`. */
2042
- dependents: {
2043
- /** Live `OwnerKey` rows whose `VouchOwner` is the machine's key, this node's own excluded. */
2044
- owners: string[];
2045
- /** Live `CadrePeer` rows whose `VouchOwner` is the machine's key, the machine itself and this node excluded. */
2046
- peers: string[];
2047
- };
2048
2178
  /** Live `CadreInvite` keys the machine could redeem to come back: targeted at it, or already redeemed by it with uses left. */
2049
2179
  invitations: string[];
2050
- /** This node's own `OwnerKey` or `CadrePeer` row was vouched by the machine. It cannot re-vouch its own rows; another owner has to re-add it afterwards. */
2180
+ }
2181
+
2182
+ /** What `CadreNode.removeMachine` wrote. All false and empty when no live row named the machine here. */
2183
+ export interface MachineRemovalResult {
2184
+ removedPeer: boolean;
2185
+ removedDeviceToken: boolean;
2186
+ /** `CadreInvite` keys whose withdrawal tombstone this call filed. */
2187
+ withdrawnInvitations: string[];
2188
+ }
2189
+
2190
+ /** Why `CadreNode.removeOwner` refuses by name (`OwnerKeyRemovalRefusedError.code`). */
2191
+ export type OwnerKeyRemovalRefusal =
2192
+ /** This node holds no enrolled owner key, so it cannot sign the removal. */
2193
+ | 'not-an-owner'
2194
+ /** No live `OwnerKey` row names the key here (never an owner, or already removed). */
2195
+ | 'unknown'
2196
+ /** The key is the only live owner key (`MinOneOwner`); no owner can remove it. */
2197
+ | 'last-owner'
2198
+ /** The key is this node's own owner key: an owner cannot sign its own row's removal (`A.Key <> old.Key`), so ask another owner key. */
2199
+ | 'self'
2200
+ /** A re-vouch of an `OwnerKey` row was asked for while this node reaches no sibling and the party has other members: the re-added row has no re-issue path. Decided at call time, so never planned. */
2201
+ | 'alone';
2202
+
2203
+ /** The rows sharing the owner keys' voucher column that a removed owner key strands unless re-vouched first. */
2204
+ export interface OwnerKeyDependents {
2205
+ /** Live `OwnerKey` rows whose `VouchOwner` is the key, this node's own excluded. */
2206
+ owners: string[];
2207
+ /** Live `CadrePeer` rows (peer ids) whose `VouchOwner` is the key, this node's own excluded. */
2208
+ peers: string[];
2209
+ }
2210
+
2211
+ /**
2212
+ * What `CadreNode.removeOwner` would do for one owner key, read without writing
2213
+ * (`CadreNode.planOwnerKeyRemoval`), for an app's confirmation step.
2214
+ */
2215
+ export interface OwnerKeyRemovalPlan {
2216
+ /** The key as `CadreNode.listOwnerKeys` lists it, or null when no live row names it here. */
2217
+ ownerKey: CadreOwnerKey | null;
2218
+ refusal: Exclude<OwnerKeyRemovalRefusal, 'alone'> | null;
2219
+ /** Rows the key vouched, which stop passing the membership predicate on every machine once it is removed unless re-vouched first. */
2220
+ dependents: OwnerKeyDependents;
2221
+ /** This node's own `CadrePeer` row was vouched by the key, so `removeOwner` re-vouches it under this node's key first, unasked: left on the removed key, every other machine would drop this node. */
2222
+ revouchesOwnRow: boolean;
2223
+ /** This node's own `OwnerKey` row was vouched by the key. It cannot re-vouch that row (the re-add's delete would be its own removal); it keeps its place in every anchor that already derived it, and another owner can re-vouch it afterwards. */
2051
2224
  ownDependency: boolean;
2225
+ /** Live owner grants the key issued, which stop being redeemable once it is removed (`OwnerGrantUsage.Authorized` wants a live issuer): an app says "the backup made with this key stops working". Withdrawn, expired and spent grants are not listed. */
2226
+ grants: OwnerGrantRow[];
2052
2227
  }
2053
2228
 
2054
- /** Options for `CadreNode.removeMachine`. */
2055
- export interface RemoveMachineOptions {
2229
+ /** Options for `CadreNode.removeOwner`. */
2230
+ export interface RemoveOwnerOptions {
2056
2231
  /** Dependents to re-vouch under this node's key before the removal; each must appear in the plan's `dependents`. */
2057
2232
  revouch?: {
2058
2233
  owners?: readonly string[];
@@ -2060,18 +2235,12 @@ export interface RemoveMachineOptions {
2060
2235
  };
2061
2236
  }
2062
2237
 
2063
- /** What `CadreNode.removeMachine` wrote. All false and empty when no live row named the machine here. */
2064
- export interface MachineRemovalResult {
2065
- removedPeer: boolean;
2066
- removedOwnerKey: boolean;
2067
- removedDeviceToken: boolean;
2068
- /** `CadreInvite` keys whose withdrawal tombstone this call filed. */
2069
- withdrawnInvitations: string[];
2070
- /** Dependents re-vouched under this node's key before the removal. */
2071
- revouched: {
2072
- owners: string[];
2073
- peers: string[];
2074
- };
2238
+ /** What `CadreNode.removeOwner` wrote. */
2239
+ export interface OwnerKeyRemovalResult {
2240
+ /** The `OwnerKey` row and its tombstone were written; false only when another owner's removal reached this node between the plan and the write. */
2241
+ removed: boolean;
2242
+ /** Rows re-vouched under this node's key before the removal: the dependents asked for, and this node's own row when the plan's `revouchesOwnRow`. */
2243
+ revouched: OwnerKeyDependents;
2075
2244
  }
2076
2245
 
2077
2246
  /**
@@ -2105,14 +2274,36 @@ export interface CadreInviteStatus {
2105
2274
 
2106
2275
  /**
2107
2276
  * What `ControlDatabase.redeemCadreInvite` did. `alreadyMember` is the idempotent
2108
- * retry after a dropped reply: the device's row (and, for an owner-granting
2109
- * invitation, its `OwnerKey` row) was already present, so nothing was written and
2110
- * no seat was spent. The stamps are the live rows' either way.
2277
+ * retry after a dropped reply: the device's row was already present, so nothing was
2278
+ * written and no seat was spent. The stamp is the live row's either way.
2111
2279
  */
2112
2280
  export interface CadreInviteRedemptionResult {
2113
2281
  alreadyMember: boolean;
2114
2282
  peerStampId: string;
2115
- ownerStampId: string | null;
2283
+ }
2284
+
2285
+ /**
2286
+ * One {@link OwnerGrantRow} with its standing, as `ControlDatabase.listOwnerGrantStatuses`
2287
+ * reports it. `live` is the redemption's own test: not withdrawn, unexpired, uses left,
2288
+ * issuer still an owner here.
2289
+ */
2290
+ export interface OwnerGrantStatus {
2291
+ grant: OwnerGrantRow;
2292
+ live: boolean;
2293
+ withdrawn: boolean;
2294
+ /** Redemptions this node holds for it (`OwnerGrantUsage` rows). */
2295
+ usesRecorded: number;
2296
+ }
2297
+
2298
+ /**
2299
+ * What `ControlDatabase.redeemOwnerGrant` did. `alreadyOwner` is the idempotent retry after
2300
+ * a dropped reply, and the case of a holder seating a key that is already an owner: the
2301
+ * key's live `OwnerKey` row was already present, so nothing was written and no use was
2302
+ * spent. `seatedStampId` is the live row's either way.
2303
+ */
2304
+ export interface OwnerGrantRedemptionResult {
2305
+ alreadyOwner: boolean;
2306
+ seatedStampId: string;
2116
2307
  }
2117
2308
 
2118
2309
  /**
@@ -2120,8 +2311,8 @@ export interface CadreInviteRedemptionResult {
2120
2311
  * `ControlDatabase.queryRevocations`: the identity triple plus the `ReissuedAt`
2121
2312
  * counter an owner bumps (`ControlDatabase.reissueRevocations`) to re-write — and
2122
2313
  * therefore re-broadcast — a tombstone that committed while the node was alone.
2123
- * Never the ledger marker: that row names no guarded table, and `queryRevocations`
2124
- * skips it, which is what keeps `tableName` a {@link RevocableTable}.
2314
+ * Never the ledger marker an older party may hold: that row names no guarded table, and
2315
+ * `queryRevocations` skips it, which is what keeps `tableName` a {@link RevocableTable}.
2125
2316
  */
2126
2317
  export interface RevocationRow {
2127
2318
  /** Which guarded table's stamp was retired. */
@@ -2138,14 +2329,6 @@ export interface RevocationRow {
2138
2329
  signerSig: string;
2139
2330
  }
2140
2331
 
2141
- /**
2142
- * Outcome of `ControlDatabase.openRevocationLedger`: `'opened'` when this call filed the
2143
- * singleton `Revocation` ledger marker, `'already-open'` when it was already filed (seen
2144
- * by the in-lock guard, or filed first by another owner and refused on the primary key).
2145
- * Either way the marker exists afterwards.
2146
- */
2147
- export type RevocationLedgerOpenResult = 'opened' | 'already-open';
2148
-
2149
2332
  /**
2150
2333
  * Outcome of {@link CadreNode.registerSelf}, surfaced so callers (e.g. the CLI
2151
2334
  * `--owner` branch) can log what happened:
@@ -2329,7 +2512,7 @@ export interface ApnsCredentials {
2329
2512
  // ============================================================================
2330
2513
 
2331
2514
  /**
2332
- * A peer entry in a control network seed.
2515
+ * A peer entry in a control network seed, or in a cadre invitation redemption reply.
2333
2516
  * Contains connection information for a cadre peer.
2334
2517
  */
2335
2518
  export interface SeedPeer {
@@ -2337,12 +2520,17 @@ export interface SeedPeer {
2337
2520
  peerId: string;
2338
2521
  /** Multiaddrs for connecting to this peer */
2339
2522
  multiaddrs: string[];
2340
- /** Whether this peer holds an owner key */
2341
- isOwner: boolean;
2342
- /** ed25519 public key (base64url) — present on owner peers for signerKey verification */
2343
- publicKey?: string;
2523
+ /**
2524
+ * Dial this peer first: the machine that minted the seed (or answered the redemption) and
2525
+ * every always-on machine (`CadrePeer.Profile = 'storage'`). A dial hint only — it grants
2526
+ * nothing, and says nothing about owner keys (`projectSeedPeers`).
2527
+ */
2528
+ preferred: boolean;
2344
2529
  }
2345
2530
 
2531
+ /** A peer a seed nominates for dialing, and the addresses to dial it at (`seedDialTargets`). */
2532
+ export type SeedDialTarget = Pick<SeedPeer, 'peerId' | 'multiaddrs'>;
2533
+
2346
2534
  /**
2347
2535
  * Control network seed for bootstrapping new nodes.
2348
2536
  * Pre-populates a new node's cache to solve the cold-start problem:
@@ -2553,15 +2741,15 @@ export interface ApplySeedResult {
2553
2741
  /** Machine-readable refusal cause, when the refusing trust policy supplied one. */
2554
2742
  code?: SeedRefusalCode;
2555
2743
  /**
2556
- * Owner-flagged seed peers this apply attempted to dial. Zero when the seed
2557
- * carried no owner peer with an address, or when the seed was rejected before
2744
+ * Preferred seed peers (`SeedPeer.preferred`) this apply attempted to dial. Zero when the
2745
+ * seed carried no preferred peer with an address, or when the seed was rejected before
2558
2746
  * the dial loop ran.
2559
2747
  */
2560
- ownerDialsAttempted: number;
2748
+ preferredDialsAttempted: number;
2561
2749
  /**
2562
- * How many of those dials threw. `ownerDialsAttempted > 0 && ownerDialsFailed
2563
- * === ownerDialsAttempted` means "seeded but stranded": the seed itself was
2564
- * accepted (`success: true`) but no owner could be reached, so this node has
2750
+ * How many of those dials threw. `preferredDialsAttempted > 0 && preferredDialsFailed
2751
+ * === preferredDialsAttempted` means "seeded but stranded": the seed itself was
2752
+ * accepted (`success: true`) but no preferred peer could be reached, so this node has
2565
2753
  * no connection to bootstrap its control database from. The node retries those
2566
2754
  * addresses on every control-cohort reconcile pass (see
2567
2755
  * `CadreNode.dialColdStartBootstrap`), so this is a signal, not a fatal error.
@@ -2573,7 +2761,7 @@ export interface ApplySeedResult {
2573
2761
  * down. Treat a non-zero value as certain failure and zero as "no failure
2574
2762
  * observed".
2575
2763
  */
2576
- ownerDialsFailed: number;
2764
+ preferredDialsFailed: number;
2577
2765
  }
2578
2766
 
2579
2767
  // ============================================================================
@@ -2587,18 +2775,13 @@ export type NodeTopology = 'public' | 'nat';
2587
2775
 
2588
2776
  /**
2589
2777
  * What an owner asks `CadreNode.createCadreInvitation` for. The invitation is a keypair the
2590
- * node mints; whoever holds the returned bundle can redeem it at any member machine.
2778
+ * node mints; whoever holds the returned bundle can redeem it at any member machine. It admits
2779
+ * a machine and grants nothing else; ownership is an owner grant (`CreateOwnerGrantOptions`).
2591
2780
  */
2592
2781
  export interface CreateCadreInvitationOptions {
2593
2782
  /** The one device that may redeem it; omit for whoever redeems first. */
2594
2783
  peerId?: string;
2595
- /** Whether redemption also makes the device an owner (seats an `OwnerKey` row). */
2596
- grantsOwner: boolean;
2597
- /**
2598
- * Lifetime from now, in ms. Defaults to 15 minutes for an untargeted owner-granting
2599
- * invitation — a bearer credential for admin rights — and 24 hours otherwise. A negative
2600
- * value mints an already-expired invitation (tests).
2601
- */
2784
+ /** Lifetime from now, in ms; default 24 hours. A negative value mints an already-expired invitation (tests). */
2602
2785
  expiresInMs?: number;
2603
2786
  /** How many redemptions it allows; default 1. */
2604
2787
  uses?: number;
@@ -2653,19 +2836,75 @@ export type CadreJoinProgress =
2653
2836
  * least one member answered with a retryable refusal (issuer-unknown, busy, conflict, internal).
2654
2837
  */
2655
2838
  reason: 'no-member-answered' | 'member-not-ready';
2656
- outcomes: readonly CadreInviteAddressOutcome[];
2839
+ outcomes: readonly RedemptionAddressOutcome[];
2657
2840
  };
2658
2841
 
2659
2842
  /** What `CadreNode.redeemCadreInvitation` returns once a member accepted. */
2660
2843
  export interface RedeemCadreInvitationResult {
2661
2844
  /** The member machine that admitted this device, when its address named one. */
2662
2845
  peerId: string | null;
2663
- /** Whether the invitation also seated this device's key as an owner. */
2664
- grantsOwner: boolean;
2665
2846
  /** When the member accepted, ISO 8601. */
2666
2847
  redeemedAt: string;
2667
2848
  }
2668
2849
 
2850
+ /**
2851
+ * What an owner asks `CadreNode.createOwnerGrant` for. The grant is a keypair the node mints;
2852
+ * whoever holds the returned bundle can become an owner of this cadre through any member
2853
+ * machine, so it is a bearer credential for admin rights.
2854
+ */
2855
+ export interface CreateOwnerGrantOptions {
2856
+ /** The label every key the grant seats will carry; trimmed, at most 64 characters, blank meaning none. */
2857
+ label?: string;
2858
+ /**
2859
+ * Lifetime from now, in ms; default 15 minutes. `null` means NO expiry and must be passed
2860
+ * explicitly: a backup grant, kept as carefully as an owner key. A negative value mints an
2861
+ * already-expired grant (tests).
2862
+ */
2863
+ expiresInMs?: number | null;
2864
+ /** How many redemptions it allows; default 1. */
2865
+ uses?: number;
2866
+ }
2867
+
2868
+ /** What `CadreNode.createOwnerGrant` returns: the bundle, the same encoded for sharing, and how far it reaches. */
2869
+ export interface CreateOwnerGrantResult {
2870
+ grant: OwnerGrantBundle;
2871
+ /** `encodeOwnerGrant(grant)`: `sereus-owner-grant:1.` and base64url JSON, for a QR code, a paste or a file. */
2872
+ encoded: string;
2873
+ /** Whether a holder could redeem it once the minting machine is gone; see {@link CadreInvitationReach}. */
2874
+ reach: CadreInvitationReach;
2875
+ }
2876
+
2877
+ /** How `CadreNode.redeemOwnerGrant` and `joinWithOwnerGrant` dial the grant's members: the invitation's options, with the same meanings. */
2878
+ export type RedeemOwnerGrantOptions = RedeemCadreInvitationOptions;
2879
+
2880
+ /** What `CadreNode.redeemOwnerGrant` returns once the key is seated. */
2881
+ export interface RedeemOwnerGrantResult {
2882
+ /** The key seated as an owner: the signer's public key. */
2883
+ ownerKey: string;
2884
+ /** The key was already a live owner, so nothing was written and no use of the grant was spent. */
2885
+ alreadyOwner: boolean;
2886
+ /** The member machine that wrote the row, when the redemption went over the protocol and its address named one; null on the local path. */
2887
+ memberPeerId: string | null;
2888
+ }
2889
+
2890
+ /** What `CadreNode.joinWithOwnerGrant` returns once this node is a member and its key an owner. */
2891
+ export interface JoinWithOwnerGrantResult {
2892
+ /** The member machine that admitted this node, when its address named one. */
2893
+ peerId: string | null;
2894
+ /** When the member admitted this node, ISO 8601. */
2895
+ redeemedAt: string;
2896
+ /** The key seated as an owner: the signer's public key. */
2897
+ ownerKey: string;
2898
+ /** The key was already a live owner of the party, so the grant spent no use. */
2899
+ alreadyOwner: boolean;
2900
+ /**
2901
+ * The signer was wired as this node's owner signer, because its `OwnerKey` row reached this
2902
+ * node within the bound. False when the row had not synced in time: the node is a member and
2903
+ * the key an owner all the same, and `CadreNode.wireOwnKeyIfOwner(signer)` wires it later.
2904
+ */
2905
+ ownerWired: boolean;
2906
+ }
2907
+
2669
2908
  /**
2670
2909
  * Options for adding a drone via provider API.
2671
2910
  */