@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
@@ -1,7 +1,8 @@
1
1
  import type { Libp2p } from '@libp2p/interface';
2
- import type { ControlNetworkSeed, SeedAckMessage, AuthorizePeerOptions, ApplySeedResult, AddDroneOptions, AddPhoneOptions, DroneInitResult, InviteResult, CadreInvite, PeerAddressRecord, DeviceTokenRecord } from './types.js';
2
+ import type { ControlNetworkSeed, SeedAckMessage, AuthorizePeerOptions, ApplySeedResult, AddDroneOptions, AddPhoneOptions, DroneInitResult, InviteResult, CadreInvite, PeerAddressRecord, DeviceTokenRecord, RevocationRow } from './types.js';
3
3
  import type { ControlDatabase } from './control-database.js';
4
4
  import { type SeedTrustPolicy } from './seed-trust-policy.js';
5
+ import type { TrustedOwnerStore } from './trusted-owner-store.js';
5
6
  /** Protocol ID for seed delivery */
6
7
  export declare const SEED_PROTOCOL = "/sereus/seed/1.0.0";
7
8
  /**
@@ -53,9 +54,9 @@ export interface SeedBootstrapConfig {
53
54
  inviteAddressResolver?: () => Promise<string[]>;
54
55
  /**
55
56
  * Trust anchor for incoming seeds. Decides whether a signature-verified
56
- * `signerKey` should be trusted, against the receiver's known owner
57
- * keys (NOT the seed body). Defaults to `dbAnchoredTrustPolicy()`, which
58
- * rejects any signer not already in the `OwnerKey` table. An enrollment
57
+ * `signerKey` should be trusted, against the receiver's anchored owner
58
+ * keys (NOT the seed body). Defaults to `anchoredTrustPolicy()`, which
59
+ * rejects any signer not already in {@link trustedOwners}. An enrollment
59
60
  * caller can pass a per-seed override to `applySeed` instead.
60
61
  *
61
62
  * A `CadreNode` forwards its node-wide `CadreNodeConfig.seedTrustPolicy` here
@@ -63,6 +64,19 @@ export interface SeedBootstrapConfig {
63
64
  * since a network-delivered seed has no per-call override.
64
65
  */
65
66
  trustPolicy?: SeedTrustPolicy;
67
+ /**
68
+ * The node-local, NON-replicated trusted-owner anchor. Supplies
69
+ * `SeedTrustContext.knownOwnerKeys` for every {@link applySeed}, and receives
70
+ * a key accepted via a pin/TOFU (see `SeedTrustDecision.anchorAs`).
71
+ *
72
+ * Deliberately NOT `ControlDatabase.getOwnerKeys()`: the replicated
73
+ * `OwnerKey` table is pollutable — any connecting node can genesis-insert its
74
+ * own key and let it replicate — so a seed signed by a stranger's self-issued
75
+ * owner key would pass a table-anchored check. A `CadreNode` passes its
76
+ * `getTrustedOwnerStore()` here. Unset (e.g. a directly-constructed service in
77
+ * a test) means an EMPTY anchor: only a pinned/TOFU policy can accept a seed.
78
+ */
79
+ trustedOwners?: TrustedOwnerStore;
66
80
  /**
67
81
  * Time the inbound seed handler waits for the seed frame before aborting the
68
82
  * read (ms). Defaults to {@link DEFAULT_SEED_READ_TIMEOUT_MS}. Bounds a
@@ -75,6 +89,15 @@ export interface SeedBootstrapConfig {
75
89
  * returned without applying any seed.
76
90
  */
77
91
  maxConcurrentSeeds?: number;
92
+ /**
93
+ * Time {@link SeedBootstrapService.deliverSeed} waits for the whole exchange —
94
+ * dial, write, ack read — before aborting (ms). Defaults to
95
+ * {@link DEFAULT_SEED_DELIVER_TIMEOUT_MS}. Bounds the SENDER against a seed
96
+ * target that accepts the stream and then never replies; the target is a
97
+ * not-yet-trusted node during onboarding, so this is the more exposed
98
+ * direction than the receiver knobs above.
99
+ */
100
+ seedDeliverTimeoutMs?: number;
78
101
  }
79
102
  /**
80
103
  * Event callbacks for seed-related events
@@ -82,8 +105,15 @@ export interface SeedBootstrapConfig {
82
105
  export interface SeedEventCallbacks {
83
106
  /** Called when a seed is received via the protocol */
84
107
  onSeedReceived?: (partyId: string, peerId: string) => void;
85
- /** Called when a seed is successfully applied */
86
- onSeedApplied?: (partyId: string, peersAdded: number) => void;
108
+ /**
109
+ * Called when a seed is successfully applied.
110
+ *
111
+ * `seed` is the applied seed itself: the inbound protocol handler applies it
112
+ * INSIDE the service, so this callback is the only seam through which a
113
+ * `CadreNode` sees a wire-delivered seed's contents — which it needs to
114
+ * retain the owner-flagged peers as cold-start bootstrap dial targets.
115
+ */
116
+ onSeedApplied?: (partyId: string, peersAdded: number, seed: ControlNetworkSeed) => void;
87
117
  /** Called when seed application fails */
88
118
  onSeedError?: (partyId: string, error: string) => void;
89
119
  }
@@ -102,6 +132,7 @@ export declare class SeedBootstrapService {
102
132
  private readonly trustPolicy;
103
133
  private readonly seedReadTimeoutMs;
104
134
  private readonly maxConcurrentSeeds;
135
+ private readonly seedDeliverTimeoutMs;
105
136
  /** In-flight inbound seed streams, used to enforce {@link maxConcurrentSeeds}. */
106
137
  private activeStreams;
107
138
  private eventCallbacks;
@@ -137,7 +168,7 @@ export declare class SeedBootstrapService {
137
168
  }): void;
138
169
  /**
139
170
  * Authorize a new peer to join the cadre.
140
- * Signs the peer ID with the owner key and inserts into CadrePeer table.
171
+ * Signs a membership voucher with the owner key and inserts into CadrePeer table.
141
172
  *
142
173
  * The owner vouches the `PublicKey <-> PeerId` binding: rather than trust a
143
174
  * caller-supplied key, the binding is enforced by construction — `PublicKey` is
@@ -156,45 +187,70 @@ export declare class SeedBootstrapService {
156
187
  * is its own owner (it holds the owner key): the row is owner-signed
157
188
  * (satisfying `AuthorizedInsert`) AND carries a valid self-`Sig`, so it resolves
158
189
  * immediately without a follow-up self-update.
159
- */
160
- insertSelfPeerRecord(record: PeerAddressRecord): Promise<void>;
161
- /**
162
- * Shared owner-signed `CadrePeer` INSERT. Mints a fresh single-use `StampId`
163
- * and signs the voucher digest `digest(peerId, stampId)` ({@link cadrePeerVoucherDigest})
164
- * with the owner key (satisfying `AuthorizedInsert`), then writes the full record
165
- * row with the vouching (owner, signature) persisted into VouchOwner/VouchSig.
166
- * The owner signature does NOT cover the address columns — those are vouched only
167
- * as far as the owner asserts them, and a peer's own `Sig` (when present) is what
168
- * makes the row resolvable.
190
+ *
191
+ * @returns `true` when this call seated the row, `false` when a concurrent writer
192
+ * (e.g. an {@link authorizePeer} of this node's own id) had already seated it —
193
+ * in which case the row in the database is NOT this record and carries whatever
194
+ * `Sig` that writer had, so the caller must self-update to publish its signature.
195
+ */
196
+ insertSelfPeerRecord(record: PeerAddressRecord): Promise<boolean>;
197
+ /**
198
+ * Shared owner-signed `CadrePeer` INSERT — the row fields and the owner-key
199
+ * precondition; the stamp mint, voucher signature, write lock, existence check and
200
+ * membership notify are all {@link ControlDatabase.insertCadrePeer}'s (see there for
201
+ * the anti-replay and insert-race rationale). No `CadrePeer` write may bypass that
202
+ * method: the write is what admits the peer's traffic.
203
+ *
204
+ * The DB check precedes the owner-key check here, matching the order this method has
205
+ * always surfaced them (the owner-key error used to come out of the signer, after the
206
+ * DB check).
207
+ *
208
+ * @returns `true` when this call performed the INSERT, `false` when the row was
209
+ * already seated by a concurrent writer. The loser needs to know: an authorize seats
210
+ * a row with a null `Sig`, so a self-publish that lost the race must fall through to
211
+ * a self-update or its record never lands.
169
212
  */
170
213
  private insertCadrePeerRow;
171
214
  /**
172
215
  * Owner-signed INSERT of a peer's OWN self-signed `DeviceToken` row.
173
216
  *
174
217
  * Counterpart to {@link insertSelfPeerRecord} for the device-token registry: the
175
- * row is owner-signed (satisfying `DeviceToken.AuthorizedInsert`, which vouches
176
- * membership exactly as `CadrePeer.AuthorizedInsert` does) AND carries the peer's
177
- * own self-`Sig` over the token payload. The owner signature covers only the
178
- * PeerId — it does NOT vouch the token contents; the peer's `Sig` (verified at
218
+ * row is owner-signed (satisfying `DeviceToken.AuthorizedInsert` via the
219
+ * 'add'-tagged digest, {@link deviceTokenAddDigest}) AND carries the peer's
220
+ * own self-`Sig` over the token payload. The owner signature covers the WHOLE row
221
+ * (every column, ending in a freshly minted single-use `StampId`) — but covering the
222
+ * token contents is not the same as vouching them: the peer's `Sig` (verified at
179
223
  * resolve time against the bound `CadrePeer.PublicKey`) is what makes the row
180
224
  * resolvable. Used by {@link CadreNode.registerDeviceToken} for the first publish
181
225
  * when the node is its own owner.
226
+ *
227
+ * The stamp is per-INSERT, so a re-register after a clear mints a fresh one and a
228
+ * fresh signature — unaffected by the cleared row's retired stamp
229
+ * (`DeviceToken.NotRevoked`).
182
230
  */
183
231
  insertSelfDeviceToken(record: DeviceTokenRecord): Promise<void>;
184
232
  /**
185
233
  * Owner-signed DELETE of a peer's `DeviceToken` row (logout / token
186
- * invalidation). The `DeviceToken.AuthorizedInsert` constraint gates both insert
187
- * AND delete on an owner signature over `digest(old.PeerId)`, so — like
188
- * {@link removePeer} for `CadrePeer` — clearing a token requires the owner key.
234
+ * invalidation). The `DeviceToken.AuthorizedDelete` constraint validates an owner
235
+ * signature over the 'remove'-tagged digest bound to the STORED row's
236
+ * (PeerId, StampId) — deliberately distinct from the insert digest, so a captured
237
+ * insert approval can never be replayed to clear a token. Like {@link removePeer}
238
+ * for `CadrePeer`, clearing a token requires the owner key.
239
+ *
240
+ * The delete and the `Revocation` tombstone retiring the row's `StampId` commit in
241
+ * ONE transaction — `DeviceToken.RevocationRecorded` refuses a bare delete, and
242
+ * without the tombstone the stamp would free up and the never-expiring insert
243
+ * approval (which the cleared device holds a copy of) would re-seat the token. The
244
+ * tombstone carries its OWN owner signature: retiring a stamp permanently forecloses
245
+ * that row, so it is an owner action in its own right.
246
+ *
247
+ * Both digests, the stamp read, and the transaction come from
248
+ * {@link ControlDatabase.deleteDeviceToken} — one shared implementation across
249
+ * `CadrePeer` / `DeviceToken` / `Strand` / `ValidationKey`. What stays here is the
250
+ * owner-key precondition; a no-op on an already-absent row is the shared body's
251
+ * behavior, and unlike {@link removePeer} nothing here rides on it.
189
252
  */
190
253
  deleteDeviceToken(peerId: string): Promise<void>;
191
- /**
192
- * Sign a peer ID with the owner key for a `CadrePeer` / `DeviceToken`
193
- * insert-or-delete. The signed bytes come from the shared
194
- * {@link peerAuthorizationDigest} helper so the offline `cadre enroll register`
195
- * verifier checks the exact same construction. Throws if no owner key is set.
196
- */
197
- private signPeerAuthorization;
198
254
  /**
199
255
  * Return the configured owner private key, or throw if none is set. The single
200
256
  * precondition gate for every owner-signed write. {@link removePeer} /
@@ -204,38 +260,68 @@ export declare class SeedBootstrapService {
204
260
  * target row is absent (the early `stampId === null` return) instead of rejecting.
205
261
  */
206
262
  private requireOwnerPrivateKey;
263
+ /**
264
+ * The owner PUBLIC key that rides in every owner-signed write's context, non-null.
265
+ *
266
+ * The field is nullable because a read-only service carries no owner key at all, but the
267
+ * constructor derives the public key whenever `ownerPrivateKey` is set — so gating on
268
+ * {@link requireOwnerPrivateKey} first makes the pair inseparable and the second throw
269
+ * unreachable. Callers that must reject a keyless service BEFORE any DB read use this as
270
+ * their first line and get the owner-key precondition for free.
271
+ */
272
+ private requireOwnerPublicKey;
207
273
  /**
208
274
  * Sign a base64url digest with the owner key (ed25519). The single place the
209
- * owner private key is applied; callers pass the canonical digest for the specific
210
- * action ({@link cadrePeerVoucherDigest} / {@link cadrePeerRemoveDigest} /
211
- * {@link peerAuthorizationDigest}). Throws if no owner key is set.
275
+ * owner private key is applied; callers pass the canonical domain-tagged digest for
276
+ * the specific action ({@link deviceTokenAddDigest}), or the raw-bytes form via
277
+ * {@link signMessageBytes}. Throws if no owner key is set.
212
278
  */
213
279
  private signDigest;
280
+ /**
281
+ * Adapt the control database's raw-bytes `signMessage` callback (every guarded delete
282
+ * takes one) to {@link signDigest}'s base64url-string form. Both encodings hash to the
283
+ * same signed bytes (`sign` decodes its base64url input), so a signature minted here
284
+ * satisfies the same schema CHECK as one from the callers that sign the bytes directly — see
285
+ * `control-revocation-replay.spec.ts`'s "raw-bytes and digest-string signers agree".
286
+ */
287
+ private signMessageBytes;
214
288
  /**
215
289
  * Remove a peer from the cadre by owner signature.
216
290
  *
217
291
  * The `CadrePeer.AuthorizedDelete` (`check on delete`) constraint validates a
218
- * signature over the DISTINCT 'remove'-scoped digest `digest(old.PeerId, old.StampId,
219
- * 'remove')` ({@link cadrePeerRemoveDigest}) by an owner key — deliberately NOT the
220
- * insert voucher digest, so the row's stored `VouchSig` can never be replayed to delete.
292
+ * signature over the DISTINCT 'remove'-tagged digest
293
+ * `digest('CadreControl.CadrePeer', 'remove', old.PeerId, old.StampId)` by an owner
294
+ * key — deliberately NOT the insert voucher digest, so the row's stored `VouchSig` can
295
+ * never be replayed to delete.
296
+ *
297
+ * The delete and the `Revocation` tombstone retiring the row's `StampId` commit in ONE
298
+ * transaction — `CadrePeer.RevocationRecorded` refuses a bare delete, and without the
299
+ * tombstone the stamp would free up and the original admission approval (which never
300
+ * expires, and which the removed peer holds a copy of) would re-seat the row.
301
+ *
302
+ * The tombstone is separately owner-signed (satisfying `Revocation.Authorized`):
303
+ * retiring a stamp evicts that peer party-wide and permanently forecloses re-admitting
304
+ * the row, so it is an owner action in its own right, not a side effect the delete's
305
+ * signature covers.
306
+ *
307
+ * Both digests, the stamp read, the transaction, and the post-commit membership notify
308
+ * come from {@link ControlDatabase.deleteCadrePeer} — one shared implementation across
309
+ * `CadrePeer` / `DeviceToken` / `Strand` / `ValidationKey`. What stays here is the
310
+ * owner-key precondition and the absent-row gate the notify depends on (below).
221
311
  */
222
312
  removePeer(peerId: string): Promise<void>;
223
313
  /**
224
- * Owner "re-touch" of an existing `CadrePeer` membership row: bump
225
- * `UpdatedAt` under the owner branch of `CadrePeer.AuthorizedUpdate` (a
226
- * signature over `digest(peerId)` — the same construction {@link authorizePeer}
227
- * uses) so the row is re-emitted as a fresh, broadcasting transaction.
228
- *
229
- * This is the write-while-alone re-replication primitive
230
- * (`control-write-ensure-replicated`): a membership row that committed
231
- * local-only (its block's cluster ≤1 at insert) is pushed to the cohort once it
232
- * grows, by re-issuing this monotonic bump. It is an UPDATE (not the original
233
- * INSERT) because the row already exists locally; a re-INSERT would hit the
234
- * `PeerId` PK. Only the freshness stamp changes — `PublicKey` / `Multiaddr` /
235
- * `Sig` are left intact — so it is safe over a row whose peer has not
236
- * self-published (`Sig` null); the caller must skip a row that already carries a
237
- * self-`Sig` (that row is the owning peer's to refresh, and bumping `UpdatedAt`
238
- * without re-signing would invalidate its self-signature).
314
+ * Owner "re-touch" of an existing `CadrePeer` membership row: bump `UpdatedAt` and
315
+ * re-vouch the row so it is re-emitted as a fresh, broadcasting transaction. This is
316
+ * the write-while-alone re-replication primitive (`control-write-ensure-replicated`):
317
+ * a membership row that committed local-only (its block's cluster ≤1 at insert) is
318
+ * pushed to the cohort once it grows.
319
+ *
320
+ * The stamp read, the voucher signature, the write lock and the membership notify are
321
+ * {@link ControlDatabase.reauthorizeCadrePeer}'s — including the caveat that it rebinds
322
+ * `VouchOwner` to this node's owner key. What stays here is the owner-key precondition.
323
+ *
324
+ * A no-op (no throw, no notify) when the row does not exist.
239
325
  *
240
326
  * @param peerId - the membership row to re-touch.
241
327
  * @param updatedAt - the strictly-increasing freshness stamp to write.
@@ -243,6 +329,23 @@ export declare class SeedBootstrapService {
243
329
  * re-sign another peer's row) or the control database is not initialized.
244
330
  */
245
331
  reauthorizePeer(peerId: string, updatedAt: number): Promise<void>;
332
+ /**
333
+ * Owner re-issue of a batch of `Revocation` tombstones: bump each row's
334
+ * `ReissuedAt` so the storage layer re-broadcasts a tombstone that committed
335
+ * while the node was alone. The delete-while-alone counterpart of
336
+ * {@link reauthorizePeer} — a removed row cannot be re-touched (it is gone),
337
+ * but its tombstone can, and every membership read treats a retired stamp as
338
+ * absent.
339
+ *
340
+ * The signatures, the single transaction, and the strictly-monotonic
341
+ * `reissuedAt` contract are {@link ControlDatabase.reissueRevocations}'s. What
342
+ * stays here is the owner-key precondition.
343
+ *
344
+ * @returns how many tombstones were re-issued (`rows.length` on success).
345
+ * @throws if no owner private key is configured or the control database is not
346
+ * initialized.
347
+ */
348
+ reissueRevocations(rows: readonly RevocationRow[], reissuedAt: number): Promise<number>;
246
349
  /**
247
350
  * Create a seed from the current control network state.
248
351
  * The seed contains peer information and is signed by an owner.
@@ -253,10 +356,15 @@ export declare class SeedBootstrapService {
253
356
  *
254
357
  * Validates the seed signature, then evaluates a trust anchor for the
255
358
  * `signerKey` that does NOT come from the seed body: the receiver's
256
- * `OwnerKey` table (DB-anchored), optionally augmented by pinned keys or
257
- * TOFU via the configured/overriding `SeedTrustPolicy`. A forged
258
- * self-asserting seed — one that merely lists its own signer as an owner
259
- * peer — no longer passes.
359
+ * node-local {@link SeedBootstrapConfig.trustedOwners} anchor, optionally
360
+ * augmented by pinned keys or TOFU via the configured/overriding
361
+ * `SeedTrustPolicy`. A forged self-asserting seed — one that merely lists its
362
+ * own signer as an owner peer — no longer passes, and neither does one signed
363
+ * by a key a stranger genesis-inserted into the replicated `OwnerKey` table.
364
+ *
365
+ * A key accepted via a pin/TOFU is persisted into the anchor (the policy says
366
+ * so via `SeedTrustDecision.anchorAs`), so the next seed from that owner is
367
+ * anchored without re-supplying the invite.
260
368
  *
261
369
  * @param seed - The seed to apply (already transport-decoded).
262
370
  * @param options.trustPolicy - Per-call policy override (e.g. a
@@ -266,6 +374,26 @@ export declare class SeedBootstrapService {
266
374
  applySeed(seed: ControlNetworkSeed, options?: {
267
375
  trustPolicy?: SeedTrustPolicy;
268
376
  }): Promise<ApplySeedResult>;
377
+ /**
378
+ * Persist a signer that a pin/TOFU accepted into the node-local anchor, so a
379
+ * later seed from the same owner is anchored without re-supplying the invite
380
+ * or re-prompting. Only the policy decides this happens (`anchorAs` is unset
381
+ * when the key was already anchored, so a plain re-apply writes nothing) and
382
+ * `trust()` is idempotent, keeping the original provenance for a known key.
383
+ *
384
+ * Failure to PERSIST does not fail the seed: `trust()` reflects the key in the
385
+ * in-memory anchor synchronously, so this seed and the rest of the session are
386
+ * unaffected — only durability across a restart is lost, and that is logged.
387
+ *
388
+ * NOTE: anchoring a key can flip `CadrePeer` rows ALREADY present from
389
+ * unauthorized to authorized, which the write-driven membership-gate refresh
390
+ * (`ControlDatabase.mutateCadrePeer`) cannot see — no row was written. Every
391
+ * anchor mutation today rides seed application, and both seed paths refresh the
392
+ * gate explicitly afterwards (`CadreNode.applySeed`, `onSeedApplied`). If some
393
+ * future path anchors an owner OUTSIDE seed application, it owes the same
394
+ * `CadreNode.refreshMembershipGate()` — or the anchor needs its own hub.
395
+ */
396
+ private anchorAcceptedSigner;
269
397
  /**
270
398
  * Encode a seed for out-of-band delivery (e.g., QR code, copy/paste).
271
399
  */
@@ -276,8 +404,37 @@ export declare class SeedBootstrapService {
276
404
  decodeSeed(encoded: string): ControlNetworkSeed;
277
405
  /**
278
406
  * Deliver a seed directly to a peer via the /sereus/seed/1.0.0 protocol.
407
+ *
408
+ * Sender hardening: the whole exchange — dial, write, ack read — is bounded by
409
+ * {@link seedDeliverTimeoutMs}, and the ack is capped at {@link MAX_SEED_SIZE}.
410
+ * The target is a NOT-YET-TRUSTED node the instigator chose to dial during
411
+ * onboarding, so an unbounded read here is strictly more exposed than the
412
+ * membership-gated receiver paths: without the bound a target that accepts the
413
+ * stream and never replies parks this call forever, and one that streams
414
+ * arbitrary bytes as a fake ack exhausts memory.
279
415
  */
280
416
  deliverSeed(targetMultiaddr: string, seed: ControlNetworkSeed): Promise<SeedAckMessage>;
417
+ /**
418
+ * Open one stream to the target, send the seed frame, half-close, and read the ack.
419
+ *
420
+ * `signal` is the deadline from {@link deliverSeed}: it goes to `dialProtocol` so
421
+ * a timeout during connect aborts the dial, and into {@link exchangeFrame} so a
422
+ * timeout after the stream is open resets it — releasing the otherwise unbounded
423
+ * ack-read.
424
+ *
425
+ * Deliberately NOT `runOnLimitedConnection`: a wake sets it because a wake is a
426
+ * tiny frame over a relay, whereas a seed is up to 1MB and this delivery path
427
+ * does not dial relay addresses today. Changing that is a separate decision.
428
+ */
429
+ private sendSeed;
430
+ /**
431
+ * Read the ack frame a delivery target writes back, bounded by
432
+ * {@link seedDeliverTimeoutMs} and capped at {@link MAX_SEED_SIZE} — an
433
+ * untrusted target must not be able to stream unlimited bytes as a fake ack.
434
+ * Decoding runs inside {@link exchangeFrame}'s `try`, so a malformed or
435
+ * non-JSON ack resets the stream rather than leaking it.
436
+ */
437
+ private readSeedAck;
281
438
  /**
282
439
  * Get this node's circuit relay address for inclusion in seeds.
283
440
  * Returns null if no relay address is available.
@@ -296,6 +453,14 @@ export declare class SeedBootstrapService {
296
453
  * peer is an owner iff that derived key is in the `OwnerKey` set.
297
454
  * This makes any owner node markable — not just the local one — and ties
298
455
  * `isOwner` to the control table rather than to `peerId === self`.
456
+ *
457
+ * NOTE: this is the one owner lookup deliberately left on the REPLICATED
458
+ * table rather than the node-local anchor. `SeedPeer.isOwner` is a dial hint
459
+ * — the receiver dials owner-flagged peers first — not a trust decision, and
460
+ * the receiver re-derives real trust from its own anchor. So a polluted table
461
+ * costs at most a wasted dial, while anchoring here would silently drop
462
+ * legitimate co-owners this node never pinned. If `isOwner` ever gates
463
+ * anything the receiver TRUSTS, move it to the anchor.
299
464
  */
300
465
  private queryPeers;
301
466
  /**