@serfab/cadre-core 0.8.1 → 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 (197) hide show
  1. package/README.md +6 -4
  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 +1006 -101
  9. package/dist/cadre-node.js +1977 -283
  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-cohort.d.ts +15 -15
  15. package/dist/control-cohort.js +14 -14
  16. package/dist/control-cohort.js.map +1 -1
  17. package/dist/control-database.d.ts +1033 -76
  18. package/dist/control-database.js +1520 -153
  19. package/dist/control-database.js.map +1 -1
  20. package/dist/control-formation-recorder.d.ts +91 -12
  21. package/dist/control-formation-recorder.js +131 -16
  22. package/dist/control-formation-recorder.js.map +1 -1
  23. package/dist/control-schema.d.ts +1 -1
  24. package/dist/control-schema.js +774 -186
  25. package/dist/control-schema.js.map +1 -1
  26. package/dist/control-stream.d.ts +38 -4
  27. package/dist/control-stream.js +70 -4
  28. package/dist/control-stream.js.map +1 -1
  29. package/dist/control-write-retry.d.ts +151 -0
  30. package/dist/control-write-retry.js +411 -0
  31. package/dist/control-write-retry.js.map +1 -0
  32. package/dist/delegate-admission.d.ts +134 -0
  33. package/dist/delegate-admission.js +244 -0
  34. package/dist/delegate-admission.js.map +1 -0
  35. package/dist/device-token.d.ts +12 -12
  36. package/dist/device-token.js +15 -14
  37. package/dist/device-token.js.map +1 -1
  38. package/dist/ed25519-key.d.ts +68 -0
  39. package/dist/ed25519-key.js +106 -0
  40. package/dist/ed25519-key.js.map +1 -0
  41. package/dist/file-durable-slot.d.ts +20 -0
  42. package/dist/file-durable-slot.js +53 -0
  43. package/dist/file-durable-slot.js.map +1 -0
  44. package/dist/formation-approval.d.ts +157 -0
  45. package/dist/formation-approval.js +375 -0
  46. package/dist/formation-approval.js.map +1 -0
  47. package/dist/fs-atomic.d.ts +38 -0
  48. package/dist/fs-atomic.js +97 -0
  49. package/dist/fs-atomic.js.map +1 -0
  50. package/dist/identity-key.d.ts +49 -0
  51. package/dist/identity-key.js +81 -0
  52. package/dist/identity-key.js.map +1 -0
  53. package/dist/index.d.ts +16 -6
  54. package/dist/index.js +44 -10
  55. package/dist/index.js.map +1 -1
  56. package/dist/key-store-file.d.ts +2 -24
  57. package/dist/key-store-file.js +10 -77
  58. package/dist/key-store-file.js.map +1 -1
  59. package/dist/key-store.d.ts +1 -1
  60. package/dist/key-store.js +1 -1
  61. package/dist/membership-connection-gater.d.ts +146 -0
  62. package/dist/membership-connection-gater.js +180 -0
  63. package/dist/membership-connection-gater.js.map +1 -0
  64. package/dist/node-local-snapshot.d.ts +109 -0
  65. package/dist/node-local-snapshot.js +182 -0
  66. package/dist/node-local-snapshot.js.map +1 -0
  67. package/dist/peer-authorization.d.ts +150 -11
  68. package/dist/peer-authorization.js +170 -13
  69. package/dist/peer-authorization.js.map +1 -1
  70. package/dist/peer-record.d.ts +24 -11
  71. package/dist/peer-record.js +29 -13
  72. package/dist/peer-record.js.map +1 -1
  73. package/dist/push-node.d.ts +38 -0
  74. package/dist/push-node.js +48 -0
  75. package/dist/push-node.js.map +1 -0
  76. package/dist/push-notifier-apns.d.ts +10 -2
  77. package/dist/push-notifier-apns.js +14 -10
  78. package/dist/push-notifier-apns.js.map +1 -1
  79. package/dist/push-notifier-fcm.d.ts +7 -1
  80. package/dist/push-notifier-fcm.js +9 -10
  81. package/dist/push-notifier-fcm.js.map +1 -1
  82. package/dist/push-notifier.d.ts +16 -35
  83. package/dist/push-notifier.js +16 -44
  84. package/dist/push-notifier.js.map +1 -1
  85. package/dist/relay-addrs.d.ts +45 -0
  86. package/dist/relay-addrs.js +103 -0
  87. package/dist/relay-addrs.js.map +1 -0
  88. package/dist/relay-reservation.d.ts +266 -0
  89. package/dist/relay-reservation.js +613 -0
  90. package/dist/relay-reservation.js.map +1 -0
  91. package/dist/seed-bootstrap.d.ts +263 -79
  92. package/dist/seed-bootstrap.js +413 -211
  93. package/dist/seed-bootstrap.js.map +1 -1
  94. package/dist/seed-trust-policy.d.ts +50 -24
  95. package/dist/seed-trust-policy.js +28 -19
  96. package/dist/seed-trust-policy.js.map +1 -1
  97. package/dist/strand-addr-protocol.d.ts +42 -13
  98. package/dist/strand-addr-protocol.js +58 -52
  99. package/dist/strand-addr-protocol.js.map +1 -1
  100. package/dist/strand-backfill.d.ts +109 -0
  101. package/dist/strand-backfill.js +279 -0
  102. package/dist/strand-backfill.js.map +1 -0
  103. package/dist/strand-database.d.ts +5 -5
  104. package/dist/strand-database.js +4 -4
  105. package/dist/strand-database.js.map +1 -1
  106. package/dist/strand-formation-manager.d.ts +38 -9
  107. package/dist/strand-formation-manager.js +120 -17
  108. package/dist/strand-formation-manager.js.map +1 -1
  109. package/dist/strand-formation-protocol.d.ts +104 -5
  110. package/dist/strand-formation-protocol.js +233 -5
  111. package/dist/strand-formation-protocol.js.map +1 -1
  112. package/dist/strand-instance-manager.d.ts +40 -2
  113. package/dist/strand-instance-manager.js +93 -15
  114. package/dist/strand-instance-manager.js.map +1 -1
  115. package/dist/strand-member-key.d.ts +6 -6
  116. package/dist/strand-member-key.js +6 -6
  117. package/dist/strand-member-key.js.map +1 -1
  118. package/dist/strand-member-registry.d.ts +12 -12
  119. package/dist/strand-member-registry.js +22 -14
  120. package/dist/strand-member-registry.js.map +1 -1
  121. package/dist/strand-membership-writer.d.ts +532 -138
  122. package/dist/strand-membership-writer.js +800 -179
  123. package/dist/strand-membership-writer.js.map +1 -1
  124. package/dist/strand-solicitation.d.ts +99 -33
  125. package/dist/strand-solicitation.js +100 -52
  126. package/dist/strand-solicitation.js.map +1 -1
  127. package/dist/strand-transport-key.d.ts +40 -0
  128. package/dist/strand-transport-key.js +56 -0
  129. package/dist/strand-transport-key.js.map +1 -0
  130. package/dist/strand-wake-protocol.d.ts +9 -7
  131. package/dist/strand-wake-protocol.js +22 -46
  132. package/dist/strand-wake-protocol.js.map +1 -1
  133. package/dist/strand-watcher.d.ts +17 -2
  134. package/dist/strand-watcher.js +47 -3
  135. package/dist/strand-watcher.js.map +1 -1
  136. package/dist/trusted-owner-store-file.d.ts +12 -0
  137. package/dist/trusted-owner-store-file.js +39 -0
  138. package/dist/trusted-owner-store-file.js.map +1 -0
  139. package/dist/trusted-owner-store.d.ts +64 -0
  140. package/dist/trusted-owner-store.js +126 -0
  141. package/dist/trusted-owner-store.js.map +1 -0
  142. package/dist/types.d.ts +325 -50
  143. package/dist/types.js +13 -0
  144. package/dist/types.js.map +1 -1
  145. package/package.json +23 -8
  146. package/src/bootstrap-peer-store-file.ts +44 -0
  147. package/src/bootstrap-peer-store.ts +172 -0
  148. package/src/cadre-node.ts +2129 -295
  149. package/src/control-authorization.ts +114 -0
  150. package/src/control-cohort.ts +17 -17
  151. package/src/control-database.ts +1766 -191
  152. package/src/control-formation-recorder.ts +174 -21
  153. package/src/control-schema.ts +786 -198
  154. package/src/control-stream.ts +81 -4
  155. package/src/control-write-retry.ts +473 -0
  156. package/src/delegate-admission.ts +302 -0
  157. package/src/device-token.ts +19 -14
  158. package/src/ed25519-key.ts +134 -0
  159. package/src/file-durable-slot.ts +61 -0
  160. package/src/formation-approval.ts +604 -0
  161. package/src/fs-atomic.ts +107 -0
  162. package/src/identity-key.ts +109 -0
  163. package/src/index.ts +157 -24
  164. package/src/key-store-file.ts +10 -81
  165. package/src/key-store.ts +1 -1
  166. package/src/membership-connection-gater.ts +204 -0
  167. package/src/node-local-snapshot.ts +260 -0
  168. package/src/peer-authorization.ts +219 -13
  169. package/src/peer-record.ts +31 -13
  170. package/src/push-node.ts +64 -0
  171. package/src/push-notifier-apns.ts +16 -12
  172. package/src/push-notifier-fcm.ts +9 -10
  173. package/src/push-notifier.ts +16 -57
  174. package/src/relay-addrs.ts +111 -0
  175. package/src/relay-reservation.ts +823 -0
  176. package/src/seed-bootstrap.ts +490 -226
  177. package/src/seed-trust-policy.ts +65 -32
  178. package/src/strand-addr-protocol.ts +84 -53
  179. package/src/strand-backfill.ts +366 -0
  180. package/src/strand-database.ts +6 -6
  181. package/src/strand-formation-manager.ts +145 -19
  182. package/src/strand-formation-protocol.ts +310 -9
  183. package/src/strand-instance-manager.ts +113 -16
  184. package/src/strand-member-key.ts +7 -7
  185. package/src/strand-member-registry.ts +28 -23
  186. package/src/strand-membership-writer.ts +1430 -640
  187. package/src/strand-solicitation.ts +155 -90
  188. package/src/strand-transport-key.ts +58 -0
  189. package/src/strand-wake-protocol.ts +29 -46
  190. package/src/strand-watcher.ts +66 -3
  191. package/src/trusted-owner-store-file.ts +41 -0
  192. package/src/trusted-owner-store.ts +171 -0
  193. package/src/types.ts +342 -51
  194. package/dist/authority-key.d.ts +0 -41
  195. package/dist/authority-key.js +0 -52
  196. package/dist/authority-key.js.map +0 -1
  197. package/src/authority-key.ts +0 -68
@@ -1,13 +1,16 @@
1
1
  import type { Libp2p, PeerId } from '@libp2p/interface';
2
2
  import type { Multiaddr } from '@multiformats/multiaddr';
3
3
  import type { CadreNodeConfig, StrandInstance, StrandConfig, SAppConfig, CadreNodeEvents, ControlNetworkSeed, ApplySeedResult, AddDroneOptions, AddPhoneOptions, DroneInitResult, InviteResult, CadreInvite, OpenInvitation, FormStrandResult, StrandFormationDisclosure, ResolveOpts, SelfRegistrationOutcome, ServiceWakeResult, PushPlatform, DeviceTokenRecord, ResolveDeviceTokenOpts } from './types.js';
4
- import { type AuthorityKeyPair } from './authority-key.js';
4
+ import { type Ed25519KeyPair } from './ed25519-key.js';
5
+ import { type TrustedOwnerStore, type TrustSource } from './trusted-owner-store.js';
6
+ import { type BootstrapPeerStore } from './bootstrap-peer-store.js';
5
7
  import { type SAppIdLookup } from './strand-watcher.js';
6
8
  import { EnrollmentService } from './enrollment.js';
7
9
  import { ControlDatabase } from './control-database.js';
8
10
  import { SeedBootstrapService } from './seed-bootstrap.js';
9
11
  import type { SeedTrustPolicy } from './seed-trust-policy.js';
10
12
  import { StrandSolicitationService, type StrandSolicitationServiceOptions } from './strand-solicitation.js';
13
+ import { type RelayReservationState, type RelayReservationSupervisorOptions } from './relay-reservation.js';
11
14
  import type { WakeAck } from './types.js';
12
15
  import { type ConnectionPathSummary } from './diagnostics/connection-path.js';
13
16
  type EventHandler<T> = (data: T) => void;
@@ -26,11 +29,20 @@ export declare class CadreNode implements SAppIdLookup {
26
29
  * The resolved node identity key, set once by {@link resolveIdentityKey}
27
30
  * during {@link start} (from `config.keyStore`, else `config.privateKey`).
28
31
  * Left undefined when neither is configured — libp2p then generates an
29
- * ephemeral key internally and there is no exposed authority key. Every
32
+ * ephemeral key internally and there is no exposed owner key. Every
30
33
  * identity-dependent path (control node creation, self-record signing, strand
31
34
  * launch) reads this resolved field, never `config.privateKey` directly.
32
35
  */
33
36
  private identityKey;
37
+ /**
38
+ * Node-local, NON-replicated trusted-owner anchor (see
39
+ * `trusted-owner-store.ts`). Constructed (or adopted from
40
+ * `config.trustedOwners.store`) by {@link initializeTrustedOwnerStore} during
41
+ * {@link start}; deliberately NOT cleared by {@link cleanup}, so an in-memory
42
+ * anchor survives a stop()→start() cycle of the same node instance. Never
43
+ * sourced from replicated control state.
44
+ */
45
+ private trustedOwnerStore;
34
46
  private controlNode;
35
47
  private controlDatabase;
36
48
  private strandWatcher;
@@ -49,9 +61,9 @@ export declare class CadreNode implements SAppIdLookup {
49
61
  private strandAddrService;
50
62
  /**
51
63
  * Server-side push-wake fan-out. Constructed by {@link start} only when
52
- * `config.push` (FCM/APNs credentials) is present — without it the node behaves
53
- * exactly as before (no notifier, no fan-out). Owns who/when to wake
54
- * hibernating mobile peers on strand activity.
64
+ * `config.push` (an injected `PushNotifier` + policy) is present — without it
65
+ * the node behaves exactly as before (no notifier, no fan-out). Owns who/when
66
+ * to wake hibernating mobile peers on strand activity.
55
67
  */
56
68
  private pushFanoutService;
57
69
  /** Backing field for the {@link running} / {@link isRunning} getters. */
@@ -70,6 +82,25 @@ export declare class CadreNode implements SAppIdLookup {
70
82
  */
71
83
  private windowWaiters;
72
84
  private eventHandlers;
85
+ /**
86
+ * Relay multiaddrs the caller asked {@link reserveRelays} to reserve through.
87
+ * Empty (the default) means nobody asked, so {@link getRelayReservationState}
88
+ * reports `none` — this whole surface is opt-in and dormant for the CLI/host
89
+ * nodes, which use the configured `network.relayAddrs` route instead.
90
+ */
91
+ private relayReserveAddrs;
92
+ /**
93
+ * The running retry loop for {@link relayReserveAddrs}, or `null` when nobody
94
+ * asked for a reservation / there was no control node to supervise. It owns the
95
+ * in-flight flag, the last failure and the next-attempt time; this class only
96
+ * starts it, stops it and reads it.
97
+ */
98
+ private relayReserveSupervisor;
99
+ /**
100
+ * Reason {@link reserveRelays} produced no reservation when there is no
101
+ * supervisor to hold one — i.e. the pre-start `control node unavailable` case.
102
+ */
103
+ private relayReserveError;
73
104
  /** Map of strandId -> sAppConfig for sAppId filtering and management */
74
105
  private sAppConfigs;
75
106
  /**
@@ -79,6 +110,106 @@ export declare class CadreNode implements SAppIdLookup {
79
110
  * control-network node never needs to dial back to the manager.
80
111
  */
81
112
  private latestInviteAddresses;
113
+ /**
114
+ * Epoch ms until which the control-network inbound gate admits
115
+ * not-yet-authorized peers (see {@link admitInboundControlConnection} /
116
+ * {@link openEnrollmentWindow}). 0 = no window open. Opened automatically by
117
+ * {@link createInvite}; deliberately NOT reset on stop() — a stop/start cycle
118
+ * inside an outstanding invite's validity must not strand the invitee.
119
+ *
120
+ * NOTE: in-memory only, so a PROCESS restart mid-invite closes the door until
121
+ * the owner re-mints or the invitee is authorized. Fine while invites are
122
+ * short-lived and redeemed promptly; if long-lived invites or restart-prone
123
+ * hosts become normal, persist the window (or derive it from the issued-invite
124
+ * records `SeedBootstrapService` already keeps).
125
+ */
126
+ private enrollmentWindowUntil;
127
+ /**
128
+ * Lazily-parsed PeerIds of `controlNetwork.bootstrapNodes` (see
129
+ * {@link getBootstrapPeerIds}) — infrastructure the inbound gate always admits.
130
+ */
131
+ private bootstrapPeerIds;
132
+ /**
133
+ * Materialized in-memory snapshot of the AUTHORIZED member peer ids (the
134
+ * {@link listAuthorizedMembers} result), consulted by the per-stream
135
+ * control-DB gate ({@link authorizeInboundControlStream}). That predicate
136
+ * runs on EVERY inbound Optimystic control-DB stream and must never await a
137
+ * control-DB read: those reads pull blocks over the very protocols the
138
+ * predicate gates, which closes a circular wait that ends in mutual denial
139
+ * (the upstream gate is fail-closed on timeout, unlike the fail-open
140
+ * connection gater). So the set is refreshed OUT OF BAND instead — after
141
+ * start, on every {@link reconcileControlCohort} pass (15s cadence), and
142
+ * immediately after every LOCAL membership mutation so a just-vouched peer
143
+ * is admitted without waiting for the timer. That last refresh is AUTOMATIC:
144
+ * the control database notifies {@link refreshMembershipGate} after every
145
+ * committed `CadrePeer` write (see `ControlDatabase.mutateCadrePeer`), so no
146
+ * writer has to remember. A change that arrives by REPLICATION is picked up
147
+ * on the next timed refresh — bounded staleness, acceptable because this gate
148
+ * is defense in depth: rows an unadmitted peer manages to write are still
149
+ * disbelieved at read time by the voucher-anchored predicate.
150
+ */
151
+ private authorizedControlPeers;
152
+ /**
153
+ * Coalescing state for {@link refreshMembershipGate}: a pending "the snapshot
154
+ * is stale" flag, the single in-flight drain that consumes it, and the depth
155
+ * of open {@link deferMembershipGateRefresh} scopes (a burst of writes inside
156
+ * one scope collapses to a single refresh at scope exit).
157
+ */
158
+ private membershipGateDirty;
159
+ private membershipGateDrain;
160
+ private membershipGateDeferDepth;
161
+ /**
162
+ * In-memory delegate admission grants (see `delegate-admission.ts`): the
163
+ * strand-node transport peerIds this party's members have announced over the
164
+ * strand-addr RPC, admitted at the CONNECTION level so a member's NAT'd
165
+ * strand node can hold a circuit-relay reservation on this node. Consulted
166
+ * ONLY by {@link admitInboundControlConnection}; the fail-closed per-stream
167
+ * gate ({@link authorizeInboundControlStream}) never honors it.
168
+ */
169
+ private readonly delegateAdmission;
170
+ /**
171
+ * When this node last ANNOUNCED a delegate, keyed `<targetPeerId>\n<strandId>`
172
+ * (the peer announced TO, not the delegate). Throttles
173
+ * {@link refreshDelegateGrants} to once per `DELEGATE_GRANT_TTL_MS / 2` per
174
+ * (relay, strand) so the 15 s reconcile tick never becomes per-tick RPC
175
+ * chatter; the launch/resume announce passes record here too. Keys whose
176
+ * strand is no longer running are pruned on each reconcile pass.
177
+ */
178
+ private readonly delegateAnnounceAt;
179
+ /**
180
+ * Cold-start bootstrap dial targets: the owner-flagged peers of every seed
181
+ * this node has applied, keyed by peer id, valued by the seed's multiaddr
182
+ * strings (parsed lazily, at dial time). Written by
183
+ * {@link recordSeedBootstrapPeers}; read only by
184
+ * {@link dialColdStartBootstrap} while the control database still has no
185
+ * siblings to dial.
186
+ *
187
+ * A STORE rather than a plain map (see `bootstrap-peer-store.ts`), because the
188
+ * targets must outlive the process: a node seeded into a party it could not
189
+ * reach has nothing else on disk naming that party's addresses (`applySeed`
190
+ * writes no control row, and `CadrePeer` fills in only after a connection
191
+ * succeeds), so an in-memory-only set left it stranded permanently across a
192
+ * restart. Constructed (or adopted from `config.bootstrapPeers.store`) by
193
+ * {@link initializeBootstrapPeerStore} during {@link start}; deliberately NOT
194
+ * cleared by {@link cleanup}, so it survives a stop()→start() cycle of the same
195
+ * node instance — same lifecycle as {@link trustedOwnerStore}. Durability
196
+ * depends on the injected backend: Node gets a file-backed store from the CLI,
197
+ * RN/browser stay ephemeral for now.
198
+ *
199
+ * Deliberately NOT the libp2p peer store, which {@link peerStoreAddrs} already
200
+ * reads for the steady-state path. The peer store is shared with everything
201
+ * libp2p discovers, so "dial every entry" would grow into dialing arbitrary
202
+ * discovered peers as the node lives longer; this store holds exactly the peers
203
+ * an owner-signed, trust-anchored seed nominated as owners. `applySeed` merges
204
+ * the same addresses into the peer store as well, so the two never disagree —
205
+ * this one is just the precisely-scoped subset.
206
+ *
207
+ * A later seed OVERWRITES an entry rather than merging, so a re-seed after an
208
+ * owner's address changes replaces the stale address instead of accumulating
209
+ * dead ones. Entries are never evicted: they are the node's only way back into
210
+ * the party if it is ever stranded again.
211
+ */
212
+ private bootstrapPeerStore;
82
213
  /** Initial self-registration timer (see {@link scheduleSelfRegistration}). */
83
214
  private selfRegistrationTimer;
84
215
  /** TTL heartbeat that re-publishes the self record before it goes stale. */
@@ -101,27 +232,50 @@ export declare class CadreNode implements SAppIdLookup {
101
232
  private reconcileControlCohortInFlight;
102
233
  /**
103
234
  * Single-flight guard for {@link registerSelf}. Concurrent callers (the explicit
104
- * CLI `--authority` publish, the 1s startup timer, the TTL heartbeat, and the
235
+ * CLI `--owner` publish, the 1s startup timer, the TTL heartbeat, and the
105
236
  * address-change listener) share one in-flight publish so two of them can never
106
237
  * both read "no row yet" and race a duplicate INSERT (a `CadrePeer` PK conflict).
107
238
  */
108
239
  private registerSelfInFlight;
109
240
  /**
110
- * Re-replication queue for authority control writes that committed while this
111
- * node was alone — `controlNode.getConnections().length === 0` at write time
112
- * means the Optimystic commit was local-only (the block's cluster was ≤1) and
113
- * never broadcast. Maps the affected subject peerId → the write to re-issue once
114
- * the cohort grows. A later op for the same subject overwrites an earlier one
115
- * (authorize then remove → remove wins). Drained on the 0→≥1 control-connection
116
- * transition by {@link drainPendingControlReplication}.
241
+ * Re-replication queue for owner `authorizePeer` writes that committed while
242
+ * this node was alone — `controlNode.getConnections().length === 0` at write
243
+ * time means the Optimystic commit was local-only (the block's cluster was ≤1)
244
+ * and never broadcast. Maps the affected subject peerId → 'authorize', to
245
+ * re-issue once the cohort grows. Removals are NOT tracked here — a delete
246
+ * leaves a `Revocation` tombstone, tracked by {@link pendingRevocations}; a
247
+ * removePeer also clears any queued authorize for the same subject (the row is
248
+ * gone, so re-issuing the insert would be wrong). Drained on the 0→≥1
249
+ * control-connection transition by {@link drainPendingControlReplication}.
117
250
  */
118
251
  private pendingPeerWrites;
252
+ /**
253
+ * Re-replication queue for `Revocation` tombstones that committed while this
254
+ * node was alone, keyed on the retired `StampId`. Fed by the control DB's
255
+ * committed-delete seam ({@link noteGuardedDelete}), so one queue covers all
256
+ * four guarded tables (`CadrePeer` / `DeviceToken` / `Strand` /
257
+ * `ValidationKey`). Drained (owner-signed `ReissuedAt` bump, which re-broadcasts
258
+ * the tombstone) by {@link drainPendingRevocations}. Cleared on stop alongside
259
+ * {@link pendingPeerWrites} — the tombstone rows are durable, so the next
260
+ * lifetime's first-growth sweep re-covers anything dropped here.
261
+ */
262
+ private pendingRevocations;
263
+ /**
264
+ * Guards the once-per-lifetime first-growth revocation sweep (re-issue EVERY
265
+ * locally-held tombstone, covering removals from before this node started).
266
+ * Deliberately separate from {@link reconstructedLocalOnlyWrites} and set only
267
+ * after a sweep pass SUCCEEDS (including the nothing-held case) — a throwing
268
+ * sweep retries on the next growth edge instead of being lost for the lifetime.
269
+ * Reset on stop, so a stop()→start() cycle sweeps again (that second start is
270
+ * exactly a "removals from before this lifetime" case).
271
+ */
272
+ private reissuedHeldRevocations;
119
273
  /** This node's own `CadrePeer` self-write committed local-only (re-touched on growth). */
120
274
  private pendingSelfPeerWrite;
121
275
  /** This node's own `DeviceToken` self-write committed local-only (re-touched on growth). */
122
276
  private pendingSelfDeviceWrite;
123
277
  /**
124
- * Guards the one-shot, first-cohort-growth reconstruction: an authority re-touches
278
+ * Guards the one-shot, first-cohort-growth reconstruction: an owner re-touches
125
279
  * every membership row it may have authored that could be unreplicated (covering
126
280
  * writes made before this process started, which the in-memory queue cannot know).
127
281
  * Set true after the first drain so later passes only drain the in-memory queue.
@@ -218,6 +372,19 @@ export declare class CadreNode implements SAppIdLookup {
218
372
  * Start the cadre node
219
373
  */
220
374
  start(): Promise<void>;
375
+ /**
376
+ * Build the server-side push-wake fan-out over an injected {@link PushNotifier}.
377
+ *
378
+ * The notifier reaches for `node:http2`/`node:crypto`, so the cross-platform
379
+ * core never constructs it — the Node host builds it from
380
+ * `@serfab/cadre-core/push-node` and passes the instance in
381
+ * `CadreNodeConfig.push`. This node owns that instance's lifecycle from here:
382
+ * {@link cleanup} closes the fan-out, which closes the notifier (freeing the
383
+ * APNs HTTP/2 session). Every other primitive the fan-out needs (member
384
+ * enumeration, participation, direct dial, token resolve/expire) is an
385
+ * import-clean closure over this node.
386
+ */
387
+ private buildPushFanout;
221
388
  /**
222
389
  * Stop the cadre node
223
390
  */
@@ -236,11 +403,11 @@ export declare class CadreNode implements SAppIdLookup {
236
403
  * before any network bring-up. Resolution order:
237
404
  *
238
405
  * 1. Both `keyStore` and `privateKey` set ⇒ configuration error (throws).
239
- * 2. `keyStore` set ⇒ load protobuf bytes from `identityKeyId` (default
240
- * {@link DEFAULT_IDENTITY_KEY_ID}). Found ⇒ deserialize; empty ⇒ generate a
241
- * fresh Ed25519 key, persist it, and use it. A rejected `get` (access denied
242
- * / backend failure) PROPAGATES — we never generate a new key on a read
243
- * error, which would silently orphan the real identity.
406
+ * 2. `keyStore` set ⇒ {@link loadOrCreateIdentityKey} against `identityKeyId`
407
+ * (default {@link DEFAULT_IDENTITY_KEY_ID}): load when the slot is present,
408
+ * generate + persist when it is empty. A rejected `get` (access denied /
409
+ * backend failure) PROPAGATES — we never generate a new key on a read error,
410
+ * which would silently orphan the real identity.
244
411
  * 3. `privateKey` set ⇒ use it directly.
245
412
  * 4. Neither ⇒ leave undefined; libp2p generates an ephemeral key.
246
413
  *
@@ -248,12 +415,232 @@ export declare class CadreNode implements SAppIdLookup {
248
415
  * resolved key rather than regenerating or re-persisting.
249
416
  */
250
417
  private resolveIdentityKey;
418
+ /**
419
+ * Construct (or adopt) the node-local trusted-owner anchor and seed the
420
+ * out-of-band pinned keys from `config.trustedOwners`. The store is NEVER
421
+ * sourced from the replicated control DB — its entries come only from
422
+ * genesis self-trust ({@link initializeSeedBootstrap}), config pins (here),
423
+ * or runtime enrollment pins ({@link trustOwnerKeys}).
424
+ *
425
+ * Idempotent across stop()→start(): the store instance is kept, and
426
+ * re-seeding config pins is a no-op ({@link TrustedOwnerStore.trust} is
427
+ * idempotent). An injected store scoped to a different party is a
428
+ * configuration error (fail closed before any network bring-up).
429
+ */
430
+ private initializeTrustedOwnerStore;
431
+ /**
432
+ * Construct (or adopt) the node-local cold-start bootstrap-peer store (see
433
+ * {@link bootstrapPeerStore}). Synchronous: an injected store has already
434
+ * loaded its persisted targets by the time it is handed in (its `open` is the
435
+ * async part), and the in-memory fallback has nothing to load.
436
+ *
437
+ * Idempotent across stop()→start(): the store instance is kept, so retained
438
+ * targets survive a restart of the same node instance even with the ephemeral
439
+ * default. An injected store scoped to a different party is a configuration
440
+ * error (fail closed before any network bring-up) — a foreign party's addresses
441
+ * must never enter this node's dial loop.
442
+ */
443
+ private initializeBootstrapPeerStore;
444
+ /**
445
+ * The node-local cold-start bootstrap-peer store (null before {@link start}) —
446
+ * the retained owner addresses {@link reconcileControlCohort}'s cold-start
447
+ * branch re-dials. Exposed for diagnostics and for a host that wants to show
448
+ * "what would this node dial if it is stranded?".
449
+ */
450
+ getBootstrapPeerStore(): BootstrapPeerStore | null;
451
+ /**
452
+ * The node-local trusted-owner anchor (null before {@link start}). This is
453
+ * the set the authorized-membership predicate and seed-trust anchor consult —
454
+ * never the replicated `OwnerKey` table, which any stranger can pollute.
455
+ */
456
+ getTrustedOwnerStore(): TrustedOwnerStore | null;
457
+ /**
458
+ * Persist out-of-band-established owner keys into the node-local anchor —
459
+ * the runtime enrollment seam: call with `CadreInvite.ownerKeys` when
460
+ * redeeming an invite (BEFORE the first `applySeed`, so the anchor already
461
+ * holds the pins when seed trust consults it), or with an operator-supplied
462
+ * pin. Idempotent. ('genesis' provenance is reserved for the node's own
463
+ * founding key, seeded internally by {@link initializeSeedBootstrap}.)
464
+ *
465
+ * Validates every key's shape before trusting any of them (all-or-nothing):
466
+ * a malformed entry anywhere in `keys` rejects the whole call before a
467
+ * single key is anchored. For the invite route this means a `CadreInvite`
468
+ * carrying one malformed `ownerKeys` entry fails the redemption outright —
469
+ * consistent with the anchor's existing whole-record-or-nothing policy for
470
+ * a corrupt persisted entry (see `trusted-owner-store.ts`'s
471
+ * `unusableEntry: 'discard-all'`) — rather than silently anchoring a subset
472
+ * and leaving the caller to notice a key went missing.
473
+ */
474
+ trustOwnerKeys(keys: Iterable<string>, source: Exclude<TrustSource, 'genesis'>): Promise<void>;
251
475
  private createControlNode;
476
+ /**
477
+ * Map this node's config onto the control network's libp2p node options.
478
+ *
479
+ * Split out of {@link createControlNode} purely so the mapping is assertable without
480
+ * standing up a real libp2p node — `packages/cadre-core/test/cadre-node-control-node-options.spec.ts`
481
+ * calls it on a bare `new CadreNode(config)`. Read nothing else into the split; the
482
+ * only caller in production is `createControlNode`.
483
+ */
484
+ private buildControlNodeOptions;
485
+ /**
486
+ * Decide whether an inbound CONTROL-network connection from `remotePeerId`
487
+ * should be admitted — the policy behind the connection gater wired in
488
+ * {@link createControlNode}. Deny only on a positive "unauthorized outsider
489
+ * while no stranger path is open" determination; everything ambiguous admits
490
+ * and defers to the fail-closed per-stream gates (see
491
+ * `membership-connection-gater.ts` for the layer's rationale and the
492
+ * stranger-open protocol allowlist).
493
+ *
494
+ * Admits when ANY of:
495
+ * 1. a shared-baseline check admits ({@link admitControlPeerUnconditionally}
496
+ * — not running / DB torn down, absent-or-empty trusted-owner anchor, or
497
+ * configured bootstrap/relay infrastructure);
498
+ * 2. an enrollment window is open ({@link openEnrollmentWindow}, opened by
499
+ * {@link createInvite}) — the invitee dials in before it is authorized;
500
+ * 3. the peer holds a live DELEGATE ADMISSION GRANT — an authorized member
501
+ * announced it (over the strand-addr RPC) as the transport peerId of its
502
+ * own strand node, so a NAT'd member's strand node can hold a
503
+ * circuit-relay reservation here (see `delegate-admission.ts`).
504
+ * Connection only: the per-stream gate below never honors a grant;
505
+ * 4. the authorized-member set is empty — cold start: the rows that would
506
+ * authorize anyone arrive by replication over these very connections;
507
+ * 5. the peer IS an authorized member; or
508
+ * 6. an open invitation is OUTSTANDING — at least one unexpired,
509
+ * not-fully-consumed invitation this node minted or persisted (see
510
+ * `StrandSolicitationService.hasOutstandingInvitation`). A formation
511
+ * initiator is another party's peer by design and its token is only
512
+ * checkable inside the protocol, so the gate asks the coarser question
513
+ * "does this node expect a stranger at all?". Merely REGISTERING the
514
+ * responder ({@link initializeStrandSolicitation}) no longer suspends
515
+ * stranger denial — eager registration (as `reference-app-rn` does at
516
+ * bring-up) and `formStrand`'s lazy initialization both leave the gate
517
+ * armed, because neither mints an invitation.
518
+ *
519
+ * Ordering is semantically free (the checks are OR'd) but decides who pays:
520
+ * checks 1-3 are in-memory, 4/5 share one control-DB read, and only a peer
521
+ * already on the deny path reaches check 6's invitation lookup.
522
+ *
523
+ * Caveats of check 6, both self-healing:
524
+ * - the in-memory mint registry dies with the process, so after a restart
525
+ * only invitations persisted as `FormationInvite` rows still hold the
526
+ * exemption open (re-mint otherwise — same story as the enrollment window);
527
+ * - a peer holding a token whose `FormationInvite` row has not replicated to
528
+ * this node yet is denied even though the formation handler would have
529
+ * accepted it, exactly like the unreplicated-membership-row case below.
530
+ *
531
+ * NOTE: check 4/5 runs a control-DB read per inbound connection
532
+ * (`listAuthorizedMembers`); connections are rare and cadres small, and this
533
+ * layer is fail-open behind `ADMISSION_DECISION_TIMEOUT_MS`, so the live
534
+ * read is safe here — unlike the per-stream gate, which must consult the
535
+ * materialized {@link authorizedControlPeers} snapshot instead.
536
+ * NOTE: a sibling whose membership row has not yet replicated to
537
+ * this node is denied until the row converges (typically via the owner);
538
+ * either side's next outbound reconcile dial (outbound is never gated)
539
+ * re-establishes the link — self-healing, but visible as a transient deny.
540
+ */
541
+ private admitInboundControlConnection;
542
+ /**
543
+ * Hold the control-network inbound gate open for not-yet-authorized peers
544
+ * until `untilEpochMs` (extends, never shrinks, an already-open window).
545
+ * {@link createInvite} calls this automatically; a host running an
546
+ * out-of-band enrollment flow (e.g. accepting a phone whose invite this node
547
+ * never minted) can open it explicitly before the stranger dials in.
548
+ */
549
+ openEnrollmentWindow(untilEpochMs: number): void;
550
+ /**
551
+ * Record (or refresh) a delegate admission grant: `delegatePeerId` is the
552
+ * transport peerId of `announcerPeerId`'s strand-`strandId` node, admitted
553
+ * at the CONNECTION level (all a circuit-relay reservation needs) for
554
+ * `DELEGATE_GRANT_TTL_MS`. Called by the strand-addr responder after its
555
+ * authorized-membership gate has passed; public so tests can drive the
556
+ * admission policy without a full strand launch. A re-announce for the same
557
+ * (announcer, strand) REPLACES the previous delegate rather than
558
+ * accumulating. Never honored by {@link authorizeInboundControlStream}.
559
+ */
560
+ grantDelegateAdmission(announcerPeerId: string, strandId: string, delegatePeerId: string): void;
561
+ /** Is `remotePeerId` covered by a live delegate admission grant? */
562
+ hasDelegateAdmission(remotePeerId: string): boolean;
563
+ /**
564
+ * The admission checks SHARED by the connection gate
565
+ * ({@link admitInboundControlConnection}) and the per-stream control-DB gate
566
+ * ({@link authorizeInboundControlStream}) — factored so the two layers cannot
567
+ * drift. All in-memory and cheap (the stream gate runs this per inbound
568
+ * stream). Returns true when the peer must be admitted BEFORE any membership
569
+ * source is consulted:
570
+ * - the node is not fully up (`start()` in progress / DB torn down) — both
571
+ * gates exist from libp2p bring-up, before the control DB does;
572
+ * - the trusted-owner anchor is absent or empty — an un-enrolled node has
573
+ * no basis to judge anyone and MUST accept its enrollment seed;
574
+ * - the peer is one of the configured control bootstrap/relay nodes —
575
+ * operator-configured infrastructure, not cadre members.
576
+ * False only means "no unconditional admit": the caller then judges the peer
577
+ * against its own membership source (a live DB read for the fail-open
578
+ * connection gate; the materialized snapshot for the fail-closed stream gate).
579
+ */
580
+ private admitControlPeerUnconditionally;
581
+ /**
582
+ * Per-stream authorization for the four Optimystic control-DB protocols
583
+ * (`/optimystic/control-<party>/{repo,cluster,sync,block-transfer}/…`),
584
+ * wired as `authorizeInboundStream` in {@link createControlNode} — the
585
+ * fail-closed layer behind the fail-open connection gater. Control node
586
+ * ONLY: strand cohort nodes legitimately serve cross-party peers.
587
+ *
588
+ * The STRICT SUBSET of {@link admitInboundControlConnection}: the same
589
+ * "no basis to judge" admissions (shared via
590
+ * {@link admitControlPeerUnconditionally}), minus the stranger carve-outs
591
+ * (enrollment window, outstanding open invitation, delegate admission
592
+ * grant). The first two exist so a stranger can reach `/sereus/seed/1.0.0`
593
+ * and `/sereus/formation/1.0.0` — neither is gated here, and admitting a
594
+ * stranger to `repo` during an enrollment window is exactly the hole this
595
+ * gate closes. A DELEGATE-admitted connection (a member's strand node
596
+ * holding a circuit-relay reservation, see `delegate-admission.ts`) is
597
+ * likewise exactly a case this gate must still refuse: the delegate gets
598
+ * the connection and never the control DB.
599
+ *
600
+ * Admits when ANY of:
601
+ * 1. a shared-baseline check admits (not running / no DB, empty anchor,
602
+ * configured bootstrap infra);
603
+ * 2. the materialized authorized set is empty — cold start, before the
604
+ * rows that would authorize anyone have replicated in;
605
+ * 3. the peer IS in the materialized authorized set.
606
+ *
607
+ * Deliberately SYNCHRONOUS and pure in-memory — see
608
+ * {@link authorizedControlPeers} for why a control-DB read here would
609
+ * deadlock into mutual denial. With a sync predicate the upstream deadline
610
+ * (`authorizeInboundStreamTimeoutMs`) never trips, so it stays at its
611
+ * default.
612
+ *
613
+ * NOTE: the snapshot keys on PEER ID, so a node with no persistent identity
614
+ * key never lands a `CadrePeer` row ({@link registerSelf} skips) and its
615
+ * siblings deny its control-DB streams once their own snapshot is non-empty —
616
+ * owner status is a KEY, not a peer id, so being the owner does not exempt it.
617
+ * Harmless today (a real deployment persists its identity; the ephemeral-owner
618
+ * case appears only in tests, which configure the owner as bootstrap infra —
619
+ * `push-wake-e2e.integration.ts` design note 3). If an ephemeral-identity node
620
+ * ever becomes a supported deployment, this gate needs a key-based admission.
621
+ */
622
+ private authorizeInboundControlStream;
623
+ /**
624
+ * Refresh {@link authorizedControlPeers} from the control DB, best-effort: a
625
+ * failed read keeps the previous snapshot (never clears it), so a transient
626
+ * DB error can neither flip the stream gate's cold-start carve-out back open
627
+ * nor drop a legitimate member mid-flight. Never rejects.
628
+ *
629
+ * The SOLE caller is {@link drainMembershipGate}, which serializes refreshes —
630
+ * so two reads can no longer settle out of order.
631
+ */
632
+ private refreshAuthorizedControlPeers;
633
+ /**
634
+ * PeerIds of the configured control bootstrap nodes (relay/bootstrap
635
+ * infrastructure — always admitted inbound). Parsed once, lazily; an
636
+ * address without a `/p2p/<id>` component contributes nothing.
637
+ */
638
+ private getBootstrapPeerIds;
252
639
  private createStrandQueryable;
253
640
  /**
254
641
  * Schedule the node's initial self-record publish + ongoing refresh shortly
255
642
  * after start (non-blocking). {@link registerSelf} is idempotent and safely
256
- * no-ops when it cannot yet sign/insert (e.g. authority key not installed),
643
+ * no-ops when it cannot yet sign/insert (e.g. owner key not installed),
257
644
  * so the timer is harmless even when registration only becomes possible later.
258
645
  */
259
646
  private scheduleSelfRegistration;
@@ -266,10 +653,14 @@ export declare class CadreNode implements SAppIdLookup {
266
653
  * (signaling/`p2p-circuit` first), signed with the ed25519 key behind its
267
654
  * PeerId (the resolved node identity from `keyStore`/`config.privateKey`).
268
655
  * - If the row already exists: a self-signed UPDATE bumping `UpdatedAt`.
269
- * - If not, and the node is its own authority: an authority-signed INSERT that
270
- * also carries the self-signature.
271
- * - Otherwise: logs and returns (a non-authority node with no row yet must
272
- * wait for an authority to insert it; it can then self-refresh).
656
+ * - If not, and the node is its own owner: an owner-signed INSERT that
657
+ * also carries the self-signature. That INSERT is idempotent, so if an owner
658
+ * {@link authorizePeer} of this node's own id seated the row inside the
659
+ * read-then-insert window it no-ops — the publish then falls through to the
660
+ * UPDATE path (re-reading and re-signing against the row that landed) rather
661
+ * than leaving the authorize's null `Sig` in place, and reports `refreshed`.
662
+ * - Otherwise: logs and returns (a non-owner node with no row yet must
663
+ * wait for an owner to insert it; it can then self-refresh).
273
664
  *
274
665
  * Safe to call repeatedly (heartbeat / address-change driven); each successful
275
666
  * publish strictly increases `UpdatedAt`. Concurrent calls are collapsed into a
@@ -281,6 +672,13 @@ export declare class CadreNode implements SAppIdLookup {
281
672
  registerSelf(): Promise<SelfRegistrationOutcome>;
282
673
  /** The body of {@link registerSelf}; serialised by its single-flight guard. */
283
674
  private publishSelfRecord;
675
+ /**
676
+ * Sign this node's address record for publication, stamped strictly later than
677
+ * `existing` (the row it is about to replace, or null for a first insert) so a
678
+ * same-millisecond re-publish still satisfies the monotonic `UpdatedAt` rule the
679
+ * `CadrePeer.AuthorizedUpdate` self-branch enforces.
680
+ */
681
+ private signSelfRecord;
284
682
  /**
285
683
  * The ed25519 keypair (base64url) the node signs its own record with — the key
286
684
  * behind its libp2p PeerId. Sourced from the resolved {@link identityKey}
@@ -291,23 +689,23 @@ export declare class CadreNode implements SAppIdLookup {
291
689
  */
292
690
  private getSelfSigningKey;
293
691
  /**
294
- * The authority keypair (base64url Ed25519) derived from this node's resolved
295
- * identity key. In the single-key reference model the authority signing key is
296
- * *derived from* the node identity (see {@link authorityKeyFromLibp2p}), so the
692
+ * The owner keypair (base64url Ed25519) derived from this node's resolved
693
+ * identity key. In the single-key reference model the owner signing key is
694
+ * *derived from* the node identity (see {@link ed25519KeyPairFromLibp2p}), so the
297
695
  * same key material protected in a secure enclave backs both.
298
696
  *
299
- * Exposed so the hosting app retains control of authority genesis: cadre-core
697
+ * Exposed so the hosting app retains control of owner genesis: cadre-core
300
698
  * resolves + protects the identity, then the app sources this pair to drive
301
- * `ensureAuthorityKey(pub)` + `initializeSeedBootstrap(priv)` itself — cadre-core
302
- * never silently runs genesis. A future separate-authority slot would return a
699
+ * `ensureOwnerKey(pub)` + `initializeSeedBootstrap(priv)` itself — cadre-core
700
+ * never silently runs genesis. A future separate-owner slot would return a
303
701
  * distinct key here instead of the identity-derived one.
304
702
  *
305
- * @returns The base64url seed/public-key authority pair.
703
+ * @returns The base64url seed/public-key owner pair.
306
704
  * @throws If called before {@link start} has resolved the identity, or when the
307
705
  * node runs on an ephemeral libp2p key (no `keyStore`/`privateKey` configured),
308
706
  * since that key is internal to libp2p and not exposed.
309
707
  */
310
- getIdentityAuthorityKey(): AuthorityKeyPair;
708
+ getIdentityOwnerKey(): Ed25519KeyPair;
311
709
  /**
312
710
  * Collect this node's current dialable addresses for publication, signaling
313
711
  * (`/p2p-circuit`) first. Prefers the best invite/NAT-resolved set and folds
@@ -366,7 +764,7 @@ export declare class CadreNode implements SAppIdLookup {
366
764
  *
367
765
  * A per-peer failure (NAT, offline, relay down, connection-gater denial) is
368
766
  * logged and swallowed so one unreachable sibling never aborts the pass —
369
- * exactly like {@link SeedBootstrapService.applySeed}'s authority-dial loop. A
767
+ * exactly like {@link SeedBootstrapService.applySeed}'s owner-dial loop. A
370
768
  * failed dial is simply retried on the next pass.
371
769
  */
372
770
  private dialControlSibling;
@@ -386,6 +784,65 @@ export declare class CadreNode implements SAppIdLookup {
386
784
  * parse/lookup failure — never throws.
387
785
  */
388
786
  private peerStoreAddrs;
787
+ /**
788
+ * Retain a just-applied seed's owner-flagged peers as cold-start bootstrap
789
+ * dial targets (see {@link bootstrapPeerStore}).
790
+ *
791
+ * Called for every seed this node accepts, on BOTH intake paths — the
792
+ * {@link applySeed} wrapper (which may run on a throwaway service, so the
793
+ * service itself is the wrong place to keep this) and the inbound
794
+ * `/sereus/seed/1.0.0` handler via `onSeedApplied`. Peers with no address are
795
+ * skipped: there is nothing to dial.
796
+ *
797
+ * `isOwner` is the seed's own claim about its peers, exactly as
798
+ * `SeedBootstrapService.applySeed` uses it to choose its dial targets. That is
799
+ * sound here for the same reason it is sound there — the whole seed is
800
+ * signature-checked against a trust-anchored signer before this runs, and the
801
+ * flag only *selects a dial target*; a dial grants no authority.
802
+ *
803
+ * Self is excluded. `createSeed` projects EVERY `CadrePeer` row, so an owner
804
+ * that applies a seed minted after it joined finds itself in the owner list;
805
+ * retaining that would make the cold-start pass dial this node forever (the
806
+ * steady-state pass filters self out of its sibling list for the same reason).
807
+ */
808
+ private recordSeedBootstrapPeers;
809
+ /**
810
+ * Cold-start branch of the reconcile pass: re-dial the seed's owner peers
811
+ * while the control database still has no siblings to dial.
812
+ *
813
+ * `SeedBootstrapService.applySeed` dials those owners exactly ONCE, best-effort.
814
+ * When that single dial fails — owner momentarily down, relay reservation not
815
+ * up yet, NAT traversal lost the race — the joining node has an empty
816
+ * `CadrePeer` table and no connection, and the steady-state pass above cannot
817
+ * help: it dials only siblings enumerated from that very table. Retrying here
818
+ * is the only way back in.
819
+ *
820
+ * Unbounded, at the reconcile cadence, with no backoff: the branch is already
821
+ * gated by "the control database has no siblings", so it stops the moment the
822
+ * node is actually in the party, and a node that is stranded MUST keep trying —
823
+ * a give-up rule would turn a transient outage into a permanent one. The steady
824
+ * cost is one dial per bootstrap peer per pass (seeds nominate one or a few
825
+ * owners), each failing fast against an unreachable address.
826
+ * NOTE: if seeds ever carry many owner peers, or the reconcile cadence tightens
827
+ * well below its 15 s default, add per-peer backoff here.
828
+ *
829
+ * Best-effort per peer, exactly like {@link dialControlSibling}: one dead
830
+ * address never aborts the pass.
831
+ */
832
+ private dialColdStartBootstrap;
833
+ /**
834
+ * Bind a bootstrap peer's seed addresses to its peer id, so the dial
835
+ * authenticates the peer it is aiming at rather than trusting whoever answers.
836
+ *
837
+ * An address that already carries `/p2p/<id>` must carry THIS peer's id or it
838
+ * is dropped (a seed that disagrees with itself is not a dial target); one
839
+ * that carries none — an `addressOverrides`-published record, say — gets the
840
+ * id encapsulated. Unencapsulatable addresses are dropped rather than dialed
841
+ * bare.
842
+ */
843
+ private bootstrapDialAddrs;
844
+ /** Dial one cold-start bootstrap peer, best-effort. Returns whether it connected. */
845
+ private dialBootstrapPeer;
389
846
  /**
390
847
  * Whether a control write happening now would commit local-only. A sound lower
391
848
  * bound is "no connected control peers": 0 connections ⇒ the block's cluster is
@@ -395,21 +852,35 @@ export declare class CadreNode implements SAppIdLookup {
395
852
  * is caught by the cohort's periodic pull-on-read instead. Re-issuing an already-
396
853
  * replicated row is harmless (an idempotent monotonic bump), so the coarse proxy
397
854
  * is acceptable as the agreed first cut.
855
+ *
856
+ * Reads {@link getControlConnectionCount} — the public, embedder-facing form of
857
+ * the same sample — so the two cannot drift.
398
858
  */
399
859
  private committedAlone;
400
860
  /**
401
- * Record (or clear) a just-committed authority membership write in the
861
+ * Record (or clear) a just-committed owner membership write in the
402
862
  * write-while-alone re-replication queue. If the control node had no connections
403
863
  * at commit, the Optimystic write was local-only and must be re-issued once the
404
864
  * cohort grows; otherwise the write replicated and any stale queue entry for this
405
865
  * subject is dropped.
406
866
  *
407
- * A `remove` that commits alone is a security-relevant durability gap (a revoked
408
- * peer may persist as a member elsewhere), so it is logged loudly — and, because
409
- * a physical delete leaves no local trace to re-issue from, its on-growth
410
- * re-issue is best-effort only (see {@link drainPendingPeerWrites}).
867
+ * A `remove` never queues here — a delete's re-replicable half is its
868
+ * `Revocation` tombstone, which the committed-delete seam routes into
869
+ * {@link pendingRevocations} ({@link noteGuardedDelete}). The `remove` arm only
870
+ * drops any queued authorize for the subject (the row is gone; re-issuing the
871
+ * insert would resurrect it) and, when the commit was alone, logs the
872
+ * security-relevant window loudly.
411
873
  */
412
874
  private noteControlWrite;
875
+ /**
876
+ * Committed-delete seam handler ({@link GuardedDeleteListener}, wired in
877
+ * {@link start}): a guarded-table delete and its `Revocation` tombstone
878
+ * committed. If the node was alone the tombstone is local-only — queue it for
879
+ * an owner-signed re-issue on cohort growth; otherwise it broadcast, so drop
880
+ * any stale queue entry for the stamp. Synchronous by contract — bookkeeping
881
+ * only, never throws into the delete path.
882
+ */
883
+ private noteGuardedDelete;
413
884
  /**
414
885
  * Drain the write-while-alone re-replication queue: re-issue the writes that
415
886
  * committed local-only now that the cohort can broadcast them. Public so the
@@ -430,45 +901,84 @@ export declare class CadreNode implements SAppIdLookup {
430
901
  * the drain.
431
902
  */
432
903
  private retouchSelfDeviceToken;
904
+ /**
905
+ * Re-issue `Revocation` tombstones so a removal that committed while alone
906
+ * still reaches the cohort. Two regimes, one method:
907
+ *
908
+ * - **First successful pass per process** ({@link reissuedHeldRevocations}
909
+ * false): sweep EVERY locally-held tombstone from `queryRevocations()` — the
910
+ * only cover for a removal made before this process started, since the
911
+ * in-memory queue does not survive a restart. Queued stamps are part of that
912
+ * same batch (exactly once — one transaction, no double bump).
913
+ * - **Later passes**: only the tombstones queued in-session by
914
+ * {@link noteGuardedDelete}.
915
+ *
916
+ * Owner-gated: a node with no owner key cannot sign a re-issue, so stray queue
917
+ * entries are dropped (mirrors {@link drainPendingPeerWrites}). Best-effort — a
918
+ * failure is logged and leaves the queue (and the sweep flag) untouched for the
919
+ * next growth edge. A successful `reissueRevocations` exec is NOT proof of
920
+ * broadcast (the connection that fired the growth edge may not be in the
921
+ * affected block's cluster — see
922
+ * tickets/backlog/control-rereplication-broadcast-confirmation); the drain
923
+ * inherits that known gap, and a full disconnect→reconnect (or the next
924
+ * process's sweep) re-covers it.
925
+ *
926
+ * NOTE: the sweep is O(all tombstones ever) row-updates — plus one owner
927
+ * signature each — in one transaction, once per lifetime. `Revocation` is
928
+ * append-only and unbounded growth is declared acceptable for a cadre-sized
929
+ * party. If the tombstone table ever gets large, bound the sweep (e.g. persist
930
+ * a node-local high-water mark of what has been re-issued while connected)
931
+ * instead of re-touching everything; note the `Math.max(...)` spread below also
932
+ * caps out around 10^5 rows (V8 argument limit) before the size becomes merely
933
+ * a latency problem.
934
+ */
935
+ private drainPendingRevocations;
433
936
  /**
434
937
  * One-shot, first-cohort-growth reconstruction of the write-while-alone queue for
435
- * an AUTHORITY node: re-touch every membership row that may be an unreplicated
436
- * authority insert. A row is a candidate iff it is not self (handled by
938
+ * an OWNER node: re-touch every membership row that may be an unreplicated
939
+ * owner insert. A row is a candidate iff it is not self (handled by
437
940
  * {@link registerSelf}), is not already tracked in the in-memory queue (handled by
438
941
  * {@link drainPendingPeerWrites}), and carries no self-`Sig` yet (an
439
- * authority-authored row the peer has not self-published — the only kind safe to
942
+ * owner-authored row the peer has not self-published — the only kind safe to
440
943
  * bump without invalidating a self-signature). O(rows) on the small control
441
- * tables; safe to over-apply (a monotonic authority bump on an already-replicated
944
+ * tables; safe to over-apply (a monotonic owner bump on an already-replicated
442
945
  * row is a no-op-equivalent).
443
946
  *
444
- * A node with no authority private key skips this entirely — it cannot re-sign
445
- * rows for other peers, and rows it merely holds are not its to re-issue. DELETEs
446
- * cannot be reconstructed here (a removed row leaves no local trace and the schema
447
- * carries no tombstone) — see the delete-path note in {@link drainPendingPeerWrites}.
947
+ * A node with no owner private key skips this entirely — it cannot re-sign
948
+ * rows for other peers, and rows it merely holds are not its to re-issue.
949
+ * DELETEs are not reconstructed here — the `Revocation` tombstone sweep in
950
+ * {@link drainPendingRevocations} covers them (a removed row leaves no
951
+ * `CadrePeer` trace, but its tombstone is re-issuable).
448
952
  */
449
953
  private reconstructAuthoredMembership;
450
954
  /**
451
- * Drain the in-session write-while-alone queue: re-issue each pending authority
452
- * membership write now that the cohort can broadcast it. Sequential (no fan-out)
453
- * to avoid a thundering re-touch; an entry is cleared only on success, so a
454
- * failure (or a still-alone re-commit) leaves it queued for the next growth.
455
- *
456
- * - `authorize`: re-issue as an idempotent monotonic authority UPDATE
457
- * ({@link reissuePeerAuthorize}). Skipped (and cleared) if the row vanished
458
- * (raced a delete) or now carries a self-`Sig` (the peer self-published and owns
459
- * its republish).
460
- * - `remove` (security-relevant): a removePeer-while-alone physically removed the
461
- * row locally, so a re-issued DELETE matches nothing and does NOT propagate —
462
- * full delete durability requires a schema tombstone (deferred:
463
- * `control-delete-while-alone-tombstone`). We still attempt the re-issue
464
- * (harmless, and it propagates in the row-still-present case); the commit-alone
465
- * was already logged loudly in {@link noteControlWrite}.
955
+ * Body of {@link reconstructAuthoredMembership}'s sweep: re-issue each candidate
956
+ * owner-authored row, returning how many were re-touched. A per-row failure is
957
+ * logged and skipped; a teardown mid-sweep stops it.
958
+ */
959
+ private reissueAuthoredMembershipRows;
960
+ /**
961
+ * Drain the in-session write-while-alone queue: re-issue each pending owner
962
+ * authorize as an idempotent monotonic owner UPDATE
963
+ * ({@link reissuePeerAuthorize}) now that the cohort can broadcast it.
964
+ * Sequential (no fan-out) to avoid a thundering re-touch; an entry is cleared
965
+ * only on success, so a failure (or a still-alone re-commit) leaves it queued
966
+ * for the next growth. An entry is skipped (and cleared) if the row vanished
967
+ * (raced a delete) or now carries a self-`Sig` (the peer self-published and owns
968
+ * its republish). Removals are drained by {@link drainPendingRevocations}, not
969
+ * here.
466
970
  */
467
971
  private drainPendingPeerWrites;
468
972
  /**
469
- * Re-issue an authority membership row as a monotonic authority UPDATE: bump
973
+ * Body of {@link drainPendingPeerWrites}' loop. An entry is cleared only on
974
+ * success (or when there is nothing left to re-issue); a failure is logged and
975
+ * the entry stays queued for the next cohort growth.
976
+ */
977
+ private reissuePendingPeerWrites;
978
+ /**
979
+ * Re-issue an owner membership row as a monotonic owner UPDATE: bump
470
980
  * `UpdatedAt` strictly above the stored value (and the wall clock) and re-sign via
471
- * the authority branch of `CadrePeer.AuthorizedUpdate`. The row already exists
981
+ * the owner branch of `CadrePeer.AuthorizedUpdate`. The row already exists
472
982
  * locally (it committed there, just local-only), so this is an UPDATE, not the
473
983
  * original INSERT.
474
984
  */
@@ -508,11 +1018,11 @@ export declare class CadreNode implements SAppIdLookup {
508
1018
  * for any member — the `AuthorizedUpdate` self-branch verifies the new `Sig`
509
1019
  * against the bound `CadrePeer.PublicKey`). Platform/Token may change here
510
1020
  * (rotation / platform switch / reinstall are all normal self-updates).
511
- * - If not, and the node holds an authority service: an authority-signed INSERT
1021
+ * - If not, and the node holds an owner service: an owner-signed INSERT
512
1022
  * that also carries the self-signature.
513
1023
  * - Otherwise: throws. Like `CadrePeer`, the first `DeviceToken` row requires an
514
- * authority signature; a non-authority peer (e.g. a phone) must have its row
515
- * seeded by an authority — typically the server it enrolled with — before it
1024
+ * owner signature; a non-owner peer (e.g. a phone) must have its row
1025
+ * seeded by an owner — typically the server it enrolled with — before it
516
1026
  * can self-refresh. (Establishing that phone→server registration handshake is
517
1027
  * the downstream "RN registration" ticket; this node only owns the cadre-core
518
1028
  * write path.)
@@ -523,7 +1033,7 @@ export declare class CadreNode implements SAppIdLookup {
523
1033
  * @param platform - `'fcm'` (Android/Firebase) or `'apns'` (Apple).
524
1034
  * @param token - the opaque platform device/registration token.
525
1035
  * @throws if the node is not started, exposes no self-signing key, or has no
526
- * existing row and no authority service to self-insert.
1036
+ * existing row and no owner service to self-insert.
527
1037
  */
528
1038
  registerDeviceToken(platform: PushPlatform, token: string): Promise<void>;
529
1039
  /**
@@ -536,8 +1046,10 @@ export declare class CadreNode implements SAppIdLookup {
536
1046
  * 2. `publicKey <-> peerId` binding — the stored key's libp2p identity is the
537
1047
  * requested peerId,
538
1048
  * 3. a `DeviceToken` row exists with a known {@link PushPlatform},
539
- * 4. self-signature verifies against the bound `CadrePeer.PublicKey`,
540
- * 5. freshness — `updatedAt` is positive and within `opts.maxAgeMs` (default:
1049
+ * 4. the row's `StampId` is NOT retired in `CadreControl.Revocation` — the
1050
+ * read-side half of the clear (see below),
1051
+ * 5. self-signature verifies against the bound `CadrePeer.PublicKey`,
1052
+ * 6. freshness — `updatedAt` is positive and within `opts.maxAgeMs` (default:
541
1053
  * no ceiling, since a push token is valid until it rotates).
542
1054
  *
543
1055
  * A peer that is not a current member, or whose token has no backing `CadrePeer`
@@ -548,11 +1060,11 @@ export declare class CadreNode implements SAppIdLookup {
548
1060
  /**
549
1061
  * Delete this node's own `DeviceToken` row (logout / token invalidation). No-op
550
1062
  * when no row exists. Like {@link registerDeviceToken}'s first insert, the delete
551
- * is gated on an authority signature (`DeviceToken.AuthorizedInsert` covers insert
552
- * AND delete), so it requires this node's authority service; a non-authority peer
553
- * must route the clear through its authority (downstream RN registration path).
1063
+ * is gated on an owner signature (`DeviceToken.AuthorizedInsert` covers insert
1064
+ * AND delete), so it requires this node's owner service; a non-owner peer
1065
+ * must route the clear through its owner (downstream RN registration path).
554
1066
  *
555
- * @throws if the node is not started, or a row exists but no authority service is
1067
+ * @throws if the node is not started, or a row exists but no owner service is
556
1068
  * available to sign the delete.
557
1069
  */
558
1070
  clearDeviceToken(): Promise<void>;
@@ -561,10 +1073,10 @@ export declare class CadreNode implements SAppIdLookup {
561
1073
  * unregistered during a push-wake fan-out. Unlike {@link clearDeviceToken}
562
1074
  * (self-only — it hardcodes the local peerId), this takes an arbitrary peerId.
563
1075
  *
564
- * - When this node holds an authority seed service, it deletes the row
565
- * (`deleteDeviceToken` is authority-gated and accepts any peerId), so the peer
1076
+ * - When this node holds an owner seed service, it deletes the row
1077
+ * (`deleteDeviceToken` is owner-gated and accepts any peerId), so the peer
566
1078
  * is not retried until it re-registers.
567
- * - When this node is NOT an authority it cannot delete the row, so it only logs
1079
+ * - When this node is NOT an owner it cannot delete the row, so it only logs
568
1080
  * that a re-registration is needed. The fan-out's own in-memory dead-token set
569
1081
  * is what actually stops re-pushing to the dead token this process — see
570
1082
  * {@link PushFanoutService}. That set is acceptably lossy across restarts (a
@@ -659,24 +1171,32 @@ export declare class CadreNode implements SAppIdLookup {
659
1171
  /**
660
1172
  * Add a strand with its sApp configuration.
661
1173
  * The hosting application must provide the sApp schema when creating a strand.
1174
+ *
1175
+ * A rejected call leaves nothing running but DOES leave the sApp config
1176
+ * registered, deliberately: both an explicit retry and the {@link StrandWatcher}'s
1177
+ * automatic relaunch need it. So once the strand's row is visible on the control
1178
+ * network (via {@link publishStrand} or another member), a failed launch keeps
1179
+ * being re-attempted in the background — each failure re-emitting `strand:error`
1180
+ * — until it succeeds. {@link detachStrand} (reached via {@link stopStrand}) is
1181
+ * what abandons a strand for good.
662
1182
  */
663
1183
  addStrand(config: StrandConfig): Promise<StrandInstance>;
664
1184
  /**
665
1185
  * Publish a strand row to the shared control database under this node's own
666
- * authority identity, so other cadre members discover it via control-network
1186
+ * owner identity, so other cadre members discover it via control-network
667
1187
  * sync (their {@link StrandWatcher} fires `strand:discovered`).
668
1188
  *
669
- * This is the authority-signed `Strand` INSERT that {@link addStrand}
1189
+ * This is the owner-signed `Strand` INSERT that {@link addStrand}
670
1190
  * deliberately omits: `addStrand` only starts the LOCAL strand instance,
671
1191
  * whereas publishing makes the strand visible cadre-wide. A typical creator
672
1192
  * does both (start locally + publish); a discovering peer only does
673
1193
  * `addStrand` (the row already exists).
674
1194
  *
675
1195
  * The insert is signed with the ed25519 key behind this node's PeerId — which
676
- * {@link authorityKeyFromLibp2p} also exposes as the node's authority keypair,
677
- * so peer identity and authority key are one and the same. That key must be
678
- * enrolled in `AuthorityKey` (e.g. via {@link ControlDatabase.ensureAuthorityKey}
679
- * at genesis) or the schema's `Strand.Authorized` constraint rejects the write.
1196
+ * {@link ed25519KeyPairFromLibp2p} also exposes as the node's owner keypair,
1197
+ * so peer identity and owner key are one and the same. That key must be
1198
+ * enrolled in `OwnerKey` (e.g. via {@link ControlDatabase.ensureOwnerKey}
1199
+ * at genesis) or the schema's `Strand.AuthorizedInsert` constraint rejects the write.
680
1200
  * Failing loudly here is intentional: a silently-unpublished strand would run
681
1201
  * as a local-only island that no peer could ever discover or join.
682
1202
  *
@@ -684,14 +1204,70 @@ export declare class CadreNode implements SAppIdLookup {
684
1204
  * {@link addStrand}).
685
1205
  * @param type - `'o'` for open (default) or `'c'` for closed.
686
1206
  * @param memberPrivateKey - Optional membership key for a closed strand.
687
- * @throws if the node is not started, exposes no authority signing key, or the
688
- * control DB rejects the (unauthorized) insert.
1207
+ * @throws if the node is not started, exposes no owner signing key, the id is blank, or
1208
+ * the control DB rejects the (unauthorized) insert.
689
1209
  */
690
1210
  publishStrand(strandId: string, type?: 'o' | 'c', memberPrivateKey?: string): Promise<void>;
691
1211
  /**
692
- * Publish an authority-signed `FormationInvite` (open-invitation token) to the
1212
+ * Remove this party's `Strand` row from the shared control database — the owner-signed
1213
+ * inverse of {@link publishStrand}.
1214
+ *
1215
+ * Party-wide, unlike {@link stopStrand}, which only stops the strand on THIS node: every
1216
+ * cadre node watching the table sees the row vanish on its next poll (default 5 s) and
1217
+ * stops its own instance. This node converges immediately — the method forces a watcher
1218
+ * poll and stops any still-running local instance before resolving. Removing the row
1219
+ * removes OUR party's participation only: other parties in the strand keep their own
1220
+ * rows and the strand network carries on, so no cross-party sign-off is involved — this
1221
+ * is an owner-signed control-plane write like every other, not "destroy the network".
1222
+ *
1223
+ * Irreversible for a closed strand (`Type='c'`): the row carries `MemberPrivateKey`,
1224
+ * this party's membership secret for that network, and it is stored nowhere else.
1225
+ * With it gone the party can never again admit a member to that closed strand, and a
1226
+ * re-published row would carry a DIFFERENT key that does not match the membership
1227
+ * already written into the strand's RBAC layer. The strand id itself is NOT
1228
+ * blacklisted: a fresh owner-signed {@link publishStrand} re-seats it under a new
1229
+ * stamp — only the unsigned consent re-seat is permanently foreclosed (the removal's
1230
+ * `Revocation` tombstone names the id, which the consent branch of
1231
+ * `Strand.AuthorizedInsert` refuses ever after). Outstanding `FormationInvite` rows
1232
+ * bound to the removed strand are unredeemable while the row is absent (the formation
1233
+ * recorder resolves the strand as missing and rejects cleanly).
1234
+ *
1235
+ * Convergence caveats, same class as {@link enrollValidationKey}'s: a sibling that has
1236
+ * not yet synced keeps running the strand until its own watcher observes the missing
1237
+ * row; a sibling whose `strandFilter` never admitted this strand never observes the
1238
+ * removal AT ALL and keeps running its instance indefinitely — opting out of watching a
1239
+ * strand is also opting out of its party-wide removal, so such a node's only stop is its
1240
+ * own local {@link stopStrand}/`unpublishStrand` call; and a removal committed while
1241
+ * ALONE (0 control connections) deletes the row local-only. The accompanying
1242
+ * `Revocation` tombstone IS queued and re-issued on the next cohort-growth edge
1243
+ * ({@link noteGuardedDelete}/{@link drainPendingRevocations}), so the stamp retirement
1244
+ * — and with it the consent re-seat foreclosure — does propagate; but the physical row
1245
+ * deletion cannot be replayed, and `queryStrands` reads raw (no retired-stamp filter),
1246
+ * so siblings that already hold the row keep running the strand until the collection
1247
+ * itself converges (logged loudly; see the delete-while-alone durability note in
1248
+ * docs/architecture.md).
1249
+ *
1250
+ * A no-op (no throw, no tombstone) when the row is already absent — but a
1251
+ * locally-running instance of that id is still stopped.
1252
+ *
1253
+ * NOTE: control-plane only — the strand's local durable storage is retained. Stopping the
1254
+ * instance closes the `StrandDatabase` and its libp2p node but purges no blocks, so a
1255
+ * closed strand's content stays readable on disk to anyone with the data directory. If a
1256
+ * caller ever needs removal to mean "and erase the local copy", that is a separate purge
1257
+ * step, not a widening of this method.
1258
+ *
1259
+ * @param strandId - The `Strand` row id to remove (as passed to {@link publishStrand}).
1260
+ * @throws if the node is not started, exposes no owner signing key, the id is blank, or
1261
+ * the signer is not an enrolled owner (the schema's `Strand.AuthorizedDelete` rejects
1262
+ * the write and the row survives). A rejection does NOT imply the row survived: the
1263
+ * local stop runs after the control-plane delete has already committed, so a failure
1264
+ * there throws over a completed removal.
1265
+ */
1266
+ unpublishStrand(strandId: string): Promise<void>;
1267
+ /**
1268
+ * Publish an owner-signed `FormationInvite` (open-invitation token) to the
693
1269
  * shared control database, so a later {@link formStrand} redemption can be
694
- * validated against it (the consent branch of `Strand.Authorized`).
1270
+ * validated against it (the consent branch of `Strand.AuthorizedInsert`).
695
1271
  *
696
1272
  * Counterpart to {@link createOpenInvitation}, which only mints the
697
1273
  * out-of-band {@link OpenInvitation} envelope: persisting the matching
@@ -700,8 +1276,8 @@ export declare class CadreNode implements SAppIdLookup {
700
1276
  * from this row. A host minting a closed-strand invite does both (mint +
701
1277
  * publish), exactly as the integration harness's `createInvitation` does.
702
1278
  *
703
- * Signs with the same self-authority key as {@link publishStrand} (the ed25519
704
- * key behind this node's PeerId, which must be an enrolled `AuthorityKey`).
1279
+ * Signs with the same self-owner key as {@link publishStrand} (the ed25519
1280
+ * key behind this node's PeerId, which must be an enrolled `OwnerKey`).
705
1281
  * Throws loudly if the node isn't started or exposes no signing key.
706
1282
  *
707
1283
  * @param token - Invitation token (the `FormationInvite` primary key); use the
@@ -716,11 +1292,81 @@ export declare class CadreNode implements SAppIdLookup {
716
1292
  validationUrl?: string;
717
1293
  strandId?: string;
718
1294
  }): Promise<void>;
1295
+ /**
1296
+ * Enroll an approver public key allowed to sign off on `ValidationUrl` redemptions.
1297
+ *
1298
+ * A `FormationInvite` carrying a `ValidationUrl` is only redeemable when the approval
1299
+ * that comes back from that URL is signed by a key present in `CadreControl.ValidationKey`
1300
+ * — this is how a party says which outside approver it trusts. Without an enrollment,
1301
+ * every such invitation is unredeemable.
1302
+ *
1303
+ * Signs with the same self-owner key as {@link publishStrand} (the ed25519 key behind
1304
+ * this node's PeerId, which must be an enrolled `OwnerKey`). Throws loudly if the node
1305
+ * isn't started, exposes no signing key, or the key is blank — a blank key would reach
1306
+ * the database and fail the signature CHECK as an opaque constraint error.
1307
+ *
1308
+ * Enrolling this node's OWN owner key is permitted and harmless: the domain/action tags
1309
+ * baked into each authorization digest mean an owner-key signature can never satisfy the
1310
+ * approval rule and an approval can never satisfy an owner rule. No guard is needed.
1311
+ *
1312
+ * NOTE: enrollment is replicated control state, so a key enrolled here is visible to a
1313
+ * sibling cadre node only once control replication converges. A redemption that arrives
1314
+ * at a node which has not caught up is refused as not-enrolled — the same convergence
1315
+ * gap the schema records on `Strand.StampId`. Enroll before circulating the invitations
1316
+ * that depend on the key.
1317
+ *
1318
+ * @param key - Approver public key to enroll (base64url ed25519 public key).
1319
+ */
1320
+ enrollValidationKey(key: string): Promise<void>;
1321
+ /**
1322
+ * Remove an approver public key.
1323
+ *
1324
+ * Narrows who may approve FUTURE redemptions ONLY. The schema's approval CHECKs run at
1325
+ * write time, so a join that was already approved by this key stays valid — removal is
1326
+ * not retroactive and does not re-examine committed rows.
1327
+ *
1328
+ * Rotation is therefore add-then-remove, in that order: removing the only enrolled key
1329
+ * while `ValidationUrl` invitations are outstanding makes every one of them unredeemable
1330
+ * until a new key is enrolled.
1331
+ *
1332
+ * Removing a key that is not enrolled is a silent no-op (no throw, no `Revocation`
1333
+ * tombstone) — see {@link ControlDatabase.deleteValidationKey}.
1334
+ *
1335
+ * @param key - Approver public key to remove (base64url ed25519 public key).
1336
+ */
1337
+ removeValidationKey(key: string): Promise<void>;
1338
+ /**
1339
+ * The approver keys currently enrolled, sorted. Read-only, so unlike
1340
+ * {@link enrollValidationKey} / {@link removeValidationKey} it needs no owner signing
1341
+ * key — only a started node.
1342
+ */
1343
+ listValidationKeys(): Promise<string[]>;
1344
+ /**
1345
+ * Resolve the owner keypair every owner-signed control write needs — {@link publishStrand},
1346
+ * {@link unpublishStrand}, {@link publishFormationInvite}, {@link enrollValidationKey},
1347
+ * {@link removeValidationKey}
1348
+ * — failing loudly in one two-part shape (not started / no signing key, naming owner
1349
+ * genesis as the fix). Narrows `controlDatabase` for the caller: a non-null return means
1350
+ * `this.controlDatabase` is non-null too.
1351
+ *
1352
+ * @param action - Infinitive phrase naming the attempted write, e.g. `'enroll a
1353
+ * validation key'`; interpolated into both messages.
1354
+ */
1355
+ private requireOwnerSigningKey;
719
1356
  /**
720
1357
  * Shared strand launch path for both the explicit (`addStrand`) and the
721
1358
  * control-discovered (`handleStrandAdded`) entry points. Resolves the cohort
722
1359
  * seed, selects the mode, starts the strand, and registers it with the
723
1360
  * hibernation manager before emitting `strand:started`.
1361
+ *
1362
+ * Idempotent no-op when the strand manager already tracks `strand.Id` — the
1363
+ * ordinary founding sequence is `addStrand` (starts it locally) followed by
1364
+ * `publishStrand` (makes it visible), so this node's own `StrandWatcher`
1365
+ * rediscovers a row it already started and calls this again via
1366
+ * `handleStrandAdded`. Without the guard that re-entry would resolve a fresh
1367
+ * (already-connected) cohort seed and re-emit `strand:started` for an
1368
+ * instance that never stopped. The guard runs before the cohort-seed RPC
1369
+ * fan-out, so a rediscovery costs nothing beyond the map lookup.
724
1370
  */
725
1371
  private launchStrand;
726
1372
  /**
@@ -736,8 +1382,61 @@ export declare class CadreNode implements SAppIdLookup {
736
1382
  * empty (`[]`) — mode still follows membership, and the empty seed self-heals
737
1383
  * on the next resume / check-in pass. Returns an empty seed when the control DB
738
1384
  * or node is absent (not yet started / torn down).
1385
+ *
1386
+ * When `delegatePeerId` is given (a strand launch/resume is imminent), the
1387
+ * pass doubles as the delegate ANNOUNCEMENT: our circuit relays are merged
1388
+ * into the RPC targets and every request carries the delegate peerId, so each
1389
+ * receiver records an admission grant BEFORE `libp2p.start()` (re-)dials the
1390
+ * relay reservation — the gate denial there is fatal, not degraded. A
1391
+ * party-member relay answers the RPC (its control node admits us as a
1392
+ * member); a dedicated ops/ relay does not speak the protocol and the
1393
+ * per-peer failure folds to `[]` — harmless, it has no membership gate and
1394
+ * needs no grant. The relay's direct addr rides along as the dial fallback
1395
+ * for a relay we are not yet connected to.
739
1396
  */
740
1397
  private resolveCohortSeed;
1398
+ /**
1399
+ * The circuit relays this node's strand nodes would reserve through: the
1400
+ * union of the RESOLVED listen addrs' circuits — a hand-written
1401
+ * `network.listenAddrs` circuit entry or a `network.relayAddrs` entry, which
1402
+ * the strand node inherits verbatim either way — and the control node's own
1403
+ * live `/p2p-circuit` multiaddrs (a reservation this node discovered rather
1404
+ * than configured).
1405
+ *
1406
+ * NOTE: a relay the STRAND node discovers on its own (autorelay — in neither
1407
+ * source) gets no announcement, and a membership-gated one will deny it; fine
1408
+ * now, every realistic topology feeds one of the two sources.
1409
+ */
1410
+ private circuitRelayTargets;
1411
+ /**
1412
+ * Record the announce timestamps {@link refreshDelegateGrants} throttles on,
1413
+ * for every relay a delegate-carrying announce pass dialed.
1414
+ *
1415
+ * Recorded OPTIMISTICALLY at announce time: `collectStrandAddrs` folds
1416
+ * per-peer failure to `[]` and reports no per-peer success, and threading
1417
+ * success out would change its API for little gain. A failed INITIAL announce
1418
+ * is fatal-at-start anyway (the relay denies the reservation,
1419
+ * `libp2p.start()` throws, and wake/check-in re-resume retries with a fresh
1420
+ * announce); a failed REFRESH retries within `DELEGATE_GRANT_TTL_MS / 2`
1421
+ * (15 min), still inside the 30 min TTL.
1422
+ */
1423
+ private recordDelegateAnnounces;
1424
+ /**
1425
+ * Re-announce every running strand's delegate peerId to this node's circuit
1426
+ * relays, throttled to once per `DELEGATE_GRANT_TTL_MS / 2` per
1427
+ * (relay, strand). A grant must outlive the reservation: a dropped relay
1428
+ * connection makes the strand's circuit-relay transport re-dial and face the
1429
+ * connection gate again, so the relay must still hold a live grant then.
1430
+ * RELAY targets only — siblings get their announce on every launch/resume
1431
+ * seed pass, and a grant only matters where a reservation can be re-dialed.
1432
+ *
1433
+ * Strands announce CONCURRENTLY: this runs ahead of the reconcile pass's
1434
+ * sibling enumeration, and one unreachable relay costs a dial timeout per
1435
+ * target, which must not stack up per strand.
1436
+ */
1437
+ private refreshDelegateGrants;
1438
+ /** One strand's share of {@link refreshDelegateGrants}: announce to the relays whose grant is due. */
1439
+ private announceDelegateToDueRelays;
741
1440
  /**
742
1441
  * The local strand instance's dialable strand-network multiaddrs for the
743
1442
  * strand-addr RPC, ordered signaling-first (reusing the control node's
@@ -749,9 +1448,26 @@ export declare class CadreNode implements SAppIdLookup {
749
1448
  */
750
1449
  private getStrandMultiaddrs;
751
1450
  /**
752
- * Remove a strand
1451
+ * Stop a strand on THIS node only: untrack it from hibernation, drop its sApp config,
1452
+ * stop the local instance, and emit `strand:stopped`. The shared `Strand` row is left
1453
+ * intact, so on the next node restart (or watcher poll) the strand is rediscovered and
1454
+ * surfaces as `strand:discovered` again. Party-wide removal is {@link unpublishStrand}.
753
1455
  */
754
- removeStrand(strandId: string): Promise<void>;
1456
+ stopStrand(strandId: string): Promise<void>;
1457
+ /**
1458
+ * Local teardown for one strand, shared by the caller-driven {@link stopStrand} and the
1459
+ * watcher-driven `handleStrandRemoved`: untrack hibernation, drop the sApp config, stop
1460
+ * the instance, emit `strand:stopped`. Touches no control-plane row — which side of the
1461
+ * removal the node is on is the caller's concern, not this method's.
1462
+ *
1463
+ * The stop + emit are skipped when the strand manager holds no instance for `strandId` —
1464
+ * e.g. a party owner that published a strand's row but never ran it locally, or an
1465
+ * explicit {@link stopStrand} for an id this node never started. `hibernationManager`
1466
+ * untrack and the `sAppConfigs` delete stay unconditional (both are no-ops when there is
1467
+ * nothing to remove), so a launch that failed before an instance was ever tracked still
1468
+ * gets its stray sApp config cleared.
1469
+ */
1470
+ private detachStrand;
755
1471
  /**
756
1472
  * Record activity on a strand (resets hibernation timer).
757
1473
  *
@@ -828,10 +1544,58 @@ export declare class CadreNode implements SAppIdLookup {
828
1544
  * Get the control network node (for advanced use)
829
1545
  */
830
1546
  getControlNode(): Libp2p | null;
1547
+ /**
1548
+ * Start keeping a relay reservation: dial the given relay(s) from the control
1549
+ * node, ask the first one that answers for a reservation slot, wait until the
1550
+ * resulting `/p2p-circuit` address makes this node dialable — and keep a
1551
+ * supervisor running that re-drives whenever the reservation is later lost.
1552
+ *
1553
+ * For nodes that listen on the bare `/p2p-circuit` SEARCH addr (a browser tab)
1554
+ * rather than a configured `network.relayAddrs` circuit listener — see
1555
+ * `relay-reservation.ts` for why the two are alternatives and why configuring
1556
+ * both is a regression. Opt-in: nothing calls this for the CLI/host nodes.
1557
+ *
1558
+ * Resolves once the FIRST attempt has settled, exactly as it did when it drove
1559
+ * once; the retries continue in the background until {@link stop} or the next
1560
+ * `reserveRelays` call. Fail-soft — never throws. An unreachable relay, a
1561
+ * missing control node, or a timeout all resolve to a non-`reserved` status, so
1562
+ * a caller can await this during startup without a dead relay aborting the node.
1563
+ *
1564
+ * Passing an empty list stops the supervisor and resets the posture to `none`
1565
+ * without dialing or waiting.
1566
+ */
1567
+ reserveRelays(addrs: string[], opts?: RelayReservationSupervisorOptions): Promise<RelayReservationState>;
1568
+ /**
1569
+ * The node's CURRENT relay-reservation posture, recomputed from the control
1570
+ * node's live multiaddrs on every call.
1571
+ *
1572
+ * Deliberately not memoised: a reservation can be lost after
1573
+ * {@link reserveRelays} succeeds (the relay restarts, the connection drops) and
1574
+ * a cached `reserved` would let a caller mint invitations carrying circuit
1575
+ * addresses that no longer route.
1576
+ *
1577
+ * A lost reservation now recovers on its own — the supervisor {@link reserveRelays}
1578
+ * starts re-drives on a backoff, reporting `retrying` meanwhile. `error` here
1579
+ * means nothing is going to try again: no supervisor, or no control node.
1580
+ */
1581
+ getRelayReservationState(): RelayReservationState;
831
1582
  /**
832
1583
  * Get the control database (for advanced queries)
833
1584
  */
834
1585
  getControlDatabase(): ControlDatabase | null;
1586
+ /**
1587
+ * Open control-network connections right now. A lower-bound proxy for replication
1588
+ * reach: 0 connections ⇒ a control write commits local-only. The private
1589
+ * `committedAlone` write-while-alone seam is defined in terms of this.
1590
+ *
1591
+ * It approximates the precise signal (the block's cluster size), and a caller that
1592
+ * samples it AFTER a write samples a slightly wider window than `committedAlone`
1593
+ * does inside the write itself. It exists so an embedder can warn an owner before a
1594
+ * control write that this machine currently sees none of its siblings, and report
1595
+ * after one that the write may not have travelled — a warning shown unconditionally
1596
+ * is a warning people learn to ignore.
1597
+ */
1598
+ getControlConnectionCount(): number;
835
1599
  /**
836
1600
  * Force a poll of the strand watcher (for testing)
837
1601
  */
@@ -841,12 +1605,12 @@ export declare class CadreNode implements SAppIdLookup {
841
1605
  */
842
1606
  getSAppConfig(strandId: string): SAppConfig | undefined;
843
1607
  /**
844
- * Initialize the seed bootstrap service with an authority key.
1608
+ * Initialize the seed bootstrap service with an owner key.
845
1609
  * Must be called before using seed-related methods that require signing.
846
1610
  *
847
- * @param authorityPrivateKey - The authority's private key (base64url encoded)
1611
+ * @param ownerPrivateKey - The owner's private key (base64url encoded)
848
1612
  */
849
- initializeSeedBootstrap(authorityPrivateKey: string): void;
1613
+ initializeSeedBootstrap(ownerPrivateKey: string): void;
850
1614
  /**
851
1615
  * Push the multiaddrs that future invites should advertise. Pass `null` to
852
1616
  * revert to the libp2p-reported addresses (the default). The host calls this
@@ -859,7 +1623,10 @@ export declare class CadreNode implements SAppIdLookup {
859
1623
  */
860
1624
  private resolveInviteAddresses;
861
1625
  /**
862
- * Enumerate the cadre's `CadrePeer` membership.
1626
+ * Enumerate the cadre's `CadrePeer` membership — the ADDRESSABLE surface (see
1627
+ * {@link isMember}). Rows whose stamp is retired in `CadreControl.Revocation`
1628
+ * never reach here: {@link ControlDatabase.queryCadrePeers} excludes them, so a
1629
+ * revoked peer is not addressable either.
863
1630
  */
864
1631
  listMembers(): Promise<Array<{
865
1632
  peerId: string;
@@ -867,8 +1634,77 @@ export declare class CadreNode implements SAppIdLookup {
867
1634
  }>>;
868
1635
  /**
869
1636
  * Probe whether a given peer is a `CadrePeer` member.
1637
+ *
1638
+ * This is the ADDRESSABLE surface ("do I have a dialable address record for this
1639
+ * peer") — it includes this node's own self-published row. Address resolution and
1640
+ * push fan-out use this. A peer whose stamp is retired in `CadreControl.Revocation`
1641
+ * is excluded here too (the filter lives in
1642
+ * {@link ControlDatabase.queryCadrePeers}): revocation removes a peer from the
1643
+ * addressable surface, not only the trust-facing one, so a revoked peer is no
1644
+ * longer dialed, RPC'd, or handed out as an address. The trust-facing gate is
1645
+ * {@link isAuthorizedMember}.
870
1646
  */
871
1647
  isMember(peerId: string): Promise<boolean>;
1648
+ /**
1649
+ * Enumerate the party's AUTHORIZED members — the trust-facing set, distinct from
1650
+ * the addressable set ({@link listMembers}). A peer is authorized iff ALL hold:
1651
+ *
1652
+ * 1. it is not this node itself — a node publishes its own `CadrePeer` address
1653
+ * row so its dialable address rides in seeds, but "self" is not a peer this
1654
+ * node authorized;
1655
+ * 2. its `CadrePeer` row carries a complete voucher (`StampId`, `VouchOwner`,
1656
+ * `VouchSig` all non-null);
1657
+ * 3. `VouchOwner` is in the NODE-LOCAL trusted-owner anchor
1658
+ * ({@link getTrustedOwnerStore}) — never the replicated `OwnerKey` table,
1659
+ * which any stranger can genesis-pollute; and
1660
+ * 4. `VouchSig` verifies as that owner's signature over the row's voucher
1661
+ * digest ({@link verifyCadrePeerVoucher}), so the anchored owner really
1662
+ * vouched THIS peer id under THIS row's nonce; and
1663
+ * 5. the row's `StampId` is NOT retired in `CadreControl.Revocation` — enforced
1664
+ * upstream in {@link ControlDatabase.queryCadrePeers}, which drops retired rows
1665
+ * before ANY reader (this predicate included) sees them, so a row resurrected
1666
+ * by replaying the captured admission approval on a node that had not yet
1667
+ * converged on the tombstone (the write-time `NotRevoked` CHECK only sees
1668
+ * local rows) is still inert to every reader that has the tombstone.
1669
+ *
1670
+ * Fail-closed at every step: a missing anchor (pre-start), an empty anchor (a
1671
+ * not-yet-enrolled node authorizes no one), a null/partial voucher, an
1672
+ * unanchored `VouchOwner`, or a bad signature all yield "not authorized" —
1673
+ * having an address row is NOT membership. The control-network wake and
1674
+ * strand-address gates consult this set, NOT the addressable one.
1675
+ *
1676
+ * NOTE (rotation): if a party owner rotates keys and only the NEW key is pinned
1677
+ * in the anchor, rows the OLD key vouched fail check 3 until re-vouched — a
1678
+ * legit member goes un-authorized on readers that only pin the new key. Full
1679
+ * rotation handling (re-vouch on rotate) is the
1680
+ * `flip-strand-membership-rotation-known-gap` work, not this predicate's.
1681
+ */
1682
+ listAuthorizedMembers(): Promise<Array<{
1683
+ peerId: string;
1684
+ multiaddr: string | null;
1685
+ }>>;
1686
+ /**
1687
+ * Checks 2–4 of the authorized-membership predicate (see
1688
+ * {@link listAuthorizedMembers}) for one `CadrePeer` row: complete voucher,
1689
+ * `VouchOwner` in the node-local anchor, signature valid over the row's
1690
+ * (PeerId, StampId) voucher digest.
1691
+ *
1692
+ * NOTE: verifies the ed25519 signature on every call (no memo of already-
1693
+ * verified (peerId, stampId, vouchSig) triples). Cadres are a handful of
1694
+ * devices and the gates run per inbound request, so this is cheap today; if
1695
+ * membership or gate traffic ever grows, cache verified triples keyed on the
1696
+ * row's `StampId` (which rotates with every re-vouch).
1697
+ */
1698
+ private hasAnchoredVoucher;
1699
+ /**
1700
+ * Probe whether a given peer is an AUTHORIZED party member (see
1701
+ * {@link listAuthorizedMembers}) — the gate the control-network wake and
1702
+ * strand-address responders consult, NOT {@link isMember} (the addressable
1703
+ * surface). Deliberately scans the full membership (one `CadrePeer` query)
1704
+ * rather than adding a single-row read path: cadres are small, and one code
1705
+ * path keeps the predicate impossible to drift from the list.
1706
+ */
1707
+ isAuthorizedMember(peerId: string): Promise<boolean>;
872
1708
  /**
873
1709
  * Push-wake a hibernating cadre peer over the control network.
874
1710
  *
@@ -887,17 +1723,79 @@ export declare class CadreNode implements SAppIdLookup {
887
1723
  pushWake(targetPeerId: string, strandId: string, reason?: string): Promise<WakeAck>;
888
1724
  /**
889
1725
  * Enable the seed listener for receiving seeds via the /sereus/seed/1.0.0 protocol.
890
- * This is for drone nodes that need to receive seeds without being an authority.
891
- * Does not require an authority key.
1726
+ * This is for drone nodes that need to receive seeds without being an owner.
1727
+ * Does not require an owner key.
892
1728
  */
893
1729
  enableSeedListener(): void;
894
1730
  /**
895
1731
  * Get the seed bootstrap service (for advanced use)
896
1732
  */
897
1733
  getSeedBootstrapService(): SeedBootstrapService | null;
1734
+ /**
1735
+ * The event callbacks every {@link SeedBootstrapService} this node owns is
1736
+ * wired with — shared by the owner-capable service ({@link initializeSeedBootstrap})
1737
+ * and the listener-only one ({@link enableSeedListener}) so the two cannot drift.
1738
+ *
1739
+ * A seed applied by the INBOUND protocol handler writes no `CadrePeer` row of
1740
+ * its own (it merges the libp2p peer store and dials owners), so the automatic
1741
+ * write-driven refresh never fires for it — yet applying it can ANCHOR a new
1742
+ * owner key, which flips rows already present from unauthorized to authorized.
1743
+ * Hence the explicit refresh here; without it a peer the freshly anchored owner
1744
+ * vouched for stays denied until the next timed cohort reconcile.
1745
+ */
1746
+ private seedEventCallbacks;
1747
+ /**
1748
+ * Re-materialize the authorized-peer snapshot that the fail-closed per-stream
1749
+ * control-DB gate ({@link authorizeInboundControlStream}) judges against.
1750
+ *
1751
+ * NO `CadrePeer` writer needs to call this: the control database notifies it
1752
+ * after every committed member-row write (`ControlDatabase.mutateCadrePeer`,
1753
+ * wired in {@link start}), which is exactly what makes the refresh automatic
1754
+ * rather than a caller obligation. It stays public for the changes that write
1755
+ * NO row locally and so raise no notification:
1756
+ *
1757
+ * - a membership row that arrived by REPLICATION (otherwise picked up on the
1758
+ * next timed cohort reconcile — bounded staleness by design), and
1759
+ * - a newly anchored trusted owner key ({@link applySeed}), which flips rows
1760
+ * ALREADY present from unauthorized to authorized without touching them.
1761
+ *
1762
+ * Coalescing: marks the snapshot stale and resolves once a refresh that began
1763
+ * after this call has completed, so a caller that awaits it always observes
1764
+ * its own change. Concurrent callers share one read. Idempotent and
1765
+ * best-effort (a failed read keeps the previous snapshot); never rejects.
1766
+ */
1767
+ refreshMembershipGate(reason?: string): Promise<void>;
1768
+ /**
1769
+ * The single in-flight refresh loop behind {@link refreshMembershipGate}: read
1770
+ * until the stale flag stays clear, then release the slot.
1771
+ *
1772
+ * NOTE: correct only because {@link membershipGateDirty} is guaranteed TRUE at
1773
+ * entry — its sole caller sets it synchronously, with no await in between. Were
1774
+ * this ever invoked with the flag already clear, the body would never suspend,
1775
+ * so the `finally` would null the slot BEFORE the caller's `??=` filled it, and
1776
+ * the slot would be left holding an already-settled drain that every later
1777
+ * refresh reuses — an unbreakable await/re-check spin. If a second caller is
1778
+ * ever added, have it mark the flag first (or assert it here).
1779
+ */
1780
+ private drainMembershipGate;
1781
+ /**
1782
+ * Collapse a burst of `CadrePeer` writes into ONE gate refresh at scope exit.
1783
+ *
1784
+ * For loops that re-touch many rows (the write-while-alone drains), where a
1785
+ * per-row refresh would mean one full membership read per row for a snapshot
1786
+ * that only has to be correct once the loop settles.
1787
+ *
1788
+ * The depth counter is instance-level, so it also suppresses a genuinely
1789
+ * CONCURRENT external write's refresh for the life of the scope: that writer's
1790
+ * promise resolves before its peer is admitted, and admission lands at scope
1791
+ * exit instead. Acceptable — scope exit is milliseconds away (bounded by the
1792
+ * drain), versus the ~15 s reconcile interval that was the alternative before
1793
+ * any of this existed — but it is the reason these scopes stay short and rare.
1794
+ */
1795
+ private deferMembershipGateRefresh;
898
1796
  /**
899
1797
  * Authorize a new peer to join the cadre.
900
- * Signs the peer ID with the authority key and inserts into CadrePeer table.
1798
+ * Signs the peer ID with the owner key and inserts into CadrePeer table.
901
1799
  *
902
1800
  * @param peerId - The peer ID to authorize
903
1801
  * @param multiaddrs - Optional multiaddrs for the peer
@@ -905,14 +1803,14 @@ export declare class CadreNode implements SAppIdLookup {
905
1803
  authorizePeer(peerId: string, multiaddrs?: string[]): Promise<void>;
906
1804
  /**
907
1805
  * Remove a previously-authorized peer from the cadre.
908
- * Signs the peer ID with the authority key and deletes the CadrePeer row.
1806
+ * Signs the peer ID with the owner key and deletes the CadrePeer row.
909
1807
  *
910
1808
  * @param peerId - The peer ID to remove
911
1809
  */
912
1810
  removePeer(peerId: string): Promise<void>;
913
1811
  /**
914
1812
  * Create a seed from the current control network state.
915
- * The seed contains peer information and is signed by an authority.
1813
+ * The seed contains peer information and is signed by an owner.
916
1814
  */
917
1815
  createSeed(): Promise<ControlNetworkSeed>;
918
1816
  /**
@@ -921,12 +1819,19 @@ export declare class CadreNode implements SAppIdLookup {
921
1819
  * Validates the seed signature, then evaluates a trust anchor for the signer
922
1820
  * key (see `SeedTrustPolicy`). An enrollment caller can pass a per-seed
923
1821
  * `trustPolicy` override — e.g. a `pinnedKeyTrustPolicy` built from a
924
- * `CadreInvite.authorityKeys` — so a cold-start node can accept its first
1822
+ * `CadreInvite.ownerKeys` — so a cold-start node can accept its first
925
1823
  * seed without reconfiguring the service.
926
1824
  */
927
1825
  applySeed(seed: ControlNetworkSeed, options?: {
928
1826
  trustPolicy?: SeedTrustPolicy;
929
1827
  }): Promise<ApplySeedResult>;
1828
+ /**
1829
+ * Post-process an {@link applySeed} outcome: retain the seed's owner peers as
1830
+ * cold-start bootstrap targets, and surface a seed that was accepted but whose
1831
+ * every owner dial failed — the node is now seeded yet unconnected, and the
1832
+ * cold-start reconcile branch is what gets it out of that state.
1833
+ */
1834
+ private noteAppliedSeed;
930
1835
  /**
931
1836
  * Deliver a seed directly to a peer via the /sereus/seed/1.0.0 protocol.
932
1837
  */
@@ -976,7 +1881,7 @@ export declare class CadreNode implements SAppIdLookup {
976
1881
  */
977
1882
  decodeInvite(encoded: string): CadreInvite;
978
1883
  /**
979
- * Dial an authority from an invite (for phone joining via invite).
1884
+ * Dial an owner from an invite (for phone joining via invite).
980
1885
  */
981
1886
  dialInvite(invite: CadreInvite): Promise<void>;
982
1887
  /**