@serfab/cadre-core 0.9.0 → 0.10.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 (162) hide show
  1. package/README.md +4 -2
  2. package/dist/bootstrap-peer-store-file.d.ts +13 -0
  3. package/dist/bootstrap-peer-store-file.js +42 -0
  4. package/dist/bootstrap-peer-store-file.js.map +1 -0
  5. package/dist/bootstrap-peer-store.d.ts +73 -0
  6. package/dist/bootstrap-peer-store.js +128 -0
  7. package/dist/bootstrap-peer-store.js.map +1 -0
  8. package/dist/cadre-node.d.ts +918 -53
  9. package/dist/cadre-node.js +1843 -190
  10. package/dist/cadre-node.js.map +1 -1
  11. package/dist/control-authorization.d.ts +93 -0
  12. package/dist/control-authorization.js +57 -0
  13. package/dist/control-authorization.js.map +1 -0
  14. package/dist/control-database.d.ts +990 -50
  15. package/dist/control-database.js +1459 -108
  16. package/dist/control-database.js.map +1 -1
  17. package/dist/control-formation-recorder.d.ts +89 -10
  18. package/dist/control-formation-recorder.js +129 -14
  19. package/dist/control-formation-recorder.js.map +1 -1
  20. package/dist/control-schema.d.ts +1 -1
  21. package/dist/control-schema.js +774 -210
  22. package/dist/control-schema.js.map +1 -1
  23. package/dist/control-stream.d.ts +38 -4
  24. package/dist/control-stream.js +70 -4
  25. package/dist/control-stream.js.map +1 -1
  26. package/dist/control-write-retry.d.ts +151 -0
  27. package/dist/control-write-retry.js +411 -0
  28. package/dist/control-write-retry.js.map +1 -0
  29. package/dist/delegate-admission.d.ts +134 -0
  30. package/dist/delegate-admission.js +244 -0
  31. package/dist/delegate-admission.js.map +1 -0
  32. package/dist/device-token.d.ts +11 -11
  33. package/dist/device-token.js +14 -13
  34. package/dist/device-token.js.map +1 -1
  35. package/dist/ed25519-key.d.ts +26 -0
  36. package/dist/ed25519-key.js +56 -1
  37. package/dist/ed25519-key.js.map +1 -1
  38. package/dist/file-durable-slot.d.ts +20 -0
  39. package/dist/file-durable-slot.js +53 -0
  40. package/dist/file-durable-slot.js.map +1 -0
  41. package/dist/formation-approval.d.ts +157 -0
  42. package/dist/formation-approval.js +375 -0
  43. package/dist/formation-approval.js.map +1 -0
  44. package/dist/fs-atomic.d.ts +38 -0
  45. package/dist/fs-atomic.js +97 -0
  46. package/dist/fs-atomic.js.map +1 -0
  47. package/dist/identity-key.d.ts +49 -0
  48. package/dist/identity-key.js +81 -0
  49. package/dist/identity-key.js.map +1 -0
  50. package/dist/index.d.ts +16 -6
  51. package/dist/index.js +39 -5
  52. package/dist/index.js.map +1 -1
  53. package/dist/key-store-file.d.ts +2 -24
  54. package/dist/key-store-file.js +10 -77
  55. package/dist/key-store-file.js.map +1 -1
  56. package/dist/membership-connection-gater.d.ts +146 -0
  57. package/dist/membership-connection-gater.js +180 -0
  58. package/dist/membership-connection-gater.js.map +1 -0
  59. package/dist/node-local-snapshot.d.ts +109 -0
  60. package/dist/node-local-snapshot.js +182 -0
  61. package/dist/node-local-snapshot.js.map +1 -0
  62. package/dist/peer-authorization.d.ts +135 -18
  63. package/dist/peer-authorization.js +152 -21
  64. package/dist/peer-authorization.js.map +1 -1
  65. package/dist/peer-record.d.ts +21 -8
  66. package/dist/peer-record.js +26 -10
  67. package/dist/peer-record.js.map +1 -1
  68. package/dist/relay-addrs.d.ts +45 -0
  69. package/dist/relay-addrs.js +103 -0
  70. package/dist/relay-addrs.js.map +1 -0
  71. package/dist/relay-reservation.d.ts +266 -0
  72. package/dist/relay-reservation.js +613 -0
  73. package/dist/relay-reservation.js.map +1 -0
  74. package/dist/seed-bootstrap.d.ts +221 -56
  75. package/dist/seed-bootstrap.js +328 -188
  76. package/dist/seed-bootstrap.js.map +1 -1
  77. package/dist/seed-trust-policy.d.ts +47 -21
  78. package/dist/seed-trust-policy.js +24 -15
  79. package/dist/seed-trust-policy.js.map +1 -1
  80. package/dist/strand-addr-protocol.d.ts +42 -13
  81. package/dist/strand-addr-protocol.js +58 -52
  82. package/dist/strand-addr-protocol.js.map +1 -1
  83. package/dist/strand-backfill.d.ts +109 -0
  84. package/dist/strand-backfill.js +279 -0
  85. package/dist/strand-backfill.js.map +1 -0
  86. package/dist/strand-formation-manager.d.ts +38 -9
  87. package/dist/strand-formation-manager.js +120 -17
  88. package/dist/strand-formation-manager.js.map +1 -1
  89. package/dist/strand-formation-protocol.d.ts +103 -4
  90. package/dist/strand-formation-protocol.js +232 -4
  91. package/dist/strand-formation-protocol.js.map +1 -1
  92. package/dist/strand-instance-manager.d.ts +39 -1
  93. package/dist/strand-instance-manager.js +92 -14
  94. package/dist/strand-instance-manager.js.map +1 -1
  95. package/dist/strand-member-registry.d.ts +5 -5
  96. package/dist/strand-member-registry.js +18 -10
  97. package/dist/strand-member-registry.js.map +1 -1
  98. package/dist/strand-membership-writer.d.ts +482 -88
  99. package/dist/strand-membership-writer.js +757 -136
  100. package/dist/strand-membership-writer.js.map +1 -1
  101. package/dist/strand-solicitation.d.ts +99 -33
  102. package/dist/strand-solicitation.js +100 -52
  103. package/dist/strand-solicitation.js.map +1 -1
  104. package/dist/strand-transport-key.d.ts +40 -0
  105. package/dist/strand-transport-key.js +56 -0
  106. package/dist/strand-transport-key.js.map +1 -0
  107. package/dist/strand-wake-protocol.d.ts +9 -7
  108. package/dist/strand-wake-protocol.js +22 -46
  109. package/dist/strand-wake-protocol.js.map +1 -1
  110. package/dist/strand-watcher.d.ts +17 -2
  111. package/dist/strand-watcher.js +47 -3
  112. package/dist/strand-watcher.js.map +1 -1
  113. package/dist/trusted-owner-store-file.d.ts +12 -0
  114. package/dist/trusted-owner-store-file.js +39 -0
  115. package/dist/trusted-owner-store-file.js.map +1 -0
  116. package/dist/trusted-owner-store.d.ts +64 -0
  117. package/dist/trusted-owner-store.js +126 -0
  118. package/dist/trusted-owner-store.js.map +1 -0
  119. package/dist/types.d.ts +275 -20
  120. package/dist/types.js +13 -0
  121. package/dist/types.js.map +1 -1
  122. package/package.json +19 -8
  123. package/src/bootstrap-peer-store-file.ts +44 -0
  124. package/src/bootstrap-peer-store.ts +172 -0
  125. package/src/cadre-node.ts +1992 -202
  126. package/src/control-authorization.ts +114 -0
  127. package/src/control-database.ts +1704 -146
  128. package/src/control-formation-recorder.ts +172 -19
  129. package/src/control-schema.ts +786 -222
  130. package/src/control-stream.ts +81 -4
  131. package/src/control-write-retry.ts +473 -0
  132. package/src/delegate-admission.ts +302 -0
  133. package/src/device-token.ts +18 -13
  134. package/src/ed25519-key.ts +134 -69
  135. package/src/file-durable-slot.ts +61 -0
  136. package/src/formation-approval.ts +604 -0
  137. package/src/fs-atomic.ts +107 -0
  138. package/src/identity-key.ts +109 -0
  139. package/src/index.ts +136 -5
  140. package/src/key-store-file.ts +10 -81
  141. package/src/membership-connection-gater.ts +204 -0
  142. package/src/node-local-snapshot.ts +260 -0
  143. package/src/peer-authorization.ts +199 -21
  144. package/src/peer-record.ts +28 -10
  145. package/src/relay-addrs.ts +111 -0
  146. package/src/relay-reservation.ts +823 -0
  147. package/src/seed-bootstrap.ts +397 -197
  148. package/src/seed-trust-policy.ts +58 -25
  149. package/src/strand-addr-protocol.ts +84 -53
  150. package/src/strand-backfill.ts +366 -0
  151. package/src/strand-formation-manager.ts +145 -19
  152. package/src/strand-formation-protocol.ts +309 -8
  153. package/src/strand-instance-manager.ts +111 -14
  154. package/src/strand-member-registry.ts +18 -13
  155. package/src/strand-membership-writer.ts +1430 -640
  156. package/src/strand-solicitation.ts +155 -90
  157. package/src/strand-transport-key.ts +58 -0
  158. package/src/strand-wake-protocol.ts +29 -46
  159. package/src/strand-watcher.ts +66 -3
  160. package/src/trusted-owner-store-file.ts +41 -0
  161. package/src/trusted-owner-store.ts +171 -0
  162. package/src/types.ts +292 -21
@@ -18,18 +18,17 @@ export declare const STRAND_ENGINE = "quereus";
18
18
  */
19
19
  export declare const STRAND_ENGINE_VERSION = "0.1.0";
20
20
  /**
21
- * The single-digest ed25519 signer the `Strand.*` membership/RBAC constraints
22
- * verify against.
21
+ * The single-payload ed25519 signer the `Strand.Invite`/`Strand.ConsumedInvite`
22
+ * handshake constraints verify against.
23
23
  *
24
- * `schemas/strand.qsql` signs a single SHA-256 digest over a `'|'`-joined payload
25
- * (e.g. `Member.Authorized` verifies
26
- * `verify(digest(new.Key), context.ManagerSignature, A.MemberKey, 'ed25519')`).
24
+ * Those constraints sign a single SHA-256 digest over a `'|'`-joined payload
25
+ * (e.g. `InviteValid` verifies
26
+ * `verify(digest(new.Key || '|' || coalesce(new.Expiration, '')), ...)`).
27
27
  * So the signer hashes the payload to raw bytes and ed25519-signs *those bytes*:
28
28
  * SQL `digest(...)`'s default output is base64url and `verify(...)`'s default
29
29
  * `inputEncoding` is base64url, so signer and verifier operate on identical bytes.
30
- * This differs from the control-layer's multi-field `buildAuthorizationMessage`
31
- * (a single digest over MANY fields) — the strand layer is a single digest over ONE
32
- * joined payload field.
30
+ * The membership/RBAC tables (Member/Manager/MemberPeer/Revocation) instead use
31
+ * domain/action-tagged VARIADIC digests — see {@link signStrandApproval}.
33
32
  *
34
33
  * All strand keys are ed25519, so the explicit `'ed25519'` curve arg is mandatory
35
34
  * (the crypto plugin otherwise defaults to secp256k1). Mirrors the proven
@@ -43,7 +42,7 @@ export declare function signStrandPayload(payload: string, privateKeyB64: string
43
42
  /**
44
43
  * The verifier counterpart to {@link signStrandPayload}.
45
44
  *
46
- * Mirrors what every `Strand.*` constraint computes:
45
+ * Mirrors what the single-payload `Strand.*` constraints compute:
47
46
  * `verify(digest(payload), signature, publicKey, 'ed25519')`.
48
47
  * SQL `digest`'s default output and `verify`'s default `inputEncoding`/`sigEncoding`/
49
48
  * `keyEncoding` are all base64url, so the off-engine check operates on the exact
@@ -57,6 +56,50 @@ export declare function signStrandPayload(payload: string, privateKeyB64: string
57
56
  * @returns `true` iff the signature is valid for the payload under that key.
58
57
  */
59
58
  export declare function verifyStrandPayload(payload: string, signatureB64: string, publicKeyB64: string): boolean;
59
+ /**
60
+ * Sign a domain/action-tagged `Strand.*` membership/RBAC approval.
61
+ *
62
+ * Every `Authorized` branch on `Member`/`Manager`/`MemberPeer`/`Revocation`
63
+ * verifies a VARIADIC digest — `digest('<domain>', '<action>', <field>, ...,
64
+ * <StampId>)` — whose leading two elements are fixed literals (the domain tag,
65
+ * e.g. `'Strand.Member'`, and an action tag, e.g. `'add'`) and whose trailing
66
+ * element is the row's per-incarnation StampId. The tags keep an approval from
67
+ * verifying against any rule but the one it was minted for (the idiom of the
68
+ * control layer's `buildAuthorizationMessage`); the StampId binding makes it
69
+ * single-use — it authorizes exactly one incarnation of one row, and dies when
70
+ * that row's stamp is retired into `Strand.Revocation`.
71
+ *
72
+ * The caller passes the digest elements exactly as the matching SQL constraint
73
+ * lists them (each SQL argument = one array element), PRESERVING TYPE: the
74
+ * crypto plugin's digest framing is type-tagged (INTEGER 1 and TEXT '1' encode
75
+ * differently, deliberately), so `Manager.Authorized`'s promotion branch —
76
+ * whose digest includes the INTEGER `new.Generation` — must be signed over the
77
+ * NUMBER, not its string form. Integer `number` and `bigint` encode
78
+ * identically in that framing, so JS-side numbers match however the engine
79
+ * represents the SQL integer.
80
+ *
81
+ * Like {@link signStrandPayload}, the raw digest BYTES are ed25519-signed
82
+ * directly, matching SQL `verify(...)`'s default base64url input decoding.
83
+ *
84
+ * @param fields - The digest elements, in constraint order (tags first, StampId last).
85
+ * @param privateKeyB64 - The signer's base64url ed25519 private seed.
86
+ * @returns The base64url ed25519 signature over the tagged digest.
87
+ */
88
+ export declare function signStrandApproval(fields: ReadonlyArray<string | number>, privateKeyB64: string): string;
89
+ /**
90
+ * Mint a fresh per-row authorization nonce (StampId) for a `Strand.Member`,
91
+ * `Strand.Manager`, or `Strand.MemberPeer` row.
92
+ *
93
+ * 256 bits of CSPRNG output, base64url-encoded — enough entropy that a future
94
+ * legitimate stamp cannot be guessed and pre-planted into `Strand.Revocation`
95
+ * (the `RowIsGone` comment in `schemas/strand.qsql` leans on exactly this).
96
+ * Plain randomness, deliberately NOT the control layer's peer-derived
97
+ * `generateStampId`: strand rows are not per-peer, so entropy alone carries
98
+ * the uniqueness.
99
+ *
100
+ * @returns A fresh base64url stamp (43 chars for 32 bytes).
101
+ */
102
+ export declare function generateStrandStampId(): string;
60
103
  /** Parameters for {@link bootstrapFounderMembership}. */
61
104
  export interface FounderBootstrapParams {
62
105
  /** The strand id — written to `Header.Id`. */
@@ -165,8 +208,8 @@ export interface ConsumeInviteParams {
165
208
  * Redeem an invite to admit a new `Member`, atomically.
166
209
  *
167
210
  * `Member.Authorized`'s invite branch needs a `ConsumedInvite` row, while
168
- * `ConsumedInvite`'s `MemberExists`/`MemberValid` need the `Member` row — a
169
- * circular dependency. Both are deferred (subquery-bearing) checks that evaluate
211
+ * `ConsumedInvite`'s `MemberExists` needs the `Member` row — a circular
212
+ * dependency. Both are deferred (subquery-bearing) checks that evaluate
170
213
  * at COMMIT, so inserting `Member` then `ConsumedInvite` inside one explicit
171
214
  * transaction lets both rows exist when the deferred checks fire. This mirrors
172
215
  * `ControlDatabase.redeemInvitation` (Strand + FormationUsage in one txn).
@@ -174,7 +217,9 @@ export interface ConsumeInviteParams {
174
217
  * The `ConsumedInvite` insert carries an `InviteSignature` over
175
218
  * `InviteKey || '|' || MemberKey`, proving the consumer holds the invite private
176
219
  * key (the on-engine `ValidUsage` gate). The `Member` insert needs no member
177
- * signature — its admission is the existence of the matching `ConsumedInvite`.
220
+ * signature — its admission is the existence of the matching `ConsumedInvite` —
221
+ * but still mints a fresh StampId (NOT NULL on every path; a revoked-then-
222
+ * re-invited member is a fresh incarnation under a fresh stamp).
178
223
  *
179
224
  * Single-use: `ConsumedInvite`'s primary key is `InviteKey`, so a second consume
180
225
  * of the same invite is rejected by the PK (a distinct layer from the control
@@ -204,11 +249,101 @@ export interface ConsumeInviteParams {
204
249
  * `ConsumedInvite` row survives).
205
250
  */
206
251
  export declare function consumeInvite(db: Database, params: ConsumeInviteParams): Promise<void>;
252
+ /** Parameters for {@link cancelInvite}. */
253
+ export interface CancelInviteParams {
254
+ /**
255
+ * The cancelling manager's strand keypair. Its `publicKeyB64` must be a
256
+ * PRE-EXISTING `Strand.Manager` row — `CancelledInvite.Authorized` reads
257
+ * `committed.Manager`, so a manager seated in the SAME transaction cannot cancel.
258
+ */
259
+ managerKeyPair: Ed25519KeyPair;
260
+ /** The `Invite.Key` to kill. */
261
+ inviteKey: string;
262
+ }
263
+ /**
264
+ * Cancel an outstanding invitation, permanently: it may never be consumed.
265
+ *
266
+ * Files a `Strand.CancelledInvite` tombstone carrying a manager signature over
267
+ * `digest('Strand.CancelledInvite', 'cancel', InviteKey)`. The digest binds the exact
268
+ * invite key, so an approval minted for one invitation cannot cancel another.
269
+ *
270
+ * One row, one statement — no transaction needed. `CancelledInvite.Authorized` is a
271
+ * deferred (subquery-bearing) check, so it fires at this statement's own auto-commit.
272
+ *
273
+ * Insert-if-absent, matching {@link registerMemberPeer}'s and {@link revokeMember}'s
274
+ * restart-safe shape: a repeat cancellation logs and returns rather than throwing on the
275
+ * primary key. (Check-then-write is not atomic; the primary key is the real backstop —
276
+ * the guard is for the sequential repeat/restart path.)
277
+ *
278
+ * Cancellation is what makes removal a re-entry gate: it is the `ConsumedInvite` insert
279
+ * that `NotCancelled` blocks, and `Member.Authorized`'s invite branch needs a
280
+ * same-transaction FRESH `ConsumedInvite` row, so a cancelled invitation rolls the whole
281
+ * join back. Un-cancelling is impossible (`CancelledInvite.Immutable`) — to re-invite a
282
+ * party, {@link issueInvite} a fresh invitation.
283
+ *
284
+ * Enumerate what there is to cancel with {@link listOutstandingInvites}: an invitation
285
+ * names no invitee, so the strand cannot tell a manager WHICH invitations a departing
286
+ * member holds. Invitee binding is tracked as `feat-strand-invitee-bound-invites`.
287
+ *
288
+ * @param db - The closed strand's database.
289
+ * @param params - The cancelling manager's keypair and the invite key to kill.
290
+ * @throws If `CancelledInvite.Authorized` rejects (a non-manager, a manager seated in
291
+ * this transaction, or an approval minted for a different invite key) or `OnlyClosed`
292
+ * rejects (an open strand); the insert rolls back, leaving no tombstone.
293
+ */
294
+ export declare function cancelInvite(db: Database, params: CancelInviteParams): Promise<void>;
295
+ /** One redeemable invitation, as reported by {@link listOutstandingInvites}. */
296
+ export interface OutstandingInvite {
297
+ /** The `Invite.Key` (invite public key, base64url). */
298
+ inviteKey: string;
299
+ /**
300
+ * The expiry as the engine's canonical `datetime` string
301
+ * (`YYYY-MM-DDTHH:MM:SS[.frac]`, no zone suffix — see {@link canonicalDatetime}), or
302
+ * `null` for never-expires.
303
+ *
304
+ * Deliberately the STORED form, not the epoch-ms {@link IssueInviteParams.expiration}
305
+ * takes: it is what compares like-for-like against another canonicalised instant, and
306
+ * what the on-engine `NotExpired` gate sees. It is NOT a JS-parseable ISO instant —
307
+ * pass it through the engine (or `canonicalDatetime`) rather than `Date.parse`.
308
+ */
309
+ expiration: string | null;
310
+ }
311
+ /**
312
+ * The invitations that are still redeemable: neither consumed, nor cancelled, nor
313
+ * expired at `nowMs`.
314
+ *
315
+ * `Strand.Invite` is insert-only and carries no state column, so "outstanding" is not a
316
+ * property of the row — it is the `Invite` set minus the `ConsumedInvite` keys, minus
317
+ * the `CancelledInvite` keys, minus anything whose `Expiration` has passed. All three
318
+ * exclusions mirror gates a consume would hit anyway (`ConsumedInvite`'s primary key,
319
+ * `NotCancelled`, `NotExpired`).
320
+ *
321
+ * Reads via unfiltered scans and compares in JavaScript — see {@link scanInviteKeys} for
322
+ * why a full-PK where-equality is not reliable on a networked strand.
323
+ *
324
+ * Expiry is compared as `expiration > canonicalDatetime(nowMs)`: the same transform
325
+ * {@link issueInvite} uses to STORE `Invite.Expiration`, so the string comparison orders
326
+ * chronologically, and the same strict `>` the on-engine `NotExpired` gate uses (expiry
327
+ * is exclusive — an invitation is dead at its expiry instant, not after it).
328
+ *
329
+ * This is the manager's enumeration side of {@link cancelInvite}, and the "door is open"
330
+ * pre-flight behind `StrandMemberVerifier.isAuthorizedToJoin`.
331
+ *
332
+ * @param db - The closed strand's database.
333
+ * @param nowMs - The instant (epoch ms) to compare expiries against; defaults to
334
+ * `Date.now()`. Tests pin it so the comparison is deterministic, mirroring
335
+ * {@link consumeInvite}'s `nowMs` convention.
336
+ * @returns The redeemable invitations, in NO guaranteed order (the scans promise only a
337
+ * superset of the live rows, not a stable sequence — sort by `expiration` if order
338
+ * matters); empty if none.
339
+ */
340
+ export declare function listOutstandingInvites(db: Database, nowMs?: number): Promise<OutstandingInvite[]>;
207
341
  /** Parameters for {@link addMemberByManager}. */
208
342
  export interface AddMemberByManagerParams {
209
343
  /**
210
344
  * The admitting manager's strand keypair. Its `publicKeyB64` must be a
211
- * `Strand.Manager` row; it signs the new member key directly.
345
+ * `Strand.Manager` row; it signs the add-tagged digest over the new member key
346
+ * and its fresh stamp.
212
347
  */
213
348
  managerKeyPair: Ed25519KeyPair;
214
349
  /** The joining member's ed25519 PUBLIC key (base64url) — the new `Member.Key`. */
@@ -218,11 +353,14 @@ export interface AddMemberByManagerParams {
218
353
  * Admit a `Member` directly by manager signature — the sibling of the invite
219
354
  * path on `Member.Authorized`'s direct-manager branch.
220
355
  *
221
- * The constraint verifies `digest(new.Key)` against a `Manager` row matching
222
- * `context.ManagerKey`, so the payload is just the member key (no `'|'` join).
223
- * No `ConsumedInvite` is involved: this is the path a manager uses to seat a
224
- * member it already trusts (e.g. a manager-side enrollment that admits a
225
- * party already authorised out-of-band).
356
+ * The constraint verifies the add-tagged digest
357
+ * `digest('Strand.Member', 'add', new.Key, new.StampId)` against a pre-existing
358
+ * (`committed.Manager`) row matching `context.ManagerKey`. The stamp is minted
359
+ * fresh here and bound inside the signed digest, so the approval seats exactly
360
+ * this incarnation: once the member is removed (retiring the stamp), the captured
361
+ * approval can never re-seat it. No `ConsumedInvite` is involved: this is the
362
+ * path a manager uses to seat a member it already trusts (e.g. a manager-side
363
+ * enrollment that admits a party already authorised out-of-band).
226
364
  *
227
365
  * @param db - The closed strand's database.
228
366
  * @param params - The admitting manager keypair and the new member key.
@@ -230,6 +368,96 @@ export interface AddMemberByManagerParams {
230
368
  * rolls back, leaving no `Member` row.
231
369
  */
232
370
  export declare function addMemberByManager(db: Database, params: AddMemberByManagerParams): Promise<void>;
371
+ /** Parameters for {@link revokeMember}. */
372
+ export interface RevokeMemberParams {
373
+ /**
374
+ * The revoking manager's strand keypair. Its `publicKeyB64` must be a
375
+ * PRE-EXISTING `Strand.Manager` row (the manager-removal branch of
376
+ * `Member.Authorized` reads `committed.Manager` — a manager seated in the same
377
+ * transaction cannot authorize); it signs the remove-tagged digest over the
378
+ * target row's key and live stamp, and files the accompanying tombstone.
379
+ */
380
+ managerKeyPair: Ed25519KeyPair;
381
+ /** The `Member.Key` row to delete. */
382
+ memberKey: string;
383
+ }
384
+ /**
385
+ * Revoke a `Member` on the signature of an existing manager.
386
+ *
387
+ * Reads the target row's live StampId first (a quiet no-op if the row is already
388
+ * absent — a repeated or restarted revocation should not throw), then runs ONE
389
+ * transaction pairing the delete with its `Strand.Revocation` tombstone: the
390
+ * schema's `Member.RevocationRecorded` requires the tombstone in the same
391
+ * transaction, and the tombstone is what permanently retires the stamp so the
392
+ * captured admission approval can never re-seat this incarnation.
393
+ *
394
+ * The manager-removal branch of `Member.Authorized` verifies the remove-tagged
395
+ * digest `digest('Strand.Member', 'remove', old.Key, old.StampId)` against a
396
+ * `committed.Manager` row matching `context.ManagerKey` — distinct from the
397
+ * add-tagged admission payload, and bound to the live stamp, so neither a
398
+ * captured admission nor a captured PRIOR removal (whose stamp differs) replays.
399
+ *
400
+ * A revoked member cannot re-admit itself off the invite it ALREADY SPENT: that
401
+ * `ConsumedInvite` row is stale, and the invite branch requires a
402
+ * same-transaction FRESH consumption. Re-admission normally takes a fresh
403
+ * manager action ({@link addMemberByManager} or a new invite).
404
+ *
405
+ * An UNSPENT invite the revoked party holds is killed EXPLICITLY, by the manager
406
+ * calling {@link cancelInvite} on it (enumerate the candidates with
407
+ * {@link listOutstandingInvites}); a cancelled invitation can never be consumed,
408
+ * and cancellation is permanent. This removal does NOT cascade into cancellation:
409
+ * an invitation names no invitee, so there is nothing to match the removed member
410
+ * against — the strand cannot tell which invitations were meant for it, or whether
411
+ * it holds any. Binding an invitation to its invitee is tracked as
412
+ * `feat-strand-invitee-bound-invites`.
413
+ *
414
+ * A manager must resign its `Manager` row before (or in the same transaction
415
+ * as) losing membership — `Member.NotAManager` rejects un-membering a key that
416
+ * still holds a Manager row. And the strand never drops to zero members
417
+ * (`Member.MinOneMember`, a local-count floor with the same cross-node caveat
418
+ * as `MinOneManager`).
419
+ *
420
+ * @param db - The closed strand's database.
421
+ * @param params - The revoking manager keypair and the target member key.
422
+ * @throws If `Member.Authorized` rejects (a non-manager or same-transaction
423
+ * signer), `Member.NotAManager` rejects (the target still holds a Manager
424
+ * row), or `Member.MinOneMember` rejects (the removal would empty the member
425
+ * set); the whole delete+tombstone transaction rolls back.
426
+ */
427
+ export declare function revokeMember(db: Database, params: RevokeMemberParams): Promise<void>;
428
+ /** Parameters for {@link leaveStrand}. */
429
+ export interface LeaveStrandParams {
430
+ /**
431
+ * The departing member's OWN strand keypair. Its `publicKeyB64` is the
432
+ * `Member.Key` row being deleted; it self-signs the leave-tagged digest —
433
+ * the self-departure branch verifies the signature against `old.Key` itself,
434
+ * so only the departing key's holder can produce it — and files the
435
+ * accompanying tombstone (it is still a committed member until this very
436
+ * transaction removes it, which is what `Revocation.Authorized` checks). No
437
+ * manager is involved.
438
+ */
439
+ memberKeyPair: Ed25519KeyPair;
440
+ }
441
+ /**
442
+ * A member leaves the strand by deleting its own `Member` row.
443
+ *
444
+ * The self-departure branch of `Member.Authorized` verifies
445
+ * `digest('Strand.Member', 'leave', old.Key, old.StampId)` against `old.Key`
446
+ * itself via `context.MemberSignature`. The `'leave'` tag (vs the manager
447
+ * branch's `'remove'`) is belt-and-braces only — the two branches verify against
448
+ * different keys; the anti-replay fix is the stamp binding. Like
449
+ * {@link revokeMember}, the live stamp is read first (absent row → quiet no-op)
450
+ * and the delete pairs with its `Strand.Revocation` tombstone in one transaction.
451
+ *
452
+ * The same floors as {@link revokeMember} apply: a manager must resign first
453
+ * (`NotAManager`) and the last member cannot leave (`MinOneMember`).
454
+ *
455
+ * @param db - The closed strand's database.
456
+ * @param params - The departing member's own keypair.
457
+ * @throws If `Member.Authorized`, `Member.NotAManager`, or `Member.MinOneMember`
458
+ * rejects; the whole delete+tombstone transaction rolls back.
459
+ */
460
+ export declare function leaveStrand(db: Database, params: LeaveStrandParams): Promise<void>;
233
461
  /** Parameters for {@link registerMemberPeer}. */
234
462
  export interface RegisterMemberPeerParams {
235
463
  /**
@@ -248,23 +476,30 @@ export interface RegisterMemberPeerParams {
248
476
  /**
249
477
  * Register a network node (`PeerId`) as acting on behalf of a member.
250
478
  *
251
- * The member self-signs `MemberKey || '|' || PeerId` with its OWN key; the schema's
252
- * `MemberPeer.Authorized` verifies that signature against `coalesce(new.MemberKey,
253
- * old.MemberKey)` — i.e. the member key itself — so only the member that owns the
254
- * key can register peers for it. A deferred `MemberExists` additionally requires the
255
- * `Member` row to already exist, so a peer for a non-member is rejected at commit.
256
- *
257
- * Insert-if-absent on the composite PK `(MemberKey, PeerId)`: a re-register on
258
- * restart (or a redundant call) is a no-op rather than relying on the platform's
259
- * PK-uniqueness rejection (not enforced in bootstrap mode — see
260
- * `optimystic-insert-pk-uniqueness-not-enforced`). A member may register multiple
261
- * DISTINCT `PeerId`s (multi-device); each is its own row under the same `MemberKey`.
262
- *
263
- * Out of scope: peer DELETION. The schema's `MemberExists` reads `new.MemberKey`,
264
- * which is null on delete, so a `MemberPeer` delete is currently rejected by the
265
- * schema (noted in the `apply-strand-membership-schema` review). Removing a peer
266
- * would need a schema tweak (e.g. `coalesce(new.MemberKey, old.MemberKey)` in
267
- * `MemberExists`), so no `removeMemberPeer` is provided here.
479
+ * The member self-signs the add-tagged digest
480
+ * `digest('Strand.MemberPeer', 'add', new.MemberKey, new.PeerId, new.StampId)`
481
+ * with its OWN key; the schema's `MemberPeer.Authorized` verifies that signature
482
+ * against `new.MemberKey` — i.e. the member key itself — so only the member that
483
+ * owns the key can register peers for it. The stamp is minted fresh here and
484
+ * bound inside the digest, so a captured registration approval neither
485
+ * re-registers a cleared binding (its stamp is retired) nor authorizes a removal
486
+ * (wrong tag). A deferred `MemberExists` additionally requires the `Member` row
487
+ * to already exist, so a peer for a non-member is rejected at commit.
488
+ *
489
+ * Insert-if-absent: a re-register on restart (or a redundant call) is a no-op. The
490
+ * platform DOES reject a duplicate-PK insert, but a restart-safe re-register should
491
+ * succeed quietly rather than throw, so the writer guards on existence instead of
492
+ * catching. The guard ({@link memberPeerStampId}) scans the member's peers and compares
493
+ * both key columns in JavaScript rather than seeking the composite primary key — see
494
+ * {@link scanMemberPeers} for why a full-PK point lookup is not reliable on a networked
495
+ * strand. A member may register multiple DISTINCT `PeerId`s (multi-device); each is its
496
+ * own row (and its own stamp) under the same `MemberKey`.
497
+ *
498
+ * Peer DELETION is signature-checked too, not rejected outright: `Authorized` carries
499
+ * distinct delete branches (self + manager), each over its own action-tagged digest
500
+ * bound to the row's stamp. Both removal paths live in {@link removeMemberPeer}.
501
+ * UPDATE is refused outright by `NoUpdate`: a re-binding is a remove plus a fresh
502
+ * register (under a fresh stamp).
268
503
  *
269
504
  * @param db - The closed strand's database (the member already exists).
270
505
  * @param params - The member's own keypair and the peer id to bind.
@@ -272,48 +507,186 @@ export interface RegisterMemberPeerParams {
272
507
  * matching `Member` row); the insert rolls back, leaving no `MemberPeer` row.
273
508
  */
274
509
  export declare function registerMemberPeer(db: Database, params: RegisterMemberPeerParams): Promise<void>;
510
+ /**
511
+ * Every `PeerId` currently bound to `memberKey`, in storage order.
512
+ *
513
+ * The enumeration side of {@link removeMemberPeer}: a manager cleaning up after a
514
+ * revocation knows only the departed member's key, never which devices it registered, so
515
+ * cleanup is `listMemberPeers` then one `removeMemberPeer` per id. Reads only — the rows
516
+ * it returns may name a `MemberKey` with no `Member` row (see the `MemberPeer` table NOTE
517
+ * in the schema), so this is NOT a membership test.
518
+ *
519
+ * @param db - The closed strand's database.
520
+ * @param memberKey - The `MemberPeer.MemberKey` to enumerate.
521
+ * @returns The bound peer ids, empty if the member has none.
522
+ */
523
+ export declare function listMemberPeers(db: Database, memberKey: string): Promise<string[]>;
524
+ /** Parameters for the SELF branch of {@link removeMemberPeer}. */
525
+ export interface RemoveOwnPeerParams {
526
+ /**
527
+ * The member's OWN strand keypair. Its `publicKeyB64` is the `MemberPeer.MemberKey`
528
+ * of the row being deleted, and it signs the remove-tagged digest
529
+ * `digest('Strand.MemberPeer', 'remove', MemberKey, PeerId, StampId)` — the self
530
+ * branch of `MemberPeer.Authorized` verifies against the row's own member key, so
531
+ * no OTHER member's row is reachable this way, and the tag + stamp binding keep a
532
+ * captured registration approval from doubling as a removal (and vice versa).
533
+ */
534
+ memberKeyPair: Ed25519KeyPair;
535
+ /** The `PeerId` of the binding to delete. */
536
+ peerId: string;
537
+ }
538
+ /** Parameters for the MANAGER branch of {@link removeMemberPeer}. */
539
+ export interface RemoveMemberPeerByManagerParams {
540
+ /**
541
+ * The clearing manager's strand keypair. Its `publicKeyB64` must be a PRE-EXISTING
542
+ * `Strand.Manager` row (the manager branch reads `committed.Manager`, so a manager
543
+ * seated in the same transaction cannot authorize); it signs the
544
+ * manager-remove-tagged digest over the exact row and its live stamp, and files
545
+ * the accompanying tombstone.
546
+ */
547
+ managerKeyPair: Ed25519KeyPair;
548
+ /** The `MemberPeer.MemberKey` whose binding is being cleared. */
549
+ memberKey: string;
550
+ /** The `PeerId` of the binding to delete. */
551
+ peerId: string;
552
+ }
553
+ /**
554
+ * Either shape accepted by {@link removeMemberPeer}. A discriminated union rather than
555
+ * one opaque keypair: the two branches sign DIFFERENT action tags (`'remove'` self vs
556
+ * `'manager-remove'`), so each case carries exactly the fields its payload needs.
557
+ */
558
+ export type RemoveMemberPeerParams = RemoveOwnPeerParams | RemoveMemberPeerByManagerParams;
559
+ /**
560
+ * Delete a `MemberPeer` binding — either the member clearing its own, or a manager
561
+ * clearing another member's.
562
+ *
563
+ * Reads the row's live StampId first (absent → quiet no-op, mirroring
564
+ * {@link registerMemberPeer}'s insert-if-absent, so a repeated or restarted cleanup
565
+ * is not a silent zero-row "success" the caller cannot distinguish from a real one),
566
+ * then runs ONE transaction pairing the delete with its `Strand.Revocation`
567
+ * tombstone — `MemberPeer.RevocationRecorded` requires it, and the retirement is
568
+ * what keeps the captured registration approval from re-binding the cleared row.
569
+ *
570
+ * `MemberPeer` rows do NOT cascade when the member is revoked (`MemberExists` runs on
571
+ * insert only, and nothing deletes them), so a removed member's peer bindings survive
572
+ * as orphans — enumerate them with {@link listMemberPeers}, which is how a manager
573
+ * discovers what to clear. Only that member can sign the self branch, and a member being
574
+ * removed against its will has no reason to cooperate — hence the manager branch, which
575
+ * is the cleanup path after a revocation.
576
+ *
577
+ * The manager branch is deliberately NOT gated on the member already being gone: a
578
+ * manager may clear a still-present member's binding too, matching "any manager can
579
+ * remove any member". It verifies the manager-remove-tagged
580
+ * `digest('Strand.MemberPeer', 'manager-remove', old.MemberKey, old.PeerId, old.StampId)`
581
+ * against a `committed.Manager` row; the self branch verifies the remove-tagged
582
+ * sibling against the row's own member key.
583
+ *
584
+ * Cleanup is an explicit follow-up call by the revoking manager, never a cascade inside
585
+ * {@link revokeMember}: a revocation that also had to clear an unbounded number of peer
586
+ * rows in the same transaction would couple two concerns and make its failure modes
587
+ * harder to reason about.
588
+ *
589
+ * @param db - The closed strand's database.
590
+ * @param params - Either the owning member's keypair, or a manager's keypair plus the
591
+ * target member key.
592
+ * @throws If `MemberPeer.Authorized` rejects (neither branch satisfied), or if the row
593
+ * is still present after the delete (see the point-lookup note below).
594
+ */
595
+ export declare function removeMemberPeer(db: Database, params: RemoveMemberPeerParams): Promise<void>;
275
596
  /** Parameters for {@link addManager}. */
276
597
  export interface AddManagerParams {
277
598
  /**
278
599
  * An EXISTING manager's strand keypair. Its `publicKeyB64` must already be a
279
600
  * `Strand.Manager` row (the existing-manager branch of `Manager.Authorized`
280
- * rejects a non-manager once the founder manager exists); it signs the new
281
- * manager key and is bound as `context.ManagerKey`.
601
+ * rejects a non-manager once the founder manager exists); it signs the
602
+ * add-tagged digest over the new manager key, generation, and fresh stamp,
603
+ * and is bound as `context.ManagerKey`.
282
604
  */
283
605
  byManagerKeyPair: Ed25519KeyPair;
284
606
  /** The member key to promote — the new `Manager.MemberKey` row. */
285
607
  newManagerKey: string;
286
608
  }
287
609
  /**
288
- * Promote a member to `Manager` on the signature of an existing manager.
289
- *
290
- * The existing-manager branch of `Manager.Authorized` verifies
291
- * `digest(coalesce(new.MemberKey, old.MemberKey))` — `new.MemberKey` =
292
- * `newManagerKey` on insert — against a `Manager` row matching
293
- * `context.ManagerKey`. So the signer signs the new manager key directly (no
294
- * `'|'` join) and binds itself as `context.ManagerKey`.
295
- *
296
- * Once the founder manager exists, the schema's `(select count(1) from Manager)
297
- * <= 1` bootstrap shortcut no longer applies to a second add (at commit the count
298
- * includes the new row, so it is ≥ 2), so this genuinely exercises signature
299
- * verification — a non-manager signer, or a signature over the wrong key, is
300
- * rejected.
610
+ * Promote a key that is ALREADY a `Strand.Member` to `Manager` on the signature of
611
+ * an existing manager — use {@link admitManager} to admit and promote atomically.
612
+ *
613
+ * The promotion branch of `Manager.Authorized` requires the authorizer to be a
614
+ * `Manager` row of STRICTLY SMALLER `Generation` than the new row, verifying
615
+ * `digest('Strand.Manager', 'add', new.MemberKey, new.Generation, new.StampId)`
616
+ * against `context.ManagerKey`. So this writer reads the authorizer's generation,
617
+ * seats the new manager at that value + 1 (the natural successor; the schema
618
+ * enforces only the strict ordering), mints a fresh stamp, and signs the
619
+ * add-tagged digest over all three: the generation inside the signed digest keeps
620
+ * a captured promotion from replaying at a different generation, and the stamp
621
+ * keeps it from re-seating a second incarnation once this one is removed.
622
+ * `new.Generation` is an SQL INTEGER digest arg — the digest framing is
623
+ * type-tagged, so the TS side signs over the NUMBER itself
624
+ * (see {@link signStrandApproval}).
625
+ *
626
+ * The strict ordering — not this writer — is what makes a same-transaction takeover
627
+ * impossible; the `Manager.Generation` column comment in `schemas/strand.qsql` carries
628
+ * that argument. All this writer owes it is a generation strictly above the
629
+ * authorizer's, inside the signed payload.
630
+ *
631
+ * When the authorizer has NO `Manager` row (a non-manager signer, or an open
632
+ * strand with no managers at all), the scan finds nothing and the writer falls
633
+ * back to generation 1 and issues the insert anyway — deliberately letting the
634
+ * SCHEMA be the rejector (`Manager.Authorized` / `OnlyClosed`), not a
635
+ * writer-thrown error, so enforcement is pinned where it actually lives.
301
636
  *
302
637
  * @param db - The closed strand's database (founder manager already seated).
303
638
  * @param params - The authorizing manager's keypair and the new manager key.
304
- * @throws If `Manager.Authorized` rejects; the insert rolls back.
639
+ * @throws If `Manager.Authorized` rejects, or if `Manager.MemberExists` rejects because
640
+ * `newManagerKey` holds no `Member` row (admit it first, or use
641
+ * {@link admitManager}); the insert rolls back.
305
642
  */
306
643
  export declare function addManager(db: Database, params: AddManagerParams): Promise<void>;
644
+ /** Parameters for {@link admitManager} — the same shape as {@link AddManagerParams}. */
645
+ export type AdmitManagerParams = AddManagerParams;
646
+ /**
647
+ * Admit a key as a `Member` AND promote it to `Manager`, atomically — the one-step
648
+ * pair for a key that is not in the strand yet. {@link addManager} alone promotes a
649
+ * key that is already a member.
650
+ *
651
+ * ONE transaction, because `Manager.MemberExists` is a deferred check reading the LIVE
652
+ * `Member` table: the sibling `Member` insert satisfies it at the shared commit, and a
653
+ * rejection of either half rolls BOTH rows back — never a `Member` row seated by a
654
+ * failed promotion, nor a `Manager` row with no member behind it.
655
+ *
656
+ * NO new authority: both halves are signed by the SAME manager, over two distinct
657
+ * action-tagged digests — `digest('Strand.Member', 'add', …)` for the admission and
658
+ * `digest('Strand.Manager', 'add', …)` for the promotion — each bound to its own row's
659
+ * fresh stamp. This composes exactly the two approvals that manager could already
660
+ * issue separately.
661
+ *
662
+ * The promoting manager must be a PRE-transaction manager: the direct-admit branch of
663
+ * `Member.Authorized` reads `committed.Manager`, so `admitManager` cannot be chained —
664
+ * a manager seated by an `admitManager` earlier in the SAME transaction cannot admit
665
+ * the next one. (A same-transaction promotion CHAIN rooted at a pre-existing manager
666
+ * is fine; only the `Member` half needs the committed authorizer.)
667
+ *
668
+ * NOT insert-if-absent: a repeat call for a key that is already a member fails on the
669
+ * `Member` primary key, matching {@link addMemberByManager}'s unguarded shape.
670
+ *
671
+ * @param db - The closed strand's database (founder manager already seated).
672
+ * @param params - The authorizing manager's keypair and the key to admit + promote.
673
+ * @throws If either half is rejected (`Member.Authorized`, `Manager.Authorized`, a
674
+ * duplicate `Member` key, …); the whole transaction rolls back, leaving neither row.
675
+ */
676
+ export declare function admitManager(db: Database, params: AdmitManagerParams): Promise<void>;
307
677
  /** Parameters for {@link removeManager}. */
308
678
  export interface RemoveManagerParams {
309
679
  /**
310
- * The keypair authorizing the removal, bound as `context.ManagerKey` and
311
- * signing `digest(targetManagerKey)`. For an ADMIN removal this is a DIFFERENT
312
- * existing manager (satisfying the existing-manager branch); for a
680
+ * The keypair authorizing the removal, bound as `context.ManagerKey`. For an
681
+ * ADMIN removal this is a DIFFERENT existing manager, signing the
682
+ * remove-tagged digest (satisfying the existing-manager branch); for a
313
683
  * SELF-resignation it is the target's OWN keypair (`publicKeyB64 ===
314
- * targetManagerKey`, satisfying the former-manager self branch). The same
315
- * context construction satisfies whichever branch applies — see
316
- * {@link removeManager}.
684
+ * targetManagerKey`), signing the resign-tagged digest (satisfying the
685
+ * former-manager self branch). The writer picks the tag by comparing the keys —
686
+ * see {@link removeManager}. Either way this keypair also files the
687
+ * accompanying tombstone, which requires it to be a PRE-transaction MEMBER
688
+ * (`Revocation.Authorized` reads `committed.Member`) — true for every manager,
689
+ * since managers are members.
317
690
  */
318
691
  byManagerKeyPair: Ed25519KeyPair;
319
692
  /** The `Manager.MemberKey` row to delete. */
@@ -323,39 +696,60 @@ export interface RemoveManagerParams {
323
696
  * Remove a `Manager` row — either an admin removing a different manager or a
324
697
  * manager resigning itself.
325
698
  *
326
- * On a DELETE, `coalesce(new.MemberKey, old.MemberKey)` binds `old.MemberKey =
327
- * targetManagerKey` (there is no `new` row), so the signed payload is the target
328
- * key for BOTH accepting branches of `Manager.Authorized`, and a single context
329
- * construction serves both:
330
- * - **Admin removal**: `byManagerKeyPair` is a different existing manager; the
331
- * existing-manager branch verifies the signature against `A.MemberKey =
332
- * context.ManagerKey`.
333
- * - **Self-resignation**: `byManagerKeyPair` IS the target (its `publicKeyB64`
334
- * equals `targetManagerKey`); the former-manager self branch verifies
335
- * `old.MemberKey = context.ManagerKey` and the self-signature over `old.MemberKey`.
336
- *
337
- * The caller selects the case purely by which keypair it passes — no branching here.
338
- *
339
- * KNOWN PLATFORM GAP (not fixable here): the optimystic bootstrap-mode transactor
340
- * evaluates deferred (subquery-bearing) CHECK constraints only on INSERT, not on
341
- * DELETE. `Manager.Authorized` is deferred, so the platform currently accepts ANY
342
- * `Manager` delete regardless of signature — `removeManager`'s authorization is
343
- * effectively unenforced at runtime until that gap is closed (filed as
344
- * `optimystic-deferred-check-not-enforced-on-delete`). This writer still builds the
345
- * correct, signed delete so it works unchanged once enforcement lands — exactly as
346
- * the invite path issues correct inserts despite the open PK-uniqueness gap.
347
- *
348
- * KNOWN SCHEMA HAZARD (not guarded here): even with enforcement, `Manager.
349
- * Authorized`'s `(select count(1) from Manager) <= 1` bootstrap branch is true at
350
- * commit whenever a delete drops the count to ≤ 1, so removing the strand's LAST (or
351
- * second-to-last) manager would be accepted regardless of signature — and removing
352
- * the last one orphans the strand (no one can ever add another). The schema does not
353
- * prevent this; a "min-one-manager" invariant is deferred to a future schema change
354
- * rather than grown into this writer.
699
+ * Reads the target row's live `(Generation, StampId)` first (absent → quiet
700
+ * no-op, same restart-safe shape as {@link revokeMember}), then runs ONE
701
+ * transaction pairing the delete with its `Strand.Revocation` tombstone
702
+ * (`Manager.RevocationRecorded`). The two delete-side branches of
703
+ * `Manager.Authorized` verify DIFFERENT action-tagged digests, each bound to
704
+ * `(old.MemberKey, old.StampId)`:
705
+ * - **Admin removal** (`byManagerKeyPair` is a different existing manager): the
706
+ * removal branch verifies `digest('Strand.Manager', 'remove', old.MemberKey,
707
+ * old.StampId)` against `A.MemberKey = context.ManagerKey`. Deliberately NO
708
+ * generation condition: a later-generation manager may remove an
709
+ * earlier-generation one (generation is lineage, not privilege), and deletes
710
+ * are safe once inserts are — every accepting branch requires a `Manager` row
711
+ * in the post-image, and the promotion branch's generation ordering keeps
712
+ * attacker rows out of it. The stamp binding is what makes a captured removal
713
+ * single-use: a re-promoted manager carries a fresh stamp the old approval
714
+ * does not match — closing the removal half of
715
+ * `bug-strand-manager-authority-antireplay` (the insert half was already
716
+ * closed by the generation-bearing promotion payload).
717
+ * - **Self-resignation** (`byManagerKeyPair` IS the target): the former-manager
718
+ * self branch verifies `digest('Strand.Manager', 'resign', old.MemberKey,
719
+ * old.StampId)` against `old.MemberKey` itself. The distinct `'resign'` tag is
720
+ * belt-and-braces (different verifying keys); the anti-replay fix is the stamp.
721
+ *
722
+ * The caller selects the case purely by which keypair it passes; the only
723
+ * branching here is the action tag, chosen by comparing the signer to the target.
724
+ *
725
+ * The optimystic bootstrap-mode transactor evaluates deferred (subquery-bearing)
726
+ * CHECK constraints on DELETE as well as INSERT, so `Manager.Authorized` — deferred —
727
+ * IS enforced here: a signer that is neither an existing manager nor the target
728
+ * itself is rejected at commit.
729
+ *
730
+ * THE LAST MANAGER CANNOT BE REMOVED. `Manager.MinOneManager` (`check on delete`,
731
+ * deferred, so it sees the POST-delete count) rejects any delete that would leave the
732
+ * strand with zero managers — an admin-less closed strand could never admit anyone
733
+ * again, since `Invite`, `addMemberByManager`, and `addManager` all require a
734
+ * `Manager` row. The bootstrap branch of `Manager.Authorized` is gated to INSERTs
735
+ * (`old.MemberKey is null`), so it does not waive the signature check on a delete
736
+ * that drops the count toward the floor: a second-to-last removal is authorized
737
+ * exactly like any other.
738
+ *
739
+ * HAND-OFF ORDER: a sole manager transferring control must ADD the successor FIRST
740
+ * and resign SECOND. A same-transaction delete-then-insert swap is rejected — the
741
+ * bootstrap branch is gated to the founding state (at most one `Member`), and the
742
+ * successor's insert has no other manager to authorize it once the sole manager's
743
+ * row is gone.
744
+ *
745
+ * Cross-node caveat: `MinOneManager` counts rows visible to THIS transaction, so two
746
+ * nodes concurrently removing different managers can each see a survivor and still
747
+ * converge to zero. Noted in the schema; a cross-node floor is not attempted here.
355
748
  *
356
749
  * @param db - The closed strand's database.
357
750
  * @param params - The authorizing keypair and the target manager key.
358
- * @throws If `Manager.Authorized` rejects (e.g. a non-manager admin removal
359
- * while > 2 managers remain); the delete rolls back.
751
+ * @throws If `Manager.Authorized` rejects (a signer that is neither another existing
752
+ * manager nor the target itself) or `Manager.MinOneManager` rejects (the removal
753
+ * would leave no managers); the whole delete+tombstone transaction rolls back.
360
754
  */
361
755
  export declare function removeManager(db: Database, params: RemoveManagerParams): Promise<void>;