@serfab/cadre-core 0.7.1 → 0.9.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 (170) hide show
  1. package/README.md +11 -10
  2. package/dist/cadre-node.d.ts +818 -19
  3. package/dist/cadre-node.js +1952 -99
  4. package/dist/cadre-node.js.map +1 -1
  5. package/dist/canonical-datetime.d.ts +33 -0
  6. package/dist/canonical-datetime.js +38 -0
  7. package/dist/canonical-datetime.js.map +1 -0
  8. package/dist/canonical-json.d.ts +12 -0
  9. package/dist/canonical-json.js +33 -0
  10. package/dist/canonical-json.js.map +1 -0
  11. package/dist/control-cohort.d.ts +63 -0
  12. package/dist/control-cohort.js +80 -0
  13. package/dist/control-cohort.js.map +1 -0
  14. package/dist/control-database.d.ts +279 -8
  15. package/dist/control-database.js +563 -134
  16. package/dist/control-database.js.map +1 -1
  17. package/dist/control-formation-recorder.d.ts +89 -0
  18. package/dist/control-formation-recorder.js +139 -0
  19. package/dist/control-formation-recorder.js.map +1 -0
  20. package/dist/control-schema.d.ts +12 -0
  21. package/dist/control-schema.js +223 -0
  22. package/dist/control-schema.js.map +1 -0
  23. package/dist/control-stream.d.ts +54 -0
  24. package/dist/control-stream.js +104 -0
  25. package/dist/control-stream.js.map +1 -0
  26. package/dist/device-token.d.ts +50 -0
  27. package/dist/device-token.js +66 -0
  28. package/dist/device-token.js.map +1 -0
  29. package/dist/diagnostics/connection-path.d.ts +118 -0
  30. package/dist/diagnostics/connection-path.js +204 -0
  31. package/dist/diagnostics/connection-path.js.map +1 -0
  32. package/dist/diagnostics/webrtc-turn-tracker.d.ts +50 -0
  33. package/dist/diagnostics/webrtc-turn-tracker.js +196 -0
  34. package/dist/diagnostics/webrtc-turn-tracker.js.map +1 -0
  35. package/dist/ed25519-key.d.ts +42 -0
  36. package/dist/ed25519-key.js +51 -0
  37. package/dist/ed25519-key.js.map +1 -0
  38. package/dist/hibernation-manager.d.ts +79 -0
  39. package/dist/hibernation-manager.js +173 -23
  40. package/dist/hibernation-manager.js.map +1 -1
  41. package/dist/index.d.ts +25 -4
  42. package/dist/index.js +59 -4
  43. package/dist/index.js.map +1 -1
  44. package/dist/key-store-file.d.ts +52 -0
  45. package/dist/key-store-file.js +194 -0
  46. package/dist/key-store-file.js.map +1 -0
  47. package/dist/key-store.d.ts +75 -0
  48. package/dist/key-store.js +66 -0
  49. package/dist/key-store.js.map +1 -0
  50. package/dist/peer-authorization.d.ts +48 -0
  51. package/dist/peer-authorization.js +66 -0
  52. package/dist/peer-authorization.js.map +1 -0
  53. package/dist/peer-record.d.ts +89 -0
  54. package/dist/peer-record.js +117 -0
  55. package/dist/peer-record.js.map +1 -0
  56. package/dist/push-credentials.d.ts +34 -0
  57. package/dist/push-credentials.js +81 -0
  58. package/dist/push-credentials.js.map +1 -0
  59. package/dist/push-fanout.d.ts +137 -0
  60. package/dist/push-fanout.js +197 -0
  61. package/dist/push-fanout.js.map +1 -0
  62. package/dist/push-node.d.ts +38 -0
  63. package/dist/push-node.js +48 -0
  64. package/dist/push-node.js.map +1 -0
  65. package/dist/push-notifier-apns.d.ts +61 -0
  66. package/dist/push-notifier-apns.js +199 -0
  67. package/dist/push-notifier-apns.js.map +1 -0
  68. package/dist/push-notifier-fcm.d.ts +44 -0
  69. package/dist/push-notifier-fcm.js +182 -0
  70. package/dist/push-notifier-fcm.js.map +1 -0
  71. package/dist/push-notifier-shared.d.ts +18 -0
  72. package/dist/push-notifier-shared.js +25 -0
  73. package/dist/push-notifier-shared.js.map +1 -0
  74. package/dist/push-notifier.d.ts +52 -0
  75. package/dist/push-notifier.js +22 -0
  76. package/dist/push-notifier.js.map +1 -0
  77. package/dist/schema-verification.d.ts +11 -2
  78. package/dist/schema-verification.js +15 -4
  79. package/dist/schema-verification.js.map +1 -1
  80. package/dist/seed-bootstrap.d.ts +228 -21
  81. package/dist/seed-bootstrap.js +489 -168
  82. package/dist/seed-bootstrap.js.map +1 -1
  83. package/dist/seed-trust-policy.d.ts +63 -0
  84. package/dist/seed-trust-policy.js +56 -0
  85. package/dist/seed-trust-policy.js.map +1 -0
  86. package/dist/strand-addr-protocol.d.ts +132 -0
  87. package/dist/strand-addr-protocol.js +313 -0
  88. package/dist/strand-addr-protocol.js.map +1 -0
  89. package/dist/strand-cohort.d.ts +54 -0
  90. package/dist/strand-cohort.js +38 -0
  91. package/dist/strand-cohort.js.map +1 -0
  92. package/dist/strand-database.d.ts +54 -6
  93. package/dist/strand-database.js +88 -100
  94. package/dist/strand-database.js.map +1 -1
  95. package/dist/strand-formation-manager.d.ts +79 -19
  96. package/dist/strand-formation-manager.js +196 -104
  97. package/dist/strand-formation-manager.js.map +1 -1
  98. package/dist/strand-formation-protocol.d.ts +162 -0
  99. package/dist/strand-formation-protocol.js +279 -0
  100. package/dist/strand-formation-protocol.js.map +1 -0
  101. package/dist/strand-instance-manager.d.ts +73 -4
  102. package/dist/strand-instance-manager.js +167 -41
  103. package/dist/strand-instance-manager.js.map +1 -1
  104. package/dist/strand-member-key.d.ts +29 -0
  105. package/dist/strand-member-key.js +38 -0
  106. package/dist/strand-member-key.js.map +1 -0
  107. package/dist/strand-member-registry.d.ts +98 -0
  108. package/dist/strand-member-registry.js +129 -0
  109. package/dist/strand-member-registry.js.map +1 -0
  110. package/dist/strand-membership-writer.d.ts +361 -0
  111. package/dist/strand-membership-writer.js +449 -0
  112. package/dist/strand-membership-writer.js.map +1 -0
  113. package/dist/strand-solicitation.d.ts +77 -2
  114. package/dist/strand-solicitation.js +25 -4
  115. package/dist/strand-solicitation.js.map +1 -1
  116. package/dist/strand-wake-payload.d.ts +30 -0
  117. package/dist/strand-wake-payload.js +20 -0
  118. package/dist/strand-wake-payload.js.map +1 -0
  119. package/dist/strand-wake-protocol.d.ts +114 -0
  120. package/dist/strand-wake-protocol.js +257 -0
  121. package/dist/strand-wake-protocol.js.map +1 -0
  122. package/dist/strand-watcher.d.ts +6 -2
  123. package/dist/strand-watcher.js +57 -17
  124. package/dist/strand-watcher.js.map +1 -1
  125. package/dist/types.d.ts +436 -33
  126. package/dist/types.js +22 -5
  127. package/dist/types.js.map +1 -1
  128. package/package.json +18 -8
  129. package/src/cadre-node.ts +2981 -948
  130. package/src/canonical-datetime.ts +39 -0
  131. package/src/canonical-json.ts +30 -0
  132. package/src/control-cohort.ts +103 -0
  133. package/src/control-database.ts +948 -383
  134. package/src/control-formation-recorder.ts +151 -0
  135. package/src/control-schema.ts +222 -0
  136. package/src/control-stream.ts +128 -0
  137. package/src/device-token.ts +89 -0
  138. package/src/diagnostics/connection-path.ts +277 -0
  139. package/src/diagnostics/webrtc-turn-tracker.ts +222 -0
  140. package/src/ed25519-key.ts +69 -0
  141. package/src/hibernation-manager.ts +197 -28
  142. package/src/index.ts +275 -79
  143. package/src/key-store-file.ts +203 -0
  144. package/src/key-store.ts +99 -0
  145. package/src/peer-authorization.ts +81 -0
  146. package/src/peer-record.ts +146 -0
  147. package/src/push-credentials.ts +84 -0
  148. package/src/push-fanout.ts +272 -0
  149. package/src/push-node.ts +64 -0
  150. package/src/push-notifier-apns.ts +247 -0
  151. package/src/push-notifier-fcm.ts +220 -0
  152. package/src/push-notifier-shared.ts +28 -0
  153. package/src/push-notifier.ts +52 -0
  154. package/src/schema-verification.ts +18 -4
  155. package/src/seed-bootstrap.ts +563 -223
  156. package/src/seed-trust-policy.ts +107 -0
  157. package/src/strand-addr-protocol.ts +410 -0
  158. package/src/strand-cohort.ts +77 -0
  159. package/src/strand-database.ts +113 -141
  160. package/src/strand-formation-manager.ts +233 -142
  161. package/src/strand-formation-protocol.ts +391 -0
  162. package/src/strand-instance-manager.ts +207 -56
  163. package/src/strand-member-key.ts +39 -0
  164. package/src/strand-member-registry.ts +177 -0
  165. package/src/strand-membership-writer.ts +640 -0
  166. package/src/strand-solicitation.ts +93 -4
  167. package/src/strand-wake-payload.ts +32 -0
  168. package/src/strand-wake-protocol.ts +331 -0
  169. package/src/strand-watcher.ts +61 -17
  170. package/src/types.ts +1032 -582
@@ -1,10 +1,15 @@
1
1
  import type { Libp2p, PeerId } from '@libp2p/interface';
2
- import type { CadreNodeConfig, StrandInstance, StrandConfig, SAppConfig, CadreNodeEvents, ControlNetworkSeed, ApplySeedResult, AddDroneOptions, AddPhoneOptions, DroneInitResult, InviteResult, CadreInvite, OpenInvitation, FormStrandResult, StrandFormationDisclosure } from './types.js';
2
+ import type { Multiaddr } from '@multiformats/multiaddr';
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 Ed25519KeyPair } from './ed25519-key.js';
3
5
  import { type SAppIdLookup } from './strand-watcher.js';
4
6
  import { EnrollmentService } from './enrollment.js';
5
7
  import { ControlDatabase } from './control-database.js';
6
8
  import { SeedBootstrapService } from './seed-bootstrap.js';
9
+ import type { SeedTrustPolicy } from './seed-trust-policy.js';
7
10
  import { StrandSolicitationService, type StrandSolicitationServiceOptions } from './strand-solicitation.js';
11
+ import type { WakeAck } from './types.js';
12
+ import { type ConnectionPathSummary } from './diagnostics/connection-path.js';
8
13
  type EventHandler<T> = (data: T) => void;
9
14
  /**
10
15
  * CadreNode is the main entry point for a cadre member.
@@ -17,6 +22,15 @@ type EventHandler<T> = (data: T) => void;
17
22
  */
18
23
  export declare class CadreNode implements SAppIdLookup {
19
24
  private readonly config;
25
+ /**
26
+ * The resolved node identity key, set once by {@link resolveIdentityKey}
27
+ * during {@link start} (from `config.keyStore`, else `config.privateKey`).
28
+ * Left undefined when neither is configured — libp2p then generates an
29
+ * ephemeral key internally and there is no exposed owner key. Every
30
+ * identity-dependent path (control node creation, self-record signing, strand
31
+ * launch) reads this resolved field, never `config.privateKey` directly.
32
+ */
33
+ private identityKey;
20
34
  private controlNode;
21
35
  private controlDatabase;
22
36
  private strandWatcher;
@@ -25,10 +39,123 @@ export declare class CadreNode implements SAppIdLookup {
25
39
  private enrollmentService;
26
40
  private seedBootstrapService;
27
41
  private strandSolicitationService;
28
- private running;
42
+ private strandWakeService;
43
+ /**
44
+ * Control-network strand-address responder. Answers a co-cadre sibling's
45
+ * on-demand request for this node's live strand-network multiaddrs so the
46
+ * sibling can seed a strand mesh from us — the read side of the seed path that
47
+ * stops conflating control addresses with strand seeding.
48
+ */
49
+ private strandAddrService;
50
+ /**
51
+ * Server-side push-wake fan-out. Constructed by {@link start} only when
52
+ * `config.push` (an injected `PushNotifier` + policy) is present — without it
53
+ * the node behaves exactly as before (no notifier, no fan-out). Owns who/when
54
+ * to wake hibernating mobile peers on strand activity.
55
+ */
56
+ private pushFanoutService;
57
+ /** Backing field for the {@link running} / {@link isRunning} getters. */
58
+ private _running;
59
+ /**
60
+ * In-flight {@link serviceWake} operations keyed by strandId. Coalesces
61
+ * concurrent on-demand wakes for the same strand into one runtime build + one
62
+ * window + one re-hibernate decision (a second caller joins the first's
63
+ * promise), complementing {@link HibernationManager}'s wake coalescing.
64
+ */
65
+ private serviceWakePromises;
66
+ /**
67
+ * Live wake-window waiters (see {@link holdWakeWindow}). Tracked so
68
+ * {@link cleanup} can clear the timer AND resolve the promise on teardown — a
69
+ * window must never fire (or hang an in-flight serviceWake) after stop().
70
+ */
71
+ private windowWaiters;
29
72
  private eventHandlers;
30
73
  /** Map of strandId -> sAppConfig for sAppId filtering and management */
31
74
  private sAppConfigs;
75
+ /**
76
+ * Most-recently pushed invite addresses (see {@link setInviteAddresses}).
77
+ * When non-null these take priority over `libp2pNode.getMultiaddrs()` when
78
+ * minting invites — the host pushes NAT-resolved addresses here so the
79
+ * control-network node never needs to dial back to the manager.
80
+ */
81
+ private latestInviteAddresses;
82
+ /** Initial self-registration timer (see {@link scheduleSelfRegistration}). */
83
+ private selfRegistrationTimer;
84
+ /** TTL heartbeat that re-publishes the self record before it goes stale. */
85
+ private recordRefreshTimer;
86
+ /** Listener that re-publishes the self record when reachable addresses change. */
87
+ private selfPeerUpdateHandler;
88
+ /**
89
+ * Recurring proactive control-cohort dial cadence (see
90
+ * {@link reconcileControlCohort}). Wired alongside {@link recordRefreshTimer}
91
+ * in {@link startRecordRefresh} and torn down symmetrically in
92
+ * {@link stopRecordRefresh}; `.unref()`'d so it never keeps the loop alive.
93
+ */
94
+ private controlCohortReconcileTimer;
95
+ /**
96
+ * Single-flight guard for {@link reconcileControlCohort}. The eager start pass,
97
+ * the recurring interval, and the `self:peer:update` trigger can fire close
98
+ * together; collapsing concurrent passes into one in-flight run prevents two
99
+ * passes from double-dialing the same siblings (mirrors {@link registerSelfInFlight}).
100
+ */
101
+ private reconcileControlCohortInFlight;
102
+ /**
103
+ * Single-flight guard for {@link registerSelf}. Concurrent callers (the explicit
104
+ * CLI `--owner` publish, the 1s startup timer, the TTL heartbeat, and the
105
+ * address-change listener) share one in-flight publish so two of them can never
106
+ * both read "no row yet" and race a duplicate INSERT (a `CadrePeer` PK conflict).
107
+ */
108
+ private registerSelfInFlight;
109
+ /**
110
+ * Re-replication queue for owner 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}.
117
+ */
118
+ private pendingPeerWrites;
119
+ /** This node's own `CadrePeer` self-write committed local-only (re-touched on growth). */
120
+ private pendingSelfPeerWrite;
121
+ /** This node's own `DeviceToken` self-write committed local-only (re-touched on growth). */
122
+ private pendingSelfDeviceWrite;
123
+ /**
124
+ * Guards the one-shot, first-cohort-growth reconstruction: an owner re-touches
125
+ * every membership row it may have authored that could be unreplicated (covering
126
+ * writes made before this process started, which the in-memory queue cannot know).
127
+ * Set true after the first drain so later passes only drain the in-memory queue.
128
+ */
129
+ private reconstructedLocalOnlyWrites;
130
+ /** Single-flight guard for the re-replication drain (mirrors {@link registerSelfInFlight}). */
131
+ private drainControlReplicationInFlight;
132
+ /**
133
+ * Tracks the control-connection presence edge so the drain fires only on the
134
+ * 0→≥1 transition (the earliest point a re-issue can broadcast), not on every
135
+ * subsequent `connection:open`. Re-armed to false once connections return to 0.
136
+ */
137
+ private hasControlConnection;
138
+ /** `connection:open` listener driving the growth-triggered drain (teardown in {@link stopRecordRefresh}). */
139
+ private controlConnectionOpenHandler;
140
+ /** `connection:close` listener re-arming the growth edge (teardown in {@link stopRecordRefresh}). */
141
+ private controlConnectionCloseHandler;
142
+ /**
143
+ * Hooks `globalThis.RTCPeerConnection` to observe whether ICE selected a TURN
144
+ * relay candidate for each WebRTC session. Installed in {@link start}, disposed
145
+ * in {@link cleanup}; inert on Node.js (no `RTCPeerConnection`).
146
+ */
147
+ private readonly turnTracker;
148
+ /**
149
+ * Peer IDs whose current WebRTC connection was observed to be TURN-relayed.
150
+ * Populated by {@link handleTurnConnectionOpen} (drains the tracker queue on a
151
+ * `/webrtc` `connection:open`), cleared per-peer on `connection:close`. Read by
152
+ * {@link getConnectionPaths} to promote `webrtc` → `webrtc-turn` (relayed).
153
+ */
154
+ private turnRelayedPeers;
155
+ /** `connection:open` listener feeding TURN detection (teardown in {@link stopRecordRefresh}). */
156
+ private turnConnectionOpenHandler;
157
+ /** `connection:close` listener clearing a peer's TURN flag (teardown in {@link stopRecordRefresh}). */
158
+ private turnConnectionCloseHandler;
32
159
  constructor(config: CadreNodeConfig);
33
160
  /**
34
161
  * SAppIdLookup implementation - get sAppId for a strand
@@ -38,6 +165,10 @@ export declare class CadreNode implements SAppIdLookup {
38
165
  * Get the peer ID of this node (available after start)
39
166
  */
40
167
  get peerId(): PeerId | undefined;
168
+ /**
169
+ * The party ID this node serves (control-network identity).
170
+ */
171
+ get partyId(): string;
41
172
  /**
42
173
  * Get the multiaddrs of this node (available after start)
43
174
  */
@@ -46,6 +177,31 @@ export declare class CadreNode implements SAppIdLookup {
46
177
  * Check if the node is running
47
178
  */
48
179
  get isRunning(): boolean;
180
+ /**
181
+ * Synchronous lifecycle snapshot for headless callers (a mobile
182
+ * `BackgroundRunner` that boots in a background task and must *query* state
183
+ * rather than subscribe to `control:connected`/`control:disconnected`).
184
+ * Equivalent to {@link isRunning}.
185
+ */
186
+ get running(): boolean;
187
+ /**
188
+ * Synchronous readiness snapshot: whether the control network is currently
189
+ * connected (the node is running and its control-network libp2p node is up).
190
+ * Tracks the same edge the `control:connected`/`control:disconnected` events
191
+ * announce, but pollable.
192
+ */
193
+ get controlConnected(): boolean;
194
+ /**
195
+ * Classify every open control-network connection as relayed
196
+ * (`/p2p-circuit`) vs direct, tag its transport, and summarise counts plus a
197
+ * stuck-on-relay condition. Pure, read-only snapshot over
198
+ * `controlNode.getConnections()`. Returns an empty (all-zero) summary when
199
+ * the node has not been started.
200
+ *
201
+ * @param settleWindowMs - grace period before a relayed connection with no
202
+ * direct sibling is considered stuck (default 10_000ms)
203
+ */
204
+ getConnectionPaths(settleWindowMs?: number): ConnectionPathSummary;
49
205
  /**
50
206
  * Get all strand instances
51
207
  */
@@ -62,6 +218,19 @@ export declare class CadreNode implements SAppIdLookup {
62
218
  * Start the cadre node
63
219
  */
64
220
  start(): Promise<void>;
221
+ /**
222
+ * Build the server-side push-wake fan-out over an injected {@link PushNotifier}.
223
+ *
224
+ * The notifier reaches for `node:http2`/`node:crypto`, so the cross-platform
225
+ * core never constructs it — the Node host builds it from
226
+ * `@serfab/cadre-core/push-node` and passes the instance in
227
+ * `CadreNodeConfig.push`. This node owns that instance's lifecycle from here:
228
+ * {@link cleanup} closes the fan-out, which closes the notifier (freeing the
229
+ * APNs HTTP/2 session). Every other primitive the fan-out needs (member
230
+ * enumeration, participation, direct dial, token resolve/expire) is an
231
+ * import-clean closure over this node.
232
+ */
233
+ private buildPushFanout;
65
234
  /**
66
235
  * Stop the cadre node
67
236
  */
@@ -75,41 +244,599 @@ export declare class CadreNode implements SAppIdLookup {
75
244
  */
76
245
  off<K extends keyof CadreNodeEvents>(event: K, handler: EventHandler<CadreNodeEvents[K]>): void;
77
246
  private emit;
247
+ /**
248
+ * Resolve the node identity into {@link identityKey} exactly once, fail-closed,
249
+ * before any network bring-up. Resolution order:
250
+ *
251
+ * 1. Both `keyStore` and `privateKey` set ⇒ configuration error (throws).
252
+ * 2. `keyStore` set ⇒ load protobuf bytes from `identityKeyId` (default
253
+ * {@link DEFAULT_IDENTITY_KEY_ID}). Found ⇒ deserialize; empty ⇒ generate a
254
+ * fresh Ed25519 key, persist it, and use it. A rejected `get` (access denied
255
+ * / backend failure) PROPAGATES — we never generate a new key on a read
256
+ * error, which would silently orphan the real identity.
257
+ * 3. `privateKey` set ⇒ use it directly.
258
+ * 4. Neither ⇒ leave undefined; libp2p generates an ephemeral key.
259
+ *
260
+ * Idempotent: a second call (or a stop()→start() cycle) reuses the already
261
+ * resolved key rather than regenerating or re-persisting.
262
+ */
263
+ private resolveIdentityKey;
78
264
  private createControlNode;
79
265
  private createStrandQueryable;
80
266
  /**
81
- * Schedule self-registration in the CadrePeer table.
82
- * This runs as a background task after the node is started.
267
+ * Schedule the node's initial self-record publish + ongoing refresh shortly
268
+ * after start (non-blocking). {@link registerSelf} is idempotent and safely
269
+ * no-ops when it cannot yet sign/insert (e.g. owner key not installed),
270
+ * so the timer is harmless even when registration only becomes possible later.
83
271
  */
84
272
  private scheduleSelfRegistration;
85
273
  /**
86
- * Register this peer in the CadrePeer table.
87
- * This allows other cadre members to discover this node.
274
+ * Publish (or refresh) this node's own signed `CadrePeer` address record so
275
+ * other members can resolve its current signaling/relay multiaddrs from its
276
+ * PeerId alone. Public, awaitable, and idempotent.
277
+ *
278
+ * - Builds a `PeerAddressRecord` from the node's current dialable addrs
279
+ * (signaling/`p2p-circuit` first), signed with the ed25519 key behind its
280
+ * PeerId (the resolved node identity from `keyStore`/`config.privateKey`).
281
+ * - If the row already exists: a self-signed UPDATE bumping `UpdatedAt`.
282
+ * - If not, and the node is its own owner: an owner-signed INSERT that
283
+ * also carries the self-signature.
284
+ * - Otherwise: logs and returns (a non-owner node with no row yet must
285
+ * wait for an owner to insert it; it can then self-refresh).
286
+ *
287
+ * Safe to call repeatedly (heartbeat / address-change driven); each successful
288
+ * publish strictly increases `UpdatedAt`. Concurrent calls are collapsed into a
289
+ * single in-flight publish (see {@link registerSelfInFlight}) so the explicit
290
+ * startup publish and the background timers can never race a duplicate INSERT.
291
+ *
292
+ * @returns what the publish did — `inserted`, `refreshed`, or `skipped`.
293
+ */
294
+ registerSelf(): Promise<SelfRegistrationOutcome>;
295
+ /** The body of {@link registerSelf}; serialised by its single-flight guard. */
296
+ private publishSelfRecord;
297
+ /**
298
+ * The ed25519 keypair (base64url) the node signs its own record with — the key
299
+ * behind its libp2p PeerId. Sourced from the resolved {@link identityKey}
300
+ * (which a `keyStore` or `config.privateKey` supplies); returns null when
301
+ * absent (ephemeral identity) or (defensively) when it does not match the
302
+ * control node's PeerId, in which case self-publish is skipped rather than
303
+ * producing an unresolvable row.
304
+ */
305
+ private getSelfSigningKey;
306
+ /**
307
+ * The owner keypair (base64url Ed25519) derived from this node's resolved
308
+ * identity key. In the single-key reference model the owner signing key is
309
+ * *derived from* the node identity (see {@link ed25519KeyPairFromLibp2p}), so the
310
+ * same key material protected in a secure enclave backs both.
311
+ *
312
+ * Exposed so the hosting app retains control of owner genesis: cadre-core
313
+ * resolves + protects the identity, then the app sources this pair to drive
314
+ * `ensureOwnerKey(pub)` + `initializeSeedBootstrap(priv)` itself — cadre-core
315
+ * never silently runs genesis. A future separate-owner slot would return a
316
+ * distinct key here instead of the identity-derived one.
317
+ *
318
+ * @returns The base64url seed/public-key owner pair.
319
+ * @throws If called before {@link start} has resolved the identity, or when the
320
+ * node runs on an ephemeral libp2p key (no `keyStore`/`privateKey` configured),
321
+ * since that key is internal to libp2p and not exposed.
322
+ */
323
+ getIdentityOwnerKey(): Ed25519KeyPair;
324
+ /**
325
+ * Collect this node's current dialable addresses for publication, signaling
326
+ * (`/p2p-circuit`) first. Prefers the best invite/NAT-resolved set and folds
327
+ * in the relay/signaling address (the WebRTC dial input) when not already
328
+ * present.
329
+ */
330
+ private collectSelfAddrs;
331
+ /**
332
+ * Wire the ongoing self-record refresh: re-publish whenever libp2p reports an
333
+ * address change (relay reservation rotation, NAT change) and on a TTL
334
+ * heartbeat at half the freshness ceiling. Idempotent — repeated calls do not
335
+ * stack listeners/timers.
336
+ */
337
+ private startRecordRefresh;
338
+ /** Tear down the self-record refresh timers + listener (see {@link cleanup}). */
339
+ private stopRecordRefresh;
340
+ /**
341
+ * Resolve a peer's current, signed, trust-checkable multiaddrs from only its
342
+ * PeerId — the transport-agnostic input a NAT-to-NAT WebRTC (or any) dial path
343
+ * consumes, with no copy/paste of a relayed dial string.
344
+ *
345
+ * Reads the peer's `CadrePeer` record and gates it through, in order:
346
+ * 1. record present (else `[]`),
347
+ * 2. `publicKey <-> peerId` binding (the stored key's libp2p identity must be
348
+ * the requested peerId),
349
+ * 3. self-signature verifies against `publicKey`,
350
+ * 4. freshness — rejected once older than `maxAgeMs` (never a dead relay
351
+ * reservation),
352
+ * 5. the pluggable trust gate (`opts.trustPolicy`).
353
+ * Survivors are returned signaling (`/p2p-circuit`) first, filtered to
354
+ * signaling-only when requested, as parsed `Multiaddr`s (unparsable addrs
355
+ * dropped). Any gate failure yields an empty array rather than throwing.
356
+ */
357
+ resolvePeerAddrs(peerId: string, opts?: ResolveOpts): Promise<Multiaddr[]>;
358
+ /** Parse multiaddr strings, dropping (and logging) any that fail to parse. */
359
+ private parseMultiaddrs;
360
+ /**
361
+ * Run one proactive control-cohort dial pass to keep this node connected to its
362
+ * cadre siblings (so the `CadreControl` collections form a replicating cohort).
363
+ * Public so the cohort-growth-driven re-replication path
364
+ * (`control-write-ensure-replicated`) and tests can trigger a pass on demand;
365
+ * normally driven by the eager start pass, the recurring interval, and
366
+ * `self:peer:update` (all wired in {@link startRecordRefresh}).
367
+ *
368
+ * Concurrent triggers collapse into a single in-flight pass
369
+ * (see {@link reconcileControlCohortInFlight}) so two passes never double-dial.
370
+ * Best-effort throughout: a failure to resolve/dial any one sibling is logged
371
+ * and the pass continues; the whole pass is a no-op when the node is alone.
372
+ */
373
+ reconcileControlCohort(): Promise<void>;
374
+ /** Body of {@link reconcileControlCohort}; serialised by its single-flight guard. */
375
+ private runReconcileControlCohort;
376
+ /**
377
+ * Resolve one sibling's control-network dial addresses and dial it, best-effort.
378
+ * Returns whether a dial was attempted (false when no address resolves).
379
+ *
380
+ * A per-peer failure (NAT, offline, relay down, connection-gater denial) is
381
+ * logged and swallowed so one unreachable sibling never aborts the pass —
382
+ * exactly like {@link SeedBootstrapService.applySeed}'s owner-dial loop. A
383
+ * failed dial is simply retried on the next pass.
384
+ */
385
+ private dialControlSibling;
386
+ /**
387
+ * Resolve a sibling's control-network dial addresses for the reconcile pass.
388
+ *
389
+ * Primary (steady state): the signed, fresh, trust-gated control addresses from
390
+ * the converged `CadrePeer` record via {@link resolvePeerAddrs}. Fallback (cold
391
+ * start): the libp2p peerStore entries `applySeed` populated, used only while the
392
+ * record is not yet resolvable. Returns `[]` (never throws) when neither yields
393
+ * an address — that sibling is skipped this pass.
394
+ */
395
+ private resolveControlDialAddrs;
396
+ /**
397
+ * Cold-start fallback: the libp2p peerStore multiaddrs for `peerId` (seeded by
398
+ * {@link SeedBootstrapService.applySeed}). Returns `[]` on a missing entry or any
399
+ * parse/lookup failure — never throws.
400
+ */
401
+ private peerStoreAddrs;
402
+ /**
403
+ * Whether a control write happening now would commit local-only. A sound lower
404
+ * bound is "no connected control peers": 0 connections ⇒ the block's cluster is
405
+ * ≤1 ⇒ Optimystic commits without broadcasting. This is the pragmatic proxy for
406
+ * the precise signal (the block's `getClusterSize`); it over-approximates safely
407
+ * — a connected-but-not-in-this-block's-cluster write may still be local-only and
408
+ * is caught by the cohort's periodic pull-on-read instead. Re-issuing an already-
409
+ * replicated row is harmless (an idempotent monotonic bump), so the coarse proxy
410
+ * is acceptable as the agreed first cut.
411
+ */
412
+ private committedAlone;
413
+ /**
414
+ * Record (or clear) a just-committed owner membership write in the
415
+ * write-while-alone re-replication queue. If the control node had no connections
416
+ * at commit, the Optimystic write was local-only and must be re-issued once the
417
+ * cohort grows; otherwise the write replicated and any stale queue entry for this
418
+ * subject is dropped.
419
+ *
420
+ * A `remove` that commits alone is a security-relevant durability gap (a revoked
421
+ * peer may persist as a member elsewhere), so it is logged loudly — and, because
422
+ * a physical delete leaves no local trace to re-issue from, its on-growth
423
+ * re-issue is best-effort only (see {@link drainPendingPeerWrites}).
424
+ */
425
+ private noteControlWrite;
426
+ /**
427
+ * Drain the write-while-alone re-replication queue: re-issue the writes that
428
+ * committed local-only now that the cohort can broadcast them. Public so the
429
+ * 0→≥1 growth trigger ({@link handleControlConnectionChange}) and tests can drive
430
+ * it; concurrent calls collapse into one in-flight drain
431
+ * ({@link drainControlReplicationInFlight}) so two growth signals never
432
+ * double-issue. Best-effort throughout — a per-row failure leaves that entry
433
+ * queued for the next growth rather than aborting the drain.
434
+ */
435
+ drainPendingControlReplication(reason: string): Promise<void>;
436
+ /** Body of {@link drainPendingControlReplication}; serialised by its single-flight guard. */
437
+ private runDrainControlReplication;
438
+ /**
439
+ * Re-touch this node's own `DeviceToken` row (re-sign + bump `UpdatedAt` via
440
+ * {@link registerDeviceToken}) so a self device-token that committed local-only
441
+ * re-broadcasts on cohort growth. No-op when no row exists (nothing to
442
+ * re-replicate) or the stored platform is unknown. Best-effort — never throws to
443
+ * the drain.
444
+ */
445
+ private retouchSelfDeviceToken;
446
+ /**
447
+ * One-shot, first-cohort-growth reconstruction of the write-while-alone queue for
448
+ * an OWNER node: re-touch every membership row that may be an unreplicated
449
+ * owner insert. A row is a candidate iff it is not self (handled by
450
+ * {@link registerSelf}), is not already tracked in the in-memory queue (handled by
451
+ * {@link drainPendingPeerWrites}), and carries no self-`Sig` yet (an
452
+ * owner-authored row the peer has not self-published — the only kind safe to
453
+ * bump without invalidating a self-signature). O(rows) on the small control
454
+ * tables; safe to over-apply (a monotonic owner bump on an already-replicated
455
+ * row is a no-op-equivalent).
456
+ *
457
+ * A node with no owner private key skips this entirely — it cannot re-sign
458
+ * rows for other peers, and rows it merely holds are not its to re-issue. DELETEs
459
+ * cannot be reconstructed here (a removed row leaves no local trace and the schema
460
+ * carries no tombstone) — see the delete-path note in {@link drainPendingPeerWrites}.
461
+ */
462
+ private reconstructAuthoredMembership;
463
+ /**
464
+ * Drain the in-session write-while-alone queue: re-issue each pending owner
465
+ * membership write now that the cohort can broadcast it. Sequential (no fan-out)
466
+ * to avoid a thundering re-touch; an entry is cleared only on success, so a
467
+ * failure (or a still-alone re-commit) leaves it queued for the next growth.
468
+ *
469
+ * - `authorize`: re-issue as an idempotent monotonic owner UPDATE
470
+ * ({@link reissuePeerAuthorize}). Skipped (and cleared) if the row vanished
471
+ * (raced a delete) or now carries a self-`Sig` (the peer self-published and owns
472
+ * its republish).
473
+ * - `remove` (security-relevant): a removePeer-while-alone physically removed the
474
+ * row locally, so a re-issued DELETE matches nothing and does NOT propagate —
475
+ * full delete durability requires a schema tombstone (deferred:
476
+ * `control-delete-while-alone-tombstone`). We still attempt the re-issue
477
+ * (harmless, and it propagates in the row-still-present case); the commit-alone
478
+ * was already logged loudly in {@link noteControlWrite}.
479
+ */
480
+ private drainPendingPeerWrites;
481
+ /**
482
+ * Re-issue an owner membership row as a monotonic owner UPDATE: bump
483
+ * `UpdatedAt` strictly above the stored value (and the wall clock) and re-sign via
484
+ * the owner branch of `CadrePeer.AuthorizedUpdate`. The row already exists
485
+ * locally (it committed there, just local-only), so this is an UPDATE, not the
486
+ * original INSERT.
487
+ */
488
+ private reissuePeerAuthorize;
489
+ /**
490
+ * On a control connection opening, detect the 0→≥1 transition and drain the
491
+ * write-while-alone re-replication queue (single-flight; fires only on the edge,
492
+ * not on every subsequent `connection:open`). Best-effort — a drain failure is
493
+ * logged, not thrown.
88
494
  */
89
- private registerSelf;
495
+ private handleControlConnectionChange;
496
+ /** Re-arm the growth edge once all control connections drop, so a later reconnect re-drains. */
497
+ private handleControlConnectionClose;
498
+ /**
499
+ * Wire the control-connection growth listeners that drive write-while-alone
500
+ * re-replication. Wired in {@link start} (not the delayed refresh path) so no
501
+ * early `connection:open` is missed; torn down in {@link stopRecordRefresh}.
502
+ * Idempotent.
503
+ */
504
+ private wireControlConnectionListeners;
505
+ /**
506
+ * On a control connection opening: if it is a WebRTC connection, drain the TURN
507
+ * tracker for the just-settled ICE verdict and, when it relayed, mark the peer
508
+ * so {@link getConnectionPaths} classifies it `webrtc-turn` (relayed). The
509
+ * settlement↔open correlation is timing-based and best-effort; an unknown
510
+ * verdict degrades to not-relayed. Never throws to the event loop.
511
+ */
512
+ private handleTurnConnectionOpen;
513
+ /** On a control connection closing, drop any TURN-relayed flag for its peer. */
514
+ private handleTurnConnectionClose;
515
+ /**
516
+ * Publish (or refresh) this node's own self-signed `DeviceToken` row so a server
517
+ * peer can resolve its FCM/APNs push token from its PeerId alone. Mirrors
518
+ * {@link registerSelf}:
519
+ *
520
+ * - If the row already exists: a self-signed UPDATE bumping `UpdatedAt` (works
521
+ * for any member — the `AuthorizedUpdate` self-branch verifies the new `Sig`
522
+ * against the bound `CadrePeer.PublicKey`). Platform/Token may change here
523
+ * (rotation / platform switch / reinstall are all normal self-updates).
524
+ * - If not, and the node holds an owner service: an owner-signed INSERT
525
+ * that also carries the self-signature.
526
+ * - Otherwise: throws. Like `CadrePeer`, the first `DeviceToken` row requires an
527
+ * owner signature; a non-owner peer (e.g. a phone) must have its row
528
+ * seeded by an owner — typically the server it enrolled with — before it
529
+ * can self-refresh. (Establishing that phone→server registration handshake is
530
+ * the downstream "RN registration" ticket; this node only owns the cadre-core
531
+ * write path.)
532
+ *
533
+ * `UpdatedAt` strictly increases on every publish (even a same-millisecond
534
+ * re-publish), so a replayed older record is rejected by the schema.
535
+ *
536
+ * @param platform - `'fcm'` (Android/Firebase) or `'apns'` (Apple).
537
+ * @param token - the opaque platform device/registration token.
538
+ * @throws if the node is not started, exposes no self-signing key, or has no
539
+ * existing row and no owner service to self-insert.
540
+ */
541
+ registerDeviceToken(platform: PushPlatform, token: string): Promise<void>;
542
+ /**
543
+ * Resolve a cadre peer's FCM/APNs push token from only its PeerId — the input a
544
+ * server's push-wake fan-out consumes to deliver a platform push to a suspended
545
+ * app. Applies the same gating shape as {@link resolvePeerAddrs}, returning
546
+ * `null` (never throwing) on any failure:
547
+ *
548
+ * 1. membership — the peer has a `CadrePeer` row with a `PublicKey`,
549
+ * 2. `publicKey <-> peerId` binding — the stored key's libp2p identity is the
550
+ * requested peerId,
551
+ * 3. a `DeviceToken` row exists with a known {@link PushPlatform},
552
+ * 4. self-signature verifies against the bound `CadrePeer.PublicKey`,
553
+ * 5. freshness — `updatedAt` is positive and within `opts.maxAgeMs` (default:
554
+ * no ceiling, since a push token is valid until it rotates).
555
+ *
556
+ * A peer that is not a current member, or whose token has no backing `CadrePeer`
557
+ * record, resolves to `null` — a server must not attempt to push to a non-cadre
558
+ * peer.
559
+ */
560
+ resolveDeviceToken(peerId: string, opts?: ResolveDeviceTokenOpts): Promise<DeviceTokenRecord | null>;
561
+ /**
562
+ * Delete this node's own `DeviceToken` row (logout / token invalidation). No-op
563
+ * when no row exists. Like {@link registerDeviceToken}'s first insert, the delete
564
+ * is gated on an owner signature (`DeviceToken.AuthorizedInsert` covers insert
565
+ * AND delete), so it requires this node's owner service; a non-owner peer
566
+ * must route the clear through its owner (downstream RN registration path).
567
+ *
568
+ * @throws if the node is not started, or a row exists but no owner service is
569
+ * available to sign the delete.
570
+ */
571
+ clearDeviceToken(): Promise<void>;
572
+ /**
573
+ * Expire ANOTHER peer's stale `DeviceToken` after a platform reported it
574
+ * unregistered during a push-wake fan-out. Unlike {@link clearDeviceToken}
575
+ * (self-only — it hardcodes the local peerId), this takes an arbitrary peerId.
576
+ *
577
+ * - When this node holds an owner seed service, it deletes the row
578
+ * (`deleteDeviceToken` is owner-gated and accepts any peerId), so the peer
579
+ * is not retried until it re-registers.
580
+ * - When this node is NOT an owner it cannot delete the row, so it only logs
581
+ * that a re-registration is needed. The fan-out's own in-memory dead-token set
582
+ * is what actually stops re-pushing to the dead token this process — see
583
+ * {@link PushFanoutService}. That set is acceptably lossy across restarts (a
584
+ * restart re-learns staleness on the next failed send).
585
+ *
586
+ * Best-effort: never throws to the (best-effort) fan-out caller.
587
+ */
588
+ expireDeviceToken(peerId: string): Promise<void>;
90
589
  private handleStrandAdded;
91
590
  private handleStrandRemoved;
92
591
  private cleanup;
93
592
  private handleStrandIdle;
593
+ /**
594
+ * Hibernate a strand: release its strand-network resources via the strand
595
+ * manager (stop the libp2p node, close the StrandDatabase) and mark it
596
+ * `hibernating`. A quiesced strand holds no open strand-network connections,
597
+ * transports, or DB handles. No-ops if the strand is missing; if already
598
+ * quiesced (defensive), just marks status and emits.
599
+ */
94
600
  private handleStrandHibernate;
601
+ /**
602
+ * Wake a strand. If it was hibernating (quiesced — no libp2p node), re-resolve
603
+ * the cohort discovery seed and mode exactly as `launchStrand` does and rebuild
604
+ * its runtime via the strand manager. If it is still live (e.g. waking an idle
605
+ * strand, which retains its resources), just flip the status. Overlapping wake
606
+ * triggers are coalesced upstream by `HibernationManager`, so this runs once
607
+ * per wake; `resumeStrand` is itself idempotent as a backstop.
608
+ */
95
609
  private handleStrandWake;
610
+ /**
611
+ * Rebuild a quiesced strand's runtime, re-resolving the volatile cohort inputs
612
+ * first: the discovery seed may have grown and cohort membership may have
613
+ * shifted the mode `bootstrap → networked` since the strand last ran. Shared by
614
+ * the wake (`handleStrandWake`) and check-in (`handleStrandCheckIn`) paths so
615
+ * both apply the same fresh seed/mode resolution. `resumeStrand` is idempotent
616
+ * (returns the live instance unchanged) as a backstop against double-resume.
617
+ */
618
+ private resumeStrandRuntime;
619
+ /**
620
+ * Real cohort check-in for a hibernating strand (the `onCheckIn` callback).
621
+ *
622
+ * Optimystic syncs pull-on-read, not on connect, and exposes no cheap
623
+ * repo-level "pull pending" hook (`IRepo` is get/pend/commit/cancel only —
624
+ * see the review handoff). So "query the cohort for pending activity" is
625
+ * realized as a resume → bounded window → re-hibernate-if-idle cycle that
626
+ * reuses the existing quiesce/resume primitives rather than a bespoke probe:
627
+ *
628
+ * 1. Resume the strand (rebuild node + db, re-resolve cohort seed/mode) so
629
+ * its strand network can reach cohort peers — exactly as a wake does.
630
+ * 2. Hold it resumed for a bounded window, during which the app may drive
631
+ * reads (pull-on-read) and record activity.
632
+ * 3. If activity was recorded during the window, leave the strand `active`
633
+ * (the idle/hibernate timers + backoff reset take over). Otherwise
634
+ * quiesce again and leave it `hibernating`, so `HibernationManager`
635
+ * schedules the next, longer-delayed check-in.
636
+ *
637
+ * No-ops unless the strand is currently `hibernating` — a concurrent wake may
638
+ * have already resumed it.
639
+ */
640
+ private handleStrandCheckIn;
641
+ /**
642
+ * Window-then-decide for a just-resumed strand, shared by the check-in timer
643
+ * path ({@link handleStrandCheckIn}) and the on-demand {@link serviceWake}:
644
+ *
645
+ * 1. Capture the post-resume activity marker. `recordActivity` assigns a
646
+ * FRESH `Date`, so a changed reference after the window means real
647
+ * activity landed during it — not millisecond-resolution noise.
648
+ * 2. Hold the strand live for `windowMs` so its strand network reaches the
649
+ * cohort and the app can drive pull-on-read activity.
650
+ * 3. If activity landed, leave the strand `active` (return `true`); otherwise
651
+ * quiesce and mark it `hibernating` again (return `false`).
652
+ *
653
+ * @returns whether activity was observed during the window (strand left active).
654
+ */
655
+ private runWakeWindow;
656
+ /**
657
+ * Hold a just-resumed strand live for `windowMs` (default
658
+ * {@link DEFAULT_CHECKIN_WINDOW_MS} is applied by callers). A non-positive
659
+ * window resolves immediately. The pending timer is tracked in
660
+ * {@link windowWaiters} so {@link cleanup} can both clear it and resolve the
661
+ * promise on teardown — a `stop()` during an in-flight window must neither fire
662
+ * the timer afterward nor hang the awaiting check-in/serviceWake. Extracted as
663
+ * its own method so tests can stub the wait (and inject activity during it).
664
+ */
665
+ private holdWakeWindow;
666
+ /**
667
+ * Clear every in-flight wake window: cancel its timer and resolve its promise
668
+ * so any awaiting check-in/serviceWake completes promptly rather than hanging
669
+ * past teardown. Called from {@link cleanup}.
670
+ */
671
+ private clearWindowWaiters;
96
672
  /**
97
673
  * Add a strand with its sApp configuration.
98
674
  * The hosting application must provide the sApp schema when creating a strand.
99
675
  */
100
676
  addStrand(config: StrandConfig): Promise<StrandInstance>;
677
+ /**
678
+ * Publish a strand row to the shared control database under this node's own
679
+ * owner identity, so other cadre members discover it via control-network
680
+ * sync (their {@link StrandWatcher} fires `strand:discovered`).
681
+ *
682
+ * This is the owner-signed `Strand` INSERT that {@link addStrand}
683
+ * deliberately omits: `addStrand` only starts the LOCAL strand instance,
684
+ * whereas publishing makes the strand visible cadre-wide. A typical creator
685
+ * does both (start locally + publish); a discovering peer only does
686
+ * `addStrand` (the row already exists).
687
+ *
688
+ * The insert is signed with the ed25519 key behind this node's PeerId — which
689
+ * {@link ed25519KeyPairFromLibp2p} also exposes as the node's owner keypair,
690
+ * so peer identity and owner key are one and the same. That key must be
691
+ * enrolled in `OwnerKey` (e.g. via {@link ControlDatabase.ensureOwnerKey}
692
+ * at genesis) or the schema's `Strand.Authorized` constraint rejects the write.
693
+ * Failing loudly here is intentional: a silently-unpublished strand would run
694
+ * as a local-only island that no peer could ever discover or join.
695
+ *
696
+ * @param strandId - Unique strand identifier (typically the same id passed to
697
+ * {@link addStrand}).
698
+ * @param type - `'o'` for open (default) or `'c'` for closed.
699
+ * @param memberPrivateKey - Optional membership key for a closed strand.
700
+ * @throws if the node is not started, exposes no owner signing key, or the
701
+ * control DB rejects the (unauthorized) insert.
702
+ */
703
+ publishStrand(strandId: string, type?: 'o' | 'c', memberPrivateKey?: string): Promise<void>;
704
+ /**
705
+ * Publish an owner-signed `FormationInvite` (open-invitation token) to the
706
+ * shared control database, so a later {@link formStrand} redemption can be
707
+ * validated against it (the consent branch of `Strand.Authorized`).
708
+ *
709
+ * Counterpart to {@link createOpenInvitation}, which only mints the
710
+ * out-of-band {@link OpenInvitation} envelope: persisting the matching
711
+ * `FormationInvite` row is what makes the token *redeemable* — the host's
712
+ * {@link ControlFormationUsageRecorder} answers `isTokenValid`/`isTokenUsed`
713
+ * from this row. A host minting a closed-strand invite does both (mint +
714
+ * publish), exactly as the integration harness's `createInvitation` does.
715
+ *
716
+ * Signs with the same self-owner key as {@link publishStrand} (the ed25519
717
+ * key behind this node's PeerId, which must be an enrolled `OwnerKey`).
718
+ * Throws loudly if the node isn't started or exposes no signing key.
719
+ *
720
+ * @param token - Invitation token (the `FormationInvite` primary key); use the
721
+ * `token` of the {@link OpenInvitation} from {@link createOpenInvitation}.
722
+ * @param sAppId - The sApp a redeemed strand will use.
723
+ * @param options - Optional `expiresAtMs` (epoch ms), `totalUses`, `validationUrl`,
724
+ * `strandId` (bind a closed/pre-existing host strand for provision-then-record).
725
+ */
726
+ publishFormationInvite(token: string, sAppId: string, options?: {
727
+ expiresAtMs?: number;
728
+ totalUses?: number;
729
+ validationUrl?: string;
730
+ strandId?: string;
731
+ }): Promise<void>;
732
+ /**
733
+ * Shared strand launch path for both the explicit (`addStrand`) and the
734
+ * control-discovered (`handleStrandAdded`) entry points. Resolves the cohort
735
+ * seed, selects the mode, starts the strand, and registers it with the
736
+ * hibernation manager before emitting `strand:started`.
737
+ */
738
+ private launchStrand;
739
+ /**
740
+ * Resolve a strand's discovery seed. Membership (and thus mode) comes from the
741
+ * control network's CadrePeer rows; the **strand-network** bootstrap addresses
742
+ * are resolved on demand over the control mesh via the strand-addr RPC —
743
+ * deliberately NOT from `CadrePeer.Multiaddr`, which carries *control*
744
+ * addresses that must not seed the strand mesh.
745
+ *
746
+ * Only siblings we already hold an open control connection to are RPC'd: they
747
+ * are the ones that can answer right now, and dialing them by peerId reuses the
748
+ * live connection. When no connected sibling yet runs the strand the seed is
749
+ * empty (`[]`) — mode still follows membership, and the empty seed self-heals
750
+ * on the next resume / check-in pass. Returns an empty seed when the control DB
751
+ * or node is absent (not yet started / torn down).
752
+ */
753
+ private resolveCohortSeed;
754
+ /**
755
+ * The local strand instance's dialable strand-network multiaddrs for the
756
+ * strand-addr RPC, ordered signaling-first (reusing the control node's
757
+ * {@link orderSignalingFirst}). Returns `[]` when the strand is not running
758
+ * locally or has no live libp2p node (hibernating / quiescing / never
759
+ * participated) — a node only answers for a strand it is actively meshing,
760
+ * regardless of the strand's mode. A `bootstrap`-mode first node still has a
761
+ * live node, so it answers and a later sibling can dial in.
762
+ */
763
+ private getStrandMultiaddrs;
101
764
  /**
102
765
  * Remove a strand
103
766
  */
104
767
  removeStrand(strandId: string): Promise<void>;
105
768
  /**
106
- * Record activity on a strand (resets hibernation timer)
769
+ * Record activity on a strand (resets hibernation timer).
770
+ *
771
+ * Also drives the server push-wake fan-out: whatever already drives activity on
772
+ * this node's strand (its relay/app layer doing pull-on-read) additionally wakes
773
+ * hibernating mobile peers — the same imperative seam local-wake uses, with no
774
+ * new contract. No-op for the fan-out when push is not configured.
107
775
  */
108
776
  recordStrandActivity(strandId: string): void;
777
+ /**
778
+ * Explicit fan-out trigger: an always-on host/relay/sApp calls this when it
779
+ * observes activity for a strand this node participates in, to wake hibernating
780
+ * mobile members over a direct control-network dial (falling back to FCM/APNs
781
+ * for suspended phones). This is the supported, honest v1 trigger — Optimystic
782
+ * exposes no passive repo-level "new transaction" hook to drive it automatically
783
+ * (see the deferred passive-detector follow-up). No-op when push is not
784
+ * configured; best-effort (never throws — the check-in wake is the backstop).
785
+ *
786
+ * @param strandId - the strand that saw activity.
787
+ * @param reason - free-form cause hint carried in the wake (default `activity`).
788
+ */
789
+ notifyStrandActivity(strandId: string, reason?: string): void;
109
790
  /**
110
791
  * Force wake a hibernating strand
111
792
  */
112
793
  wakeStrand(strandId: string): Promise<void>;
794
+ /**
795
+ * Force a single strand to hibernate immediately, bypassing the idle/hibernate
796
+ * timers — the background-entry path. No-op if the strand is realtime
797
+ * (never-hibernate latency hint), already hibernating, or unknown.
798
+ *
799
+ * Routes through {@link HibernationManager.forceHibernate}, which cancels the
800
+ * strand's pending idle/hibernate (and check-in) timers — so a stale timer
801
+ * can't re-fire on or resurrect the strand — then runs the same `onHibernate`
802
+ * path as the timer (`quiesceStrand` + `status='hibernating'` +
803
+ * `strand:hibernating`). Unlike the timer path it does NOT re-arm check-ins:
804
+ * the strand stays down until the caller drives a wake (e.g. {@link serviceWake}).
805
+ */
806
+ hibernateStrand(strandId: string): Promise<void>;
807
+ /**
808
+ * Force-hibernate every tracked strand whose latency hint is not realtime,
809
+ * tolerating per-strand failure (one strand failing to quiesce never aborts
810
+ * the others). Realtime strands are left running — the caller keeps the control
811
+ * connection and realtime strands alive for as long as the OS permits.
812
+ *
813
+ * @returns the strandIds actually hibernated (now in `hibernating` status);
814
+ * realtime strands are excluded.
815
+ */
816
+ hibernateAll(): Promise<string[]>;
817
+ /**
818
+ * On-demand equivalent of a check-in cycle, for a push-delivered wake on
819
+ * mobile: resume the strand, hold it live for `windowMs` so its strand network
820
+ * reaches the cohort and the app can pull pending activity, then re-hibernate
821
+ * if no activity was recorded (else leave it active).
822
+ *
823
+ * Idempotent / coalesced two ways: concurrent `serviceWake`s for the same
824
+ * strand share one in-flight operation ({@link serviceWakePromises}), and the
825
+ * underlying resume coalesces with a racing push-wake via
826
+ * {@link HibernationManager}'s wake coalescing — one runtime build, one window,
827
+ * one re-hibernate decision. Returns `{ serviced: false }` (never throws) when
828
+ * the node is not running or the strand is unknown, and surfaces a resume
829
+ * failure as `{ serviced: true, hadActivity: false }` after re-hibernating.
830
+ *
831
+ * @param strandId - the strand a push said has pending activity.
832
+ * @param opts.windowMs - override the live-window duration (defaults to the
833
+ * configured `checkInWindowMs` / {@link DEFAULT_CHECKIN_WINDOW_MS}).
834
+ */
835
+ serviceWake(strandId: string, opts?: {
836
+ windowMs?: number;
837
+ }): Promise<ServiceWakeResult>;
838
+ /** Body of {@link serviceWake}; serialised per-strand by its coalescing guard. */
839
+ private runServiceWake;
113
840
  /**
114
841
  * Get the control network node (for advanced use)
115
842
  */
@@ -127,16 +854,81 @@ export declare class CadreNode implements SAppIdLookup {
127
854
  */
128
855
  getSAppConfig(strandId: string): SAppConfig | undefined;
129
856
  /**
130
- * Initialize the seed bootstrap service with an authority key.
857
+ * Initialize the seed bootstrap service with an owner key.
131
858
  * Must be called before using seed-related methods that require signing.
132
859
  *
133
- * @param authorityPrivateKey - The authority's private key (base64url encoded)
860
+ * @param ownerPrivateKey - The owner's private key (base64url encoded)
861
+ */
862
+ initializeSeedBootstrap(ownerPrivateKey: string): void;
863
+ /**
864
+ * Push the multiaddrs that future invites should advertise. Pass `null` to
865
+ * revert to the libp2p-reported addresses (the default). The host calls this
866
+ * at spawn and on every NAT change.
867
+ */
868
+ setInviteAddresses(addresses: string[] | null): void;
869
+ /**
870
+ * Resolve the addresses to embed in invites. Prefers pushed addresses, then
871
+ * any config-supplied resolver, then the libp2p-observed multiaddrs.
872
+ */
873
+ private resolveInviteAddresses;
874
+ /**
875
+ * Enumerate the cadre's `CadrePeer` membership.
876
+ */
877
+ listMembers(): Promise<Array<{
878
+ peerId: string;
879
+ multiaddr: string | null;
880
+ }>>;
881
+ /**
882
+ * Probe whether a given peer is a `CadrePeer` member.
883
+ *
884
+ * This is the ADDRESSABLE surface ("do I have a dialable address record for this
885
+ * peer") — it includes this node's own self-published row. Address resolution and
886
+ * push fan-out use this. The trust-facing gate is {@link isAuthorizedMember}.
887
+ */
888
+ isMember(peerId: string): Promise<boolean>;
889
+ /**
890
+ * Enumerate the party's AUTHORIZED members — the trust-facing set, distinct from
891
+ * the addressable set ({@link listMembers}). Excludes this node itself: a node
892
+ * publishes its own `CadrePeer` address row so its dialable address rides in seeds,
893
+ * but "self" is not a peer this node authorized. The control-network wake and
894
+ * strand-address gates consult this set, NOT the addressable one.
895
+ *
896
+ * NOTE: this is the addressable set minus self for now. The real trust predicate
897
+ * (voucher recorded on the row ∈ node-local trusted-owner anchor, signature
898
+ * verified) lands with the `membership-node-local-owner-anchor` /
899
+ * `membership-authorized-predicate-and-gates` tickets; keeping the filter in this one
900
+ * method means that change touches a single place.
901
+ */
902
+ listAuthorizedMembers(): Promise<Array<{
903
+ peerId: string;
904
+ multiaddr: string | null;
905
+ }>>;
906
+ /**
907
+ * Probe whether a given peer is an AUTHORIZED party member (see
908
+ * {@link listAuthorizedMembers}) — the gate the control-network wake and
909
+ * strand-address responders consult, NOT {@link isMember} (the addressable surface).
134
910
  */
135
- initializeSeedBootstrap(authorityPrivateKey: string): void;
911
+ isAuthorizedMember(peerId: string): Promise<boolean>;
912
+ /**
913
+ * Push-wake a hibernating cadre peer over the control network.
914
+ *
915
+ * Resolves the target's signed control-network address from its `CadrePeer`
916
+ * record (via {@link resolvePeerAddrs}, signaling/relay first — so a NAT'd peer
917
+ * is reachable through its circuit-relay address), dials `WAKE_PROTOCOL`, sends
918
+ * the {@link WakeRequest}, and returns the peer's {@link WakeAck}. The receiver
919
+ * gates the request on cadre membership and only resumes a strand it already
920
+ * participates in.
921
+ *
922
+ * @param targetPeerId - The hibernating cadre peer to wake.
923
+ * @param strandId - The strand the caller knows has pending activity.
924
+ * @param reason - Optional cause hint, e.g. `"activity"` or `"manual"`.
925
+ * @throws if the node is not started or the target has no dialable address.
926
+ */
927
+ pushWake(targetPeerId: string, strandId: string, reason?: string): Promise<WakeAck>;
136
928
  /**
137
929
  * Enable the seed listener for receiving seeds via the /sereus/seed/1.0.0 protocol.
138
- * This is for drone nodes that need to receive seeds without being an authority.
139
- * Does not require an authority key.
930
+ * This is for drone nodes that need to receive seeds without being an owner.
931
+ * Does not require an owner key.
140
932
  */
141
933
  enableSeedListener(): void;
142
934
  /**
@@ -145,7 +937,7 @@ export declare class CadreNode implements SAppIdLookup {
145
937
  getSeedBootstrapService(): SeedBootstrapService | null;
146
938
  /**
147
939
  * Authorize a new peer to join the cadre.
148
- * Signs the peer ID with the authority key and inserts into CadrePeer table.
940
+ * Signs the peer ID with the owner key and inserts into CadrePeer table.
149
941
  *
150
942
  * @param peerId - The peer ID to authorize
151
943
  * @param multiaddrs - Optional multiaddrs for the peer
@@ -153,21 +945,28 @@ export declare class CadreNode implements SAppIdLookup {
153
945
  authorizePeer(peerId: string, multiaddrs?: string[]): Promise<void>;
154
946
  /**
155
947
  * Remove a previously-authorized peer from the cadre.
156
- * Signs the peer ID with the authority key and deletes the CadrePeer row.
948
+ * Signs the peer ID with the owner key and deletes the CadrePeer row.
157
949
  *
158
950
  * @param peerId - The peer ID to remove
159
951
  */
160
952
  removePeer(peerId: string): Promise<void>;
161
953
  /**
162
954
  * Create a seed from the current control network state.
163
- * The seed contains peer information and is signed by an authority.
955
+ * The seed contains peer information and is signed by an owner.
164
956
  */
165
957
  createSeed(): Promise<ControlNetworkSeed>;
166
958
  /**
167
959
  * Apply a seed to populate the peer cache and enable connections.
168
- * Validates the seed signature before applying.
960
+ *
961
+ * Validates the seed signature, then evaluates a trust anchor for the signer
962
+ * key (see `SeedTrustPolicy`). An enrollment caller can pass a per-seed
963
+ * `trustPolicy` override — e.g. a `pinnedKeyTrustPolicy` built from a
964
+ * `CadreInvite.ownerKeys` — so a cold-start node can accept its first
965
+ * seed without reconfiguring the service.
169
966
  */
170
- applySeed(seed: ControlNetworkSeed): Promise<ApplySeedResult>;
967
+ applySeed(seed: ControlNetworkSeed, options?: {
968
+ trustPolicy?: SeedTrustPolicy;
969
+ }): Promise<ApplySeedResult>;
171
970
  /**
172
971
  * Deliver a seed directly to a peer via the /sereus/seed/1.0.0 protocol.
173
972
  */
@@ -217,7 +1016,7 @@ export declare class CadreNode implements SAppIdLookup {
217
1016
  */
218
1017
  decodeInvite(encoded: string): CadreInvite;
219
1018
  /**
220
- * Dial an authority from an invite (for phone joining via invite).
1019
+ * Dial an owner from an invite (for phone joining via invite).
221
1020
  */
222
1021
  dialInvite(invite: CadreInvite): Promise<void>;
223
1022
  /**