@serfab/cadre-core 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/README.md +293 -274
  2. package/dist/cadre-invite-protocol.d.ts +383 -0
  3. package/dist/cadre-invite-protocol.js +774 -0
  4. package/dist/cadre-invite-protocol.js.map +1 -0
  5. package/dist/cadre-join-request.d.ts +42 -0
  6. package/dist/cadre-join-request.js +144 -0
  7. package/dist/cadre-join-request.js.map +1 -0
  8. package/dist/cadre-node.d.ts +849 -210
  9. package/dist/cadre-node.js +1574 -374
  10. package/dist/cadre-node.js.map +1 -1
  11. package/dist/claim-proof.d.ts +45 -0
  12. package/dist/claim-proof.js +132 -0
  13. package/dist/claim-proof.js.map +1 -0
  14. package/dist/control-authorization.d.ts +80 -19
  15. package/dist/control-authorization.js +55 -13
  16. package/dist/control-authorization.js.map +1 -1
  17. package/dist/control-cohort.js +1 -1
  18. package/dist/control-cohort.js.map +1 -1
  19. package/dist/control-database.d.ts +605 -108
  20. package/dist/control-database.js +1299 -280
  21. package/dist/control-database.js.map +1 -1
  22. package/dist/control-protocol-guard.d.ts +142 -0
  23. package/dist/control-protocol-guard.js +293 -0
  24. package/dist/control-protocol-guard.js.map +1 -0
  25. package/dist/control-schema.d.ts +1 -1
  26. package/dist/control-schema.js +817 -1067
  27. package/dist/control-schema.js.map +1 -1
  28. package/dist/control-stream.d.ts +4 -0
  29. package/dist/control-stream.js +22 -7
  30. package/dist/control-stream.js.map +1 -1
  31. package/dist/delegate-admission.d.ts +1 -1
  32. package/dist/delegate-admission.js +1 -1
  33. package/dist/device-token.d.ts +11 -9
  34. package/dist/device-token.js +14 -12
  35. package/dist/device-token.js.map +1 -1
  36. package/dist/ed25519-key.d.ts +22 -9
  37. package/dist/ed25519-key.js +47 -10
  38. package/dist/ed25519-key.js.map +1 -1
  39. package/dist/fetch-impl.d.ts +12 -0
  40. package/dist/fetch-impl.js +23 -0
  41. package/dist/fetch-impl.js.map +1 -0
  42. package/dist/file-durable-slot.js +2 -1
  43. package/dist/file-durable-slot.js.map +1 -1
  44. package/dist/file-safe-name.d.ts +25 -0
  45. package/dist/file-safe-name.js +29 -0
  46. package/dist/file-safe-name.js.map +1 -0
  47. package/dist/formation-approval.js +5 -11
  48. package/dist/formation-approval.js.map +1 -1
  49. package/dist/fs-atomic.d.ts +0 -19
  50. package/dist/fs-atomic.js +3 -24
  51. package/dist/fs-atomic.js.map +1 -1
  52. package/dist/index.d.ts +22 -10
  53. package/dist/index.js +54 -11
  54. package/dist/index.js.map +1 -1
  55. package/dist/invitation-bootstrap.d.ts +50 -12
  56. package/dist/invitation-bootstrap.js +76 -13
  57. package/dist/invitation-bootstrap.js.map +1 -1
  58. package/dist/joined-strand-store.d.ts +7 -0
  59. package/dist/joined-strand-store.js +9 -0
  60. package/dist/joined-strand-store.js.map +1 -1
  61. package/dist/key-store-file.js +2 -1
  62. package/dist/key-store-file.js.map +1 -1
  63. package/dist/link-budget.d.ts +25 -33
  64. package/dist/link-budget.js +25 -35
  65. package/dist/link-budget.js.map +1 -1
  66. package/dist/machine-removal.d.ts +78 -0
  67. package/dist/machine-removal.js +166 -0
  68. package/dist/machine-removal.js.map +1 -0
  69. package/dist/membership-connection-gater.d.ts +191 -151
  70. package/dist/membership-connection-gater.js +236 -173
  71. package/dist/membership-connection-gater.js.map +1 -1
  72. package/dist/node-claim-payload.d.ts +74 -0
  73. package/dist/node-claim-payload.js +224 -0
  74. package/dist/node-claim-payload.js.map +1 -0
  75. package/dist/node-default-transports.d.ts +27 -0
  76. package/dist/node-default-transports.js +35 -0
  77. package/dist/node-default-transports.js.map +1 -0
  78. package/dist/node-transports.d.ts +30 -0
  79. package/dist/node-transports.js +40 -0
  80. package/dist/node-transports.js.map +1 -0
  81. package/dist/owner-anchor-sync.d.ts +22 -0
  82. package/dist/owner-anchor-sync.js +143 -0
  83. package/dist/owner-anchor-sync.js.map +1 -0
  84. package/dist/peer-authorization.d.ts +113 -18
  85. package/dist/peer-authorization.js +225 -29
  86. package/dist/peer-authorization.js.map +1 -1
  87. package/dist/peer-dial.d.ts +143 -27
  88. package/dist/peer-dial.js +276 -31
  89. package/dist/peer-dial.js.map +1 -1
  90. package/dist/peer-join-backfill.d.ts +26 -2
  91. package/dist/peer-join-backfill.js +60 -12
  92. package/dist/peer-join-backfill.js.map +1 -1
  93. package/dist/peer-record.d.ts +17 -52
  94. package/dist/peer-record.js +20 -58
  95. package/dist/peer-record.js.map +1 -1
  96. package/dist/pending-join-runner.d.ts +66 -58
  97. package/dist/pending-join-runner.js +136 -145
  98. package/dist/pending-join-runner.js.map +1 -1
  99. package/dist/primary-lan-address.d.ts +8 -0
  100. package/dist/primary-lan-address.js +46 -0
  101. package/dist/primary-lan-address.js.map +1 -0
  102. package/dist/provider/client.d.ts +131 -0
  103. package/dist/provider/client.js +226 -0
  104. package/dist/provider/client.js.map +1 -0
  105. package/dist/provider/descriptor.d.ts +41 -0
  106. package/dist/provider/descriptor.js +103 -0
  107. package/dist/provider/descriptor.js.map +1 -0
  108. package/dist/provider/index.d.ts +9 -0
  109. package/dist/provider/index.js +10 -0
  110. package/dist/provider/index.js.map +1 -0
  111. package/dist/provider/oauth.d.ts +125 -0
  112. package/dist/provider/oauth.js +199 -0
  113. package/dist/provider/oauth.js.map +1 -0
  114. package/dist/provider/rent-node.d.ts +101 -0
  115. package/dist/provider/rent-node.js +220 -0
  116. package/dist/provider/rent-node.js.map +1 -0
  117. package/dist/push-fanout.d.ts +8 -2
  118. package/dist/push-fanout.js +8 -2
  119. package/dist/push-fanout.js.map +1 -1
  120. package/dist/relay-addrs.d.ts +9 -4
  121. package/dist/relay-addrs.js +9 -4
  122. package/dist/relay-addrs.js.map +1 -1
  123. package/dist/relay-reservation.d.ts +51 -16
  124. package/dist/relay-reservation.js +107 -24
  125. package/dist/relay-reservation.js.map +1 -1
  126. package/dist/seed-bootstrap.d.ts +165 -123
  127. package/dist/seed-bootstrap.js +313 -275
  128. package/dist/seed-bootstrap.js.map +1 -1
  129. package/dist/seed-trust-policy.d.ts +98 -37
  130. package/dist/seed-trust-policy.js +188 -2
  131. package/dist/seed-trust-policy.js.map +1 -1
  132. package/dist/strand-addr-protocol.d.ts +24 -26
  133. package/dist/strand-addr-protocol.js +19 -26
  134. package/dist/strand-addr-protocol.js.map +1 -1
  135. package/dist/strand-commit-durability.d.ts +54 -0
  136. package/dist/strand-commit-durability.js +74 -0
  137. package/dist/strand-commit-durability.js.map +1 -0
  138. package/dist/strand-database.d.ts +23 -8
  139. package/dist/strand-database.js +41 -13
  140. package/dist/strand-database.js.map +1 -1
  141. package/dist/strand-first-sync-gate.d.ts +7 -1
  142. package/dist/strand-first-sync-gate.js +7 -1
  143. package/dist/strand-first-sync-gate.js.map +1 -1
  144. package/dist/strand-formation-protocol.js +5 -2
  145. package/dist/strand-formation-protocol.js.map +1 -1
  146. package/dist/strand-fret-addrs.d.ts +19 -1
  147. package/dist/strand-fret-addrs.js +27 -9
  148. package/dist/strand-fret-addrs.js.map +1 -1
  149. package/dist/strand-instance-manager.d.ts +81 -19
  150. package/dist/strand-instance-manager.js +216 -105
  151. package/dist/strand-instance-manager.js.map +1 -1
  152. package/dist/strand-move.d.ts +124 -0
  153. package/dist/strand-move.js +438 -0
  154. package/dist/strand-move.js.map +1 -0
  155. package/dist/strand-network-config.js +7 -6
  156. package/dist/strand-network-config.js.map +1 -1
  157. package/dist/strand-peer-redial.d.ts +113 -0
  158. package/dist/strand-peer-redial.js +236 -0
  159. package/dist/strand-peer-redial.js.map +1 -0
  160. package/dist/strand-revocation-enforcer.d.ts +11 -11
  161. package/dist/strand-revocation-enforcer.js +11 -11
  162. package/dist/strand-wake-protocol.d.ts +13 -16
  163. package/dist/strand-wake-protocol.js +23 -27
  164. package/dist/strand-wake-protocol.js.map +1 -1
  165. package/dist/trusted-owner-store.d.ts +35 -5
  166. package/dist/trusted-owner-store.js +50 -8
  167. package/dist/trusted-owner-store.js.map +1 -1
  168. package/dist/types.d.ts +512 -89
  169. package/dist/types.js +10 -0
  170. package/dist/types.js.map +1 -1
  171. package/package.json +19 -10
  172. package/src/cadre-invite-protocol.ts +1052 -0
  173. package/src/cadre-join-request.ts +167 -0
  174. package/src/cadre-node.ts +1703 -386
  175. package/src/claim-proof.ts +147 -0
  176. package/src/control-authorization.ts +203 -117
  177. package/src/control-cohort.ts +1 -1
  178. package/src/control-database.ts +4663 -3453
  179. package/src/control-protocol-guard.ts +348 -0
  180. package/src/control-schema.ts +828 -1078
  181. package/src/control-stream.ts +26 -7
  182. package/src/delegate-admission.ts +1 -1
  183. package/src/device-token.ts +14 -10
  184. package/src/ed25519-key.ts +47 -10
  185. package/src/fetch-impl.ts +23 -0
  186. package/src/file-durable-slot.ts +64 -63
  187. package/src/file-safe-name.ts +32 -0
  188. package/src/formation-approval.ts +604 -610
  189. package/src/fs-atomic.ts +92 -117
  190. package/src/index.ts +789 -648
  191. package/src/invitation-bootstrap.ts +91 -13
  192. package/src/joined-strand-store.ts +508 -498
  193. package/src/key-store-file.ts +142 -141
  194. package/src/link-budget.ts +25 -36
  195. package/src/machine-removal.ts +216 -0
  196. package/src/membership-connection-gater.ts +259 -191
  197. package/src/node-claim-payload.ts +259 -0
  198. package/src/node-default-transports.ts +38 -0
  199. package/src/node-transports.ts +49 -0
  200. package/src/owner-anchor-sync.ts +171 -0
  201. package/src/peer-authorization.ts +258 -29
  202. package/src/peer-dial.ts +604 -315
  203. package/src/peer-join-backfill.ts +62 -11
  204. package/src/peer-record.ts +159 -197
  205. package/src/pending-join-runner.ts +729 -735
  206. package/src/primary-lan-address.ts +47 -0
  207. package/src/provider/client.ts +326 -0
  208. package/src/provider/descriptor.ts +129 -0
  209. package/src/provider/index.ts +59 -0
  210. package/src/provider/oauth.ts +285 -0
  211. package/src/provider/rent-node.ts +275 -0
  212. package/src/push-fanout.ts +8 -2
  213. package/src/relay-addrs.ts +9 -4
  214. package/src/relay-reservation.ts +1211 -1121
  215. package/src/seed-bootstrap.ts +1502 -1448
  216. package/src/seed-trust-policy.ts +353 -140
  217. package/src/strand-addr-protocol.ts +26 -35
  218. package/src/strand-commit-durability.ts +116 -0
  219. package/src/strand-database.ts +46 -14
  220. package/src/strand-first-sync-gate.ts +7 -1
  221. package/src/strand-formation-protocol.ts +1039 -1036
  222. package/src/strand-fret-addrs.ts +179 -145
  223. package/src/strand-instance-manager.ts +2026 -1883
  224. package/src/strand-move.ts +585 -0
  225. package/src/strand-network-config.ts +7 -6
  226. package/src/strand-peer-redial.ts +307 -0
  227. package/src/strand-revocation-enforcer.ts +11 -11
  228. package/src/strand-wake-protocol.ts +23 -30
  229. package/src/trusted-owner-store.ts +242 -171
  230. package/src/types.ts +2687 -2242
@@ -2,8 +2,8 @@ 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, PendingJoinRow, PeerAddressRecord, CadrePeerRow, RevocationRow, RevocationLedgerOpenResult, DeviceTokenRecord, DeviceTokenRow } from './types.js';
6
- import type { ControlTable, RevocableTable, ControlDomain, ControlAction } from './control-authorization.js';
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';
7
7
  import type { ControlRetryAbandonment } from './control-retry.js';
8
8
  export type { ControlTable, RevocableTable, ControlDomain, ControlAction } from './control-authorization.js';
9
9
  /**
@@ -50,13 +50,15 @@ export declare class FormationAbortedError extends Error {
50
50
  });
51
51
  }
52
52
  /**
53
- * The invitation's seat budget is already spent: the count of recorded `FormationUsage` rows
54
- * for the token has reached `FormationInvite.TotalUses`, so this redemption cannot be given a
55
- * seat.
53
+ * The invitation's seat budget is already spent: the count of recorded usage rows for the
54
+ * 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.
56
57
  *
57
58
  * Raised off the committed-count check ({@link ControlDatabase.assertSeatRemains}) ahead of
58
59
  * the write, inside {@link ControlDatabase.redeemInvitation} /
59
- * {@link ControlDatabase.recordFormationUsage}, and off the unlocked pre-check
60
+ * {@link ControlDatabase.recordFormationUsage}, off the same check for a cadre invitation
61
+ * ({@link ControlDatabase.redeemCadreInvite}), and off the unlocked pre-check
60
62
  * `ControlFormationUsageRecorder.authorizeUsage` runs before the manager issues a membership
61
63
  * pass — and off nothing else. It exists because
62
64
  * without it the refusal surfaces as a generic `CHECK constraint failed: Authorized` from the
@@ -73,6 +75,20 @@ export declare class InvitationExhaustedError extends Error {
73
75
  readonly totalUses: number;
74
76
  constructor(token: string, usesRecorded: number, totalUses: number);
75
77
  }
78
+ /**
79
+ * {@link ControlDatabase.seatCadreInvite} was refused by `CadreInvite.AuthorizedInsert`: the
80
+ * row's `IssuerKey` is not an owner on THIS node, or its `IssuerSig` does not verify. The
81
+ * common cause is the first — the issuing owner's `OwnerKey` row has not replicated here yet,
82
+ * or that owner has since been removed — which is why the redemption protocol maps this to a
83
+ * retryable code; a tampered row fails the same way and simply never succeeds.
84
+ */
85
+ export declare class CadreInviteIssuerUnknownError extends Error {
86
+ readonly inviteKey: string;
87
+ readonly issuerKey: string;
88
+ constructor(inviteKey: string, issuerKey: string, options?: {
89
+ cause?: unknown;
90
+ });
91
+ }
76
92
  /**
77
93
  * What a recorded `FormationUsage` row came out as.
78
94
  *
@@ -84,20 +100,26 @@ export interface FormationUsageResult {
84
100
  /** The single-use nonce written to `UsageStampId` (the row's primary key) — the caller's, or a freshly minted one. */
85
101
  usageStampId: string;
86
102
  }
103
+ /**
104
+ * Parse a nullable stored `datetime` column into epoch ms, mapping both "absent"
105
+ * and "unparseable" to null — i.e. "no expiry". Shared by every `ExpiresAt`
106
+ * reader so the NaN guard cannot drift between them.
107
+ */
108
+ export declare function parseNullableStoredDatetimeMs(value: string | number | null | undefined): number | null;
87
109
  /**
88
110
  * Build the canonical authorization message that owner signatures are bound to.
89
111
  *
90
112
  * The message is a SINGLE framed SHA-256 digest over the ordered field vector from
91
113
  * {@link controlAuthorizationFields} (the crypto plugin's injective multi-field
92
114
  * encoding): two fixed literals — the domain tag naming the table rule and the action
93
- * tag — followed by the row fields in the schema's fixed order, with the single-use
94
- * StampId as the final field where the table has one:
115
+ * tag — then the party id, then the row fields in the schema's fixed order, with the
116
+ * single-use StampId as the final field where the table has one:
95
117
  *
96
- * message = sha256(encodeFields([domain, action, field_1, ..., StampId])) // raw digest bytes
118
+ * message = sha256(encodeFields([domain, action, partyId, field_1, ..., StampId])) // raw digest bytes
97
119
  *
98
120
  * ed25519 signs these raw digest bytes DIRECTLY (no second hash). The SQL constraints
99
121
  * verify the identical bytes with one variadic call
100
- * (`verify(digest('CadreControl.X', 'add', field_1, ...), context.Signature, A.Key, 'ed25519')`):
122
+ * (`verify(digest('CadreControl.X', 'add', party_id(), field_1, ...), context.Signature, A.Key, 'ed25519')`):
101
123
  * SQL `digest(...)` returns the base64url string of the same digest, which `verify`'s default
102
124
  * base64url input encoding decodes back to those raw bytes — so signer and verifier
103
125
  * operate on the same bytes. Every field is TEXT on both sides (the SQL columns are
@@ -105,11 +127,33 @@ export interface FormationUsageResult {
105
127
  * type tags agree. Binding the row contents closes captured-stamp replay; the leading
106
128
  * domain/action tags scope the signature to ONE table rule, so an approval minted for
107
129
  * one constraint can never satisfy another (e.g. a ValidationKey enrollment can no
108
- * longer double as an OwnerKey enrollment). Single source of truth: every signed writer
109
- * (and every test/harness signer) MUST build the message through this function with the
110
- * schema's tags and field order, or `verify` will reject the row.
130
+ * longer double as an OwnerKey enrollment); the party id scopes it to ONE party, so two
131
+ * parties sharing an owner key never accept each other's approvals. `partyId` is the
132
+ * party the signer is configured for, and the SQL side reads its own through the
133
+ * node-local `party_id()` ({@link registerPartyIdFunction}). Single source of truth:
134
+ * every signed writer (and every test/harness signer) MUST build the message through
135
+ * this function with the schema's tags and field order, or `verify` will reject the row.
136
+ */
137
+ export declare function buildAuthorizationMessage(domain: BoundControlDomain, action: ControlAction, partyId: string, rowFields: string[]): Uint8Array;
138
+ /**
139
+ * Register the zero-argument scalar `party_id()` on `db`: the party id this machine is
140
+ * configured for, which every party-bound `digest(...)` in the control schema puts third.
141
+ * Node-local by design — a `context` value would be the writer's choice (a cohort member
142
+ * that re-ran the statement would replay it as a literal), and a stored `Party` table would
143
+ * be replicated data the schema already treats as untrusted. A cohort member that re-runs a
144
+ * statement therefore evaluates its OWN party id, and the schema text stays identical for
145
+ * every party. Each `Database` carries its own registration, so a process serving two
146
+ * parties holds two.
147
+ *
148
+ * NOTE: flagged deterministic, so the planner may fold the call into a plan; plans are
149
+ * per `Database` today. If Quereus ever shares plans across `Database` instances, drop
150
+ * the DETERMINISTIC flag or one party's id would be folded into another's checks.
151
+ *
152
+ * `ControlDatabase.initialize` calls this before hydrating persisted schemas, because a
153
+ * warm restart parses the persisted CHECK text, which calls it. Exported for specs that
154
+ * build their own `Database` around a copy of a bound constraint.
111
155
  */
112
- export declare function buildAuthorizationMessage(domain: ControlDomain, action: ControlAction, rowFields: string[]): Uint8Array;
156
+ export declare function registerPartyIdFunction(db: Database, partyId: string): void;
113
157
  /**
114
158
  * The exact bytes an outside approver signs to authorize ONE redemption of a
115
159
  * `ValidationUrl`-bearing `FormationInvite` — the TS mirror of the `'vouch'` digest in
@@ -125,6 +169,10 @@ export declare function buildAuthorizationMessage(domain: ControlDomain, action:
125
169
  * that same nonce, then pass BOTH the nonce and the signature to
126
170
  * {@link ControlDatabase.redeemInvitation} / {@link ControlDatabase.recordFormationUsage}:
127
171
  * signing one nonce and inserting another fails the CHECK.
172
+ *
173
+ * No party id, unlike every owner digest: the approver is sent only these five fields
174
+ * (`formation-approval.ts`), and the `token` it binds is a random secret seated in one
175
+ * party's `FormationInvite` table (the schema's `FormationUsage.Authorized` comment).
128
176
  */
129
177
  export declare function formationVouchMessage(fields: {
130
178
  token: string;
@@ -149,7 +197,9 @@ export declare function formationVouchMessage(fields: {
149
197
  * the strand when it signs (a bound invite's host strand arrives only in the result
150
198
  * frame; an unbound strand is minted by the responder). The responder cannot
151
199
  * substitute one anyway — a bound invite is pinned to its own strand by `Authorized`,
152
- * and an unbound redemption mints a fresh strand, so there is no victim to name.
200
+ * and an unbound redemption mints a fresh strand, so there is no victim to name. The
201
+ * party id is absent for the same reason (it too arrives only in the result frame) and
202
+ * the `token` scopes the digest to one party instead, as for {@link formationVouchMessage}.
153
203
  */
154
204
  export declare function formationConsentMessage(fields: {
155
205
  token: string;
@@ -157,6 +207,37 @@ export declare function formationConsentMessage(fields: {
157
207
  peerKey: string;
158
208
  disclosure: string;
159
209
  }): Uint8Array;
210
+ /**
211
+ * The exact bytes an owner signs to seat a `CadreInvite` row — the TS mirror of the `'add'`
212
+ * digest in `CadreInvite.AuthorizedInsert`, over the whole row in the schema's order with
213
+ * nullable columns as `''` (`cadreInviteRowFields`, shared with the base64url verifier
214
+ * `peer-authorization.ts`'s `cadreInviteAddDigest`). `expiresAt` must already be the stored
215
+ * canonical `datetime` string ({@link canonicalDatetime}), as {@link ControlDatabase.insertCadreInvite}
216
+ * produces it, or the signed segment will not match what the CHECK sees after coercion.
217
+ */
218
+ export declare function cadreInviteAddMessage(partyId: string, row: CadreInviteSignedFields): Uint8Array;
219
+ /** The fields both redemption signatures cover; see {@link cadreInviteRedeemMessage}. */
220
+ export interface CadreInviteRedemptionFields {
221
+ inviteKey: string;
222
+ /** The single-use nonce the redeeming device minted for this redemption (`generateStampId`). */
223
+ usageStampId: string;
224
+ /** The redeeming device's own ed25519 public key (base64url). */
225
+ peerKey: string;
226
+ }
227
+ /**
228
+ * The exact bytes the HOLDER of a cadre invitation signs, with the invitation's private key,
229
+ * to redeem it once — the TS mirror of the `'redeem'` digest `CadreInviteUsage.InvitePossessed`
230
+ * verifies against the invitation's public key. Binding the device and the nonce means a
231
+ * captured redemption can admit no other device and spend no other use.
232
+ */
233
+ export declare function cadreInviteRedeemMessage(partyId: string, fields: CadreInviteRedemptionFields): Uint8Array;
234
+ /**
235
+ * The exact bytes the DEVICE signs, with its own key, to consent to being admitted by one
236
+ * redemption — the TS mirror of the `'consent'` digest `CadreInviteUsage.PeerConsented`
237
+ * verifies against `PeerKey`. Same fields as {@link cadreInviteRedeemMessage} under a
238
+ * distinct action tag, so neither signature can stand in for the other.
239
+ */
240
+ export declare function cadreInviteConsentMessage(partyId: string, fields: CadreInviteRedemptionFields): Uint8Array;
160
241
  /** The control tables keyed by a strand id whose repeat insert a caller may treat as idempotent. */
161
242
  type StrandIdTable = Extract<RevocableTable, 'Strand' | 'JoinedStrand'>;
162
243
  /**
@@ -176,8 +257,8 @@ type StrandIdTable = Extract<RevocableTable, 'Strand' | 'JoinedStrand'>;
176
257
  */
177
258
  export declare function isStrandIdConflict(error: unknown, table: StrandIdTable): boolean;
178
259
  /**
179
- * Did {@link ControlDatabase.insertPendingJoin} fail because the party already holds a
180
- * `PendingJoin` row for this invitation? The caller adopts the existing row instead.
260
+ * Did {@link ControlDatabase.insertJoinRequest} fail because the party already holds a
261
+ * `JoinRequest` row for this invitation? The caller adopts the existing request instead.
181
262
  * Text-matched, failing closed, like {@link isStrandIdConflict}.
182
263
  *
183
264
  * NOTE: the existing row may be one {@link ControlDatabase.queryPendingJoin} hides (its stamp
@@ -186,22 +267,24 @@ export declare function isStrandIdConflict(error: unknown, table: StrandIdTable)
186
267
  */
187
268
  export declare function isPendingJoinConflict(error: unknown): boolean;
188
269
  /**
189
- * The `PendingJoin.Id` of an invitation: the base64url sha256 `digest` of its token. One row
270
+ * The `JoinRequest.Id` of an invitation: the base64url sha256 `digest` of its token. One row
190
271
  * per invitation, and the `Revocation` tombstone that names a removed row carries this, never
191
272
  * the bearer token itself.
192
273
  */
193
274
  export declare function pendingJoinId(token: string): string;
275
+ /** A `JoinRequest` row as written: every column but the stamp, which each write mints. */
276
+ export type JoinRequestFields = Omit<JoinRequestRow, 'StampId'>;
277
+ /** A pending join as re-written: its request fields and its outcome, under a stamp the write mints. */
278
+ export type PendingJoinFields = Omit<PendingJoin, 'StampId'>;
194
279
  /**
195
- * {@link ControlDatabase.replacePendingJoin} found that the live `PendingJoin` row is not the
196
- * incarnation the caller read: another write replaced or removed it first (a sibling machine
197
- * recorded an outcome, or the user dismissed the join). Nothing was written; the caller
198
- * re-reads and decides again. `liveStampId` is null when the row is gone.
280
+ * A pending-join write found that the live join is not the one the caller read: another write
281
+ * recorded an outcome, asked again or dismissed it first. Nothing was written; the caller
282
+ * decides again from `live`, which is null when the request is gone.
199
283
  */
200
284
  export declare class PendingJoinChangedError extends Error {
201
285
  readonly id: string;
202
- readonly expectedStampId: string;
203
- readonly liveStampId: string | null;
204
- constructor(id: string, expectedStampId: string, liveStampId: string | null);
286
+ readonly live: PendingJoin | null;
287
+ constructor(id: string, live: PendingJoin | null);
205
288
  }
206
289
  /**
207
290
  * The one `CadreControl.Revocation` row that retires nothing, filed once by an owner
@@ -214,6 +297,24 @@ export declare const REVOCATION_LEDGER_MARKER: {
214
297
  readonly rowKey: "ledger";
215
298
  readonly stampId: "opened";
216
299
  };
300
+ /**
301
+ * 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.
309
+ *
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.
316
+ */
317
+ export declare function projectSeedPeers(rows: readonly CadrePeerRow[], ownerKeys: ReadonlySet<string>): SeedPeer[];
217
318
  /**
218
319
  * Guarded tables a node may reap locally once their tombstone has committed — the
219
320
  * tables whose `AuthorizedDelete` carries the REAP branch (see the constraint comment on
@@ -222,15 +323,29 @@ export declare const REVOCATION_LEDGER_MARKER: {
222
323
  * secrets stored nowhere else (tickets/backlog/debt-strand-tombstone-reap.md owns any
223
324
  * future change). `JoinedStrand` also carries a `MemberPrivateKey` but IS reapable: that
224
325
  * secret is the joined strand's shared read key, which its founding party and every member
225
- * hold, so a reaped row is recoverable by re-forming. `PendingJoin` is reapable for the same
326
+ * hold, so a reaped row is recoverable by re-forming. `JoinRequest` is reapable for the same
226
327
  * reason: the inviting party and the user also hold its invitation. `OwnerKey` has no
227
328
  * production removal path and `MinOneOwner` makes an automated owner-key reap a
228
- * party-bricking hazard.
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.
229
331
  */
230
- export declare const REAPABLE_TABLES: readonly ["CadrePeer", "DeviceToken", "ValidationKey", "JoinedStrand", "PendingJoin"];
332
+ export declare const REAPABLE_TABLES: readonly ["CadrePeer", "DeviceToken", "ValidationKey", "JoinedStrand", "JoinRequest"];
231
333
  export type ReapableTable = (typeof REAPABLE_TABLES)[number];
334
+ /** A `FormationInvite` row as {@link ControlDatabase.queryFormationInvite} reads it. */
335
+ export interface FormationInviteRow {
336
+ 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
+ }
232
346
  /**
233
- * Notified after a `CadreControl.CadrePeer` row write has COMMITTED.
347
+ * Notified after a membership write (a `CadreControl.CadrePeer` or `OwnerKey` row, see
348
+ * {@link ControlDatabase.mutateCadrePeer}) has COMMITTED.
234
349
  *
235
350
  * The one hook the party-membership snapshot a node admits control-DB traffic
236
351
  * against ({@link CadreNode.refreshMembershipGate}) hangs off, so that snapshot
@@ -240,19 +355,22 @@ export type ReapableTable = (typeof REAPABLE_TABLES)[number];
240
355
  */
241
356
  export type MembershipChangeListener = (reason: string) => Promise<void>;
242
357
  /**
243
- * Identity of the tombstone a committed guarded delete wrote — a
244
- * {@link RevocationRow} minus its `ReissuedAt` counter, which a fresh tombstone
245
- * always seats at 0 (`FreshTombstone`) and so carries no information.
358
+ * Identity of the tombstone a committed guarded delete wrote — the triple a
359
+ * {@link RevocationRow} is keyed and signed over. The counter is left out because a
360
+ * fresh tombstone always seats it at 0 (`FreshTombstone`), and the signer pair because
361
+ * the writer that files the row supplies it.
246
362
  */
247
- export type RevokedRowRef = Omit<RevocationRow, 'reissuedAt'>;
363
+ 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'>;
248
366
  /**
249
367
  * Notified after a guarded-table delete — and the `CadreControl.Revocation`
250
368
  * tombstone it writes in the same transaction — has COMMITTED.
251
369
  *
252
370
  * The seam the write-while-alone re-replication queue hangs off
253
- * ({@link CadreNode.noteGuardedDelete}): one listener covers all four guarded
254
- * tables (`CadrePeer` / `DeviceToken` / `Strand` / `ValidationKey`) because every
255
- * owner delete funnels through {@link deleteGuardedRow}. Synchronous — the
371
+ * ({@link CadreNode.noteGuardedDelete}): one listener covers every guarded table
372
+ * because every owner delete funnels through {@link deleteGuardedRow} (or the
373
+ * two-table `deleteStrandAndPartyKey`). Synchronous — the
256
374
  * handler only records the tombstone's identity; it must not throw (the notifier
257
375
  * swallows and logs anyway — a committed delete never fails because bookkeeping did).
258
376
  */
@@ -271,7 +389,12 @@ export type GuardedDeleteListener = (revocation: RevokedRowRef) => void;
271
389
  */
272
390
  export type ControlWriteAbandonedListener = (abandonment: ControlRetryAbandonment) => void;
273
391
  export interface ControlDatabaseConfig {
274
- /** Party ID for the control network */
392
+ /**
393
+ * Party ID for the control network. Also the party every owner and peer signature over a
394
+ * control row binds ({@link buildAuthorizationMessage}), read on the SQL side through
395
+ * `party_id()` ({@link registerPartyIdFunction}). Must be non-empty: parties with an empty
396
+ * id would share approvals.
397
+ */
275
398
  partyId: string;
276
399
  /**
277
400
  * Optional path to the control schema file.
@@ -476,17 +599,19 @@ export declare class ControlDatabase {
476
599
  /** Single-row sibling of {@link queryJoinedStrands}: the joined strand with this id, or null. */
477
600
  queryJoinedStrand(strandId: string): Promise<StrandRow | null>;
478
601
  /**
479
- * Every join this party asked for and has not dismissed (`CadreControl.PendingJoin`),
480
- * pending or finished.
602
+ * Every join this party asked for and has not dismissed, pending or finished: each
603
+ * `CadreControl.JoinRequest` row with its `JoinSuccess` / `JoinFailure` row.
481
604
  *
482
- * Rows whose `StampId` is retired in `Revocation` are dropped, as {@link queryCadrePeers}
483
- * drops them: such a row is either a replaced or dismissed incarnation this node still
484
- * holds until the reap sweep removes it, or one re-seated by a replayed approval on a node
485
- * that had not yet seen the tombstone. Either way it must not bring a dismissed join back.
605
+ * A request whose `StampId` is retired in `Revocation` is dropped, as {@link queryCadrePeers}
606
+ * drops such rows: it is a re-asked or dismissed incarnation this node still holds until the
607
+ * reap sweep removes it, or one re-seated by a replayed approval on a node that had not yet
608
+ * seen the tombstone. Either way it must not bring a dismissed join back.
486
609
  */
487
- queryPendingJoins(): Promise<PendingJoinRow[]>;
488
- /** Single-row sibling of {@link queryPendingJoins}, with the same retired-stamp filter: the row with this id, or null. */
489
- queryPendingJoin(id: string): Promise<PendingJoinRow | null>;
610
+ queryPendingJoins(): Promise<PendingJoin[]>;
611
+ /** Single-row sibling of {@link queryPendingJoins}, with the same retired-stamp filter: the join with this id, or null. */
612
+ queryPendingJoin(id: string): Promise<PendingJoin | null>;
613
+ /** The raw read behind {@link queryPendingJoin}, with no retired-stamp filter. `retry: false` inside a locked write body. */
614
+ private readPendingJoin;
490
615
  /**
491
616
  * Count rows in a CadreControl table as seen by THIS database instance.
492
617
  *
@@ -506,6 +631,9 @@ export declare class ControlDatabase {
506
631
  /**
507
632
  * Check whether any owner key exists in the control database.
508
633
  * Used to decide whether a fresh-party genesis insert is required.
634
+ *
635
+ * Deliberately RAW (no retired-stamp filter, unlike {@link getOwnerKeys}): the genesis
636
+ * insert is refused by the schema whenever any physical row exists, so this must see one.
509
637
  */
510
638
  hasOwnerKey(): Promise<boolean>;
511
639
  /**
@@ -521,8 +649,41 @@ export declare class ControlDatabase {
521
649
  * trusted only if it is already enrolled here (see `SeedTrustPolicy`). It is
522
650
  * also the owner-identity source for `queryPeers`, decoupling owner
523
651
  * status from the libp2p transport peer ID.
524
- */
525
- getOwnerKeys(): Promise<Set<string>>;
652
+ *
653
+ * Rows whose `StampId` is retired in `CadreControl.Revocation` are excluded, as
654
+ * {@link queryCadrePeers} excludes them: `OwnerKey` is never reaped (`MinOneOwner` makes
655
+ * an automated reap able to empty the table), so a node that received the tombstone
656
+ * without the delete (a removal committed while the remover was alone, re-issued on growth)
657
+ * keeps the removed owner's physical row next to it. Without the filter a removed owner
658
+ * would still count as an owner in every membership snapshot and seed-trust read, still
659
+ * read as the issuer of a live invitation and still be preferred as a dial target. The
660
+ * write side filters for itself: every owner lookup in the schema disqualifies a row whose
661
+ * stamp is retired (`schemas/control.qsql` header), so a removed owner cannot sign here
662
+ * once its tombstone has arrived, row or no row.
663
+ *
664
+ * `retry: false` from a caller that reads under the write lock, for the reason on
665
+ * {@link queryCadrePeers}.
666
+ */
667
+ getOwnerKeys(retry?: boolean): Promise<Set<string>>;
668
+ /**
669
+ * Every live `OwnerKey` row with its persisted proof ({@link OwnerKeyRow}) — what the
670
+ * trusted-owner anchor sync derives from (`CadreNode.syncOwnerAnchor`). Rows whose
671
+ * `StampId` is retired in `CadreControl.Revocation` are excluded here, as
672
+ * {@link queryCadrePeers} excludes them and for the reason on {@link getOwnerKeys}, which
673
+ * reads through this. A row with no stamp (the schema permits it; no writer produces one)
674
+ * is dropped too: no proof can bind to it, so no reader could ever derive it.
675
+ *
676
+ * `retry: false` from the membership-gate refresh, which runs as the control database's
677
+ * membership listener with that database's write lock held — the reason on
678
+ * {@link queryCadrePeers}; both reads here take the flag.
679
+ *
680
+ * @param retired - the retired `OwnerKey` stamps, when the caller has already read the
681
+ * 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.
685
+ */
686
+ queryOwnerKeyRows(retry?: boolean, retired?: ReadonlySet<string>): Promise<OwnerKeyRow[]>;
526
687
  /**
527
688
  * Enumerate the CadrePeer rows (cadre membership) for admin/membership reads.
528
689
  * Includes the persisted voucher columns, which
@@ -560,6 +721,17 @@ export declare class ControlDatabase {
560
721
  * lock is the same defect.
561
722
  */
562
723
  queryCadrePeers(retry?: boolean): Promise<CadrePeerRow[]>;
724
+ /**
725
+ * 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}).
728
+ *
729
+ * Read through {@link queryCadrePeers}, never a raw `CadrePeer` select, so a removed
730
+ * member's addresses are never packed into a seed or handed to a redeeming device as a
731
+ * dial hint: a revoked peer is off the addressable surface everywhere, and this is one of
732
+ * its exits.
733
+ */
734
+ querySeedPeers(): Promise<SeedPeer[]>;
563
735
  /**
564
736
  * Every approver public key currently enrolled in `CadreControl.ValidationKey` — the
565
737
  * set a `ValidationUrl` redemption's approval signature must be signed by (see
@@ -689,19 +861,22 @@ export declare class ControlDatabase {
689
861
  */
690
862
  private readRevokedStampRows;
691
863
  /**
692
- * Every locally-held `CadreControl.Revocation` tombstone — identity triple plus its
693
- * `ReissuedAt` counter. Consumed by the cohort-growth re-issue sweep, which
694
- * enumerates what this node holds before {@link reissueRevocations} re-broadcasts
695
- * it, and by the reap sweep ({@link reapRevokedRows}). Plain scan with no `where`, so
696
- * the composite-primary-key point-lookup hazard (see the statement comment in
864
+ * Every locally-held `CadreControl.Revocation` tombstone — identity triple, its
865
+ * `ReissuedAt` counter and the stored signer pair. Consumed by the cohort-growth re-issue
866
+ * sweep, which enumerates what this node holds before {@link reissueRevocations}
867
+ * re-broadcasts it, and by the reap sweep ({@link reapRevokedRows}). Plain scan with no
868
+ * `where`, so the composite-primary-key point-lookup hazard (see the statement comment in
697
869
  * {@link reissueRevocations}) does not arise. Unlocked, like every other read.
698
870
  *
699
871
  * Skips the ledger marker ({@link REVOCATION_LEDGER_MARKER}): it retires nothing, so
700
872
  * neither sweep may reap or re-sign it, and skipping it keeps `RevocationRow.tableName`
701
873
  * a {@link RevocableTable}. Filtered on `TableName` alone, in TypeScript: `RowIsGone`
702
874
  * admits no other row under `'Revocation'`.
875
+ *
876
+ * `retry: false` from a caller that reads under the write lock, for the reason on
877
+ * {@link queryCadrePeers}.
703
878
  */
704
- queryRevocations(): Promise<RevocationRow[]>;
879
+ queryRevocations(retry?: boolean): Promise<RevocationRow[]>;
705
880
  /**
706
881
  * Read a single peer's address record (the full `CadrePeer` row) by PeerId.
707
882
  *
@@ -775,6 +950,99 @@ export declare class ControlDatabase {
775
950
  * Insert the initial owner key (bootstrap - no existing owners required)
776
951
  */
777
952
  insertOwnerKey(key: string): Promise<void>;
953
+ /**
954
+ * Owner-signed INSERT of a further `OwnerKey` row, notifying the membership listener once
955
+ * it has committed. The signed-add branch of `OwnerKey.Authorized`: the signer must be an
956
+ * 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`).
959
+ *
960
+ * Through {@link mutateCadrePeer}, the membership hub. The liveness read, the stamp and the
961
+ * signature all sit inside the locked body: a {@link lockedWithRetry} re-run starts over a
962
+ * rolled-back attempt with a fresh stamp, as {@link redeemCadreInvite} does.
963
+ *
964
+ * NOTE: re-adding a removed owner from a machine that still holds its retired physical row
965
+ * is refused here by name: `OwnerKey` is not reaped, so a machine that received the
966
+ * tombstone without the delete (see {@link getOwnerKeys}) keeps the row, and an insert there
967
+ * would collide on the primary key. Re-add from the machine that removed the key, or any
968
+ * machine the delete reached; if re-adding removed owners becomes routine, the fix is an
969
+ * `OwnerKey` reap branch, gated so it can never take the table below `MinOneOwner`.
970
+ *
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.
975
+ */
976
+ insertOwnerKeyVouched(key: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
977
+ /**
978
+ * Owner-signed removal of one `OwnerKey` row plus the `Revocation` tombstone retiring its
979
+ * stamp, notifying the membership listener once it has committed. The remove branch of
980
+ * `OwnerKey.Authorized` requires a signer other than the removed key, and `MinOneOwner`
981
+ * keeps the table from emptying; both are refused by name here first, read from this
982
+ * node's live owners ({@link getOwnerKeys}), so a caller gets a clear error rather than a
983
+ * constraint failure. The schema stays the authority: a concurrent removal elsewhere can
984
+ * make the local count wrong in either direction, and then `MinOneOwner` or the tombstone's
985
+ * primary key decides (see the NOTE on {@link deleteGuardedRow}).
986
+ *
987
+ * Through {@link mutateCadrePeer}, the membership hub, which notifies whenever the body
988
+ * resolves, the absent-key answer included.
989
+ *
990
+ * @returns `true` when the row was removed, `false` when `key` is not a live owner here
991
+ * (nothing written, no tombstone).
992
+ * @throws when `key` is `ownerKey`, or is the only live owner.
993
+ */
994
+ deleteOwnerKey(key: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
995
+ /**
996
+ * Re-vouch a live `OwnerKey` row under this owner's signature: in ONE transaction, the
997
+ * 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
1000
+ * this is the only way to rebind a row's voucher; the anchor derivation lets the re-add
1001
+ * 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.
1004
+ *
1005
+ * Through {@link mutateCadrePeer}, the membership hub. The stamp read runs inside the locked
1006
+ * body; a row whose stamp is already retired (held without its delete) is reported as absent
1007
+ * rather than tombstoned twice.
1008
+ *
1009
+ * @returns `true` when the row was re-vouched, `false` when `key` is not a live owner here.
1010
+ * @throws when `key` is `ownerKey` (an owner cannot sign its own row's removal).
1011
+ */
1012
+ revouchOwnerKey(key: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
1013
+ /**
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.
1022
+ *
1023
+ * Through {@link mutateCadrePeer}, the membership hub. Every stamp is read inside the locked
1024
+ * 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.
1027
+ *
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.
1031
+ *
1032
+ * @throws when `target.ownerKey` is `ownerKey`, or is the only live owner here.
1033
+ */
1034
+ removeMachine(target: {
1035
+ peerId: string;
1036
+ ownerKey: string | null;
1037
+ inviteKeys: readonly string[];
1038
+ }, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<MachineRemovalWrites>;
1039
+ /**
1040
+ * 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.
1044
+ */
1045
+ private signMachineRemovals;
778
1046
  /**
779
1047
  * Insert a strand into the control database using an owner signature.
780
1048
  *
@@ -887,45 +1155,62 @@ export declare class ControlDatabase {
887
1155
  */
888
1156
  deleteJoinedStrand(strandId: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
889
1157
  /**
890
- * Record, party-wide, a join this party asked for (`PendingJoin` row), using an owner
891
- * signature over every column of the row (see {@link pendingJoinAddFields}). Returns the
892
- * row as written, with the fresh `StampId` a later {@link replacePendingJoin} names.
1158
+ * Record, party-wide, a join this party asked for: a `JoinRequest` row under a fresh stamp,
1159
+ * owner-signed over every column. Returns the pending join as written.
893
1160
  *
894
- * Fails with a `PendingJoin.Id` uniqueness violation when the party already holds a row for
895
- * this invitation; {@link isPendingJoinConflict} identifies that case.
1161
+ * Fails with a `JoinRequest.Id` uniqueness violation when the party already holds a request
1162
+ * for this invitation; {@link isPendingJoinConflict} identifies that case.
896
1163
  */
897
- insertPendingJoin(row: Omit<PendingJoinRow, 'StampId'>, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<PendingJoinRow>;
1164
+ insertJoinRequest(request: JoinRequestFields, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<PendingJoin>;
898
1165
  /**
899
- * Replace the live `PendingJoin` row `next.Id` with `next`, under a fresh stamp — the only way
900
- * an outcome is written, since the table refuses updates. Returns the row as written.
1166
+ * Record the outcome of the pending join `expected`: its owner-signed `JoinSuccess` /
1167
+ * `JoinFailure` row, keyed by the request's stamp. A join recorded over a failure deletes the
1168
+ * failure row in the same transaction (the schema authorizes that delete by the join, and
1169
+ * leaves the one-outcome rule in that direction to this writer). Returns the pending join as
1170
+ * written.
901
1171
  *
902
- * One locked transaction deletes the row (owner-signed `'remove'` digest, with its
903
- * `Revocation` tombstone) and inserts its successor, so no reader ever sees the id missing in
904
- * between. The live row must still be the incarnation the caller read: when its `StampId` is
905
- * not `expectedStampId`, or the row is gone, this throws {@link PendingJoinChangedError} and
906
- * writes nothing. That check is local; a sibling machine's replacement or removal that lands
907
- * between it and the commit fails the transaction instead (its tombstone for the same stamp,
908
- * or its successor row, collides on a primary key), and the caller re-reads either way.
1172
+ * The live join must still be what the caller read: the same request incarnation with the
1173
+ * same outcome kind. Otherwise this throws {@link PendingJoinChangedError} and writes nothing.
1174
+ * That check is local; a sibling machine's write landing between it and the commit fails the
1175
+ * transaction instead (its outcome row collides on the primary key, or its tombstone retires
1176
+ * the stamp), and the caller re-reads either way.
1177
+ */
1178
+ recordJoinOutcome(expected: PendingJoin, outcome: JoinOutcome, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<PendingJoin>;
1179
+ /**
1180
+ * Re-write the pending join `next` (request and outcome) under a fresh stamp, retiring the
1181
+ * live request incarnation `expectedStampId`: how a finished request is asked for again, and
1182
+ * how a join written while this machine was alone is re-broadcast. One locked transaction
1183
+ * deletes the outcome row and the request (owner-signed `'remove'` digest, with its
1184
+ * `Revocation` tombstone) and inserts the successors, so no reader ever sees the id missing in
1185
+ * between. Throws {@link PendingJoinChangedError} and writes nothing when the live request is
1186
+ * not `expectedStampId`.
909
1187
  *
910
1188
  * Signatures and the new stamp are minted before the lock, so a retried attempt re-presents
911
1189
  * the same signed messages (the contract on {@link withWriteLock}). A retry after an attempt
912
- * that committed but reported failure throws {@link PendingJoinChangedError} whose
913
- * `liveStampId` is the returned row's own stamp, so a caller that re-reads finds its write.
1190
+ * that committed but reported failure throws {@link PendingJoinChangedError} whose live join
1191
+ * carries the returned stamp, so a caller that re-reads finds its write.
914
1192
  */
915
- replacePendingJoin(expectedStampId: string, next: Omit<PendingJoinRow, 'StampId'>, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<PendingJoinRow>;
1193
+ rewritePendingJoin(expectedStampId: string, next: PendingJoinFields, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<PendingJoin>;
916
1194
  /**
917
- * Owner-signed removal of one `PendingJoin` row, as {@link deleteJoinedStrand} removes its
918
- * row: `'remove'`-tagged digest over (Id, StampId), `Revocation` tombstone in the same
919
- * transaction. A no-op (no throw, no tombstone) when the row does not exist — `false` then,
920
- * `true` when a row was actually removed.
1195
+ * Owner-signed removal of one pending join, as {@link deleteJoinedStrand} removes its row:
1196
+ * `'remove'`-tagged digest over (Id, StampId), `Revocation` tombstone in the same transaction,
1197
+ * and the request's outcome row with it. A no-op (no throw, no tombstone) when there is no
1198
+ * request — `false` then, `true` when one was actually removed.
921
1199
  */
922
1200
  deletePendingJoin(id: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
923
1201
  /**
924
- * Mint a fresh stamp for `row` and the owner signature `PendingJoin.AuthorizedInsert`
925
- * verifies, returning the row as it will be written and the parameters of
926
- * {@link PENDING_JOIN_INSERT_SQL}.
1202
+ * Mint a fresh stamp for `request` and the owner signature `JoinRequest.AuthorizedInsert`
1203
+ * verifies, returning the row as it will be written and its insert.
1204
+ */
1205
+ private signJoinRequestInsert;
1206
+ /**
1207
+ * Remove the outcome row of the request incarnation `requestStampId`, whichever table holds
1208
+ * it. Unsigned: the schema authorizes the delete by the tombstone retiring that stamp, filed
1209
+ * in the same transaction or already committed (a reap).
927
1210
  */
928
- private signPendingJoinInsert;
1211
+ private deleteJoinOutcomes;
1212
+ /** One unsigned outcome delete (see {@link deleteJoinOutcomes}; a failure's is also authorized by the join superseding it). */
1213
+ private deleteJoinOutcome;
929
1214
  /**
930
1215
  * Insert a validation key into the control database using an owner signature.
931
1216
  *
@@ -1000,6 +1285,7 @@ export declare class ControlDatabase {
1000
1285
  multiaddr: string;
1001
1286
  updatedAt: number;
1002
1287
  sig: string | null;
1288
+ profile: NodeProfile | null;
1003
1289
  }, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
1004
1290
  /**
1005
1291
  * Owner "re-touch" of an existing `CadrePeer` membership row: bump `UpdatedAt` and
@@ -1011,7 +1297,8 @@ export declare class ControlDatabase {
1011
1297
  * local-only (its block's cluster ≤1 at insert) is pushed to the cohort once it grows, by
1012
1298
  * re-issuing this monotonic bump. It is an UPDATE (not the original INSERT) because the row
1013
1299
  * already exists locally; a re-INSERT would hit the `PeerId` PK. Only the freshness stamp
1014
- * and voucher change — `PublicKey` / `Multiaddr` / `Sig` are left intact — so it is safe
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
1015
1302
  * over a row whose peer has not self-published (`Sig` null); the caller must skip a row
1016
1303
  * that already carries a self-`Sig` (that row is the owning peer's to refresh, and bumping
1017
1304
  * `UpdatedAt` without re-signing would invalidate its self-signature).
@@ -1033,6 +1320,11 @@ export declare class ControlDatabase {
1033
1320
  * change the member set. Keeping the rule uniform ("every CadrePeer mutator notifies")
1034
1321
  * beats a per-method exception the next reader has to relearn.
1035
1322
  *
1323
+ * Also clears `VouchUsage`: a row carries exactly one proof (the owner branch of
1324
+ * `AuthorizedUpdate` requires it null), so re-vouching an invitation-admitted row moves it
1325
+ * onto the signature and its invitation is no longer consulted for it — the way an owner
1326
+ * retires a device's dependency on an invitation it may later withdraw.
1327
+ *
1036
1328
  * NOTE: this rebinds VouchOwner to the CALLING owner's key, and the authorized-membership
1037
1329
  * predicate (`CadreNode.listAuthorizedMembers`) now judges rows by that column against each
1038
1330
  * reader's node-local anchor. Benign today because the only caller — the write-while-alone
@@ -1089,15 +1381,9 @@ export declare class ControlDatabase {
1089
1381
  * Owner-signed delete of one guarded row plus the `Revocation` tombstone retiring its
1090
1382
  * stamp, in ONE transaction. The single body behind EVERY guarded delete —
1091
1383
  * {@link deleteStrand}, {@link deleteValidationKey}, {@link deleteCadrePeer},
1092
- * {@link deleteDeviceToken}; see any of them for the per-table security rationale. Each
1093
- * of those is a thin named wrapper, so callers never pass a table name and this
1094
- * generic shape stays off the public surface.
1095
- *
1096
- * `OwnerKey` is excluded from `table` only because no owner-key removal path exists in
1097
- * production yet — the schema has the `'remove'` branch (`OwnerKey.Authorized`, which
1098
- * requires a DIFFERENT owner as signer) and `control-revocation-replay.spec.ts` drives
1099
- * it with hand-rolled SQL. Widen this and add a wrapper when that path lands; the body
1100
- * needs no change.
1384
+ * {@link deleteDeviceToken}, {@link deleteOwnerKey}; see any of them for the per-table
1385
+ * security rationale. Each of those is a thin named wrapper, so callers never pass a
1386
+ * table name and this generic shape stays off the public surface.
1101
1387
  *
1102
1388
  * The row's CURRENT stamp is read first and signed over, so the remove digest binds to
1103
1389
  * this exact row instance. A no-op (no throw, no tombstone) when the row is absent —
@@ -1105,6 +1391,9 @@ export declare class ControlDatabase {
1105
1391
  * second read (e.g. {@link CadreNode.unpublishStrand}'s committed-while-alone warning,
1106
1392
  * which must not fire for a no-op).
1107
1393
  *
1394
+ * `alongside`, when given, runs inside the transaction before the removal, for a table whose
1395
+ * row has dependants the schema requires removed with it ({@link deletePendingJoin}).
1396
+ *
1108
1397
  * NOTE: the stamp read is outside the transaction. A concurrent writer that removes
1109
1398
  * the row in between makes the signature bind a stamp that is no longer live; the
1110
1399
  * delete then matches nothing and the tombstone insert collides with the other
@@ -1119,7 +1408,7 @@ export declare class ControlDatabase {
1119
1408
  /**
1120
1409
  * Delete the row incarnation `removal` names and file its tombstone — the statement pair
1121
1410
  * every owner removal of a guarded row runs ({@link deleteGuardedRow},
1122
- * {@link deleteStrandAndPartyKey}, {@link replacePendingJoin}). The caller supplies the
1411
+ * {@link deleteStrandAndPartyKey}, {@link rewritePendingJoin}). The caller supplies the
1123
1412
  * transaction: the schema's `RevocationRecorded` check refuses the delete unless the
1124
1413
  * tombstone commits with it.
1125
1414
  *
@@ -1130,6 +1419,21 @@ export declare class ControlDatabase {
1130
1419
  * come from closed literal unions — no caller-supplied string reaches the statement.
1131
1420
  */
1132
1421
  private execGuardedRemoval;
1422
+ /**
1423
+ * 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.
1428
+ *
1429
+ * `ReissuedAt` is named explicitly at 0 (the only value `FreshTombstone` accepts) rather
1430
+ * than leaning on the column default — the seat-at-zero rule is load-bearing for
1431
+ * {@link reissueRevocations}' monotonic bump, so state it at the write site.
1432
+ *
1433
+ * `SignerKey` / `SignerSig` store the context pair (`Revocation.Authorized` pins them
1434
+ * equal), so a node that receives the row by replication can tell which owner filed it.
1435
+ */
1436
+ private execTombstone;
1133
1437
  /**
1134
1438
  * Fire the guarded-delete listener for one committed tombstone, swallowing (and
1135
1439
  * logging) a listener throw — a committed delete never fails because bookkeeping did.
@@ -1218,9 +1522,9 @@ export declare class ControlDatabase {
1218
1522
  * a retired stamp, and which a revoked drone cannot produce at all. The trade is
1219
1523
  * recurring failure noise every cycle against removing the one stale row nobody else
1220
1524
  * reads from this node (every other node already filters it by tombstone). A revoked
1221
- * node therefore keeps its own copy of its own row; "a node that learns it has been
1222
- * revoked should shut itself down" is a distinct behaviour and its own ticket.
1223
- * `ValidationKey`, `JoinedStrand` and `PendingJoin` have no self notion and are not
1525
+ * node therefore keeps its own copy of its own row; learning that it was removed is a
1526
+ * separate behaviour (`CadreNode.removedFromCadre`, the `cadre:removed` event).
1527
+ * `ValidationKey`, `JoinedStrand` and `JoinRequest` have no self notion and are not
1224
1528
  * special-cased.
1225
1529
  *
1226
1530
  * A per-row failure is logged and skipped rather than aborting the sweep: an abort would
@@ -1362,15 +1666,18 @@ export declare class ControlDatabase {
1362
1666
  */
1363
1667
  setControlWriteAbandonedListener(listener: ControlWriteAbandonedListener | null): void;
1364
1668
  /**
1365
- * Run a `CadrePeer` row mutation and notify the membership listener once it has
1669
+ * Run a membership-relevant mutation and notify the membership listener once it has
1366
1670
  * COMMITTED.
1367
1671
  *
1368
- * EVERY `CadrePeer` writer goes through here, with one documented exception
1369
- * ({@link updateSelfPeerRecord}, which cannot change the snapshot) — that is what makes
1370
- * the party-membership snapshot refresh automatic rather than a caller obligation. A
1672
+ * Covers every write that can change who is a member or an owner: EVERY `CadrePeer`
1673
+ * writer, with one documented exception ({@link updateSelfPeerRecord}, which cannot
1674
+ * 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
1677
+ * rather than a caller obligation. The name predates the `OwnerKey` writers. A
1371
1678
  * writer necessarily holds the target node's ControlDatabase (the `SeedBootstrapService`'s
1372
- * event callbacks are NOT a viable seam: the temp services `CadreNode.applySeed`/
1373
- * `dialInvite` build, and services constructed outside `CadreNode` entirely, never get
1679
+ * event callbacks are NOT a viable seam: the temp service `CadreNode.applySeed`
1680
+ * builds, and services constructed outside `CadreNode` entirely, never get
1374
1681
  * callbacks wired), so this is the one point on the write path that cannot be bypassed.
1375
1682
  *
1376
1683
  * `body` owns its own transaction if it needs one and must COMMIT before returning, so
@@ -1707,14 +2014,13 @@ export declare class ControlDatabase {
1707
2014
  * locked write body — same per-call opt-out, and for the same reason, as
1708
2015
  * {@link queryStampId}'s. Every unlocked caller keeps the default retried path.
1709
2016
  */
1710
- queryFormationInvite(token: string, retry?: boolean): Promise<{
1711
- token: string;
1712
- sAppId: string;
1713
- expiresAtMs: number | null;
1714
- totalUses: number | null;
1715
- validationUrl: string | null;
1716
- strandId: string | null;
1717
- } | null>;
2017
+ queryFormationInvite(token: string, retry?: boolean): Promise<FormationInviteRow | null>;
2018
+ /**
2019
+ * Every `FormationInvite` row, expired and spent ones included, in the shape
2020
+ * {@link queryFormationInvite} returns one. Read by a strand move
2021
+ * (`exportStrandBundle`), which decides per row what is still redeemable.
2022
+ */
2023
+ queryFormationInvites(): Promise<FormationInviteRow[]>;
1718
2024
  /**
1719
2025
  * Count `FormationUsage` rows recorded against a token (uses consumed so far).
1720
2026
  *
@@ -1769,6 +2075,197 @@ export declare class ControlDatabase {
1769
2075
  * unlimited-use invite anywhere in the scan short-circuits all of them.
1770
2076
  */
1771
2077
  hasOutstandingFormationInvite(nowMs?: number): Promise<boolean>;
2078
+ /**
2079
+ * Mint and insert an owner-signed `CadreInvite`: an invitation to join THIS cadre that any
2080
+ * member machine can later redeem on the owner's behalf ({@link redeemCadreInvite}).
2081
+ *
2082
+ * The owner signs the whole row ({@link cadreInviteAddMessage}) — `ExpiresAt` through
2083
+ * {@link canonicalDatetime} so the signed segment byte-matches the stored `datetime`, as
2084
+ * {@link insertFormationInvite} does — and the stored `IssuerKey`/`IssuerSig` are pinned to
2085
+ * that signer by `CadreInvite.AuthorizedInsert`. The signature is part of the row because
2086
+ * the row travels: the returned {@link CadreInviteRow} is what the invitation bundle
2087
+ * carries to the holder, and {@link seatCadreInvite} re-inserts it verbatim on a member
2088
+ * that has not received it by replication.
2089
+ *
2090
+ * `key` is the invitation's ed25519 PUBLIC key; the caller minted the pair and hands the
2091
+ * private half to the invitee out of band. It is never stored here (`Strand.Invite` has the
2092
+ * same shape).
2093
+ *
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
+ * @returns the row as stored, `expiresAt` in its canonical form
2100
+ */
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>;
2108
+ /**
2109
+ * Seat an owner-signed `CadreInvite` row received inside an invitation bundle, under its own
2110
+ * 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.
2112
+ *
2113
+ * @returns `true` when this call inserted the row, `false` when it was already present.
2114
+ * @throws {CadreInviteIssuerUnknownError} when `CadreInvite.AuthorizedInsert` refuses it: the
2115
+ * 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.
2117
+ */
2118
+ seatCadreInvite(row: CadreInviteRow): Promise<boolean>;
2119
+ /** Bare `CadreInvite` insert shared by {@link insertCadreInvite} and {@link seatCadreInvite}; the caller holds the lock. */
2120
+ private execCadreInviteInsert;
2121
+ /**
2122
+ * Read one `CadreInvite` row by key, or null when absent. Withdrawn rows are returned too —
2123
+ * the row is the holder's proof and never goes away; liveness is {@link hasLiveCadreInvite}'s
2124
+ * question, and `CadreInviteUsage.Authorized`'s at redemption.
2125
+ *
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.
2128
+ */
2129
+ queryCadreInvite(key: string, retry?: boolean): Promise<CadreInviteRow | null>;
2130
+ /**
2131
+ * Every `CadreInvite` row this node holds, withdrawn and expired ones included (see
2132
+ * {@link queryCadreInvite}). Read by the membership predicate when at least one `CadrePeer`
2133
+ * row is invitation-admitted; `retry: false` is forwarded from
2134
+ * {@link CadreNode.listAuthorizedMembers} on the membership-gate refresh path for
2135
+ * {@link queryCadrePeers}' reason.
2136
+ */
2137
+ queryCadreInvites(retry?: boolean): Promise<CadreInviteRow[]>;
2138
+ /**
2139
+ * Every `CadreInviteUsage` row this node holds — the redemption records the membership
2140
+ * predicate verifies invitation-admitted rows against. Same `retry` contract as
2141
+ * {@link queryCadreInvites}.
2142
+ */
2143
+ queryCadreInviteUsages(retry?: boolean): Promise<CadreInviteUsageRow[]>;
2144
+ /**
2145
+ * How many times a cadre invitation has been redeemed (its `CadreInviteUsage` rows), served
2146
+ * through the `CadreInviteUsageByInvite` index. As {@link countFormationUsage}: a permissive
2147
+ * PRE-check, not the cap — the cap is `CadreInviteUsage.Authorized`'s own committed count.
2148
+ *
2149
+ * `retry: false` is passed by {@link assertCadreInviteSeatRemains}, inside the locked body.
2150
+ */
2151
+ countCadreInviteUsage(key: string, retry?: boolean): Promise<number>;
2152
+ /**
2153
+ * Is any cadre invitation still redeemable — not withdrawn, unexpired, with uses below its
2154
+ * `TotalUses`, and issued by a current owner? The conditions `CadreInviteUsage.Authorized`
2155
+ * applies, read ahead of time so the control-network connection gate can answer "does this
2156
+ * node expect a stranger?" with no key to ask about, exactly as
2157
+ * {@link hasOutstandingFormationInvite} answers it for strand formation. The expiry
2158
+ * comparison is `expiresAtMs <= now`, so an invitation the redemption would refuse never
2159
+ * holds the gate open.
2160
+ *
2161
+ * Withdrawal is read through {@link queryRevokedStamps}, the per-table retired-stamp seam
2162
+ * every other revocation-aware reader uses (and the one an isolated node can still answer).
2163
+ */
2164
+ hasLiveCadreInvite(nowMs?: number): Promise<boolean>;
2165
+ /**
2166
+ * 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.
2173
+ */
2174
+ isCadreInviteLive(invite: CadreInviteRow, nowMs?: number): Promise<boolean>;
2175
+ /**
2176
+ * Every `CadreInvite` row this node holds with its standing: how many redemptions are
2177
+ * recorded, whether it is withdrawn, and whether it is still redeemable — the same
2178
+ * conditions {@link hasLiveCadreInvite} applies, answered per row for an owner's listing
2179
+ * rather than short-circuited. One usage count per row, so this is the owner's
2180
+ * occasional read, not a gate's.
2181
+ */
2182
+ listCadreInviteStatuses(nowMs?: number): Promise<CadreInviteStatus[]>;
2183
+ /**
2184
+ * Owner-signed WITHDRAWAL of a cadre invitation: file the `Revocation` tombstone retiring its
2185
+ * stamp, with the row kept. The row stays because every device the invitation admitted
2186
+ * proves its membership through it (`CadrePeer.AuthorizedInsert`'s consent branch,
2187
+ * `verifyInvitationAdmission`); `CadreInvite.NoDelete` refuses a delete, and
2188
+ * `Revocation.RowIsGone` admits a `'CadreInvite'` tombstone while the row lives. Afterwards
2189
+ * `CadreInviteUsage.Authorized` refuses every further redemption, and `CadreInvite.NotRevoked`
2190
+ * refuses re-seating the row from the holder's signed copy on any node holding the
2191
+ * tombstone.
2192
+ *
2193
+ * Idempotent: a no-op (no throw, nothing written) when the row is absent locally or already
2194
+ * withdrawn — `false` then, `true` when this call filed the tombstone. The owner must hold the
2195
+ * row to withdraw it, since the tombstone names the row's stamp.
2196
+ *
2197
+ * Fires the guarded-delete listener like a removal does, so a withdrawal committed while the
2198
+ * node was alone is queued for re-issue on cohort growth ({@link CadreNode.noteGuardedDelete}).
2199
+ */
2200
+ withdrawCadreInvite(key: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
2201
+ /**
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
2205
+ * transaction. Inside the locked body (every read `retry: false`); an absent or already
2206
+ * withdrawn key is logged and left out.
2207
+ */
2208
+ private signCadreInviteWithdrawals;
2209
+ /**
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`.
2218
+ *
2219
+ * The two signatures are the holder's and the device's, minted on the device over
2220
+ * {@link cadreInviteRedeemMessage} / {@link cadreInviteConsentMessage}; this machine only
2221
+ * verifies them (by writing the row). `peerId` must be the identity multihash of `peerKey`:
2222
+ * the schema cannot derive it, so a writer asserts the pair and every reader re-derives it
2223
+ * (`verifyInvitationAdmission`) — a mismatched pair is written and then never trusted.
2224
+ *
2225
+ * 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.
2231
+ *
2232
+ * Through {@link mutateCadrePeer}, so the membership hub is notified; inside the locked body
2233
+ * every read passes `retry: false` and every stamp is minted per attempt, so a
2234
+ * {@link lockedWithRetry} re-run starts clean.
2235
+ *
2236
+ * @throws {InvitationExhaustedError} when the invitation's seats are all spent (by name,
2237
+ * ahead of the schema's generic `Authorized` refusal — see {@link assertSeatRemains}).
2238
+ */
2239
+ redeemCadreInvite(params: {
2240
+ inviteKey: string;
2241
+ peerId: string;
2242
+ /** The device's ed25519 public key (base64url); `peerId` is its identity multihash. */
2243
+ peerKey: string;
2244
+ /** The device's dialable addresses, if known; stored comma-joined as `authorizePeer` does. */
2245
+ multiaddrs?: string[];
2246
+ /** Single-use nonce minted by the device; both signatures cover it. */
2247
+ usageStampId: string;
2248
+ /** The holder's signature with the invitation private key over the `'redeem'` digest. */
2249
+ inviteSig: string;
2250
+ /** The device's signature with its own key over the `'consent'` digest. */
2251
+ peerSig: string;
2252
+ nowMs?: number;
2253
+ }): 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
+ /**
2262
+ * Refuse a redemption that would consume a seat the cadre invitation does not have, by
2263
+ * name ({@link InvitationExhaustedError}) — the cadre-invitation twin of
2264
+ * {@link assertSeatRemains}, for the same reason: the schema's cap clause refuses as a
2265
+ * generic `Authorized` failure a caller would otherwise report as retryable. Reads the
2266
+ * committed count inside the locked body, so on a same-node race the loser is refused here.
2267
+ */
2268
+ private assertCadreInviteSeatRemains;
1772
2269
  /**
1773
2270
  * Close the database and cleanup resources.
1774
2271
  *