@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
@@ -2,9 +2,12 @@ import { Database } from '@quereus/quereus';
2
2
  import type { SqlParameters } from '@quereus/quereus';
3
3
  import type { Libp2p } from '@libp2p/interface';
4
4
  import type { IRepo } from '@optimystic/db-core';
5
- import type { StrandRow, JoinRequestRow, JoinOutcome, PendingJoin, NodeProfile, PeerAddressRecord, CadrePeerRow, OwnerKeyRow, RevocationRow, RevocationLedgerOpenResult, DeviceTokenRecord, DeviceTokenRow, CadreInviteRow, CadreInviteUsageRow, CadreInviteRedemptionResult, CadreInviteStatus, SeedPeer, MachineRemovalResult } from './types.js';
6
- import type { ControlTable, RevocableTable, BoundControlDomain, ControlAction, CadreInviteSignedFields } from './control-authorization.js';
5
+ import type { StrandRow, JoinRequestRow, JoinOutcome, PendingJoin, NodeProfile, PeerAddressRecord, CadrePeerRow, OwnerKeyRow, RevocationRow, DeviceTokenRecord, DeviceTokenRow, CadreInviteRow, CadreInviteUsageRow, CadreInviteRedemptionResult, CadreInviteStatus, OwnerGrantRow, OwnerGrantUsageRow, OwnerGrantRedemptionResult, OwnerGrantStatus, SeedPeer, MachineRemovalResult } from './types.js';
6
+ import type { ControlTable, RevocableTable, BoundControlDomain, ControlAction, CadreInviteSignedFields, OwnerGrantSignedFields } from './control-authorization.js';
7
+ import type { ControlWriteRetryOptions } from './control-write-retry.js';
7
8
  import type { ControlRetryAbandonment } from './control-retry.js';
9
+ import type { TableChangeWatch, TableChangeWatchOptions } from './table-change-watch.js';
10
+ import type { FormationInviteFacts } from './formation-token.js';
8
11
  export type { ControlTable, RevocableTable, ControlDomain, ControlAction } from './control-authorization.js';
9
12
  /**
10
13
  * Generate a unique stamp ID for transaction authorization.
@@ -18,12 +21,11 @@ export declare function generateStampId(peerId: string): string;
18
21
  * present locally.
19
22
  *
20
23
  * Consent is bound to the strand's one-off `StampId` (see the consent branch of
21
- * `Strand.AuthorizedInsert`), so the writer must read the live row before inserting.
22
- * The ordinary "host strand has not converged on this responder yet" case is caught
23
- * earlier and reported as `missing` by `ControlFormationUsageRecorder.resolveStrand`;
24
- * reaching here means the row vanished between that check and the write, which is a
25
- * genuine race and is surfaced rather than left to fail the deferred `StrandExists`
26
- * CHECK at commit (which would drop the write with a far less legible error).
24
+ * `Strand.AuthorizedInsert`), so a writer not handed the stamp reads the live row before
25
+ * inserting, and throws this when the row is absent rather than leaving it to fail the
26
+ * deferred `StrandExists` CHECK at commit (a far less legible error). The formation
27
+ * responder hands the stamp over from `ControlFormationUsageRecorder.resolveStrand`, which
28
+ * reports an absent row as `missing`; a row removed after that read fails the CHECK instead.
27
29
  */
28
30
  export declare class MissingHostStrandError extends Error {
29
31
  readonly strandId: string;
@@ -52,22 +54,23 @@ export declare class FormationAbortedError extends Error {
52
54
  /**
53
55
  * The invitation's seat budget is already spent: the count of recorded usage rows for the
54
56
  * invitation has reached its `TotalUses`, so this redemption cannot be given a seat. `token`
55
- * is the `FormationInvite.Token` or the `CadreInvite.Key`, whichever table the caller
56
- * redeemed against.
57
+ * is the `FormationInvite.Token`, the `CadreInvite.Key` or the `OwnerGrant.Key`, whichever
58
+ * table the caller redeemed against.
57
59
  *
58
60
  * Raised off the committed-count check ({@link ControlDatabase.assertSeatRemains}) ahead of
59
61
  * the write, inside {@link ControlDatabase.redeemInvitation} /
60
62
  * {@link ControlDatabase.recordFormationUsage}, off the same check for a cadre invitation
61
- * ({@link ControlDatabase.redeemCadreInvite}), and off the unlocked pre-check
62
- * `ControlFormationUsageRecorder.authorizeUsage` runs before the manager issues a membership
63
- * pass — and off nothing else. It exists because
63
+ * ({@link ControlDatabase.redeemCadreInvite}) and an owner grant
64
+ * ({@link ControlDatabase.redeemOwnerGrant}), and off the unlocked re-check
65
+ * `ControlFormationUsageRecorder.authorizeUsage` runs after an outside approval, before the
66
+ * manager issues a membership pass — and off nothing else. It exists because
64
67
  * without it the refusal surfaces as a generic `CHECK constraint failed: Authorized` from the
65
68
  * schema's own count-based cap clause, which the manager reports as a retryable
66
69
  * `Formation conflict, retry` — telling the joiner to retry something that can never succeed.
67
70
  * Two redemptions of one token racing on the SAME node serialize behind the local write
68
- * queue, so the loser reads the winner's committed row and is refused here by name —
69
- * `StrandFormationManager.validateToken`'s `isTokenUsed` check cannot catch that, since it
70
- * ran before either redemption wrote anything.
71
+ * queue, so the loser reads the winner's committed row and is refused here by name — the
72
+ * use count `StrandFormationManager.validateToken` reads cannot catch that, since it was
73
+ * read before either redemption wrote anything.
71
74
  */
72
75
  export declare class InvitationExhaustedError extends Error {
73
76
  readonly token: string;
@@ -89,6 +92,35 @@ export declare class CadreInviteIssuerUnknownError extends Error {
89
92
  cause?: unknown;
90
93
  });
91
94
  }
95
+ /**
96
+ * {@link ControlDatabase.seatOwnerGrant} was refused by `OwnerGrant.AuthorizedInsert`: the
97
+ * grant twin of {@link CadreInviteIssuerUnknownError}, for the same two causes (the issuer's
98
+ * `OwnerKey` row has not replicated here yet or has since been removed; or a tampered row).
99
+ * Its own class rather than a shared one so a caller that handles invitation and grant
100
+ * bundles can tell by `instanceof` which credential was refused.
101
+ */
102
+ export declare class OwnerGrantIssuerUnknownError extends Error {
103
+ readonly grantKey: string;
104
+ readonly issuerKey: string;
105
+ constructor(grantKey: string, issuerKey: string, options?: {
106
+ cause?: unknown;
107
+ });
108
+ }
109
+ /**
110
+ * An issued row (a `CadreInvite` or an `OwnerGrant`) the cadre holds that this node cannot read
111
+ * yet: {@link ControlDatabase.seatCadreInvite} / {@link ControlDatabase.seatOwnerGrant} had its
112
+ * insert refused by a cohort member already holding the row, and a refreshed read here still
113
+ * finds nothing; or the redemption that follows the seat finds no row. It clears once this
114
+ * node's store catches up, which is why the redemption paths answer it with the retryable
115
+ * `conflict` code.
116
+ */
117
+ export declare class IssuedRowNotHereYetError extends Error {
118
+ readonly table: IssuedTable;
119
+ readonly key: string;
120
+ constructor(table: IssuedTable, key: string, options?: {
121
+ cause?: unknown;
122
+ });
123
+ }
92
124
  /**
93
125
  * What a recorded `FormationUsage` row came out as.
94
126
  *
@@ -238,15 +270,133 @@ export declare function cadreInviteRedeemMessage(partyId: string, fields: CadreI
238
270
  * distinct action tag, so neither signature can stand in for the other.
239
271
  */
240
272
  export declare function cadreInviteConsentMessage(partyId: string, fields: CadreInviteRedemptionFields): Uint8Array;
273
+ /**
274
+ * The exact bytes an owner signs to seat an `OwnerGrant` row — the TS mirror of the `'add'`
275
+ * digest in `OwnerGrant.AuthorizedInsert`, over the whole row in the schema's order with
276
+ * nullable columns as `''` (`ownerGrantRowFields`, shared with the base64url verifier
277
+ * `peer-authorization.ts`'s `ownerGrantAddDigest`). `expiresAt` must already be the stored
278
+ * canonical `datetime` string, as {@link ControlDatabase.insertOwnerGrant} produces it, for
279
+ * the reason on {@link cadreInviteAddMessage}.
280
+ */
281
+ export declare function ownerGrantAddMessage(partyId: string, row: OwnerGrantSignedFields): Uint8Array;
282
+ /** The fields both owner-grant redemption signatures cover; see {@link ownerGrantRedeemMessage}. */
283
+ export interface OwnerGrantRedemptionFields {
284
+ grantKey: string;
285
+ /** The single-use nonce the holder minted for this redemption (`generateStampId`). */
286
+ usageStampId: string;
287
+ /** The owner key being seated (base64url ed25519). */
288
+ seatedKey: string;
289
+ }
290
+ /**
291
+ * The exact bytes the HOLDER of an owner grant signs, with the grant's private key, to redeem
292
+ * it once — the TS mirror of the `'redeem'` digest `OwnerGrantUsage.GrantPossessed` verifies
293
+ * against the grant's public key. Binding the seated key and the nonce means a captured
294
+ * redemption can seat no other key and spend no other use.
295
+ */
296
+ export declare function ownerGrantRedeemMessage(partyId: string, fields: OwnerGrantRedemptionFields): Uint8Array;
297
+ /**
298
+ * The exact bytes the SEATED key signs, proving the holder holds it — the TS mirror of the
299
+ * `'consent'` digest `OwnerGrantUsage.OwnerConsented` verifies against `SeatedKey`. Same
300
+ * fields as {@link ownerGrantRedeemMessage} under a distinct action tag, so neither
301
+ * signature can stand in for the other.
302
+ */
303
+ export declare function ownerGrantConsentMessage(partyId: string, fields: OwnerGrantRedemptionFields): Uint8Array;
304
+ /**
305
+ * A guarded table of owner-issued credentials: signed rows that travel inside a bundle, are
306
+ * seated verbatim by any member ({@link ControlDatabase.seatCadreInvite},
307
+ * {@link ControlDatabase.seatOwnerGrant}), and are never deleted (`NoDelete`) because the
308
+ * rows they admitted prove themselves through them. Withdrawing one is a tombstone alone
309
+ * ({@link ControlDatabase.withdrawCadreInvite}, {@link ControlDatabase.withdrawOwnerGrant}).
310
+ */
311
+ type IssuedTable = Extract<RevocableTable, 'CadreInvite' | 'OwnerGrant'>;
312
+ /**
313
+ * An owner key's label as its row stores it: trimmed, a blank one left out, and refused past
314
+ * {@link OWNER_KEY_MAX_LABEL} by name rather than by the schema's `LabelLength` check.
315
+ * Idempotent, so a caller that checks before opening an owner approval can pass the result on.
316
+ */
317
+ export declare function ownerKeyLabel(label: string | null | undefined): string | null;
318
+ /**
319
+ * Signs one owner digest's raw bytes (no pre-hash) and returns a base64url Ed25519 signature:
320
+ * the callback every owner-signed writer takes. An `OwnerSign` from an open
321
+ * `OwnerSigner.approve` fits it, and so does a synchronous signer.
322
+ *
323
+ * NOTE: writers that read the stamp they sign do so inside the write lock, so the lock is held
324
+ * across these calls, and a `lockedWithRetry` re-run signs again inside the same approval.
325
+ * Free for a software signer. If a prompting signer ever holds the lock long enough to stall
326
+ * other control writes, sign outside the lock and re-check the stamp inside it.
327
+ */
328
+ export type ControlSign = (message: Uint8Array) => string | Promise<string>;
329
+ /**
330
+ * Signs this node's own address record over its `CadrePeer` row as it stands —
331
+ * {@link ControlDatabase.updateSelfPeerRecord}'s callback, run inside the locked write body on
332
+ * every attempt, so the record written is always stamped past the row that was actually read.
333
+ */
334
+ export type SelfRecordSigner = (current: PeerAddressRecord) => PeerAddressRecord;
335
+ /**
336
+ * Which owner re-touch {@link ControlDatabase.reauthorizeCadrePeer} makes over a `CadrePeer` row:
337
+ *
338
+ * - `'rebroadcast'` — the write-while-alone drain re-emitting a row that committed local-only. It
339
+ * bumps `UpdatedAt` so the row goes out as a fresh transaction, and writes only while the row's
340
+ * `Sig` is null: once the machine has self-published, the row is its to refresh, and a bump
341
+ * without a re-sign would leave its signature unverifiable.
342
+ * - `'revouch'` — an owner rebinding the row's voucher to its own key before the vouching key is
343
+ * removed (`CadreNode.removeOwner`). Written whether or not the machine has self-published, and
344
+ * leaves `UpdatedAt` untouched, so a self-signed record stays verifiable.
345
+ */
346
+ export type CadrePeerRetouchMode = 'rebroadcast' | 'revouch';
347
+ /**
348
+ * What {@link ControlDatabase.reauthorizeCadrePeer} found and did: the row was re-touched; no
349
+ * readable row exists (absent, or its stamp retired); or — `'rebroadcast'` only — the machine has
350
+ * self-published since, so the row is no longer the owner's to re-emit. The two no-write outcomes
351
+ * are decided over the row as read inside the locked body, so a caller can act on them.
352
+ */
353
+ export type CadrePeerRetouchOutcome = 'retouched' | 'absent' | 'self-published';
354
+ /** A formation invitation written with a founding ({@link FoundingRows}), bound to the strand founded. */
355
+ export interface FoundingInvite {
356
+ /** The invitation token, the `FormationInvite` primary key. */
357
+ token: string;
358
+ /** The sApp a redeemed strand will use. */
359
+ sAppId: string;
360
+ /** Epoch ms after which it cannot be redeemed; omit for never. */
361
+ expiresAtMs?: number;
362
+ /** How many redemptions it allows; omit for unlimited. */
363
+ totalUses?: number;
364
+ /** Web hook sent the disclosure; a `ValidationKey` then signs off each redemption. */
365
+ validationUrl?: string;
366
+ }
367
+ /** The control rows one founding writes together; see {@link ControlDatabase.insertFoundingRows}. */
368
+ export interface FoundingRows {
369
+ strand: {
370
+ id: string;
371
+ type: 'o' | 'c';
372
+ memberPrivateKey?: string;
373
+ };
374
+ /** This party's membership identity key for a closed strand; absent for an open strand, or when a stored key is adopted. */
375
+ partyKey?: string;
376
+ /** A formation invitation bound to this strand (its `StrandId` is `strand.id`). */
377
+ formationInvite?: FoundingInvite;
378
+ }
379
+ /** What an owner asks {@link ControlDatabase.insertCadreInvite} (or {@link ControlDatabase.signCadreInvite}) to sign. */
380
+ export interface CadreInviteFields {
381
+ /** The invitation public key (base64url ed25519), the row's primary key. */
382
+ key: string;
383
+ /** The device the invitation is for, or null for whoever redeems first. */
384
+ peerId?: string | null;
385
+ /** Epoch ms after which it cannot be redeemed; omit for never. */
386
+ expiresAtMs?: number | null;
387
+ /** How many redemptions it allows; omit for unlimited. */
388
+ totalUses?: number | null;
389
+ }
241
390
  /** The control tables keyed by a strand id whose repeat insert a caller may treat as idempotent. */
242
- type StrandIdTable = Extract<RevocableTable, 'Strand' | 'JoinedStrand'>;
391
+ type StrandIdTable = Extract<RevocableTable, 'Strand' | 'StrandPartyKey' | 'JoinedStrand'>;
243
392
  /**
244
393
  * Did this write to `table` fail because the strand id is already seated there?
245
394
  *
246
395
  * {@link CadreNode.publishStrand} uses this to tell "my own earlier publish already
247
396
  * landed" (re-read, and no-op when the row matches) apart from every other rejection —
248
397
  * an unauthorized signer, a retired stamp — which must keep surfacing. For `JoinedStrand`
249
- * the same test tells "another machine of this party already published this join" apart.
398
+ * the same test tells "another machine of this party already published this join" apart, and
399
+ * for `StrandPartyKey` it tells a founding that this party's key for the id is already seated.
250
400
  *
251
401
  * Matched by TEXT, not by type: the typed engine error does not survive the trip out of
252
402
  * optimystic (same constraint the retry classifiers in `control-write-retry.ts` document).
@@ -256,6 +406,13 @@ type StrandIdTable = Extract<RevocableTable, 'Strand' | 'JoinedStrand'>;
256
406
  * reword reddens there.
257
407
  */
258
408
  export declare function isStrandIdConflict(error: unknown, table: StrandIdTable): boolean;
409
+ /**
410
+ * Did a seat of an issued row fail because a cohort member already holds a row under its key?
411
+ * {@link ControlDatabase.seatCadreInvite} / {@link ControlDatabase.seatOwnerGrant} reach this
412
+ * on a node whose store lags the cohort: its guard read missed the row, and the member that
413
+ * checked the commit refused the insert. Text-matched, failing closed, like {@link isStrandIdConflict}.
414
+ */
415
+ export declare function isIssuedRowKeyConflict(error: unknown, table: IssuedTable): boolean;
259
416
  /**
260
417
  * Did {@link ControlDatabase.insertJoinRequest} fail because the party already holds a
261
418
  * `JoinRequest` row for this invitation? The caller adopts the existing request instead.
@@ -286,35 +443,19 @@ export declare class PendingJoinChangedError extends Error {
286
443
  readonly live: PendingJoin | null;
287
444
  constructor(id: string, live: PendingJoin | null);
288
445
  }
289
- /**
290
- * The one `CadreControl.Revocation` row that retires nothing, filed once by an owner
291
- * ({@link ControlDatabase.openRevocationLedger}) so the table is never a never-written
292
- * block. The schema's table comment says why that matters and why the row can never read
293
- * as a retirement; `RowIsGone` pins this exact triple.
294
- */
295
- export declare const REVOCATION_LEDGER_MARKER: {
296
- readonly tableName: "Revocation";
297
- readonly rowKey: "ledger";
298
- readonly stampId: "opened";
299
- };
300
446
  /**
301
447
  * Project `CadrePeer` rows into the {@link SeedPeer} list a seed, or a cadre invitation
302
- * redemption reply, carries.
303
- *
304
- * Owner identity is sourced from the `OwnerKey` table, not from the transport peer id. An
305
- * Ed25519 libp2p peer id embeds its public key (identity multihash), so each peer's ed25519
306
- * key is derivable from its `PeerId`; a peer is an owner iff that derived key is in
307
- * `ownerKeys`. This makes any owner node markable — not just the local one. A non-Ed25519
308
- * or unparsable id yields a non-owner rather than failing the whole projection.
448
+ * redemption reply, carries. `minterPeerId` is the machine minting the seed or answering the
449
+ * redemption; it contributes an entry only through its own `CadrePeer` row.
309
450
  *
310
- * NOTE: this is the one owner lookup deliberately left on the REPLICATED table rather than
311
- * the node-local anchor. `SeedPeer.isOwner` is a dial hint — the receiver dials owner-flagged
312
- * peers first — not a trust decision, and the receiver re-derives real trust from its own
313
- * anchor. So a polluted table costs at most a wasted dial, while anchoring here would
314
- * silently drop legitimate co-owners this node never pinned. If `isOwner` ever gates
315
- * anything the receiver TRUSTS, move it to the anchor.
451
+ * A peer is `preferred` when it is the minter, the one machine certain to be reachable by the
452
+ * receiver at that moment, since it is delivering the seed or the reply, or an always-on
453
+ * machine (`Profile = 'storage'`), the ones that stay dialable. Preference is a dial hint and
454
+ * reads no `OwnerKey` row: owner keys are not machines, and a receiver derives trust from its
455
+ * own anchor, never from this list. A `Profile` is self-declared and not checked here, so a
456
+ * machine misdeclaring one costs the receiver a wasted dial at most.
316
457
  */
317
- export declare function projectSeedPeers(rows: readonly CadrePeerRow[], ownerKeys: ReadonlySet<string>): SeedPeer[];
458
+ export declare function projectSeedPeers(rows: readonly CadrePeerRow[], minterPeerId: string): SeedPeer[];
318
459
  /**
319
460
  * Guarded tables a node may reap locally once their tombstone has committed — the
320
461
  * tables whose `AuthorizedDelete` carries the REAP branch (see the constraint comment on
@@ -326,22 +467,14 @@ export declare function projectSeedPeers(rows: readonly CadrePeerRow[], ownerKey
326
467
  * hold, so a reaped row is recoverable by re-forming. `JoinRequest` is reapable for the same
327
468
  * reason: the inviting party and the user also hold its invitation. `OwnerKey` has no
328
469
  * production removal path and `MinOneOwner` makes an automated owner-key reap a
329
- * party-bricking hazard. `CadreInvite` is never deleted at all (its tombstone is a
330
- * withdrawal over a row that stays), so there is nothing to reap.
470
+ * party-bricking hazard. `CadreInvite` and `OwnerGrant` are never deleted at all (their
471
+ * tombstones are withdrawals over rows that stay), so there is nothing to reap.
331
472
  */
332
473
  export declare const REAPABLE_TABLES: readonly ["CadrePeer", "DeviceToken", "ValidationKey", "JoinedStrand", "JoinRequest"];
333
474
  export type ReapableTable = (typeof REAPABLE_TABLES)[number];
334
475
  /** A `FormationInvite` row as {@link ControlDatabase.queryFormationInvite} reads it. */
335
- export interface FormationInviteRow {
476
+ export interface FormationInviteRow extends FormationInviteFacts {
336
477
  token: string;
337
- sAppId: string;
338
- /** Parsed epoch ms of the stored `datetime`; null when the invitation never expires. */
339
- expiresAtMs: number | null;
340
- /** Null is unlimited. */
341
- totalUses: number | null;
342
- validationUrl: string | null;
343
- /** A bound invitation's host strand; null on the responder-provisions path. */
344
- strandId: string | null;
345
478
  }
346
479
  /**
347
480
  * Notified after a membership write (a `CadreControl.CadrePeer` or `OwnerKey` row, see
@@ -361,8 +494,6 @@ export type MembershipChangeListener = (reason: string) => Promise<void>;
361
494
  * the writer that files the row supplies it.
362
495
  */
363
496
  export type RevokedRowRef = Pick<RevocationRow, 'tableName' | 'rowKey' | 'stampId'>;
364
- /** What {@link ControlDatabase.removeMachine} wrote: `CadreNode.removeMachine`'s result, less the re-vouches it made first. */
365
- export type MachineRemovalWrites = Pick<MachineRemovalResult, 'removedPeer' | 'removedOwnerKey' | 'removedDeviceToken' | 'withdrawnInvitations'>;
366
497
  /**
367
498
  * Notified after a guarded-table delete — and the `CadreControl.Revocation`
368
499
  * tombstone it writes in the same transaction — has COMMITTED.
@@ -400,6 +531,8 @@ export interface ControlDatabaseConfig {
400
531
  * Optional path to the control schema file.
401
532
  * If not provided, uses the embedded schema for cross-platform compatibility.
402
533
  * Only use this if you need to override the default schema (e.g., for testing).
534
+ * The file has the shape of `schemas/control.qsql`: one `declare schema CadreControl`
535
+ * followed by one plain `apply schema CadreControl` ({@link controlSchemaFileDeclaration}).
403
536
  */
404
537
  schemaPath?: string;
405
538
  /** Libp2p node for the control network (injected) */
@@ -414,6 +547,7 @@ export interface ControlDatabaseConfig {
414
547
  export declare class ControlDatabase {
415
548
  private db;
416
549
  private collectionFactory;
550
+ private txnBridge;
417
551
  private readonly config;
418
552
  private initialized;
419
553
  private membershipListener;
@@ -421,6 +555,18 @@ export declare class ControlDatabase {
421
555
  private controlWriteAbandonedListener;
422
556
  /** Tail of the local-write chain — see {@link withWriteLock}. */
423
557
  private writeQueue;
558
+ /** Subscriptions handed out by {@link watchTables}. */
559
+ private readonly tableWatches;
560
+ /** The cached membership tables; see {@link readMembershipRows}. */
561
+ private readonly revocationView;
562
+ private readonly cadrePeerView;
563
+ private readonly ownerKeyView;
564
+ private readonly cadreInviteView;
565
+ private readonly cadreInviteUsageView;
566
+ private readonly ownerGrantView;
567
+ private readonly ownerGrantUsageView;
568
+ /** Each membership view by the qualified table it copies, for {@link watchMembershipViews}, {@link watchMembershipTables} and {@link invalidateMembershipViews}. */
569
+ private readonly membershipViews;
424
570
  /**
425
571
  * Locked write bodies currently running (see {@link runWriteBody}); what
426
572
  * {@link readRowsOnce} consults to route an unlocked read around a write that has
@@ -447,7 +593,33 @@ export declare class ControlDatabase {
447
593
  * Initialize the database - load schema and register plugins
448
594
  */
449
595
  initialize(): Promise<void>;
596
+ /**
597
+ * Drop a membership view whenever its table changes: a commit made here, or a change this
598
+ * node's storage takes in from elsewhere. `onInvalidate` is the whole consumer, so the run
599
+ * does nothing; no backstop, since {@link MEMBERSHIP_VIEW_MAX_AGE_MS} bounds what the
600
+ * subscription cannot hear. {@link close} disposes it with the rest.
601
+ */
602
+ private watchMembershipViews;
603
+ /**
604
+ * {@link watchTables} on the membership tables: the seven the membership views copy
605
+ * (`CadrePeer`, `OwnerKey`, `Revocation`, `CadreInvite`, `CadreInviteUsage`, `OwnerGrant`,
606
+ * `OwnerGrantUsage`), which are exactly what the membership gate and the owner-anchor sync
607
+ * read. `CadreNode.start` takes its gate watch here, so the gate hears every table its
608
+ * reads can depend on and no list of them lives outside this class.
609
+ */
610
+ watchMembershipTables(onChange: () => Promise<void>, options: TableChangeWatchOptions): TableChangeWatch;
611
+ /**
612
+ * Empty every membership view, so the next read of each loads afresh and no load in
613
+ * flight installs. Besides {@link initialize} and {@link close}, `CadreNode` calls it when
614
+ * a sibling becomes reachable (its identify on a new connection): a machine that was offline
615
+ * while that sibling committed learns of the commit only when it next reads the block from
616
+ * storage, and dropping the views makes the pass that starts then read now rather than after
617
+ * `MEMBERSHIP_VIEW_MAX_AGE_MS`.
618
+ */
619
+ invalidateMembershipViews(): void;
450
620
  private loadSchema;
621
+ /** Apply the declared control schema, retrying transient cohort failures. */
622
+ private applyControlSchema;
451
623
  /**
452
624
  * One UNRETRIED drain of a control read to rows — every read in this class bottoms out
453
625
  * here (almost all via {@link readRows}, which adds the transient-failure retry), and
@@ -578,6 +750,39 @@ export declare class ControlDatabase {
578
750
  * under-lock callback seam is ever added, audit its read graph the same way.
579
751
  */
580
752
  private readRows;
753
+ /**
754
+ * {@link readRows}, also saying whether the attempt that answered took the committed-read
755
+ * path — which a {@link MembershipView} load needs, since it may not keep such rows (see
756
+ * {@link membershipView}).
757
+ */
758
+ private readRowsRouted;
759
+ /**
760
+ * A view over one membership table, loaded by its full scan `sql` through {@link readRows}'s
761
+ * funnel. Rows a committed read served are never kept: that read may initialize an untouched
762
+ * table provisionally, and the Optimystic plugin arms a table's replicated-change bridge only
763
+ * on its full initialization (`OptimysticTable.initializeForCommittedRead`), so no later change
764
+ * event would ever drop them.
765
+ */
766
+ private membershipView;
767
+ /**
768
+ * A membership table's raw rows, from its {@link MembershipView}: `Revocation`, `CadrePeer`
769
+ * and `OwnerKey`, the tables every membership, owner and peer-record read starts from, and
770
+ * `CadreInvite`, `CadreInviteUsage`, `OwnerGrant` and `OwnerGrantUsage`, the chains every
771
+ * membership refresh verifies invitation-admitted machines and grant-seated owner keys through.
772
+ * Callers build fresh result objects from them and never mutate them.
773
+ *
774
+ * Reads the table directly instead when a caller under the write lock (`retry: false`) has an
775
+ * explicit transaction open: such a read sees the transaction's own uncommitted rows, which
776
+ * the view does not hold, and a rollback fires no change event, so keeping them would cache
777
+ * rows that never existed. No reader of these tables runs inside {@link inTransaction} today;
778
+ * this keeps one added later, or a spec's raw-handle transaction, correct.
779
+ *
780
+ * Reads that stay plain SQL on purpose: {@link queryStampId} (sees retired rows), the
781
+ * schema's CHECK constraints, which read inside the committing transaction, the redemption's
782
+ * one read of the held credential row ({@link queryCadreInvite},
783
+ * {@link queryOwnerGrant}), and the indexed usage counts.
784
+ */
785
+ private readMembershipRows;
581
786
  /**
582
787
  * Query all strands from the control database
583
788
  */
@@ -589,10 +794,18 @@ export declare class ControlDatabase {
589
794
  * validated invitee during provision-then-record formation.
590
795
  */
591
796
  queryStrand(strandId: string): Promise<StrandRow | null>;
797
+ /**
798
+ * {@link queryStrand} plus the row's `StampId`, in one read: the formation responder
799
+ * resolves a bound invite's host strand with it and later records consent against that
800
+ * stamp ({@link recordFormationUsage}'s `strandStampId`) without reading the row again.
801
+ */
802
+ queryStrandWithStamp(strandId: string): Promise<(StrandRow & {
803
+ StampId: string;
804
+ }) | null>;
592
805
  /**
593
806
  * Every strand this party joined from another party (`CadreControl.JoinedStrand`), shaped
594
807
  * as a {@link StrandRow} so a reader can treat it like one of the party's own. A joiner
595
- * never founds, so `FounderOwnerKey` is always null. Read raw, like {@link queryStrands}:
808
+ * never founds, so `FounderOwnerKey` and `FounderPeerId` are always null. Read raw, like {@link queryStrands}:
596
809
  * no retired-stamp filter.
597
810
  */
598
811
  queryJoinedStrands(): Promise<StrandRow[]>;
@@ -634,14 +847,54 @@ export declare class ControlDatabase {
634
847
  *
635
848
  * Deliberately RAW (no retired-stamp filter, unlike {@link getOwnerKeys}): the genesis
636
849
  * insert is refused by the schema whenever any physical row exists, so this must see one.
850
+ * Served from the `OwnerKey` membership view ({@link readMembershipRows}), which holds every
851
+ * physical row, so a genesis caller's pre-check and {@link ensureOwnerKey}'s re-check share
852
+ * one storage read.
637
853
  */
638
854
  hasOwnerKey(): Promise<boolean>;
639
855
  /**
640
856
  * Idempotent genesis: insert `key` as the founding owner key only when
641
857
  * the party has none yet. Returns true if it inserted, false if an owner
642
858
  * key already existed (so a repeat `--owner` start is a no-op).
643
- */
644
- ensureOwnerKey(key: string): Promise<boolean>;
859
+ *
860
+ * Before the insert, gives every control table its empty collection
861
+ * ({@link commitEmptyControlCollections}). A collection committed while a machine is alone
862
+ * forks if another machine creates the same collection independently; before genesis the
863
+ * party has no owner, so no other machine can have been admitted and none can hold any of
864
+ * its collections. The order makes the step repeatable: a crash between it and the insert
865
+ * leaves no `OwnerKey` row, so the next start runs genesis again and commits only the
866
+ * collections still missing, while a founder restart finds the row and commits nothing, so
867
+ * it can never write an empty second history beside a table another machine has since
868
+ * written. A failure propagates with nothing inserted.
869
+ *
870
+ * @param label - the user's name for the key ({@link insertOwnerKey}).
871
+ */
872
+ ensureOwnerKey(key: string, label?: string): Promise<boolean>;
873
+ /**
874
+ * Commit an empty collection for every `CadreControl` table, index tree and unique-enforcement
875
+ * tree that has none, so a read of a table nobody has written is answered from this machine's
876
+ * storage instead of asking the block's cohort to prove the collection absent — a round trip
877
+ * per read on a party of several machines, and an error or a wait while they are offline.
878
+ * Joining machines get these collections the ordinary way, by reading them from the cohort.
879
+ *
880
+ * Under the write lock: the helper commits whatever is staged in those collections outside any
881
+ * SQL transaction, which is sound only while nothing else writes.
882
+ *
883
+ * Parties founded before this step, and tables a later control schema version adds, have no
884
+ * such collections, and nothing creates them later: the start-time schema apply runs on every
885
+ * machine, possibly alone, where committing one would be exactly the fork genesis avoids.
886
+ *
887
+ * NOTE: one `sync()` per collection, in sequence: 32 collections for today's schema (17 tables,
888
+ * 3 indexes, 12 `unique` columns). Measured 2026-10-20 on a lone `CadreNode` over the network
889
+ * transactor (started, then `ensureOwnerKey` alone, 3 runs each): 64–105 ms in memory and
890
+ * 750–820 ms on `FileRawStorage`, about 25 ms per collection on disk, roughly 1/75 of the edge
891
+ * node's 60 s `DEFAULT_OWNER_GENESIS_TIMEOUT_MS`. Under the whole cadre-core suite's load (151
892
+ * geneses) it took a median of 159 ms and at most 4.6 s. If genesis latency starts to matter
893
+ * (phone flash under launch contention runs 50–90 ms per storage operation, see the NOTE in
894
+ * `initialize`), ask Optimystic for an API that commits them as one batch, the one strand
895
+ * founding would use too; do not build a private batching path.
896
+ */
897
+ private commitEmptyControlCollections;
645
898
  /**
646
899
  * Collect every owner key (`CadreControl.OwnerKey.Key`) as a set.
647
900
  *
@@ -666,8 +919,9 @@ export declare class ControlDatabase {
666
919
  */
667
920
  getOwnerKeys(retry?: boolean): Promise<Set<string>>;
668
921
  /**
669
- * Every live `OwnerKey` row with its persisted proof ({@link OwnerKeyRow}) — what the
670
- * trusted-owner anchor sync derives from (`CadreNode.syncOwnerAnchor`). Rows whose
922
+ * Every live `OwnerKey` row with its label and persisted proof ({@link OwnerKeyRow}) — what
923
+ * the trusted-owner anchor sync derives from (`CadreNode.syncOwnerAnchor`) and the owner-key
924
+ * listing shows (`CadreNode.listOwnerKeys`). Rows whose
671
925
  * `StampId` is retired in `CadreControl.Revocation` are excluded here, as
672
926
  * {@link queryCadrePeers} excludes them and for the reason on {@link getOwnerKeys}, which
673
927
  * reads through this. A row with no stamp (the schema permits it; no writer produces one)
@@ -679,9 +933,8 @@ export declare class ControlDatabase {
679
933
  *
680
934
  * @param retired - the retired `OwnerKey` stamps, when the caller has already read the
681
935
  * tombstones (`CadreNode.syncOwnerAnchor` reads {@link queryRevocations} for their
682
- * signers); omitted, they are read here. Before the ledger marker is filed every
683
- * `Revocation` read is a cohort consult, so the sync passing its own set keeps the
684
- * membership refresh at one such read for the anchor.
936
+ * signers); omitted, they are read here. Both come from the same `Revocation` view
937
+ * ({@link readMembershipRows}), so passing the set saves only the filtering.
685
938
  */
686
939
  queryOwnerKeyRows(retry?: boolean, retired?: ReadonlySet<string>): Promise<OwnerKeyRow[]>;
687
940
  /**
@@ -696,12 +949,8 @@ export declare class ControlDatabase {
696
949
  * {@link queryRevokedStamps} rather than inlining the SQL, so tests can interpose on
697
950
  * that seam to model the replicated live-row-plus-tombstone merge state.
698
951
  *
699
- * NOTE: runs a second query (the retired-stamp set) per call, on the membership-gate
700
- * refresh path. Its cost is storage consults, not rows (see {@link queryRevokedStamps}):
701
- * while `Revocation` has never been written every call consults the cohort about it, and
702
- * the ledger marker ({@link openRevocationLedger}) is what makes it a held block. Folding
703
- * the exclusion into one statement would not help (it reads the same block), and caching
704
- * the set would hide a tombstone arriving by replication.
952
+ * Both tables come from their membership views ({@link readMembershipRows}), so a call after
953
+ * the first costs no storage read until one of the two tables changes.
705
954
  *
706
955
  * NOTE: this filter is HAND-MIRRORED by the gate suites' fake control databases
707
956
  * (`test/membership-gate-helpers.ts` and `test/cadre-node-authorized-surface.spec.ts`),
@@ -723,15 +972,15 @@ export declare class ControlDatabase {
723
972
  queryCadrePeers(retry?: boolean): Promise<CadrePeerRow[]>;
724
973
  /**
725
974
  * The cadre's peers as a seed or a redemption reply carries them: every live `CadrePeer`
726
- * row with its addresses, each flagged `isOwner` when the key behind its peer id is in
727
- * the replicated `OwnerKey` table ({@link projectSeedPeers}).
975
+ * row with its addresses, the minter and every always-on machine marked preferred
976
+ * ({@link projectSeedPeers}).
728
977
  *
729
978
  * Read through {@link queryCadrePeers}, never a raw `CadrePeer` select, so a removed
730
979
  * member's addresses are never packed into a seed or handed to a redeeming device as a
731
980
  * dial hint: a revoked peer is off the addressable surface everywhere, and this is one of
732
981
  * its exits.
733
982
  */
734
- querySeedPeers(): Promise<SeedPeer[]>;
983
+ querySeedPeers(minterPeerId: string): Promise<SeedPeer[]>;
735
984
  /**
736
985
  * Every approver public key currently enrolled in `CadreControl.ValidationKey` — the
737
986
  * set a `ValidationUrl` redemption's approval signature must be signed by (see
@@ -762,17 +1011,18 @@ export declare class ControlDatabase {
762
1011
  * guard. A read retrying there would sleep its backoff HOLDING the write lock
763
1012
  * (`retryControlWrite`'s contract is that backoff sleeps happen with no lock held), and
764
1013
  * the write funnel already re-runs the body's reads when it re-runs the body. The
765
- * unlocked callers — the public stamp readers, {@link reauthorizeCadrePeer} and
766
- * {@link reapRevokedRow}, whose stamp reads run BEFORE their locks are taken — keep the
767
- * default retried path. Explicit per call rather than an ambient flag: unlocked reads
768
- * run concurrently with a locked body, so `this`-state cannot tell the two apart.
1014
+ * unlocked callers — the public stamp readers and {@link reapRevokedRow}, whose stamp
1015
+ * read runs BEFORE its lock is taken — keep the default retried path. Explicit per call
1016
+ * rather than an ambient flag: unlocked reads run concurrently with a locked body, so
1017
+ * `this`-state cannot tell the two apart. The same flag threads through
1018
+ * {@link findCadrePeerRow} for the two writers that read the whole row inside their body.
769
1019
  */
770
1020
  private queryStampId;
771
1021
  /**
772
1022
  * `CadrePeer` stamp nonce — read by {@link SeedBootstrapService.removePeer} as its
773
1023
  * row-present gate before it delegates to {@link deleteCadrePeer}. The in-class
774
- * `CadrePeer` writers ({@link insertCadrePeer}, {@link reauthorizeCadrePeer}) go
775
- * straight to {@link queryStampId}.
1024
+ * `CadrePeer` writers go straight to {@link queryStampId} ({@link insertCadrePeer}) or read
1025
+ * the whole row ({@link reauthorizeCadrePeer}, through {@link findCadrePeerRow}).
776
1026
  */
777
1027
  queryCadrePeerStampId(peerId: string): Promise<string | null>;
778
1028
  /**
@@ -809,33 +1059,34 @@ export declare class ControlDatabase {
809
1059
  * ({@link queryCadrePeers}, {@link queryPeerRecord},
810
1060
  * {@link CadreNode.resolveDeviceToken}) drop any row whose stamp appears here.
811
1061
  *
812
- * NOTE: the per-call cost that matters is the storage layer's, not the row count. While
813
- * `Revocation` has never been written, this node does not hold its block, and Optimystic
814
- * consults a block's cohort on every read of a block it does not hold: 1 consult per
815
- * call (measured), each a round trip to every other member on a multi-machine party. The
816
- * ledger marker ({@link openRevocationLedger}) ends that — a held block is re-consulted
817
- * at most once per read-repair window (10 s). Do not cache the set instead: a cache
818
- * cannot see a tombstone arriving by replication, so it would delay a revocation. The
819
- * row count only grows (append-only) but stays small while removals are rare; revisit if
1062
+ * Served from the `Revocation` membership view ({@link readMembershipRows}), dropped by
1063
+ * every change to the table, local or replicated into this node's storage, so a tombstone
1064
+ * arriving by replication is seen on the next call. The cost that matters is the storage
1065
+ * layer's, paid by a cold view's load: while `Revocation` has never been written, this node
1066
+ * does not hold its block, and Optimystic consults a block's cohort on every read of a block
1067
+ * it does not hold, each a round trip to every other member on a multi-machine party. The row
1068
+ * count only grows (append-only) but stays small while removals are rare; revisit if
820
1069
  * removals become routine and the scan itself shows up in a profile.
821
1070
  *
822
1071
  * An isolated node that has never received the `Revocation` block reads it as holding no
823
- * revocations instead of throwing — see {@link readRevokedStampRows}.
1072
+ * revocations instead of throwing — see {@link readRevocationRowsOrNone}.
824
1073
  *
825
1074
  * `retry: false` is forwarded by {@link queryCadrePeers} for the one caller that reads
826
1075
  * under the write lock — see that method's note.
827
1076
  */
828
1077
  queryRevokedStamps(tableName: RevocableTable, retry?: boolean): Promise<Set<string>>;
829
1078
  /**
830
- * The `Revocation` scan behind {@link queryRevokedStamps}, answering NO ROWS when the read
831
- * fails `cohort-unreachable` ({@link isCohortUnreachableRead}): this node does not hold
1079
+ * The `Revocation` rows behind {@link queryRevokedStamps}, answering NO ROWS when the view's
1080
+ * load fails `cohort-unreachable` ({@link isCohortUnreachableRead}): this node does not hold
832
1081
  * the block and could ask no other cohort member about it. Every other failure rethrows.
833
1082
  * The retry in {@link readRows} runs first, so a node whose connectivity returns within
834
- * the read budget still gets the real answer.
1083
+ * the read budget still gets the real answer. The empty answer is never installed in the
1084
+ * view (a failed load keeps nothing), so the next call asks again.
835
1085
  *
836
1086
  * Without this, a machine cut off from every other machine in its party before it ever
837
- * received the block — the block does not exist anywhere until the owner files the
838
- * ledger marker ({@link openRevocationLedger}) on a connected reconcile pass — cannot
1087
+ * received the block — a joining machine gets it only by reading it from the cohort, and
1088
+ * on a party founded before genesis created every control collection it exists nowhere
1089
+ * until the first tombstone or the marker ({@link REVOCATION_LEDGER_MARKER}) — cannot
839
1090
  * answer ANY membership, peer-record or device-token lookup, although it holds those
840
1091
  * rows locally.
841
1092
  *
@@ -854,12 +1105,12 @@ export declare class ControlDatabase {
854
1105
  *
855
1106
  * NOTE: an isolated node pays the whole read retry (up to
856
1107
  * `CONTROL_READ_RETRY_BUDGET_MS`, plus the slow attempt that exhausted it) before this
857
- * answers, and {@link queryPeerRecord} then issues its row scan on top. If an isolated
858
- * node's lookups are seen exceeding the admission gate's 2 s deadline
859
- * (`ADMISSION_DECISION_TIMEOUT_MS`), skip the retry for this read when the first attempt
860
- * fails `cohort-unreachable`.
1108
+ * answers, on every call since the answer is not kept, and {@link queryPeerRecord} may then
1109
+ * load the `CadrePeer` view on top. If an isolated node's lookups are seen exceeding the
1110
+ * admission gate's 2 s deadline (`ADMISSION_DECISION_TIMEOUT_MS`), skip the retry for this
1111
+ * read when the first attempt fails `cohort-unreachable`.
861
1112
  */
862
- private readRevokedStampRows;
1113
+ private readRevocationRowsOrNone;
863
1114
  /**
864
1115
  * Every locally-held `CadreControl.Revocation` tombstone — identity triple, its
865
1116
  * `ReissuedAt` counter and the stored signer pair. Consumed by the cohort-growth re-issue
@@ -868,17 +1119,18 @@ export declare class ControlDatabase {
868
1119
  * `where`, so the composite-primary-key point-lookup hazard (see the statement comment in
869
1120
  * {@link reissueRevocations}) does not arise. Unlocked, like every other read.
870
1121
  *
871
- * Skips the ledger marker ({@link REVOCATION_LEDGER_MARKER}): it retires nothing, so
872
- * neither sweep may reap or re-sign it, and skipping it keeps `RevocationRow.tableName`
873
- * a {@link RevocableTable}. Filtered on `TableName` alone, in TypeScript: `RowIsGone`
874
- * admits no other row under `'Revocation'`.
1122
+ * Skips the ledger marker ({@link REVOCATION_LEDGER_MARKER}) an older party may hold: it
1123
+ * retires nothing, so neither sweep may reap or re-sign it, and skipping it keeps
1124
+ * `RevocationRow.tableName` a {@link RevocableTable}. Filtered on `TableName` alone, in
1125
+ * TypeScript: `RowIsGone` admits no other row under `'Revocation'`.
875
1126
  *
876
1127
  * `retry: false` from a caller that reads under the write lock, for the reason on
877
1128
  * {@link queryCadrePeers}.
878
1129
  */
879
1130
  queryRevocations(retry?: boolean): Promise<RevocationRow[]>;
880
1131
  /**
881
- * Read a single peer's address record (the full `CadrePeer` row) by PeerId.
1132
+ * Read a single peer's address record (the full `CadrePeer` row) by PeerId, found in the
1133
+ * `CadrePeer` membership view ({@link readMembershipRows}).
882
1134
  *
883
1135
  * Returns null when no row exists, or when the row's `StampId` is retired in
884
1136
  * `CadreControl.Revocation` — a revoked peer reads as absent, so the resolver
@@ -894,10 +1146,22 @@ export declare class ControlDatabase {
894
1146
  * The split `addrs` re-join to the exact stored `Multiaddr` (split-on-`,` is
895
1147
  * the inverse of join-on-`,`), so the resolver re-verifies over the same bytes
896
1148
  * the publisher signed.
1149
+ *
1150
+ * `retry: false` from the one caller that reads under the write lock,
1151
+ * {@link updateSelfPeerRecord}'s body, for the reason on {@link queryCadrePeers}.
1152
+ */
1153
+ queryPeerRecord(peerId: string, retry?: boolean): Promise<PeerAddressRecord | null>;
1154
+ /**
1155
+ * One readable `CadrePeer` row by PeerId from the membership view, or null when no row exists
1156
+ * or its stamp is retired in `Revocation` — the single-row read behind {@link queryPeerRecord}
1157
+ * and the two writers that decide over the row inside their locked body
1158
+ * ({@link updateSelfPeerRecord}, {@link reauthorizeCadrePeer}). Same retired-stamp rule as
1159
+ * {@link queryCadrePeers}, and the same `retry` contract.
897
1160
  */
898
- queryPeerRecord(peerId: string): Promise<PeerAddressRecord | null>;
1161
+ private findCadrePeerRow;
899
1162
  /**
900
- * Apply a peer's own self-signed address-record update to an existing row.
1163
+ * Apply a peer's own self-signed address-record update to its existing row: read the row
1164
+ * under the write lock, have `sign` build the record over it, and write that record.
901
1165
  *
902
1166
  * Authorization is carried entirely by the record: the `Sig` column (verified
903
1167
  * by the `AuthorizedUpdate` self-branch against the stored `PublicKey`) plus
@@ -906,6 +1170,21 @@ export declare class ControlDatabase {
906
1170
  * exists. `PublicKey` is intentionally not in the SET list (it is immutable on
907
1171
  * self-update and the constraint enforces `new.PublicKey = old.PublicKey`).
908
1172
  *
1173
+ * The read, the decision and the signature all run INSIDE the locked body, which is what
1174
+ * lets it opt into {@link LOST_UPDATE_RETRY_POLICY}: the row has a second writer, the owner's
1175
+ * re-touch ({@link reauthorizeCadrePeer}), and the two fire together while a cadre starts.
1176
+ * When this write loses that race the commit is refused as a lost update and re-run, and the
1177
+ * re-run reads the row the rival left — including its bumped `UpdatedAt`, which a record
1178
+ * signed before the race could no longer exceed — and signs past it. A record signed outside
1179
+ * the body would be re-presented unchanged and refused again, this time by the schema's
1180
+ * monotonic rule, which is correctly never retried. The row's read inside is `retry: false`
1181
+ * (see {@link queryStampId}'s NOTE).
1182
+ *
1183
+ * Returns the record actually written, so the caller's bookkeeping describes the row rather
1184
+ * than the first record it signed; null, and nothing written, when no readable row exists:
1185
+ * the row is absent, or its stamp is retired in `Revocation` (a revoked node reads its own row
1186
+ * as absent every heartbeat, and must not refresh it).
1187
+ *
909
1188
  * Deliberately NOT wrapped in {@link mutateCadrePeer}, the one `CadrePeer` mutator that
910
1189
  * is not: it only ever touches THIS node's own row (sole caller
911
1190
  * `CadreNode.publishSelfRecord`), and `CadreNode.listAuthorizedMembers` filters self out
@@ -918,8 +1197,11 @@ export declare class ControlDatabase {
918
1197
  * row, and a self insert happens once at startup, so the wasted refresh is a single read
919
1198
  * — cheaper than a conditional carve-out inside the shared writer. Only the repeating
920
1199
  * path is worth exempting.
1200
+ *
1201
+ * @param peerId - the row to refresh: this node's own.
1202
+ * @param sign - builds the signed record over the row as read; called once per attempt.
921
1203
  */
922
- updateSelfPeerRecord(record: PeerAddressRecord): Promise<void>;
1204
+ updateSelfPeerRecord(peerId: string, sign: SelfRecordSigner): Promise<PeerAddressRecord | null>;
923
1205
  /**
924
1206
  * Read a single peer's device push token (the full `DeviceToken` row) by PeerId.
925
1207
  *
@@ -948,19 +1230,34 @@ export declare class ControlDatabase {
948
1230
  updateSelfDeviceToken(record: DeviceTokenRecord): Promise<void>;
949
1231
  /**
950
1232
  * Insert the initial owner key (bootstrap - no existing owners required)
1233
+ *
1234
+ * @param label - the user's name for the key, at most {@link OWNER_KEY_MAX_LABEL}
1235
+ * characters once trimmed; blank is stored as none.
951
1236
  */
952
- insertOwnerKey(key: string): Promise<void>;
1237
+ insertOwnerKey(key: string, label?: string): Promise<void>;
953
1238
  /**
954
1239
  * Owner-signed INSERT of a further `OwnerKey` row, notifying the membership listener once
955
1240
  * it has committed. The signed-add branch of `OwnerKey.Authorized`: the signer must be an
956
1241
  * owner in the pre-transaction snapshot, its signature covers the `'add'` digest over
957
- * (Key, StampId), and the stored `VouchOwner`/`VouchSig` are that same pair, so a reader
958
- * can later judge the add against its own anchor (`verifyOwnerKeyVoucher`).
1242
+ * (Key, Label, StampId), and the stored `VouchOwner`/`VouchSig` are that same pair, so a
1243
+ * reader can later judge the add against its own anchor (`verifyOwnerKeyVoucher`).
959
1244
  *
960
1245
  * Through {@link mutateCadrePeer}, the membership hub. The liveness read, the stamp and the
961
1246
  * signature all sit inside the locked body: a {@link lockedWithRetry} re-run starts over a
962
1247
  * rolled-back attempt with a fresh stamp, as {@link redeemCadreInvite} does.
963
1248
  *
1249
+ * @param label - the user's name for the key, as {@link insertOwnerKey}'s.
1250
+ * @returns `true` when this call seated the row, `false` when `key` is already a live owner
1251
+ * here (nothing written, whatever its label).
1252
+ * @throws when this node holds `key`'s row under a retired stamp
1253
+ * ({@link liveOwnerKeyStamp}), or when the schema refuses the signer.
1254
+ */
1255
+ insertOwnerKeyVouched(key: string, ownerKey: string, signMessage: ControlSign, label?: string | null): Promise<boolean>;
1256
+ /**
1257
+ * The stamp of `key`'s live `OwnerKey` row here, or null when this node holds none: the
1258
+ * insert-if-absent read of {@link insertOwnerKeyVouched} and {@link redeemOwnerGrant}. Inside
1259
+ * the locked body (both reads `retry: false`; see queryStampId's NOTE).
1260
+ *
964
1261
  * NOTE: re-adding a removed owner from a machine that still holds its retired physical row
965
1262
  * is refused here by name: `OwnerKey` is not reaped, so a machine that received the
966
1263
  * tombstone without the delete (see {@link getOwnerKeys}) keeps the row, and an insert there
@@ -968,12 +1265,15 @@ export declare class ControlDatabase {
968
1265
  * machine the delete reached; if re-adding removed owners becomes routine, the fix is an
969
1266
  * `OwnerKey` reap branch, gated so it can never take the table below `MinOneOwner`.
970
1267
  *
971
- * @returns `true` when this call seated the row, `false` when `key` is already a live owner
972
- * here (nothing written).
973
- * @throws when this node holds `key`'s row under a retired stamp (above), or when the
974
- * schema refuses the signer.
1268
+ * @throws when this node holds `key`'s row under a retired stamp (above).
975
1269
  */
976
- insertOwnerKeyVouched(key: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
1270
+ private liveOwnerKeyStamp;
1271
+ /**
1272
+ * The signed-add statement of {@link insertOwnerKeyVouched} and {@link revouchOwnerKey}, the
1273
+ * stored voucher being the signing pair. Bare `exec`: callers hold the write lock, which is
1274
+ * NOT re-entrant.
1275
+ */
1276
+ private execOwnerKeyVouchedInsert;
977
1277
  /**
978
1278
  * Owner-signed removal of one `OwnerKey` row plus the `Revocation` tombstone retiring its
979
1279
  * stamp, notifying the membership listener once it has committed. The remove branch of
@@ -991,56 +1291,58 @@ export declare class ControlDatabase {
991
1291
  * (nothing written, no tombstone).
992
1292
  * @throws when `key` is `ownerKey`, or is the only live owner.
993
1293
  */
994
- deleteOwnerKey(key: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
1294
+ deleteOwnerKey(key: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
995
1295
  /**
996
1296
  * Re-vouch a live `OwnerKey` row under this owner's signature: in ONE transaction, the
997
1297
  * current row's guarded delete plus tombstone ({@link deleteOwnerKey}'s body) and an insert
998
- * of the same key under a fresh stamp, signed by `ownerKey` ({@link insertOwnerKeyVouched}'s
999
- * body), as {@link rewritePendingJoin} re-seats a request. `OwnerKey` has `NoUpdate`, so
1298
+ * of the same key and label under a fresh stamp, signed by `ownerKey`
1299
+ * ({@link insertOwnerKeyVouched}'s body), as {@link rewritePendingJoin} re-seats a request. `OwnerKey` has `NoUpdate`, so
1000
1300
  * this is the only way to rebind a row's voucher; the anchor derivation lets the re-add
1001
1301
  * under a fresh stamp beat the older tombstone (`deriveOwnerAnchor`). For
1002
- * {@link removeMachine}: the owners a machine being removed had added keep their anchor
1003
- * place everywhere once their row stands on a remaining owner's signature.
1302
+ * `CadreNode.removeOwner`: the owners a key being removed had added keep their anchor place
1303
+ * everywhere once their row stands on a remaining owner's signature.
1004
1304
  *
1005
- * Through {@link mutateCadrePeer}, the membership hub. The stamp read runs inside the locked
1305
+ * Through {@link mutateCadrePeer}, the membership hub. The row read runs inside the locked
1006
1306
  * body; a row whose stamp is already retired (held without its delete) is reported as absent
1007
1307
  * rather than tombstoned twice.
1008
1308
  *
1009
1309
  * @returns `true` when the row was re-vouched, `false` when `key` is not a live owner here.
1010
1310
  * @throws when `key` is `ownerKey` (an owner cannot sign its own row's removal).
1011
1311
  */
1012
- revouchOwnerKey(key: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
1312
+ revouchOwnerKey(key: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
1013
1313
  /**
1014
- * Remove one machine completely, in ONE transaction: its `CadrePeer` row, its `OwnerKey`
1015
- * row when `target.ownerKey` names one, its `DeviceToken` row when it has one, each with
1016
- * the `Revocation` tombstone retiring its stamp ({@link deleteGuardedRow}'s statement pair,
1017
- * composed bare as {@link deleteStrandAndPartyKey} composes it), plus a withdrawal tombstone
1018
- * for every `CadreInvite` in `target.inviteKeys` that is still live here
1019
- * ({@link withdrawCadreInvite}'s body). One transaction so no reader sees the machine half
1020
- * removed: a row gone but its push token resolvable, or its owner key gone but an invitation
1021
- * it could redeem again still open.
1314
+ * Remove one machine completely, in ONE transaction: its `CadrePeer` row and its
1315
+ * `DeviceToken` row when it has one, each with the `Revocation` tombstone retiring its stamp
1316
+ * ({@link deleteGuardedRow}'s statement pair, composed bare as {@link deleteStrandAndPartyKey}
1317
+ * composes it), plus a withdrawal tombstone for every `CadreInvite` in `target.inviteKeys`
1318
+ * that is still live here ({@link withdrawCadreInvite}'s body). One transaction so no reader
1319
+ * sees the machine half removed: a row gone but its push token resolvable, or an invitation
1320
+ * it could redeem again still open. Owner keys are never touched: they are the person's, and
1321
+ * removing one is {@link deleteOwnerKey}.
1022
1322
  *
1023
1323
  * Through {@link mutateCadrePeer}, the membership hub. Every stamp is read inside the locked
1024
1324
  * body, and a row whose stamp is already retired (held without its delete) is skipped rather
1025
- * than tombstoned twice. The by-name owner refusals of {@link deleteOwnerKey} apply to the
1026
- * owner-key arm. Nothing to remove writes nothing and returns all false.
1325
+ * than tombstoned twice. Nothing to remove writes nothing and returns all false.
1027
1326
  *
1028
- * The caller decides WHAT to remove, which owner key is the machine's and which invitations
1029
- * it could use, from its plan (`CadreNode.planMachineRemoval`); this method only refuses to
1030
- * write an inconsistent set.
1327
+ * Opted into {@link LOST_UPDATE_RETRY_POLICY} for the reason on {@link deleteCadrePeer}: every
1328
+ * stamp and signature is minted inside the body, and the invitation keys come from the
1329
+ * caller's plan, which a self-publish does not touch (a withdrawal of an invitation withdrawn
1330
+ * since collides on `Revocation`'s primary key — a constraint failure, never retried, as
1331
+ * before). A lost update after a sibling tree of the transaction already synced arrives as a
1332
+ * partial commit and stays declined.
1031
1333
  *
1032
- * @throws when `target.ownerKey` is `ownerKey`, or is the only live owner here.
1334
+ * The caller decides which invitations the machine could use, from its plan
1335
+ * (`CadreNode.planMachineRemoval`).
1033
1336
  */
1034
1337
  removeMachine(target: {
1035
1338
  peerId: string;
1036
- ownerKey: string | null;
1037
1339
  inviteKeys: readonly string[];
1038
- }, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<MachineRemovalWrites>;
1340
+ }, ownerKey: string, signMessage: ControlSign): Promise<MachineRemovalResult>;
1039
1341
  /**
1040
1342
  * The guarded removals {@link removeMachine} runs, signed over each row's current stamp:
1041
- * the `CadrePeer` row, the `OwnerKey` row when named, the `DeviceToken` row when present.
1042
- * Inside the locked body (every read `retry: false`); one `Revocation` scan serves the
1043
- * retired-stamp check for all three tables and the last-owner refusal.
1343
+ * the `CadrePeer` row and the `DeviceToken` row, each when present. Inside the locked body
1344
+ * (every read `retry: false`); one `Revocation` scan serves the retired-stamp check for both
1345
+ * tables.
1044
1346
  */
1045
1347
  private signMachineRemovals;
1046
1348
  /**
@@ -1062,7 +1364,29 @@ export declare class ControlDatabase {
1062
1364
  * with the owner's private key, returning a base64url signature
1063
1365
  * @param memberPrivateKey - Optional private key for membership in closed strands
1064
1366
  */
1065
- insertStrand(strandId: string, type: 'o' | 'c', ownerKey: string, signMessage: (message: Uint8Array) => string, memberPrivateKey?: string): Promise<void>;
1367
+ insertStrand(strandId: string, type: 'o' | 'c', ownerKey: string, signMessage: ControlSign, memberPrivateKey?: string): Promise<void>;
1368
+ /** The stamped, owner-signed `Strand` insert that {@link insertStrand} and {@link insertFoundingRows} run. */
1369
+ private signStrandInsert;
1370
+ /**
1371
+ * Insert one founding's control rows — the `Strand` row, a closed strand's `StrandPartyKey`
1372
+ * row, and a formation invitation bound to the strand — in ONE transaction: one commit, and
1373
+ * never a strand row without the key or invitation written beside it.
1374
+ *
1375
+ * Each row is stamped and signed exactly as its single-row writer ({@link insertStrand},
1376
+ * {@link insertStrandPartyKey}, {@link insertFormationInvite}) does it, and all of that — the
1377
+ * invitation's {@link canonicalDatetime} read included — happens BEFORE the write lock is
1378
+ * taken: signing may prompt the user, and the lock held across a prompt would stall every
1379
+ * other control write. A {@link lockedWithRetry} re-run re-presents the same signed rows.
1380
+ *
1381
+ * No row's `AuthorizedInsert` reads another of these rows (`StrandPartyKey` and
1382
+ * `FormationInvite` check only `OwnerKey` and `Revocation`), so their order is free; the
1383
+ * `Strand` row goes first, so a founding repeated over a stored row is refused on `Strand.Id`.
1384
+ *
1385
+ * A uniqueness rejection rolls the whole transaction back. The caller resolves it and writes
1386
+ * again: a `Strand.Id` conflict is another founder's row, a `StrandPartyKey.Id` conflict a key
1387
+ * stored with no strand row ({@link isStrandIdConflict}; see `CadreNode.publishStrand`).
1388
+ */
1389
+ insertFoundingRows(rows: FoundingRows, ownerKey: string, signMessage: ControlSign): Promise<void>;
1066
1390
  /**
1067
1391
  * Delete a strand from the control database using an owner signature.
1068
1392
  *
@@ -1090,7 +1414,7 @@ export declare class ControlDatabase {
1090
1414
  * A no-op (no throw, no tombstone) when the row does not exist — `false` then, `true`
1091
1415
  * when a row was actually removed.
1092
1416
  */
1093
- deleteStrand(strandId: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
1417
+ deleteStrand(strandId: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
1094
1418
  /**
1095
1419
  * The locked body of {@link deleteStrand}: the `Strand` delete + tombstone, plus — when
1096
1420
  * one exists — the strand's `StrandPartyKey` delete + tombstone, all in ONE
@@ -1120,7 +1444,9 @@ export declare class ControlDatabase {
1120
1444
  * @param signMessage - Function that ed25519-signs the raw message bytes (no pre-hash)
1121
1445
  * with the owner's private key, returning a base64url signature.
1122
1446
  */
1123
- insertStrandPartyKey(strandId: string, privateKey: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<void>;
1447
+ insertStrandPartyKey(strandId: string, privateKey: string, ownerKey: string, signMessage: ControlSign): Promise<void>;
1448
+ /** The stamped, owner-signed `StrandPartyKey` insert that {@link insertStrandPartyKey} and {@link insertFoundingRows} run. */
1449
+ private signStrandPartyKeyInsert;
1124
1450
  /**
1125
1451
  * Owner-signed removal of one `StrandPartyKey` row — the party's own membership
1126
1452
  * identity for that strand, stored nowhere else, so this is as destructive as
@@ -1133,7 +1459,7 @@ export declare class ControlDatabase {
1133
1459
  * `Revocation` tombstone in the same transaction. A no-op (no throw, no tombstone)
1134
1460
  * when the row does not exist — `false` then, `true` when a row was actually removed.
1135
1461
  */
1136
- deleteStrandPartyKey(strandId: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
1462
+ deleteStrandPartyKey(strandId: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
1137
1463
  /**
1138
1464
  * Record, party-wide, a strand this party joined from another party (`JoinedStrand` row),
1139
1465
  * using an owner signature.
@@ -1146,14 +1472,14 @@ export declare class ControlDatabase {
1146
1472
  * Fails with a `JoinedStrand.Id` uniqueness violation when the id is already recorded;
1147
1473
  * `isStrandIdConflict(error, 'JoinedStrand')` identifies that case.
1148
1474
  */
1149
- insertJoinedStrand(row: Pick<StrandRow, 'Id' | 'Type' | 'MemberPrivateKey'>, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<void>;
1475
+ insertJoinedStrand(row: Pick<StrandRow, 'Id' | 'Type' | 'MemberPrivateKey'>, ownerKey: string, signMessage: ControlSign): Promise<void>;
1150
1476
  /**
1151
1477
  * Owner-signed removal of one `JoinedStrand` row. Mirrors {@link deleteStrandPartyKey}:
1152
1478
  * `'remove'`-tagged digest over (Id, StampId), `Revocation` tombstone in the same
1153
1479
  * transaction. A no-op (no throw, no tombstone) when the row does not exist — `false`
1154
1480
  * then, `true` when a row was actually removed.
1155
1481
  */
1156
- deleteJoinedStrand(strandId: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
1482
+ deleteJoinedStrand(strandId: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
1157
1483
  /**
1158
1484
  * Record, party-wide, a join this party asked for: a `JoinRequest` row under a fresh stamp,
1159
1485
  * owner-signed over every column. Returns the pending join as written.
@@ -1161,7 +1487,7 @@ export declare class ControlDatabase {
1161
1487
  * Fails with a `JoinRequest.Id` uniqueness violation when the party already holds a request
1162
1488
  * for this invitation; {@link isPendingJoinConflict} identifies that case.
1163
1489
  */
1164
- insertJoinRequest(request: JoinRequestFields, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<PendingJoin>;
1490
+ insertJoinRequest(request: JoinRequestFields, ownerKey: string, signMessage: ControlSign): Promise<PendingJoin>;
1165
1491
  /**
1166
1492
  * Record the outcome of the pending join `expected`: its owner-signed `JoinSuccess` /
1167
1493
  * `JoinFailure` row, keyed by the request's stamp. A join recorded over a failure deletes the
@@ -1175,7 +1501,7 @@ export declare class ControlDatabase {
1175
1501
  * transaction instead (its outcome row collides on the primary key, or its tombstone retires
1176
1502
  * the stamp), and the caller re-reads either way.
1177
1503
  */
1178
- recordJoinOutcome(expected: PendingJoin, outcome: JoinOutcome, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<PendingJoin>;
1504
+ recordJoinOutcome(expected: PendingJoin, outcome: JoinOutcome, ownerKey: string, signMessage: ControlSign): Promise<PendingJoin>;
1179
1505
  /**
1180
1506
  * Re-write the pending join `next` (request and outcome) under a fresh stamp, retiring the
1181
1507
  * live request incarnation `expectedStampId`: how a finished request is asked for again, and
@@ -1190,14 +1516,14 @@ export declare class ControlDatabase {
1190
1516
  * that committed but reported failure throws {@link PendingJoinChangedError} whose live join
1191
1517
  * carries the returned stamp, so a caller that re-reads finds its write.
1192
1518
  */
1193
- rewritePendingJoin(expectedStampId: string, next: PendingJoinFields, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<PendingJoin>;
1519
+ rewritePendingJoin(expectedStampId: string, next: PendingJoinFields, ownerKey: string, signMessage: ControlSign): Promise<PendingJoin>;
1194
1520
  /**
1195
1521
  * Owner-signed removal of one pending join, as {@link deleteJoinedStrand} removes its row:
1196
1522
  * `'remove'`-tagged digest over (Id, StampId), `Revocation` tombstone in the same transaction,
1197
1523
  * and the request's outcome row with it. A no-op (no throw, no tombstone) when there is no
1198
1524
  * request — `false` then, `true` when one was actually removed.
1199
1525
  */
1200
- deletePendingJoin(id: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
1526
+ deletePendingJoin(id: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
1201
1527
  /**
1202
1528
  * Mint a fresh stamp for `request` and the owner signature `JoinRequest.AuthorizedInsert`
1203
1529
  * verifies, returning the row as it will be written and its insert.
@@ -1224,7 +1550,7 @@ export declare class ControlDatabase {
1224
1550
  * @param signMessage - Function that ed25519-signs the raw message bytes (no pre-hash)
1225
1551
  * with the owner's private key, returning a base64url signature
1226
1552
  */
1227
- insertValidationKey(key: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<void>;
1553
+ insertValidationKey(key: string, ownerKey: string, signMessage: ControlSign): Promise<void>;
1228
1554
  /**
1229
1555
  * Delete a validation key from the control database using an owner signature.
1230
1556
  *
@@ -1241,7 +1567,7 @@ export declare class ControlDatabase {
1241
1567
  * A no-op (no throw, no tombstone) when the row does not exist — `false` then, `true`
1242
1568
  * when a row was actually removed.
1243
1569
  */
1244
- deleteValidationKey(key: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
1570
+ deleteValidationKey(key: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
1245
1571
  /**
1246
1572
  * Owner-vouched INSERT of one `CadrePeer` membership row, notifying the membership
1247
1573
  * listener once it has committed.
@@ -1286,34 +1612,46 @@ export declare class ControlDatabase {
1286
1612
  updatedAt: number;
1287
1613
  sig: string | null;
1288
1614
  profile: NodeProfile | null;
1289
- }, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
1290
- /**
1291
- * Owner "re-touch" of an existing `CadrePeer` membership row: bump `UpdatedAt` and
1292
- * rewrite `VouchOwner`/`VouchSig` under the owner branch of `CadrePeer.AuthorizedUpdate`
1293
- * (a signature over the SAME `'vouch'`-tagged digest {@link insertCadrePeer} builds), so
1294
- * the row is re-emitted as a fresh, broadcasting transaction.
1295
- *
1296
- * This is the write-while-alone re-replication primitive: a membership row that committed
1297
- * local-only (its block's cluster ≤1 at insert) is pushed to the cohort once it grows, by
1298
- * re-issuing this monotonic bump. It is an UPDATE (not the original INSERT) because the row
1299
- * already exists locally; a re-INSERT would hit the `PeerId` PK. Only the freshness stamp
1300
- * and voucher change — `PublicKey` / `Multiaddr` / `Sig` / `Profile` are left intact (the
1301
- * owner branch requires them kept: they are the machine's own claim) — so it is safe
1302
- * over a row whose peer has not self-published (`Sig` null); the caller must skip a row
1303
- * that already carries a self-`Sig` (that row is the owning peer's to refresh, and bumping
1304
- * `UpdatedAt` without re-signing would invalidate its self-signature).
1305
- *
1306
- * Signs over the row's CURRENT `StampId` (unchanged by the re-touch), read BEFORE the lock
1307
- * is taken. An absent row returns `false` WITHOUT notifying: nothing was written, so the
1308
- * membership snapshot cannot have changed.
1309
- *
1310
- * NOTE: that stamp read is outside the lock, same as {@link deleteGuardedRow}'s. A writer
1311
- * that removes the row in between makes the `update` match nothing, and this still returns
1312
- * `true` and notifies — harmless today (the sole caller, the write-while-alone drain, only
1313
- * logs the result, and a spurious notify just re-reads the member set), and a remove-then-
1314
- * re-add in the same window fails loudly instead, since the signature binds the retired
1315
- * stamp. If a caller ever acts on `true` as proof the row was written, fold the stamp read
1316
- * into the locked body.
1615
+ }, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
1616
+ /**
1617
+ * Owner "re-touch" of an existing `CadrePeer` membership row: rewrite `VouchOwner`/`VouchSig`
1618
+ * (and, re-broadcasting, bump `UpdatedAt`) under the owner branch of
1619
+ * `CadrePeer.AuthorizedUpdate` — a signature over the SAME `'vouch'`-tagged digest
1620
+ * {@link insertCadrePeer} builds — so the row is re-emitted as a fresh, broadcasting
1621
+ * transaction, or stands on a different owner's voucher.
1622
+ *
1623
+ * Re-broadcasting is the write-while-alone re-replication primitive: a membership row that
1624
+ * committed local-only (its block's cluster ≤1 at insert) is pushed to the cohort once it
1625
+ * grows, by re-issuing this monotonic bump. It is an UPDATE (not the original INSERT) because
1626
+ * the row already exists locally; a re-INSERT would hit the `PeerId` PK. Only the freshness
1627
+ * stamp and voucher change — `PublicKey` / `Multiaddr` / `Sig` / `Profile` are left intact
1628
+ * (the owner branch requires them kept: they are the machine's own claim) — so it is safe
1629
+ * over a row whose peer has not self-published (`Sig` null), and this method itself skips a
1630
+ * row that already carries a self-`Sig` (that row is the owning peer's to refresh, and
1631
+ * bumping `UpdatedAt` without re-signing would invalidate its self-signature).
1632
+ *
1633
+ * The two modes ({@link CadrePeerRetouchMode}) differ in what they write: `'rebroadcast'`
1634
+ * bumps `UpdatedAt` to `max(now, current + 1)` from the row read inside the body and writes
1635
+ * only while the row's `Sig` is null; `'revouch'` leaves `UpdatedAt` out of the `set` list and
1636
+ * writes whether or not the machine has self-published (the owner branch has no monotonic
1637
+ * rule, so the column simply keeps its value).
1638
+ *
1639
+ * The row read (stamp, `Sig`, `UpdatedAt`), the mode's decision and the signature over the
1640
+ * row's CURRENT `StampId` (unchanged by the re-touch) all run INSIDE the locked body, which is
1641
+ * what lets both modes opt into {@link LOST_UPDATE_RETRY_POLICY}: the row's other writer is
1642
+ * the machine's own publish ({@link updateSelfPeerRecord}), and the drain's re-broadcast fires
1643
+ * at the moment that publish does. A re-run after losing to it reads the self-signed row and
1644
+ * reports `'self-published'` instead of writing the owner branch over it — which would rewrite
1645
+ * `UpdatedAt` under a `Sig` signed over the old value and make the record unverifiable to
1646
+ * resolvers until the machine's next heartbeat. The decision has to be the body's for that;
1647
+ * a caller's pre-check outside the lock is only a fast path (see below).
1648
+ *
1649
+ * An absent row — none, or its stamp retired, or a stampless row no writer produces — is
1650
+ * `'absent'`. Checked once outside the lock too, as `'self-published'` is: the common case
1651
+ * writes nothing, takes no lock and does NOT notify, so an absent target stays quiet
1652
+ * ({@link SeedBootstrapService.removePeer} keeps the same gate for the same reason). The
1653
+ * in-body re-check covers the race; a no-write outcome reached there notifies spuriously,
1654
+ * which only re-reads the member set.
1317
1655
  *
1318
1656
  * Notifies like the insert/remove paths even though this is "only" a re-touch: it rewrites
1319
1657
  * VouchOwner/VouchSig, which the authorized-membership predicate judges on, so it CAN
@@ -1327,21 +1665,22 @@ export declare class ControlDatabase {
1327
1665
  *
1328
1666
  * NOTE: this rebinds VouchOwner to the CALLING owner's key, and the authorized-membership
1329
1667
  * predicate (`CadreNode.listAuthorizedMembers`) now judges rows by that column against each
1330
- * reader's node-local anchor. Benign today because the only caller — the write-while-alone
1331
- * drain — re-touches solely rows this node itself authored (`pendingPeerWrites`), so the
1332
- * voucher is rewritten to the key that already signed it. If a future path ever lets one
1333
- * owner re-touch a row a DIFFERENT owner vouched, the voucher silently flips: readers that
1334
- * anchor the original owner but not this one would drop a legitimate member. Such a path
1335
- * must re-vouch deliberately (or preserve the existing VouchOwner/VouchSig) rather than
1336
- * inherit this rebinding.
1668
+ * reader's node-local anchor. The two callers rebind on purpose: the write-while-alone drain
1669
+ * re-touches solely rows this node itself authored (`pendingPeerWrites`), so the voucher is
1670
+ * rewritten to the key that already signed it, and `CadreNode.removeOwner` moves rows off a
1671
+ * key it is about to remove. A new caller that re-touches a row a DIFFERENT owner vouched
1672
+ * flips the voucher silently: readers that anchor the original owner but not this one would
1673
+ * drop a legitimate member. It must re-vouch deliberately (or preserve the existing
1674
+ * VouchOwner/VouchSig) rather than inherit this rebinding.
1337
1675
  *
1338
1676
  * @param peerId - the membership row to re-touch.
1339
- * @param updatedAt - the strictly-increasing freshness stamp to write.
1677
+ * @param mode - re-broadcast or re-vouch ({@link CadrePeerRetouchMode}).
1340
1678
  * @param ownerKey - public key of the re-vouching owner, rewritten into `VouchOwner`.
1341
- * @param signMessage - ed25519-signs the raw message bytes with that owner's private key.
1342
- * @returns `true` when the row was re-touched, `false` when no row exists (no notify).
1679
+ * @param signMessage - ed25519-signs the raw message bytes with that owner's private key;
1680
+ * called inside the lock, once per attempt (the NOTE on {@link ControlSign}).
1681
+ * @returns what was found and done ({@link CadrePeerRetouchOutcome}).
1343
1682
  */
1344
- reauthorizeCadrePeer(peerId: string, updatedAt: number, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
1683
+ reauthorizeCadrePeer(peerId: string, mode: CadrePeerRetouchMode, ownerKey: string, signMessage: ControlSign): Promise<CadrePeerRetouchOutcome>;
1345
1684
  /**
1346
1685
  * Owner-signed removal of one `CadrePeer` membership row, notifying the membership
1347
1686
  * listener once it has committed.
@@ -1361,8 +1700,14 @@ export declare class ControlDatabase {
1361
1700
  *
1362
1701
  * A no-op (no throw, no tombstone) when the row does not exist — `false` then, `true`
1363
1702
  * when a row was actually removed.
1703
+ *
1704
+ * Opted into {@link LOST_UPDATE_RETRY_POLICY}: the row's machine may publish its own record
1705
+ * at the moment of the removal (its first publish lands about a second after it starts), and
1706
+ * the removal that loses is refused as a lost update. {@link deleteGuardedRow} reads the stamp
1707
+ * and signs inside the body, and a self-publish never changes the stamp the `'remove'` digest
1708
+ * binds, so the re-run reads the published row and removes it.
1364
1709
  */
1365
- deleteCadrePeer(peerId: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
1710
+ deleteCadrePeer(peerId: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
1366
1711
  /**
1367
1712
  * Owner-signed removal of one peer's `DeviceToken` row (logout / token invalidation).
1368
1713
  *
@@ -1376,7 +1721,7 @@ export declare class ControlDatabase {
1376
1721
  * A no-op (no throw, no tombstone) when the row does not exist — `false` then, `true`
1377
1722
  * when a row was actually removed.
1378
1723
  */
1379
- deleteDeviceToken(peerId: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
1724
+ deleteDeviceToken(peerId: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
1380
1725
  /**
1381
1726
  * Owner-signed delete of one guarded row plus the `Revocation` tombstone retiring its
1382
1727
  * stamp, in ONE transaction. The single body behind EVERY guarded delete —
@@ -1421,10 +1766,9 @@ export declare class ControlDatabase {
1421
1766
  private execGuardedRemoval;
1422
1767
  /**
1423
1768
  * File the owner-signed `Revocation` tombstone retiring one row incarnation — the second
1424
- * statement of every guarded removal ({@link execGuardedRemoval}) and the whole of a
1425
- * `CadreInvite` withdrawal ({@link withdrawCadreInvite}), whose row stays — or the ledger
1426
- * marker ({@link openRevocationLedger}), which retires nothing. The caller supplies the
1427
- * lock and, where one is needed, the transaction.
1769
+ * statement of every guarded removal ({@link execGuardedRemoval}) and the whole of an
1770
+ * issued row's withdrawal ({@link withdrawIssuedRow}), whose row stays. The caller
1771
+ * supplies the lock and, where one is needed, the transaction.
1428
1772
  *
1429
1773
  * `ReissuedAt` is named explicitly at 0 (the only value `FreshTombstone` accepts) rather
1430
1774
  * than leaning on the column default — the seat-at-zero rule is load-bearing for
@@ -1462,9 +1806,10 @@ export declare class ControlDatabase {
1462
1806
  * statement, and without the clause the reap would delete the owner's brand-new row.
1463
1807
  * With it the statement matches nothing and the reap is a silent no-op (the schema
1464
1808
  * branch would also refuse that delete, but as a constraint error thrown into a
1465
- * background sweep — matched-nothing is the better failure mode). Like
1466
- * {@link reauthorizeCadrePeer}, a race lost AFTER the guard still returns `true`;
1467
- * harmless — nothing was deleted, and the next sweep re-reads.
1809
+ * background sweep — matched-nothing is the better failure mode). A race lost AFTER the
1810
+ * guard still returns `true` here (unlike {@link reauthorizeCadrePeer}, which reads inside
1811
+ * its lock and reports what it found); harmless — nothing was deleted, and the next sweep
1812
+ * re-reads.
1468
1813
  *
1469
1814
  * Writes NOTHING to `Revocation`: `RevocationRecorded` is satisfied by the same
1470
1815
  * committed tombstone that authorizes the reap, so a reap files no second tombstone.
@@ -1491,7 +1836,7 @@ export declare class ControlDatabase {
1491
1836
  * present clause are the shape {@link redeemInvitation}'s consent-branch insert ships).
1492
1837
  *
1493
1838
  * Driven automatically by {@link reapRevokedRows}, which the periodic control-cohort
1494
- * reconcile pass runs while the node has at least one control connection.
1839
+ * reconcile pass runs while a sibling is reachable (`CadreNode.hasReachableControlSibling`).
1495
1840
  */
1496
1841
  reapRevokedRow(table: ReapableTable, rowKey: string, stampId: string): Promise<boolean>;
1497
1842
  /**
@@ -1502,8 +1847,8 @@ export declare class ControlDatabase {
1502
1847
  * carry the delete (replaying a delete of an already-absent row is a no-op), so a node
1503
1848
  * that converged on a removal while holding the removed row keeps that row as inert
1504
1849
  * garbage until something walks the tombstones and drops it. `CadreNode`'s periodic
1505
- * control-cohort reconcile pass is that something, gated on the node having at least one
1506
- * control connection (a reap is a write, and a write committed alone is local-only).
1850
+ * control-cohort reconcile pass is that something, gated on a sibling being reachable
1851
+ * (a reap is a write, and a write committed alone is local-only).
1507
1852
  *
1508
1853
  * Enumerated UNLOCKED, deleted per-row LOCKED: {@link reapRevokedRow} takes the write
1509
1854
  * lock itself (via {@link mutateCadrePeer} / {@link execWrite}) and the lock is not
@@ -1536,12 +1881,11 @@ export declare class ControlDatabase {
1536
1881
  *
1537
1882
  * NOTE: cost is O(tombstones), not O(live rows) — one {@link queryRevocations} scan plus
1538
1883
  * one {@link queryStampId} point lookup per tombstone, and the empty early return makes
1539
- * the common case (a party that has never revoked anyone; {@link queryRevocations} skips
1540
- * the ledger marker) a single scan per pass. That scan consults the cohort on every pass
1541
- * only while `Revocation` has never been written; the ledger marker
1542
- * ({@link openRevocationLedger}), filed by the same connected pass, makes it a held block.
1884
+ * the common case (a party that has never revoked anyone) a single scan per pass, served
1885
+ * from the `Revocation` membership view
1886
+ * ({@link readMembershipRows}) unless the table changed since the last read.
1543
1887
  * But `Revocation` is append-only and unbounded, so this is O(all tombstones ever) point
1544
- * lookups on every reconcile tick. Fine while revocations stay rare for a cadre-sized
1888
+ * lookups on every connected reconcile pass. Fine while revocations stay rare for a cadre-sized
1545
1889
  * party; if the table ever grows, persist a node-local high-water mark of what has
1546
1890
  * already been reaped instead of re-scanning everything (the same bound the sweep in
1547
1891
  * `CadreNode.drainPendingRevocations` wants).
@@ -1575,45 +1919,7 @@ export declare class ControlDatabase {
1575
1919
  *
1576
1920
  * Returns how many UPDATE statements ran (`rows.length` on success).
1577
1921
  */
1578
- reissueRevocations(rows: readonly RevocationRow[], reissuedAt: number, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<number>;
1579
- /**
1580
- * Owner-signed filing of the singleton `Revocation` ledger marker
1581
- * ({@link REVOCATION_LEDGER_MARKER}), the one row in that table that retires nothing.
1582
- *
1583
- * Why it exists: while `Revocation` has never been written, this node does not hold its
1584
- * block, and Optimystic consults a block's cohort on EVERY read of a block it does not
1585
- * hold — and every membership lookup, and every guarded insert's `NotRevoked` check,
1586
- * reads this table. Once any row exists the block is held and re-checked at most once per
1587
- * read-repair window, like every other populated table. The schema's table comment says
1588
- * why the row can never read as a retirement.
1589
- *
1590
- * Insert-if-absent, shaped like {@link reissueRevocations}: the signature is minted
1591
- * OUTSIDE the locked body so a retried attempt re-presents the same one, and the guard
1592
- * runs INSIDE it so it sees a concurrent local writer's committed row. The guard scans
1593
- * the `'Revocation'` rows and compares the stamp in TypeScript rather than seeking the
1594
- * full composite primary key, which is served as a point lookup that can miss an existing
1595
- * row on a networked database (tickets/backlog/debt-composite-pk-point-lookup-unreliable-untracked).
1596
- *
1597
- * Two owners filing at once: the loser either sees the marker in its guard or is refused
1598
- * on the primary key, and both answer `'already-open'`.
1599
- *
1600
- * The caller decides WHEN. A marker committed while the node is alone is local-only and
1601
- * can fork the collection, so {@link CadreNode} files it only while connected; this
1602
- * method does not look at connectivity.
1603
- *
1604
- * @param ownerKey - owner public key for the write context (`Revocation.Authorized`).
1605
- * @param signMessage - ed25519-signs the raw message bytes (no pre-hash) with that owner's
1606
- * private key, returning a base64url signature.
1607
- * @returns `'opened'` when this call filed the marker, `'already-open'` when it was
1608
- * already there.
1609
- */
1610
- openRevocationLedger(ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<RevocationLedgerOpenResult>;
1611
- /**
1612
- * Whether the ledger marker is present locally: a scan of the `'Revocation'` rows with the
1613
- * stamp compared in TypeScript (see {@link openRevocationLedger} for why not a seek).
1614
- * Called only inside the locked write body, so never retried (see queryStampId's NOTE).
1615
- */
1616
- private revocationLedgerFiled;
1922
+ reissueRevocations(rows: readonly RevocationRow[], reissuedAt: number, ownerKey: string, signMessage: ControlSign): Promise<number>;
1617
1923
  /**
1618
1924
  * Run `body` between `beginTransaction` and `commit`, rolling back on failure.
1619
1925
  *
@@ -1645,8 +1951,21 @@ export declare class ControlDatabase {
1645
1951
  * {@link CadreNode}, which wires this in `start()` and clears it on teardown. A
1646
1952
  * second call replaces the first rather than fanning out, so a stale node can
1647
1953
  * never keep receiving notifications for a database it no longer owns.
1954
+ *
1955
+ * Only this database's own writes notify here, awaited inside the write so the gate is
1956
+ * current before the write returns. A membership change replicated in from another
1957
+ * machine reaches the gate through the subscription `CadreNode.start` takes on the
1958
+ * membership tables ({@link watchTables}), a coalesced run up to 50 ms after the change
1959
+ * lands in storage; one storage never hears of waits for the reconcile pass's backstop.
1648
1960
  */
1649
1961
  setMembershipChangeListener(listener: MembershipChangeListener | null): void;
1962
+ /**
1963
+ * Subscribe `onChange` to changes in control tables (`'CadreControl.Strand'`, …): commits
1964
+ * made here and changes replicated into this node's storage alike, on the single-flight
1965
+ * schedule `table-change-watch.ts` describes. {@link close} disposes every subscription
1966
+ * still live before it drains the write queue.
1967
+ */
1968
+ watchTables(tables: readonly string[], onChange: () => Promise<void>, options: TableChangeWatchOptions): TableChangeWatch;
1650
1969
  /**
1651
1970
  * Wire (or clear, with null) the single listener notified after every committed
1652
1971
  * guarded-table delete. Same ownership contract as
@@ -1672,8 +1991,9 @@ export declare class ControlDatabase {
1672
1991
  * Covers every write that can change who is a member or an owner: EVERY `CadrePeer`
1673
1992
  * writer, with one documented exception ({@link updateSelfPeerRecord}, which cannot
1674
1993
  * change the snapshot), the `OwnerKey` writers ({@link insertOwnerKeyVouched},
1675
- * {@link deleteOwnerKey}) and the invitation redemption ({@link redeemCadreInvite}),
1676
- * which seats both. That is what makes the party-membership snapshot refresh automatic
1994
+ * {@link deleteOwnerKey}), the invitation redemption ({@link redeemCadreInvite}), which
1995
+ * seats both, and the owner-grant redemption ({@link redeemOwnerGrant}). That is what
1996
+ * makes the party-membership snapshot refresh automatic
1677
1997
  * rather than a caller obligation. The name predates the `OwnerKey` writers. A
1678
1998
  * writer necessarily holds the target node's ControlDatabase (the `SeedBootstrapService`'s
1679
1999
  * event callbacks are NOT a viable seam: the temp service `CadreNode.applySeed`
@@ -1700,8 +2020,11 @@ export declare class ControlDatabase {
1700
2020
  * exhaustion.
1701
2021
  *
1702
2022
  * @param reason - Label for the log line only (e.g. `'peer-insert'`).
2023
+ * @param policy - The retry policy, default unless the body has earned a wider one: the three
2024
+ * `CadrePeer` writers whose bodies re-read the row and decide inside pass
2025
+ * `LOST_UPDATE_RETRY_POLICY` (its `NOTE:` names them and the rule).
1703
2026
  */
1704
- mutateCadrePeer<T>(reason: string, body: () => Promise<T>): Promise<T>;
2027
+ mutateCadrePeer<T>(reason: string, body: () => Promise<T>, policy?: ControlWriteRetryOptions): Promise<T>;
1705
2028
  /**
1706
2029
  * Run one local write under the database-wide write lock, serializing it against
1707
2030
  * every other local writer on this ControlDatabase.
@@ -1714,8 +2037,8 @@ export declare class ControlDatabase {
1714
2037
  * class: every public write method of this class runs its statement(s) under it, and
1715
2038
  * `SeedBootstrapService` reaches it through {@link execWrite} rather than touching the
1716
2039
  * `Database` directly, so it is covered too. The race is
1717
- * real, not theoretical: the background self-record publish on a control connection
1718
- * opening (`CadreNode.drainPendingControlReplication` → `registerSelf`) collided with
2040
+ * real, not theoretical: the background self-record publish when a sibling becomes
2041
+ * reachable (`CadreNode.drainPendingControlReplication` → `registerSelf`) collided with
1719
2042
  * a foreground `authorizePeer` — both `CadrePeer` inserts — and tripped the boundary
1720
2043
  * assert.
1721
2044
  *
@@ -1739,6 +2062,11 @@ export declare class ControlDatabase {
1739
2062
  * re-runnable means a retry re-runs the body's reads too, which is what makes it safe.
1740
2063
  * Signatures and stamp ids minted OUTSIDE the body are deliberately NOT re-minted per
1741
2064
  * attempt: a retry re-presents the exact signed message the first attempt presented.
2065
+ * A body opted into `LOST_UPDATE_RETRY_POLICY` is the exception, and must mint INSIDE:
2066
+ * its re-run is a re-decision over a row another writer changed, not a re-presentation of
2067
+ * the same write, so its read of the row, its decision and its signature all run in the
2068
+ * body ({@link updateSelfPeerRecord}, {@link reauthorizeCadrePeer}, {@link deleteGuardedRow}
2069
+ * under {@link deleteCadrePeer}, {@link removeMachine}).
1742
2070
  */
1743
2071
  withWriteLock<T>(fn: () => Promise<T>): Promise<T>;
1744
2072
  /**
@@ -1760,13 +2088,18 @@ export declare class ControlDatabase {
1760
2088
  * backoff never stalls the other local writers queued behind it. Safe because every
1761
2089
  * locked body is atomic and re-runnable (the contract on {@link withWriteLock}).
1762
2090
  *
1763
- * Also carries {@link loadSchema}'s distributed DDL, the one caller that runs BEFORE
2091
+ * Also carries {@link applyControlSchema}'s distributed DDL, the one caller that runs BEFORE
1764
2092
  * `initialized` is set — so it reaches this method directly rather than through
1765
2093
  * {@link execWrite}, whose {@link ensureInitialized} would reject it. The write queue is
1766
2094
  * necessarily empty at that point, so the lock is uncontended there; it is taken anyway
1767
- * rather than carving out a lockless path. It is also the ONLY caller that passes a
1768
- * `policy` — everything else takes the default, whose classifier and attempt count are
1769
- * unchanged by that call site existing.
2095
+ * rather than carving out a lockless path.
2096
+ *
2097
+ * Two policies reach here besides the default: `SCHEMA_INIT_RETRY_POLICY` from that one
2098
+ * call site, and `LOST_UPDATE_RETRY_POLICY` from the `CadrePeer` writers that re-read the
2099
+ * row inside their body ({@link updateSelfPeerRecord} directly; {@link reauthorizeCadrePeer},
2100
+ * {@link deleteCadrePeer} and {@link removeMachine} through {@link mutateCadrePeer}'s
2101
+ * policy parameter). Everything else takes the default, whose classifier and attempt count
2102
+ * are unchanged by those call sites existing.
1770
2103
  *
1771
2104
  * `label` names the operation in this loop's debug lines and nothing else
1772
2105
  * ({@link ControlWriteRetryOptions.label}). Every call site in this class supplies one:
@@ -1863,12 +2196,17 @@ export declare class ControlDatabase {
1863
2196
  * @param options - Optional `expiresAtMs` (epoch ms), `totalUses`, `validationUrl`,
1864
2197
  * `strandId` (bind to a pre-existing host strand for provision-then-record)
1865
2198
  */
1866
- insertFormationInvite(token: string, sAppId: string, ownerKey: string, signMessage: (message: Uint8Array) => string, options?: {
2199
+ insertFormationInvite(token: string, sAppId: string, ownerKey: string, signMessage: ControlSign, options?: {
1867
2200
  expiresAtMs?: number;
1868
2201
  totalUses?: number;
1869
2202
  validationUrl?: string;
1870
2203
  strandId?: string;
1871
2204
  }): Promise<void>;
2205
+ /**
2206
+ * The stamped, owner-signed `FormationInvite` insert that {@link insertFormationInvite} and
2207
+ * {@link insertFoundingRows} run. Reads the engine once, for the canonical `ExpiresAt`.
2208
+ */
2209
+ private signFormationInviteInsert;
1872
2210
  /**
1873
2211
  * Redeem a `FormationInvite` by inserting the `Strand` row and a matching
1874
2212
  * `FormationUsage` row **atomically, in one transaction**.
@@ -1939,14 +2277,13 @@ export declare class ControlDatabase {
1939
2277
  * Use {@link redeemInvitation} instead when the strand must be created by
1940
2278
  * consent atomically with the usage.
1941
2279
  *
1942
- * The strand's live `StampId` is read first and written onto the usage row:
2280
+ * The strand's live `StampId` is written onto the usage row:
1943
2281
  * `FormationUsage.StrandExists` matches the (id, stamp) PAIR, and
1944
2282
  * `Strand.AuthorizedInsert`'s consent branch reads the same pair back, so a consent
1945
- * record authorizes exactly the strand ROW it was recorded against. A missing strand
1946
- * THROWS here rather than being left to the deferred `StrandExists` CHECK — the
1947
- * ordinary "host strand has not converged yet" case is already reported as `missing`
1948
- * by {@link ControlFormationUsageRecorder.resolveStrand}, so an absent row at this
1949
- * point is a genuine race and deserves a named error, not a silent rollback.
2283
+ * record authorizes exactly the strand ROW it was recorded against. A caller that already
2284
+ * read the stamp passes it (`strandStampId`); otherwise it is read here first, and a
2285
+ * missing strand THROWS {@link MissingHostStrandError} rather than being left to the
2286
+ * deferred `StrandExists` CHECK.
1950
2287
  *
1951
2288
  * The row is keyed by the joiner's own nonce (`UsageStampId` is the primary key), so this
1952
2289
  * path — the one production actually races on (a bound invite published by `cadre-web` /
@@ -1981,6 +2318,13 @@ export declare class ControlDatabase {
1981
2318
  * {@link queryFormationInvite} read; `null` means "no invite row" / unlimited.
1982
2319
  */
1983
2320
  totalUses?: number | null;
2321
+ /**
2322
+ * The host strand's `StampId`, when the caller already read the row
2323
+ * ({@link queryStrandWithStamp}); `undefined` reads it here. A stamp is fixed for the life
2324
+ * of its row, so one read earlier is the stamp `StrandExists` compares, unless the row was
2325
+ * removed (and re-seated) in between, which that CHECK refuses at commit.
2326
+ */
2327
+ strandStampId?: string;
1984
2328
  }): Promise<FormationUsageResult>;
1985
2329
  /**
1986
2330
  * Refuse a redemption that would consume a seat the invite does not have.
@@ -2034,7 +2378,7 @@ export declare class ControlDatabase {
2034
2378
  * permissive PRE-check that runs ahead of it, and a transiently short read costs each of
2035
2379
  * them only a worse outcome for the ATTEMPT, never a seat the invitation did not pay for:
2036
2380
  * {@link assertSeatRemains} loses its named exhaustion error and falls back to the CHECK's
2037
- * generic refusal; `ControlFormationUsageRecorder.isTokenUsed` reports not-used and lets
2381
+ * generic refusal; `ControlFormationUsageRecorder.readToken` reports a seat free and lets
2038
2382
  * the redemption proceed to the CHECK, which decides; {@link hasOutstandingFormationInvite}
2039
2383
  * holds the stranger-admission door open slightly longer.
2040
2384
  *
@@ -2060,12 +2404,12 @@ export declare class ControlDatabase {
2060
2404
  /**
2061
2405
  * Is any `FormationInvite` row still redeemable — unexpired AND with usage
2062
2406
  * below its `TotalUses`? A null `ExpiresAt` never expires and a null
2063
- * `TotalUses` is unlimited, matching {@link ControlFormationUsageRecorder}'s
2064
- * per-token semantics (`isTokenValid` / `isTokenUsed`).
2407
+ * `TotalUses` is unlimited, matching the per-token semantics of `classifyTokenRead`
2408
+ * (`formation-token.ts`).
2065
2409
  *
2066
2410
  * Answers the control-network connection gate's coarse "does this node expect
2067
2411
  * a stranger?" question, which has no token to ask about. The expiry
2068
- * comparison is `expiresAtMs <= now` — identical to `isTokenValid`'s — so an
2412
+ * comparison is `expiresAtMs <= now` — identical to `isInviteExpired`'s — so an
2069
2413
  * invite the formation handler would reject can never hold the gate open.
2070
2414
  *
2071
2415
  * The scan is deliberately not pushed into SQL: nothing else here compares a
@@ -2091,31 +2435,53 @@ export declare class ControlDatabase {
2091
2435
  * private half to the invitee out of band. It is never stored here (`Strand.Invite` has the
2092
2436
  * same shape).
2093
2437
  *
2094
- * @param invite.key - the invitation public key (base64url ed25519), the row's primary key
2095
- * @param invite.peerId - the device the invitation is for, or null for whoever redeems first
2096
- * @param invite.grantsOwner - whether redemption also seats an `OwnerKey` row for the device
2097
- * @param invite.expiresAtMs - epoch ms after which it cannot be redeemed; omit for never
2098
- * @param invite.totalUses - how many redemptions it allows; omit for unlimited
2099
2438
  * @returns the row as stored, `expiresAt` in its canonical form
2100
2439
  */
2101
- insertCadreInvite(invite: {
2102
- key: string;
2103
- peerId?: string | null;
2104
- grantsOwner: boolean;
2105
- expiresAtMs?: number | null;
2106
- totalUses?: number | null;
2107
- }, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<CadreInviteRow>;
2440
+ insertCadreInvite(invite: CadreInviteFields, ownerKey: string, signMessage: ControlSign): Promise<CadreInviteRow>;
2441
+ /**
2442
+ * Build and owner-sign a `CadreInvite` row WITHOUT inserting it: the signing half of
2443
+ * {@link insertCadreInvite}, which `CadreNode.joinWithOwnerGrant` uses alone for the
2444
+ * invitation a node issues to itself, since a store that holds no `OwnerKey` row for the
2445
+ * issuer would refuse the local insert and the row is seated at a member from the bundle
2446
+ * instead. `ExpiresAt` is canonicalized by this database's engine, which is a format, not a
2447
+ * store, so the row reads the same wherever it is seated.
2448
+ */
2449
+ signCadreInvite(invite: CadreInviteFields, ownerKey: string, signMessage: ControlSign): Promise<CadreInviteRow>;
2108
2450
  /**
2109
2451
  * Seat an owner-signed `CadreInvite` row received inside an invitation bundle, under its own
2110
2452
  * stored `IssuerKey`/`IssuerSig` as the write context — insert-if-absent, so a member that
2111
- * already holds the row (by replication, or from an earlier redemption attempt) is a no-op.
2453
+ * already holds the row (by replication, or from an earlier redemption attempt) is a no-op,
2454
+ * and so is one whose insert the cohort refuses because a member there holds it.
2112
2455
  *
2113
2456
  * @returns `true` when this call inserted the row, `false` when it was already present.
2114
2457
  * @throws {CadreInviteIssuerUnknownError} when `CadreInvite.AuthorizedInsert` refuses it: the
2115
2458
  * issuer is not an owner on this node (not replicated yet, or removed), or the signature does
2116
- * not verify. Every other refusal (a withdrawn stamp, `NotRevoked`) propagates as is.
2459
+ * not verify. {@link IssuedRowNotHereYetError} when the cohort holds the row and this node
2460
+ * cannot read it yet. Every other refusal (a withdrawn stamp, `NotRevoked`) propagates as is.
2117
2461
  */
2118
2462
  seatCadreInvite(row: CadreInviteRow): Promise<boolean>;
2463
+ /**
2464
+ * Insert-if-absent of an issued row from a bundle's copy, under the write lock: the body of
2465
+ * {@link seatCadreInvite} and {@link seatOwnerGrant}. `exec` runs the bare insert under the
2466
+ * row's own stored issuer pair.
2467
+ *
2468
+ * @returns `true` when this call inserted the row, `false` when it was already present —
2469
+ * here, or at the cohort member that refused the insert for holding it
2470
+ * ({@link adoptCohortHeldRow}).
2471
+ * @throws `refuse(cause)` when the table's `AuthorizedInsert` refuses the row;
2472
+ * {@link IssuedRowNotHereYetError} when the cohort holds the row and this node still cannot
2473
+ * read it; every other refusal (a withdrawn stamp, `NotRevoked`) propagates as is.
2474
+ */
2475
+ private seatIssuedRow;
2476
+ /**
2477
+ * The seat's insert was refused because a cohort member already holds a row under `key`, which
2478
+ * this node's guard read missed because its store lags the cohort. A bundle's copy is verbatim
2479
+ * and every column is under the issuer's signature, so the held row is this one: it counts as
2480
+ * seated once a refreshing read (unlocked, so retried as any read) finds it here.
2481
+ *
2482
+ * @throws {IssuedRowNotHereYetError} when the read still does not find it.
2483
+ */
2484
+ private adoptCohortHeldRow;
2119
2485
  /** Bare `CadreInvite` insert shared by {@link insertCadreInvite} and {@link seatCadreInvite}; the caller holds the lock. */
2120
2486
  private execCadreInviteInsert;
2121
2487
  /**
@@ -2123,8 +2489,10 @@ export declare class ControlDatabase {
2123
2489
  * the row is the holder's proof and never goes away; liveness is {@link hasLiveCadreInvite}'s
2124
2490
  * question, and `CadreInviteUsage.Authorized`'s at redemption.
2125
2491
  *
2126
- * `retry: false` is passed by {@link redeemCadreInvite}, which reads INSIDE its locked write
2127
- * body — the same per-call opt-out, for the same reason, as {@link queryStampId}'s.
2492
+ * A redemption reads the row once, before its write, and carries it into
2493
+ * {@link isCadreInviteLive} and {@link redeemCadreInvite}. `retry: false` is passed by
2494
+ * {@link redeemCadreInvite} when it was handed none and reads INSIDE its locked write body —
2495
+ * the same per-call opt-out, for the same reason, as {@link queryStampId}'s.
2128
2496
  */
2129
2497
  queryCadreInvite(key: string, retry?: boolean): Promise<CadreInviteRow | null>;
2130
2498
  /**
@@ -2138,7 +2506,7 @@ export declare class ControlDatabase {
2138
2506
  /**
2139
2507
  * Every `CadreInviteUsage` row this node holds — the redemption records the membership
2140
2508
  * predicate verifies invitation-admitted rows against. Same `retry` contract as
2141
- * {@link queryCadreInvites}.
2509
+ * {@link queryCadreInvites}; both come from their membership views ({@link readMembershipRows}).
2142
2510
  */
2143
2511
  queryCadreInviteUsages(retry?: boolean): Promise<CadreInviteUsageRow[]>;
2144
2512
  /**
@@ -2149,6 +2517,8 @@ export declare class ControlDatabase {
2149
2517
  * `retry: false` is passed by {@link assertCadreInviteSeatRemains}, inside the locked body.
2150
2518
  */
2151
2519
  countCadreInviteUsage(key: string, retry?: boolean): Promise<number>;
2520
+ /** The one-row count read behind {@link countCadreInviteUsage} and {@link countOwnerGrantUsage}. */
2521
+ private countUsageRows;
2152
2522
  /**
2153
2523
  * Is any cadre invitation still redeemable — not withdrawn, unexpired, with uses below its
2154
2524
  * `TotalUses`, and issued by a current owner? The conditions `CadreInviteUsage.Authorized`
@@ -2164,14 +2534,24 @@ export declare class ControlDatabase {
2164
2534
  hasLiveCadreInvite(nowMs?: number): Promise<boolean>;
2165
2535
  /**
2166
2536
  * Is this one cadre invitation still redeemable here — the conditions
2167
- * {@link hasLiveCadreInvite} applies, for one invitation? The row judged is the one this node
2168
- * holds under `invite.key`; when it holds none, `invite` itself (a bundle's copy, not seated
2169
- * yet), but only if its issuer signature verifies, because an unsigned copy would otherwise be
2170
- * judged by whichever owner it names as issuer. A permissive pre-check like
2171
- * {@link countCadreInviteUsage}: {@link seatCadreInvite} and the redemption's own write are
2172
- * what enforce every condition.
2537
+ * {@link hasLiveCadreInvite} applies, for one invitation? The row judged is `held`, the one this
2538
+ * node holds under `invite.key` ({@link queryCadreInvite}, read by the caller); when it holds
2539
+ * none, `invite` itself (a bundle's copy, not seated yet), but only if its issuer signature
2540
+ * verifies, because an unsigned copy would otherwise be judged by whichever owner it names as
2541
+ * issuer. A permissive pre-check like {@link countCadreInviteUsage}: {@link seatCadreInvite}
2542
+ * and the redemption's own write are what enforce every condition.
2543
+ *
2544
+ * The use count read here is read again inside the redemption's write lock
2545
+ * ({@link assertCadreInviteSeatRemains}), on purpose: this one decides whether the device is
2546
+ * pushed the control store before the write, so the holder of a spent invitation is refused
2547
+ * without being sent anything; the in-lock one decides the seat, against a concurrent redemption.
2548
+ */
2549
+ isCadreInviteLive(invite: CadreInviteRow, held: CadreInviteRow | null, nowMs?: number): Promise<boolean>;
2550
+ /**
2551
+ * The body of {@link isCadreInviteLive} and {@link isOwnerGrantLive}: is `row` (the one held,
2552
+ * or a verified bundle copy; null for neither) still open and under its use cap?
2173
2553
  */
2174
- isCadreInviteLive(invite: CadreInviteRow, nowMs?: number): Promise<boolean>;
2554
+ private issuedRowLive;
2175
2555
  /**
2176
2556
  * Every `CadreInvite` row this node holds with its standing: how many redemptions are
2177
2557
  * recorded, whether it is withdrawn, and whether it is still redeemable — the same
@@ -2180,6 +2560,11 @@ export declare class ControlDatabase {
2180
2560
  * occasional read, not a gate's.
2181
2561
  */
2182
2562
  listCadreInviteStatuses(nowMs?: number): Promise<CadreInviteStatus[]>;
2563
+ /**
2564
+ * The body of {@link listCadreInviteStatuses} and {@link listOwnerGrantStatuses}: each of
2565
+ * `rows` with its withdrawal, its recorded uses and whether it is still redeemable.
2566
+ */
2567
+ private issuedRowStandings;
2183
2568
  /**
2184
2569
  * Owner-signed WITHDRAWAL of a cadre invitation: file the `Revocation` tombstone retiring its
2185
2570
  * stamp, with the row kept. The row stays because every device the invitation admitted
@@ -2197,24 +2582,29 @@ export declare class ControlDatabase {
2197
2582
  * Fires the guarded-delete listener like a removal does, so a withdrawal committed while the
2198
2583
  * node was alone is queued for re-issue on cohort growth ({@link CadreNode.noteGuardedDelete}).
2199
2584
  */
2200
- withdrawCadreInvite(key: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
2585
+ withdrawCadreInvite(key: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
2586
+ /**
2587
+ * The body of {@link withdrawCadreInvite} and {@link withdrawOwnerGrant}: under the write
2588
+ * lock, file the owner-signed tombstone retiring the row's stamp with the row kept, and fire
2589
+ * the guarded-delete listener for it (so a withdrawal made while alone is re-issued).
2590
+ */
2591
+ private withdrawIssuedRow;
2201
2592
  /**
2202
- * The withdrawal tombstones for the `CadreInvite` rows among `keys` that are held here and
2203
- * not yet withdrawn, signed and ready for {@link execTombstone}: the body
2204
- * {@link withdrawCadreInvite} files one of and {@link removeMachine} files several of in one
2593
+ * The withdrawal tombstones for the `table` rows among `keys` that are held here and not
2594
+ * yet withdrawn, signed and ready for {@link execTombstone}: the body
2595
+ * {@link withdrawIssuedRow} files one of and {@link removeMachine} files several of in one
2205
2596
  * transaction. Inside the locked body (every read `retry: false`); an absent or already
2206
2597
  * withdrawn key is logged and left out.
2207
2598
  */
2208
- private signCadreInviteWithdrawals;
2599
+ private signWithdrawals;
2209
2600
  /**
2210
- * Redeem a seated `CadreInvite` on the owner's behalf: admit `peerId` as a cadre member and,
2211
- * when the invitation grants ownership, as an owner — with NO owner signature. One
2212
- * transaction writes the `CadrePeer` row (consent branch of `AuthorizedInsert`), the
2213
- * `OwnerKey` row when granted (consent branch of `Authorized`), and last the
2214
- * `CadreInviteUsage` row that authorizes both; the usage row's own checks (`PeerExists`,
2215
- * `OwnerExists`) read the rows back, so the three are mutually bound to this exact
2216
- * redemption and none can be held in reserve. The invitation must already be committed
2217
- * ({@link seatCadreInvite}): every consent branch reads `committed.CadreInvite`.
2601
+ * Redeem a seated `CadreInvite` on the owner's behalf: admit `peerId` as a cadre member —
2602
+ * with NO owner signature, and as a machine only (ownership is {@link redeemOwnerGrant}'s).
2603
+ * One transaction writes the `CadrePeer` row (consent branch of `AuthorizedInsert`) and then
2604
+ * the `CadreInviteUsage` row that authorizes it; the usage row's own check (`PeerExists`)
2605
+ * reads the peer row back, so the two are mutually bound to this exact redemption and
2606
+ * neither can be held in reserve. The invitation must already be committed
2607
+ * ({@link seatCadreInvite}): the consent branch reads `committed.CadreInvite`.
2218
2608
  *
2219
2609
  * The two signatures are the holder's and the device's, minted on the device over
2220
2610
  * {@link cadreInviteRedeemMessage} / {@link cadreInviteConsentMessage}; this machine only
@@ -2223,26 +2613,41 @@ export declare class ControlDatabase {
2223
2613
  * (`verifyInvitationAdmission`) — a mismatched pair is written and then never trusted.
2224
2614
  *
2225
2615
  * Idempotent for a retry after a dropped reply: when the device's row is already present
2226
- * (and its `OwnerKey` row too, when granted) nothing is written and no seat is spent —
2227
- * `alreadyMember: true`. A present row whose `OwnerKey` row is missing under an
2228
- * owner-granting invitation is completed: the usage row then names the existing peer stamp.
2229
- * The seat check ({@link assertCadreInviteSeatRemains}) runs after that, so the retry of a
2230
- * single-use redemption is answered `alreadyMember` rather than refused as exhausted.
2616
+ * nothing is written and no seat is spent — `alreadyMember: true`. The seat check
2617
+ * ({@link assertCadreInviteSeatRemains}) runs after that, so the retry of a single-use
2618
+ * redemption is answered `alreadyMember` rather than refused as exhausted.
2231
2619
  *
2232
2620
  * Through {@link mutateCadrePeer}, so the membership hub is notified; inside the locked body
2233
2621
  * every read passes `retry: false` and every stamp is minted per attempt, so a
2234
2622
  * {@link lockedWithRetry} re-run starts clean.
2235
2623
  *
2236
2624
  * @throws {InvitationExhaustedError} when the invitation's seats are all spent (by name,
2237
- * ahead of the schema's generic `Authorized` refusal — see {@link assertSeatRemains}).
2625
+ * ahead of the schema's generic `Authorized` refusal — see {@link assertSeatRemains});
2626
+ * {@link IssuedRowNotHereYetError} when no `CadreInvite` row is readable here.
2238
2627
  */
2239
2628
  redeemCadreInvite(params: {
2240
2629
  inviteKey: string;
2630
+ /**
2631
+ * The row this node holds under `inviteKey`, read before the call ({@link queryCadreInvite});
2632
+ * omitted, it is read inside the locked body. Never a bundle's copy: the requester chooses a
2633
+ * copy's `TotalUses` and `IssuerKey`, and the seat check must judge the stored cap. The
2634
+ * schema refuses every update and delete of the row (`NoUpdate`, `NoDelete`), so the earlier
2635
+ * read is the row a locked read would return; withdrawal is a `Revocation` row, which the schema's
2636
+ * `NotRevoked` and `Authorized` checks still read at commit.
2637
+ */
2638
+ held?: CadreInviteRow;
2241
2639
  peerId: string;
2242
2640
  /** The device's ed25519 public key (base64url); `peerId` is its identity multihash. */
2243
2641
  peerKey: string;
2244
2642
  /** The device's dialable addresses, if known; stored comma-joined as `authorizePeer` does. */
2245
2643
  multiaddrs?: string[];
2644
+ /**
2645
+ * The rest of the device's own signed address record over `multiaddrs`, already verified by
2646
+ * the caller (`admittedRecord` in `cadre-invite-protocol.ts`): stored as the row's
2647
+ * `UpdatedAt`, `Sig` and `Profile`, so the row resolves to the device's addresses from its
2648
+ * admission. Omitted, the row is written unsigned, `UpdatedAt` at this machine's clock.
2649
+ */
2650
+ record?: Pick<PeerAddressRecord, 'updatedAt' | 'profile' | 'sig'>;
2246
2651
  /** Single-use nonce minted by the device; both signatures cover it. */
2247
2652
  usageStampId: string;
2248
2653
  /** The holder's signature with the invitation private key over the `'redeem'` digest. */
@@ -2251,13 +2656,6 @@ export declare class ControlDatabase {
2251
2656
  peerSig: string;
2252
2657
  nowMs?: number;
2253
2658
  }): Promise<CadreInviteRedemptionResult>;
2254
- /**
2255
- * The rows a redemption would seat that are already present: the device's `CadrePeer`
2256
- * stamp, and — only when the invitation grants ownership — its `OwnerKey` stamp. Raw stamp
2257
- * reads ({@link queryStampId}), so a physically present row counts; inside the locked
2258
- * body, so never retried.
2259
- */
2260
- private readSeatedAdmission;
2261
2659
  /**
2262
2660
  * Refuse a redemption that would consume a seat the cadre invitation does not have, by
2263
2661
  * name ({@link InvitationExhaustedError}) — the cadre-invitation twin of
@@ -2266,10 +2664,150 @@ export declare class ControlDatabase {
2266
2664
  * committed count inside the locked body, so on a same-node race the loser is refused here.
2267
2665
  */
2268
2666
  private assertCadreInviteSeatRemains;
2667
+ /** The body of {@link assertCadreInviteSeatRemains} and {@link assertOwnerGrantSeatRemains}; `countUsage` reads inside the locked body. */
2668
+ private assertIssuedSeatRemains;
2669
+ /**
2670
+ * Mint and insert an owner-signed `OwnerGrant`: a grant of ownership to whoever holds the
2671
+ * grant's private half, redeemable through any member machine ({@link redeemOwnerGrant})
2672
+ * to seat an owner key the holder generates or already holds. No machine is admitted and no
2673
+ * peer id is named. The grant twin of {@link insertCadreInvite}: the owner signs the whole
2674
+ * row ({@link ownerGrantAddMessage}), `ExpiresAt` through {@link canonicalDatetime}, and
2675
+ * the returned row is what the grant bundle carries, re-inserted verbatim by
2676
+ * {@link seatOwnerGrant} on a member that has not received it by replication.
2677
+ *
2678
+ * @param grant.key - the grant's ed25519 PUBLIC key (base64url), the row's primary key; the
2679
+ * caller minted the pair and hands the private half to the holder out of band
2680
+ * @param grant.label - the label every key this grant seats will carry
2681
+ * ({@link ownerKeyLabel}'s rule: trimmed, at most 64 characters, blank meaning none); the
2682
+ * issuer's choice, which the holder cannot change
2683
+ * @param grant.expiresAtMs - epoch ms after which it cannot be redeemed; omit or null for
2684
+ * never. The short default lifetime is the node API's to apply, not this writer's.
2685
+ * @param grant.totalUses - how many redemptions it allows; omit for unlimited
2686
+ * @returns the row as stored, `expiresAt` in its canonical form
2687
+ */
2688
+ insertOwnerGrant(grant: {
2689
+ key: string;
2690
+ label?: string | null;
2691
+ expiresAtMs?: number | null;
2692
+ totalUses?: number | null;
2693
+ }, ownerKey: string, signMessage: ControlSign): Promise<OwnerGrantRow>;
2694
+ /**
2695
+ * Seat an owner-signed `OwnerGrant` row received inside a grant bundle, under its own stored
2696
+ * `IssuerKey`/`IssuerSig` as the write context — insert-if-absent, as {@link seatCadreInvite}.
2697
+ *
2698
+ * @returns `true` when this call inserted the row, `false` when it was already present.
2699
+ * @throws {OwnerGrantIssuerUnknownError} when `OwnerGrant.AuthorizedInsert` refuses it: the
2700
+ * issuer is not an owner on this node (not replicated yet, or removed), or the signature does
2701
+ * not verify. {@link IssuedRowNotHereYetError} when the cohort holds the row and this node
2702
+ * cannot read it yet. Every other refusal (a withdrawn stamp, `NotRevoked`) propagates as is.
2703
+ */
2704
+ seatOwnerGrant(row: OwnerGrantRow): Promise<boolean>;
2705
+ /** Bare `OwnerGrant` insert shared by {@link insertOwnerGrant} and {@link seatOwnerGrant}; the caller holds the lock. */
2706
+ private execOwnerGrantInsert;
2707
+ /**
2708
+ * Read one `OwnerGrant` row by key, or null when absent. Withdrawn rows are returned too,
2709
+ * as {@link queryCadreInvite} returns them: the row is the holder's proof and never goes
2710
+ * away. Read once per redemption and carried into {@link isOwnerGrantLive} and
2711
+ * {@link redeemOwnerGrant}; `retry: false` is passed by {@link redeemOwnerGrant} when it was
2712
+ * handed none and reads inside its locked body.
2713
+ */
2714
+ queryOwnerGrant(key: string, retry?: boolean): Promise<OwnerGrantRow | null>;
2715
+ /**
2716
+ * Every `OwnerGrant` row this node holds, withdrawn and expired ones included. Read by the
2717
+ * anchor sync when at least one `OwnerKey` row is consent-seated (`CadreNode.loadOwnerGrantChain`);
2718
+ * `retry: false` is forwarded from the membership-gate refresh for {@link queryCadrePeers}' reason.
2719
+ */
2720
+ queryOwnerGrants(retry?: boolean): Promise<OwnerGrantRow[]>;
2721
+ /** Every `OwnerGrantUsage` row this node holds — the redemption records a grant-seated `OwnerKey` row is verified against. Same `retry` contract as {@link queryOwnerGrants}; both come from their membership views ({@link readMembershipRows}). */
2722
+ queryOwnerGrantUsages(retry?: boolean): Promise<OwnerGrantUsageRow[]>;
2723
+ /**
2724
+ * How many times an owner grant has been redeemed (its `OwnerGrantUsage` rows), served
2725
+ * through the `OwnerGrantUsageByGrant` index. A permissive PRE-check like
2726
+ * {@link countCadreInviteUsage}: the cap is `OwnerGrantUsage.Authorized`'s own committed count.
2727
+ */
2728
+ countOwnerGrantUsage(key: string, retry?: boolean): Promise<number>;
2729
+ /**
2730
+ * Is this one owner grant still redeemable here — not withdrawn, unexpired, uses left, issuer
2731
+ * still an owner — judged as {@link isCadreInviteLive} judges an invitation: by `held`, the row
2732
+ * this node holds under `grant.key` ({@link queryOwnerGrant}, read by the caller), else by
2733
+ * `grant` itself (a bundle's copy, not seated yet) when its issuer signature verifies. A
2734
+ * permissive pre-check; {@link seatOwnerGrant} and the redemption's own write enforce every
2735
+ * condition. Its use count is read again inside the write lock, for
2736
+ * {@link isCadreInviteLive}'s reason.
2737
+ */
2738
+ isOwnerGrantLive(grant: OwnerGrantRow, held: OwnerGrantRow | null, nowMs?: number): Promise<boolean>;
2739
+ /**
2740
+ * Every `OwnerGrant` row this node holds with its standing — recorded redemptions, withdrawal,
2741
+ * and whether it is still redeemable — for an owner's listing, as {@link listCadreInviteStatuses}.
2742
+ */
2743
+ listOwnerGrantStatuses(nowMs?: number): Promise<OwnerGrantStatus[]>;
2744
+ /**
2745
+ * Owner-signed WITHDRAWAL of an owner grant: file the `Revocation` tombstone retiring its
2746
+ * stamp, with the row kept, exactly as {@link withdrawCadreInvite} withdraws an invitation
2747
+ * and for the same reason (every key the grant seated proves itself through the row;
2748
+ * `OwnerGrant.NoDelete`, `Revocation.RowIsGone`). Afterwards `OwnerGrantUsage.Authorized`
2749
+ * refuses every further redemption and `OwnerGrant.NotRevoked` refuses re-seating the
2750
+ * holder's copy on any node holding the tombstone. Keys already seated stay owners;
2751
+ * removing one is {@link deleteOwnerKey}.
2752
+ *
2753
+ * Idempotent: `false` (nothing written) when the row is absent here or already withdrawn,
2754
+ * `true` when this call filed the tombstone. Fires the guarded-delete listener, so a
2755
+ * withdrawal committed while alone is re-issued on cohort growth.
2756
+ */
2757
+ withdrawOwnerGrant(key: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
2758
+ /**
2759
+ * Redeem a seated `OwnerGrant` on the issuer's behalf: seat `seatedKey` as an owner with NO
2760
+ * owner signature. One transaction writes the `OwnerKey` row (the grant branch of
2761
+ * `Authorized`: `VouchOwner` the issuer, `VouchSig` null, `VouchUsage` the usage stamp,
2762
+ * `Label` the grant's) and then the `OwnerGrantUsage` row that authorizes it; the usage
2763
+ * row's `SeatedExists` reads the owner row back, so the two are bound to this exact
2764
+ * redemption and neither can be held in reserve. The grant must already be committed
2765
+ * ({@link seatOwnerGrant}): both consent reads are of `committed.OwnerGrant`. No machine is
2766
+ * admitted and no peer id is named.
2767
+ *
2768
+ * The two signatures are minted where the private keys are (the grant's and the seated
2769
+ * key's), over {@link ownerGrantRedeemMessage} / {@link ownerGrantConsentMessage}; this
2770
+ * machine only verifies them, by writing the row.
2771
+ *
2772
+ * Idempotent, and a no-op for a key that is already an owner: when `seatedKey` has a live
2773
+ * `OwnerKey` row here nothing is written and no use is spent — `alreadyOwner: true` with
2774
+ * that row's stamp. The seat check ({@link assertOwnerGrantSeatRemains}) runs after that, so
2775
+ * the retry of a single-use redemption is answered `alreadyOwner` rather than refused as
2776
+ * exhausted. A key removed earlier is seated afresh under a new stamp (`NotRevoked` passes),
2777
+ * except on a machine that still holds its retired row, which refuses by name
2778
+ * ({@link liveOwnerKeyStamp}).
2779
+ *
2780
+ * Through {@link mutateCadrePeer}, so the membership hub is notified and the anchors follow;
2781
+ * inside the locked body every read passes `retry: false` and the stamp is minted per
2782
+ * attempt, so a {@link lockedWithRetry} re-run starts clean.
2783
+ *
2784
+ * @throws {InvitationExhaustedError} when the grant's uses are all spent (by name, ahead of
2785
+ * the schema's generic `Authorized` refusal); its `token` is the grant key.
2786
+ * {@link IssuedRowNotHereYetError} when no `OwnerGrant` row is readable here.
2787
+ */
2788
+ redeemOwnerGrant(params: {
2789
+ grantKey: string;
2790
+ /** The row this node holds under `grantKey`, read before the call; omitted, it is read inside the locked body. Never a bundle's copy, for {@link redeemCadreInvite}'s reason. */
2791
+ held?: OwnerGrantRow;
2792
+ /** The owner key to seat (base64url ed25519): the holder generated it, or already holds it. */
2793
+ seatedKey: string;
2794
+ /** Single-use nonce minted by the holder; both signatures cover it. */
2795
+ usageStampId: string;
2796
+ /** The holder's signature with the grant private key over the `'redeem'` digest. */
2797
+ grantSig: string;
2798
+ /** The seated key's signature over the `'consent'` digest. */
2799
+ ownerSig: string;
2800
+ nowMs?: number;
2801
+ }): Promise<OwnerGrantRedemptionResult>;
2802
+ /** {@link assertCadreInviteSeatRemains} for an owner grant: refuse an exhausted grant by name, ahead of the write. */
2803
+ private assertOwnerGrantSeatRemains;
2269
2804
  /**
2270
2805
  * Close the database and cleanup resources.
2271
2806
  *
2272
- * Drains the local-write chain first: {@link withWriteLock} can park a write behind
2807
+ * Disposes every {@link watchTables} subscription before anything else, so a commit that
2808
+ * lands while the write chain drains schedules no run, and again before dropping the handle.
2809
+ *
2810
+ * Then drains the local-write chain: {@link withWriteLock} can park a write behind
2273
2811
  * others across an await, and a queued closure evaluates `this.db!` only when it
2274
2812
  * finally runs — so nulling the handle out from under it would throw a TypeError on a
2275
2813
  * null handle instead of committing. The tail never rejects (it swallows both