@serfab/cadre-core 0.7.1 → 0.8.1

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 (166) hide show
  1. package/README.md +9 -8
  2. package/dist/authority-key.d.ts +41 -0
  3. package/dist/authority-key.js +52 -0
  4. package/dist/authority-key.js.map +1 -0
  5. package/dist/cadre-node.d.ts +770 -11
  6. package/dist/cadre-node.js +1900 -88
  7. package/dist/cadre-node.js.map +1 -1
  8. package/dist/canonical-datetime.d.ts +33 -0
  9. package/dist/canonical-datetime.js +38 -0
  10. package/dist/canonical-datetime.js.map +1 -0
  11. package/dist/canonical-json.d.ts +12 -0
  12. package/dist/canonical-json.js +33 -0
  13. package/dist/canonical-json.js.map +1 -0
  14. package/dist/control-cohort.d.ts +63 -0
  15. package/dist/control-cohort.js +80 -0
  16. package/dist/control-cohort.js.map +1 -0
  17. package/dist/control-database.d.ts +259 -5
  18. package/dist/control-database.js +541 -128
  19. package/dist/control-database.js.map +1 -1
  20. package/dist/control-formation-recorder.d.ts +89 -0
  21. package/dist/control-formation-recorder.js +139 -0
  22. package/dist/control-formation-recorder.js.map +1 -0
  23. package/dist/control-schema.d.ts +12 -0
  24. package/dist/control-schema.js +199 -0
  25. package/dist/control-schema.js.map +1 -0
  26. package/dist/control-stream.d.ts +54 -0
  27. package/dist/control-stream.js +104 -0
  28. package/dist/control-stream.js.map +1 -0
  29. package/dist/device-token.d.ts +50 -0
  30. package/dist/device-token.js +66 -0
  31. package/dist/device-token.js.map +1 -0
  32. package/dist/diagnostics/connection-path.d.ts +118 -0
  33. package/dist/diagnostics/connection-path.js +204 -0
  34. package/dist/diagnostics/connection-path.js.map +1 -0
  35. package/dist/diagnostics/webrtc-turn-tracker.d.ts +50 -0
  36. package/dist/diagnostics/webrtc-turn-tracker.js +196 -0
  37. package/dist/diagnostics/webrtc-turn-tracker.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 +26 -0
  51. package/dist/peer-authorization.js +40 -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-notifier-apns.d.ts +53 -0
  63. package/dist/push-notifier-apns.js +195 -0
  64. package/dist/push-notifier-apns.js.map +1 -0
  65. package/dist/push-notifier-fcm.d.ts +38 -0
  66. package/dist/push-notifier-fcm.js +183 -0
  67. package/dist/push-notifier-fcm.js.map +1 -0
  68. package/dist/push-notifier-shared.d.ts +18 -0
  69. package/dist/push-notifier-shared.js +25 -0
  70. package/dist/push-notifier-shared.js.map +1 -0
  71. package/dist/push-notifier.d.ts +71 -0
  72. package/dist/push-notifier.js +50 -0
  73. package/dist/push-notifier.js.map +1 -0
  74. package/dist/schema-verification.d.ts +11 -2
  75. package/dist/schema-verification.js +15 -4
  76. package/dist/schema-verification.js.map +1 -1
  77. package/dist/seed-bootstrap.d.ts +197 -9
  78. package/dist/seed-bootstrap.js +396 -137
  79. package/dist/seed-bootstrap.js.map +1 -1
  80. package/dist/seed-trust-policy.d.ts +63 -0
  81. package/dist/seed-trust-policy.js +56 -0
  82. package/dist/seed-trust-policy.js.map +1 -0
  83. package/dist/strand-addr-protocol.d.ts +132 -0
  84. package/dist/strand-addr-protocol.js +313 -0
  85. package/dist/strand-addr-protocol.js.map +1 -0
  86. package/dist/strand-cohort.d.ts +54 -0
  87. package/dist/strand-cohort.js +38 -0
  88. package/dist/strand-cohort.js.map +1 -0
  89. package/dist/strand-database.d.ts +54 -6
  90. package/dist/strand-database.js +88 -100
  91. package/dist/strand-database.js.map +1 -1
  92. package/dist/strand-formation-manager.d.ts +79 -19
  93. package/dist/strand-formation-manager.js +196 -104
  94. package/dist/strand-formation-manager.js.map +1 -1
  95. package/dist/strand-formation-protocol.d.ts +162 -0
  96. package/dist/strand-formation-protocol.js +279 -0
  97. package/dist/strand-formation-protocol.js.map +1 -0
  98. package/dist/strand-instance-manager.d.ts +73 -4
  99. package/dist/strand-instance-manager.js +167 -41
  100. package/dist/strand-instance-manager.js.map +1 -1
  101. package/dist/strand-member-key.d.ts +29 -0
  102. package/dist/strand-member-key.js +38 -0
  103. package/dist/strand-member-key.js.map +1 -0
  104. package/dist/strand-member-registry.d.ts +98 -0
  105. package/dist/strand-member-registry.js +129 -0
  106. package/dist/strand-member-registry.js.map +1 -0
  107. package/dist/strand-membership-writer.d.ts +361 -0
  108. package/dist/strand-membership-writer.js +449 -0
  109. package/dist/strand-membership-writer.js.map +1 -0
  110. package/dist/strand-solicitation.d.ts +77 -2
  111. package/dist/strand-solicitation.js +25 -4
  112. package/dist/strand-solicitation.js.map +1 -1
  113. package/dist/strand-wake-payload.d.ts +30 -0
  114. package/dist/strand-wake-payload.js +20 -0
  115. package/dist/strand-wake-payload.js.map +1 -0
  116. package/dist/strand-wake-protocol.d.ts +114 -0
  117. package/dist/strand-wake-protocol.js +257 -0
  118. package/dist/strand-wake-protocol.js.map +1 -0
  119. package/dist/strand-watcher.d.ts +6 -2
  120. package/dist/strand-watcher.js +57 -17
  121. package/dist/strand-watcher.js.map +1 -1
  122. package/dist/types.d.ts +406 -23
  123. package/dist/types.js +22 -5
  124. package/dist/types.js.map +1 -1
  125. package/package.json +14 -8
  126. package/src/authority-key.ts +68 -0
  127. package/src/cadre-node.ts +2937 -948
  128. package/src/canonical-datetime.ts +39 -0
  129. package/src/canonical-json.ts +30 -0
  130. package/src/control-cohort.ts +103 -0
  131. package/src/control-database.ts +931 -383
  132. package/src/control-formation-recorder.ts +151 -0
  133. package/src/control-schema.ts +198 -0
  134. package/src/control-stream.ts +128 -0
  135. package/src/device-token.ts +89 -0
  136. package/src/diagnostics/connection-path.ts +277 -0
  137. package/src/diagnostics/webrtc-turn-tracker.ts +222 -0
  138. package/src/hibernation-manager.ts +197 -28
  139. package/src/index.ts +273 -79
  140. package/src/key-store-file.ts +203 -0
  141. package/src/key-store.ts +99 -0
  142. package/src/peer-authorization.ts +53 -0
  143. package/src/peer-record.ts +146 -0
  144. package/src/push-credentials.ts +84 -0
  145. package/src/push-fanout.ts +272 -0
  146. package/src/push-notifier-apns.ts +243 -0
  147. package/src/push-notifier-fcm.ts +221 -0
  148. package/src/push-notifier-shared.ts +28 -0
  149. package/src/push-notifier.ts +93 -0
  150. package/src/schema-verification.ts +18 -4
  151. package/src/seed-bootstrap.ts +464 -188
  152. package/src/seed-trust-policy.ts +107 -0
  153. package/src/strand-addr-protocol.ts +410 -0
  154. package/src/strand-cohort.ts +77 -0
  155. package/src/strand-database.ts +113 -141
  156. package/src/strand-formation-manager.ts +233 -142
  157. package/src/strand-formation-protocol.ts +391 -0
  158. package/src/strand-instance-manager.ts +207 -56
  159. package/src/strand-member-key.ts +39 -0
  160. package/src/strand-member-registry.ts +177 -0
  161. package/src/strand-membership-writer.ts +640 -0
  162. package/src/strand-solicitation.ts +93 -4
  163. package/src/strand-wake-payload.ts +32 -0
  164. package/src/strand-wake-protocol.ts +331 -0
  165. package/src/strand-watcher.ts +61 -17
  166. package/src/types.ts +1012 -582
package/src/types.ts CHANGED
@@ -1,582 +1,1012 @@
1
- import type { Libp2p, PeerId, PrivateKey } from '@libp2p/interface';
2
- import type { IRawStorage, Libp2pTransports } from '@optimystic/db-p2p';
3
- import type { StrandDatabase } from './strand-database.js';
4
-
5
- /**
6
- * Node profile determines storage participation
7
- */
8
- export type NodeProfile = 'transaction' | 'storage';
9
-
10
- /**
11
- * Strand filter configuration - determines which strands this node participates in
12
- */
13
- export type StrandFilter =
14
- | { mode: 'all' } // All strands (default for servers)
15
- | { mode: 'sAppId'; sAppId: string } // Only strands for specific sApp
16
- | { mode: 'strandId'; strandId: string } // Single specific strand
17
- | { mode: 'none' }; // Control network only
18
-
19
- /**
20
- * Latency hint for strand hibernation behavior
21
- */
22
- export type LatencyHint = 'realtime' | 'interactive' | 'background' | 'archive';
23
-
24
- /**
25
- * Hibernation timeout configuration per latency hint (in milliseconds)
26
- */
27
- export interface HibernationTimeouts {
28
- /** Time before transitioning from active to idle */
29
- idleTimeout: number;
30
- /** Time before transitioning from idle to hibernating */
31
- hibernateTimeout: number;
32
- /** Interval for check-ins while hibernating */
33
- checkInInterval: number;
34
- }
35
-
36
- /**
37
- * Default hibernation timeouts per latency hint
38
- */
39
- export const HIBERNATION_TIMEOUTS: Record<LatencyHint, HibernationTimeouts> = {
40
- realtime: {
41
- idleTimeout: Infinity, // Never idle
42
- hibernateTimeout: Infinity, // Never hibernate
43
- checkInInterval: Infinity // N/A
44
- },
45
- interactive: {
46
- idleTimeout: 5 * 60 * 1000, // 5 minutes
47
- hibernateTimeout: 15 * 60 * 1000, // 15 minutes after idle
48
- checkInInterval: 30 * 1000 // 30 seconds
49
- },
50
- background: {
51
- idleTimeout: 1 * 60 * 1000, // 1 minute
52
- hibernateTimeout: 5 * 60 * 1000, // 5 minutes after idle
53
- checkInInterval: 5 * 60 * 1000 // 5 minutes
54
- },
55
- archive: {
56
- idleTimeout: 10 * 1000, // 10 seconds
57
- hibernateTimeout: 30 * 1000, // 30 seconds after idle
58
- checkInInterval: 60 * 60 * 1000 // 1 hour
59
- }
60
- };
61
-
62
- /**
63
- * Storage provider type - either an IRawStorage instance or a factory function.
64
- * Factory functions are useful for creating strand-specific storage instances.
65
- */
66
- export type RawStorageProvider = IRawStorage | ((strandId: string) => IRawStorage);
67
-
68
- /**
69
- * Storage configuration for storage profile nodes
70
- */
71
- export interface StorageConfig {
72
- /**
73
- * Storage provider - either an IRawStorage instance or a factory function.
74
- *
75
- * For Node.js environments, use FileRawStorage from @optimystic/db-p2p-storage-fs:
76
- * ```typescript
77
- * import { FileRawStorage } from '@optimystic/db-p2p-storage-fs';
78
- * storage: { provider: (strandId) => new FileRawStorage(`./data/${strandId}`) }
79
- * ```
80
- *
81
- * For React Native, use the appropriate storage from @optimystic/db-p2p-storage-rn:
82
- * ```typescript
83
- * import { RNRawStorage } from '@optimystic/db-p2p-storage-rn';
84
- * storage: { provider: (strandId) => new RNRawStorage(strandId) }
85
- * ```
86
- *
87
- * For in-memory storage (testing):
88
- * ```typescript
89
- * import { MemoryRawStorage } from '@optimystic/db-p2p';
90
- * storage: { provider: () => new MemoryRawStorage() }
91
- * ```
92
- */
93
- provider: RawStorageProvider;
94
- quotaBytes?: number;
95
- }
96
-
97
- /**
98
- * Network configuration for libp2p
99
- */
100
- export interface NetworkConfig {
101
- listenAddrs?: string[];
102
- announceAddrs?: string[];
103
- relayAddrs?: string[];
104
- /**
105
- * Enable circuit relay server - allows this node to relay connections for other peers.
106
- * When undefined, defaults to true for storage profile nodes (they typically have
107
- * better connectivity and uptime), false for transaction profile nodes.
108
- */
109
- enableRelay?: boolean;
110
- /**
111
- * Custom libp2p transports. When omitted, the default transports from
112
- * `@optimystic/db-p2p` are used (TCP + circuit relay for Node.js).
113
- *
114
- * React Native apps must supply WebSocket-based transports because TCP
115
- * is not available in the RN runtime:
116
- * ```typescript
117
- * import { webSockets } from '@libp2p/websockets';
118
- * import { circuitRelayTransport } from '@libp2p/circuit-relay-v2';
119
- *
120
- * network: {
121
- * transports: [webSockets(), circuitRelayTransport()],
122
- * listenAddrs: [] // RN nodes typically cannot listen
123
- * }
124
- * ```
125
- */
126
- transports?: Libp2pTransports;
127
- /**
128
- * Optional async resolver returning the multiaddrs to embed in invites
129
- * (and other authority-address contexts). When unset, `libp2pNode.getMultiaddrs()`
130
- * is used. Hosts behind NAT supply this to substitute their DDNS hostname
131
- * and externally-mapped port — see `@serfab/cadre-host`'s NatService.
132
- */
133
- inviteAddressResolver?: () => Promise<string[]>;
134
- }
135
-
136
- /**
137
- * Hibernation configuration
138
- */
139
- export interface HibernationConfig {
140
- enabled: boolean;
141
- defaultLatencyHint?: LatencyHint;
142
- /** Custom timeouts per latency hint (overrides defaults) */
143
- customTimeouts?: Partial<Record<LatencyHint, Partial<HibernationTimeouts>>>;
144
- }
145
-
146
- /**
147
- * Control network configuration
148
- */
149
- export interface ControlNetworkConfig {
150
- partyId: string;
151
- bootstrapNodes: string[];
152
- /** Optional path to the control schema file (defaults to bundled schema) */
153
- schemaPath?: string;
154
- }
155
-
156
- /**
157
- * Main configuration for a CadreNode
158
- */
159
- export interface CadreNodeConfig {
160
- /** If provided, use this keypair for the node identity */
161
- privateKey?: PrivateKey;
162
-
163
- /** Control network connection settings */
164
- controlNetwork: ControlNetworkConfig;
165
-
166
- /** Node profile: transaction-only or storage */
167
- profile: NodeProfile;
168
-
169
- /** Which strands to participate in */
170
- strandFilter?: StrandFilter;
171
-
172
- /** Storage configuration (only for storage profile) */
173
- storage?: StorageConfig;
174
-
175
- /** Network configuration */
176
- network?: NetworkConfig;
177
-
178
- /** Hibernation configuration */
179
- hibernation?: HibernationConfig;
180
-
181
- /** Polling interval for strand watcher in ms (default: 5000) */
182
- strandWatchInterval?: number;
183
- }
184
-
185
- /**
186
- * Status of a strand instance
187
- */
188
- export type StrandStatus =
189
- | 'starting'
190
- | 'active'
191
- | 'idle'
192
- | 'hibernating'
193
- | 'stopping'
194
- | 'stopped'
195
- | 'error';
196
-
197
- /**
198
- * App information from strand header
199
- */
200
- export interface SAppInfo {
201
- id: string;
202
- version: string;
203
- schema: string;
204
- signature?: string;
205
- }
206
-
207
- /**
208
- * Strand instance state
209
- */
210
- export interface StrandInstance {
211
- strandId: string;
212
- status: StrandStatus;
213
- sAppInfo?: SAppInfo;
214
-
215
- /** The libp2p node for this strand (only when active/idle) */
216
- libp2pNode?: Libp2p;
217
-
218
- /** The Quereus database for this strand (only when active/idle) */
219
- database?: StrandDatabase;
220
-
221
- /** Membership info for closed strands */
222
- memberKey?: string;
223
- memberPrivateKey?: string;
224
-
225
- /** Activity tracking */
226
- connectedPeers: number;
227
- lastActivity: Date;
228
- nextCheckIn?: Date;
229
-
230
- /** Latency hint from app or override */
231
- latencyHint: LatencyHint;
232
-
233
- /** Error message if status is 'error' */
234
- error?: string;
235
- }
236
-
237
- /**
238
- * Strand row from control network - basic membership info
239
- */
240
- export interface StrandRow {
241
- Id: string;
242
- MemberPrivateKey: string | null;
243
- Type: 'o' | 'c';
244
- }
245
-
246
- /**
247
- * sApp configuration provided by the hosting application when creating a strand.
248
- * This is what the app developer provides - NOT loaded from the network.
249
- */
250
- export interface SAppConfig {
251
- /** Public key of the sApp author */
252
- id: string;
253
- /** Version of the sApp */
254
- version: string;
255
- /** The declarative schema DDL */
256
- schema: string;
257
- /** Author's signature over the schema for verification (omit to skip verification) */
258
- signature?: string;
259
- /** Latency hint for hibernation behavior (optional, defaults to config) */
260
- latencyHint?: LatencyHint;
261
- }
262
-
263
- /**
264
- * Lifecycle mode for a strand.
265
- *
266
- * - `bootstrap`: newly created locally; no remote peers exist for this strand yet.
267
- * Schema apply and data operations route through a local (non-network) transactor
268
- * so a solo node can fully initialize without any peer round trips.
269
- * - `networked`: strand has (or is expected to have) remote peers. Operations route
270
- * through the network transactor.
271
- *
272
- * Transition `bootstrap -> networked` happens at first remote-peer enrollment for
273
- * the strand; because the transactor is fixed at plugin registration time, the
274
- * transition requires stopping and restarting the strand.
275
- */
276
- export type StrandMode = 'bootstrap' | 'networked';
277
-
278
- /**
279
- * Full strand configuration when adding a strand via the API
280
- */
281
- export interface StrandConfig {
282
- /** Strand row from control network */
283
- strandRow: StrandRow;
284
- /** sApp configuration provided by the hosting application */
285
- sAppConfig: SAppConfig;
286
- /**
287
- * Lifecycle mode for this strand instance. Defaults to `'networked'` when omitted
288
- * to preserve the historical behavior for multi-peer callers.
289
- */
290
- mode?: StrandMode;
291
- }
292
-
293
- /**
294
- * Result of creating a new cadre peer
295
- */
296
- export interface CreatePeerResult {
297
- peerId: PeerId;
298
- privateKey: Uint8Array;
299
- }
300
-
301
- // ============================================================================
302
- // Member Registration API Types (from api.md)
303
- // ============================================================================
304
-
305
- /**
306
- * Registration data for joining a strand as a member.
307
- * Sent from invited member to any cadre member to accept invitation.
308
- */
309
- export interface MemberRegistration {
310
- /** The strand being joined */
311
- strandId: string;
312
- /** The member's public key for this strand */
313
- key: string;
314
- /** Peer IDs of the member's cadre nodes that will participate */
315
- peerIds: string[];
316
- }
317
-
318
- /**
319
- * Result of member registration
320
- */
321
- export interface MemberRegistrationResult {
322
- success: boolean;
323
- reason?: string;
324
- }
325
-
326
- // ============================================================================
327
- // Strand Solicitation API Types (from api.md)
328
- // ============================================================================
329
-
330
- /**
331
- * Open invitation for strand formation.
332
- * Shared out-of-band to allow strangers to form strands.
333
- */
334
- export interface OpenInvitation {
335
- /** Unique token identifying this invitation */
336
- token: string;
337
- /** The sApp that will be used for the strand */
338
- sAppId: string;
339
- /** When this invitation expires */
340
- expiration: Date;
341
- /** Bootstrap addresses to contact the inviter's cadre */
342
- bootstrap: string[];
343
- }
344
-
345
- /**
346
- * Result of forming a strand via open invitation
347
- */
348
- export interface FormStrandResult {
349
- /** The member key assigned to the initiator for this strand */
350
- memberKey: string;
351
- /** Private key for the invitation (for signing future messages) */
352
- invitePrivateKey: string;
353
- /** The strand that was created */
354
- strandId: string;
355
- }
356
-
357
- /**
358
- * Result of validating a strand formation request
359
- */
360
- export interface ValidateFormationResult {
361
- /** Public key for validating this formation */
362
- validationKey: string;
363
- /** Signature over the formation approval */
364
- validationSignature: string;
365
- }
366
-
367
- /**
368
- * Disclosure object provided during strand formation.
369
- * Contains identity and context information from the initiator.
370
- */
371
- export interface StrandFormationDisclosure {
372
- /** Initiator's party identifier */
373
- partyId?: string;
374
- /** Additional identity bundle (app-specific) */
375
- identityBundle?: unknown;
376
- /** Human-readable purpose/reason for forming the strand */
377
- purpose?: string;
378
- /** Additional app-specific metadata */
379
- metadata?: Record<string, unknown>;
380
- }
381
-
382
- /**
383
- * Arachnode ring participation stub - will be implemented when arachnode is built
384
- */
385
- export interface ArachnodeConfig {
386
- /** Enable Arachnode subsystem (Ring Zulu + storage rings). Storage-profile only. */
387
- enableRingZulu: boolean;
388
- /** Storage ring participation (storage profile only) */
389
- storageRing?: {
390
- /** Which ring to participate in based on capacity */
391
- ring: number;
392
- /** Partition within the ring */
393
- partition?: number;
394
- };
395
- }
396
-
397
- /**
398
- * Events emitted by CadreNode
399
- */
400
- export interface CadreNodeEvents {
401
- 'strand:started': { strandId: string };
402
- 'strand:stopped': { strandId: string };
403
- 'strand:error': { strandId: string; error: Error };
404
- 'strand:idle': { strandId: string };
405
- 'strand:hibernating': { strandId: string };
406
- 'strand:waking': { strandId: string };
407
- 'control:connected': void;
408
- 'control:disconnected': void;
409
- /** Emitted when a seed is received via the seed protocol */
410
- 'seed:received': { partyId: string; peerId: string };
411
- /** Emitted when a seed is successfully applied */
412
- 'seed:applied': { partyId: string; peersAdded: number };
413
- /** Emitted when seed application fails */
414
- 'seed:error': { partyId: string; error: string };
415
- }
416
-
417
- // ============================================================================
418
- // Seed Bootstrap API Types (from architecture.md)
419
- // ============================================================================
420
-
421
- /**
422
- * A peer entry in a control network seed.
423
- * Contains connection information for a cadre peer.
424
- */
425
- export interface SeedPeer {
426
- /** Peer ID (libp2p identity) */
427
- peerId: string;
428
- /** Multiaddrs for connecting to this peer */
429
- multiaddrs: string[];
430
- /** Whether this peer holds an authority key */
431
- isAuthority: boolean;
432
- /** ed25519 public key (base64url) — present on authority peers for signerKey verification */
433
- publicKey?: string;
434
- }
435
-
436
- /**
437
- * Control network seed for bootstrapping new nodes.
438
- * Pre-populates a new node's cache to solve the cold-start problem:
439
- * new nodes need control data to validate connections, but can't get
440
- * data without connecting first.
441
- */
442
- export interface ControlNetworkSeed {
443
- /** Party ID this seed belongs to */
444
- partyId: string;
445
- /** Known peers in the cadre */
446
- peers: SeedPeer[];
447
- /** Optional signed transactions to pre-populate the cache */
448
- transactions?: SignedTransaction[];
449
- /** Signature over the seed by an authority key */
450
- signature: string;
451
- /** The authority key that signed this seed */
452
- signerKey: string;
453
- }
454
-
455
- /**
456
- * A signed transaction for inclusion in seeds.
457
- * Allows pre-populating the control network cache.
458
- */
459
- export interface SignedTransaction {
460
- /** Transaction ID */
461
- id: string;
462
- /** Serialized transaction data */
463
- data: string;
464
- /** Signature over the transaction */
465
- signature: string;
466
- }
467
-
468
- /**
469
- * Message sent from instigator to new node during seed delivery.
470
- * Delivered via /sereus/seed/1.0.0 libp2p protocol.
471
- */
472
- export interface SeedMessage {
473
- /** Party ID for the cadre */
474
- partyId: string;
475
- /** Known peers in the cadre */
476
- peers: SeedPeer[];
477
- /** Optional transactions to pre-populate cache */
478
- transactions?: SignedTransaction[];
479
- /** Signature by an authority key */
480
- signature: string;
481
- /** The authority key that signed this message */
482
- signerKey: string;
483
- }
484
-
485
- /**
486
- * Acknowledgment message from new node to instigator.
487
- */
488
- export interface SeedAckMessage {
489
- /** Whether the seed was accepted */
490
- accepted: boolean;
491
- /** Reason for rejection (if not accepted) */
492
- reason?: string;
493
- }
494
-
495
- /**
496
- * Options for authorizing a new peer in the cadre.
497
- */
498
- export interface AuthorizePeerOptions {
499
- /** Peer ID to authorize */
500
- peerId: string;
501
- /** Optional multiaddrs for the peer */
502
- multiaddrs?: string[];
503
- }
504
-
505
- /**
506
- * Result of applying a seed to a node.
507
- */
508
- export interface ApplySeedResult {
509
- /** Whether the seed was successfully applied */
510
- success: boolean;
511
- /** Number of peers added to the peer store */
512
- peersAdded: number;
513
- /** Error message if not successful */
514
- error?: string;
515
- }
516
-
517
- // ============================================================================
518
- // Seed Helper Types
519
- // ============================================================================
520
-
521
- /**
522
- * Node network topology type for seed helper scenarios.
523
- */
524
- export type NodeTopology = 'public' | 'nat';
525
-
526
- /**
527
- * Invite code for phone-to-server enrollment.
528
- * Used when a server invites a phone to join the cadre.
529
- */
530
- export interface CadreInvite {
531
- /** Party ID of the cadre */
532
- partyId: string;
533
- /** Multiaddrs to dial the authority */
534
- authorityAddrs: string[];
535
- /** Optional invite token for validation */
536
- token?: string;
537
- /** Timestamp when invite was created */
538
- createdAt: number;
539
- /** Optional expiration timestamp */
540
- expiresAt?: number;
541
- }
542
-
543
- /**
544
- * Options for adding a drone via provider API.
545
- */
546
- export interface AddDroneOptions {
547
- /** Peer ID of the new drone (returned from provider) */
548
- dronePeerId: string;
549
- /** Multiaddrs of the drone (returned from provider) */
550
- droneMultiaddrs: string[];
551
- }
552
-
553
- /**
554
- * Options for adding a phone via invite flow.
555
- */
556
- export interface AddPhoneOptions {
557
- /** Peer ID of the phone (sent by phone when it connects) */
558
- phonePeerId: string;
559
- /** Invite token for validation (must match issued invite) */
560
- token?: string;
561
- }
562
-
563
- /**
564
- * Result of preparing a seed for drone initialization.
565
- */
566
- export interface DroneInitResult {
567
- /** The seed to send to the provider API */
568
- seed: ControlNetworkSeed;
569
- /** Base64url encoded seed for transport */
570
- encodedSeed: string;
571
- }
572
-
573
- /**
574
- * Result of creating an invite for a phone.
575
- */
576
- export interface InviteResult {
577
- /** The invite to share out-of-band */
578
- invite: CadreInvite;
579
- /** Base64url encoded invite for QR/link */
580
- encodedInvite: string;
581
- }
582
-
1
+ import type { ConnectionGater, Libp2p, PeerId, PrivateKey } from '@libp2p/interface';
2
+ import type { IRawStorage, Libp2pTransports } from '@optimystic/db-p2p';
3
+ import type { IRepo } from '@optimystic/db-core';
4
+ import type { StrandDatabase } from './strand-database.js';
5
+ import type { SeedTrustPolicy } from './seed-trust-policy.js';
6
+ import type { KeyStore, KeyId } from './key-store.js';
7
+
8
+ /**
9
+ * Extended Libp2p node with the coordinatedRepo attached by db-p2p's
10
+ * createLibp2pNode after node creation (not surfaced on the base Libp2p type).
11
+ */
12
+ export interface Libp2pNodeWithRepo extends Libp2p {
13
+ coordinatedRepo: IRepo;
14
+ }
15
+
16
+ /**
17
+ * Node profile determines storage participation
18
+ */
19
+ export type NodeProfile = 'transaction' | 'storage';
20
+
21
+ /**
22
+ * Strand filter configuration - determines which strands this node participates in
23
+ */
24
+ export type StrandFilter =
25
+ | { mode: 'all' } // All strands (default for servers)
26
+ | { mode: 'sAppId'; sAppId: string } // Only strands for specific sApp
27
+ | { mode: 'strandId'; strandId: string } // Single specific strand
28
+ | { mode: 'none' }; // Control network only
29
+
30
+ /**
31
+ * Latency hint for strand hibernation behavior
32
+ */
33
+ export type LatencyHint = 'realtime' | 'interactive' | 'background' | 'archive';
34
+
35
+ /**
36
+ * Hibernation timeout configuration per latency hint (in milliseconds)
37
+ */
38
+ export interface HibernationTimeouts {
39
+ /** Time before transitioning from active to idle */
40
+ idleTimeout: number;
41
+ /** Time before transitioning from idle to hibernating */
42
+ hibernateTimeout: number;
43
+ /**
44
+ * Base interval for the FIRST check-in after hibernating. Successive
45
+ * no-activity check-ins escalate this delay by {@link checkInBackoffFactor}
46
+ * (see {@link HibernationManager}'s self-rescheduling check-in chain).
47
+ */
48
+ checkInInterval: number;
49
+ /**
50
+ * Multiplier applied to the check-in delay after each check-in that finds no
51
+ * pending activity (must be >= 1; 1 disables escalation). Default 2 →
52
+ * doubling: base, base×2, base×4, … until {@link checkInMaxInterval}.
53
+ */
54
+ checkInBackoffFactor: number;
55
+ /**
56
+ * Upper bound on the escalating check-in delay — the concrete realization of
57
+ * the architecture's "minutes → hours → days" ceiling for this hint. Backoff
58
+ * never schedules a delay longer than this.
59
+ */
60
+ checkInMaxInterval: number;
61
+ }
62
+
63
+ /**
64
+ * Default hibernation timeouts per latency hint.
65
+ *
66
+ * `checkInInterval` is the BASE delay; `checkInMaxInterval` is the per-hint
67
+ * ceiling the exponential backoff escalates toward (interactive minutes→~1h,
68
+ * background minutes→~6h, archive ~1h→~3 days).
69
+ */
70
+ export const HIBERNATION_TIMEOUTS: Record<LatencyHint, HibernationTimeouts> = {
71
+ realtime: {
72
+ idleTimeout: Infinity, // Never idle
73
+ hibernateTimeout: Infinity, // Never hibernate
74
+ checkInInterval: Infinity, // N/A
75
+ checkInBackoffFactor: 2,
76
+ checkInMaxInterval: Infinity // N/A
77
+ },
78
+ interactive: {
79
+ idleTimeout: 5 * 60 * 1000, // 5 minutes
80
+ hibernateTimeout: 15 * 60 * 1000, // 15 minutes after idle
81
+ checkInInterval: 30 * 1000, // base: 30 seconds
82
+ checkInBackoffFactor: 2,
83
+ checkInMaxInterval: 60 * 60 * 1000 // cap: ~1 hour
84
+ },
85
+ background: {
86
+ idleTimeout: 1 * 60 * 1000, // 1 minute
87
+ hibernateTimeout: 5 * 60 * 1000, // 5 minutes after idle
88
+ checkInInterval: 5 * 60 * 1000, // base: 5 minutes
89
+ checkInBackoffFactor: 2,
90
+ checkInMaxInterval: 6 * 60 * 60 * 1000 // cap: ~6 hours
91
+ },
92
+ archive: {
93
+ idleTimeout: 10 * 1000, // 10 seconds
94
+ hibernateTimeout: 30 * 1000, // 30 seconds after idle
95
+ checkInInterval: 60 * 60 * 1000, // base: 1 hour
96
+ checkInBackoffFactor: 2,
97
+ checkInMaxInterval: 3 * 24 * 60 * 60 * 1000 // cap: ~3 days
98
+ }
99
+ };
100
+
101
+ /**
102
+ * Storage provider type - either an IRawStorage instance or a factory function.
103
+ * Factory functions are useful for creating strand-specific storage instances.
104
+ */
105
+ export type RawStorageProvider = IRawStorage | ((strandId: string) => IRawStorage);
106
+
107
+ /**
108
+ * Storage configuration for storage profile nodes
109
+ */
110
+ export interface StorageConfig {
111
+ /**
112
+ * Storage provider - either an IRawStorage instance or a factory function.
113
+ *
114
+ * For Node.js environments, use FileRawStorage from @optimystic/db-p2p-storage-fs:
115
+ * ```typescript
116
+ * import { FileRawStorage } from '@optimystic/db-p2p-storage-fs';
117
+ * storage: { provider: (strandId) => new FileRawStorage(`./data/${strandId}`) }
118
+ * ```
119
+ *
120
+ * For React Native, use the appropriate storage from @optimystic/db-p2p-storage-rn:
121
+ * ```typescript
122
+ * import { RNRawStorage } from '@optimystic/db-p2p-storage-rn';
123
+ * storage: { provider: (strandId) => new RNRawStorage(strandId) }
124
+ * ```
125
+ *
126
+ * For in-memory storage (testing):
127
+ * ```typescript
128
+ * import { MemoryRawStorage } from '@optimystic/db-p2p';
129
+ * storage: { provider: () => new MemoryRawStorage() }
130
+ * ```
131
+ */
132
+ provider: RawStorageProvider;
133
+ quotaBytes?: number;
134
+ }
135
+
136
+ /**
137
+ * Network configuration for libp2p
138
+ */
139
+ export interface NetworkConfig {
140
+ listenAddrs?: string[];
141
+ announceAddrs?: string[];
142
+ relayAddrs?: string[];
143
+ /**
144
+ * Enable circuit relay server - allows this node to relay connections for other peers.
145
+ * When undefined, defaults to true for storage profile nodes (they typically have
146
+ * better connectivity and uptime), false for transaction profile nodes.
147
+ */
148
+ enableRelay?: boolean;
149
+ /**
150
+ * Custom libp2p transports. When omitted, the default transports from
151
+ * `@optimystic/db-p2p` are used (TCP + circuit relay for Node.js).
152
+ *
153
+ * React Native apps must supply WebSocket-based transports because TCP
154
+ * is not available in the RN runtime:
155
+ * ```typescript
156
+ * import { webSockets } from '@libp2p/websockets';
157
+ * import { circuitRelayTransport } from '@libp2p/circuit-relay-v2';
158
+ *
159
+ * network: {
160
+ * transports: [webSockets(), circuitRelayTransport()],
161
+ * listenAddrs: [] // RN nodes typically cannot listen
162
+ * }
163
+ * ```
164
+ */
165
+ transports?: Libp2pTransports;
166
+ /**
167
+ * Optional async resolver returning the multiaddrs to embed in invites
168
+ * (and other authority-address contexts). When unset, `libp2pNode.getMultiaddrs()`
169
+ * is used. Hosts behind NAT supply this to substitute their DDNS hostname
170
+ * and externally-mapped port — see `@serfab/cadre-host`'s NatService.
171
+ */
172
+ inviteAddressResolver?: () => Promise<string[]>;
173
+ /**
174
+ * Optional libp2p connection gater, threaded to both the control node and every
175
+ * strand's cohort node. libp2p's browser default denies dialing insecure
176
+ * WebSockets and private/loopback addresses; callers that must dial local or
177
+ * unsecured peers (the web reference dialing a `127.0.0.1/.../ws` responder,
178
+ * Playwright e2e, RN simulators) supply a permissive gater here — e.g.
179
+ * `{ denyDialMultiaddr: () => false }`. See `@optimystic/db-p2p`'s
180
+ * `libp2p-node-base` for the underlying option.
181
+ */
182
+ connectionGater?: ConnectionGater;
183
+ /**
184
+ * Tuning for the proactive control-cohort dial routine
185
+ * ({@link CadreNode.reconcileControlCohort}), which keeps a party's control
186
+ * nodes connected so the `CadreControl` collections form a replicating cohort.
187
+ * Both fields are optional; omit for the defaults
188
+ * ({@link DEFAULT_CONTROL_COHORT_TARGET_DEGREE} /
189
+ * {@link DEFAULT_CONTROL_COHORT_RECONCILE_MS}).
190
+ */
191
+ controlCohort?: {
192
+ /**
193
+ * Cap on the number of NON-authority siblings dialed per reconcile pass
194
+ * (backbone/authority members are always dialed and do not count). Defaults
195
+ * to {@link DEFAULT_CONTROL_COHORT_TARGET_DEGREE}.
196
+ */
197
+ targetDegree?: number;
198
+ /**
199
+ * Recurring reconcile cadence in ms. Defaults to
200
+ * {@link DEFAULT_CONTROL_COHORT_RECONCILE_MS}.
201
+ */
202
+ reconcileMs?: number;
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Hibernation configuration
208
+ */
209
+ export interface HibernationConfig {
210
+ enabled: boolean;
211
+ defaultLatencyHint?: LatencyHint;
212
+ /** Custom timeouts per latency hint (overrides defaults) */
213
+ customTimeouts?: Partial<Record<LatencyHint, Partial<HibernationTimeouts>>>;
214
+ /**
215
+ * How long a hibernating strand stays resumed during a check-in to let its
216
+ * strand network connect to reachable cohort peers (and the app drive
217
+ * pull-on-read activity) before re-hibernating if still idle. Defaults to
218
+ * {@link DEFAULT_CHECKIN_WINDOW_MS}.
219
+ */
220
+ checkInWindowMs?: number;
221
+ }
222
+
223
+ /**
224
+ * Default duration of the resume-and-probe window during a strand check-in.
225
+ * See {@link HibernationConfig.checkInWindowMs}.
226
+ */
227
+ export const DEFAULT_CHECKIN_WINDOW_MS = 15 * 1000;
228
+
229
+ /**
230
+ * Control network configuration
231
+ */
232
+ export interface ControlNetworkConfig {
233
+ partyId: string;
234
+ bootstrapNodes: string[];
235
+ /** Optional path to the control schema file (defaults to bundled schema) */
236
+ schemaPath?: string;
237
+ }
238
+
239
+ /**
240
+ * Main configuration for a CadreNode
241
+ */
242
+ export interface CadreNodeConfig {
243
+ /**
244
+ * If provided, use this keypair for the node identity (direct injection).
245
+ * Mutually exclusive with {@link keyStore} — supplying both is a configuration
246
+ * error (fail closed) thrown by `start()` before any network bring-up.
247
+ */
248
+ privateKey?: PrivateKey;
249
+
250
+ /**
251
+ * Pluggable secure store for node key material. When set, the node loads its
252
+ * identity from `keyStore` under {@link identityKeyId}, generating + persisting
253
+ * a fresh Ed25519 key on first run (protobuf bytes are the canonical stored
254
+ * form). Mutually exclusive with {@link privateKey}. Absent ⇒ legacy behavior
255
+ * (use `privateKey`, else libp2p generates an ephemeral key).
256
+ *
257
+ * In the single-key reference model the authority signing key is *derived from*
258
+ * the node identity (see {@link CadreNode.getIdentityAuthorityKey}), so
259
+ * protecting the identity in a secure enclave protects the authority key too.
260
+ * A future separate-authority slot (`authorityKeyId`) is anticipated but not
261
+ * built here.
262
+ */
263
+ keyStore?: KeyStore;
264
+
265
+ /** Slot id for the node identity in {@link keyStore}. Default: `'cadre/identity'`. */
266
+ identityKeyId?: KeyId;
267
+
268
+ /** Control network connection settings */
269
+ controlNetwork: ControlNetworkConfig;
270
+
271
+ /** Node profile: transaction-only or storage */
272
+ profile: NodeProfile;
273
+
274
+ /** Which strands to participate in */
275
+ strandFilter?: StrandFilter;
276
+
277
+ /** Storage configuration (only for storage profile) */
278
+ storage?: StorageConfig;
279
+
280
+ /** Network configuration */
281
+ network?: NetworkConfig;
282
+
283
+ /** Hibernation configuration */
284
+ hibernation?: HibernationConfig;
285
+
286
+ /** Polling interval for strand watcher in ms (default: 5000) */
287
+ strandWatchInterval?: number;
288
+
289
+ /**
290
+ * Require a valid author signature on every sApp schema before a strand is
291
+ * formed or joined. Defaults to true (fail closed): an unsigned schema is
292
+ * rejected before any libp2p node or schema DDL is brought up. Set false ONLY
293
+ * for dev/test where unsigned demo schemas are used.
294
+ */
295
+ requireSignedSchemas?: boolean;
296
+
297
+ /**
298
+ * Node-wide default trust anchor for INBOUND control-network seeds. Forwarded
299
+ * into every SeedBootstrapService this node constructs (authority, receive-only
300
+ * listener, and the temp service used by applySeed when no service exists), and
301
+ * used as the service-level default the libp2p seed-protocol handler relies on
302
+ * (that path has no per-call override seam). Defaults to dbAnchoredTrustPolicy()
303
+ * inside SeedBootstrapService when unset — a cold-start node then rejects every
304
+ * seed. A per-call `applySeed(seed, { trustPolicy })` override still wins over
305
+ * this default for callers that hold out-of-band material (e.g. a pinned key
306
+ * from a CadreInvite).
307
+ */
308
+ seedTrustPolicy?: SeedTrustPolicy;
309
+
310
+ /**
311
+ * Platform push-delivery credentials (FCM and/or APNs). When present, the node
312
+ * can deliver strand-wake data messages to suspended mobile peers over the
313
+ * platform push channel — the server fan-out constructs a `PushNotifier` from
314
+ * this. Absent ⇒ no platform push (control-network push-wake only). Injected
315
+ * per node by `cadre-host`; `privateKey` fields are secrets and are never logged.
316
+ */
317
+ push?: PushCredentials;
318
+ }
319
+
320
+ /**
321
+ * Status of a strand instance
322
+ */
323
+ export type StrandStatus =
324
+ | 'starting'
325
+ | 'active'
326
+ | 'idle'
327
+ | 'hibernating'
328
+ | 'stopping'
329
+ | 'stopped'
330
+ | 'error';
331
+
332
+ /**
333
+ * App information from strand header
334
+ */
335
+ export interface SAppInfo {
336
+ id: string;
337
+ version: string;
338
+ schema: string;
339
+ signature?: string;
340
+ }
341
+
342
+ /**
343
+ * Strand instance state
344
+ */
345
+ export interface StrandInstance {
346
+ strandId: string;
347
+ status: StrandStatus;
348
+ sAppInfo?: SAppInfo;
349
+
350
+ /** The libp2p node for this strand (only when active/idle) */
351
+ libp2pNode?: Libp2p;
352
+
353
+ /** The Quereus database for this strand (only when active/idle) */
354
+ database?: StrandDatabase;
355
+
356
+ /** Membership info for closed strands */
357
+ memberKey?: string;
358
+ memberPrivateKey?: string;
359
+
360
+ /** Activity tracking */
361
+ connectedPeers: number;
362
+ lastActivity: Date;
363
+ nextCheckIn?: Date;
364
+
365
+ /** Latency hint from app or override */
366
+ latencyHint: LatencyHint;
367
+
368
+ /** Error message if status is 'error' */
369
+ error?: string;
370
+ }
371
+
372
+ /**
373
+ * Outcome of an on-demand {@link CadreNode.serviceWake} — the mobile push-wake
374
+ * cycle (resume → bounded window → re-hibernate-if-idle) run imperatively for a
375
+ * single strand.
376
+ */
377
+ export interface ServiceWakeResult {
378
+ /** The strand the wake was requested for. */
379
+ strandId: string;
380
+ /**
381
+ * Whether the wake was actually serviced. `false` when the node is not
382
+ * running, or the strand is unknown to this node (not a member-participated
383
+ * strand) — never a thrown error, so a background task can branch on it.
384
+ */
385
+ serviced: boolean;
386
+ /**
387
+ * Whether activity landed during the wake window. `true` leaves the strand
388
+ * active; `false` re-hibernates it (or the strand was already live, in which
389
+ * case it reflects current liveness). Always `false` when `serviced` is `false`.
390
+ */
391
+ hadActivity: boolean;
392
+ }
393
+
394
+ /**
395
+ * Strand row from control network - basic membership info
396
+ */
397
+ export interface StrandRow {
398
+ Id: string;
399
+ MemberPrivateKey: string | null;
400
+ Type: 'o' | 'c';
401
+ }
402
+
403
+ /**
404
+ * sApp configuration provided by the hosting application when creating a strand.
405
+ * This is what the app developer provides - NOT loaded from the network.
406
+ */
407
+ export interface SAppConfig {
408
+ /** Public key of the sApp author */
409
+ id: string;
410
+ /** Version of the sApp */
411
+ version: string;
412
+ /** The declarative schema DDL */
413
+ schema: string;
414
+ /**
415
+ * Author's signature over the schema for verification. Required under the
416
+ * default `requireSignedSchemas` node policy — a config without it is rejected
417
+ * before strand bring-up. Optional in the type because authoring/serialization
418
+ * must represent an as-yet-unsigned config; omitting it is honored only when
419
+ * the node explicitly relaxes the policy (`requireSignedSchemas: false`, dev/test).
420
+ */
421
+ signature?: string;
422
+ /** Latency hint for hibernation behavior (optional, defaults to config) */
423
+ latencyHint?: LatencyHint;
424
+ }
425
+
426
+ /**
427
+ * Lifecycle mode for a strand.
428
+ *
429
+ * - `bootstrap`: newly created locally; no remote peers exist for this strand yet.
430
+ * Schema apply and data operations route through a local (non-network) transactor
431
+ * so a solo node can fully initialize without any peer round trips.
432
+ * - `networked`: strand has (or is expected to have) remote peers. Operations route
433
+ * through the network transactor.
434
+ *
435
+ * Transition `bootstrap -> networked` happens at first remote-peer enrollment for
436
+ * the strand; because the transactor is fixed at plugin registration time, the
437
+ * transition requires stopping and restarting the strand.
438
+ */
439
+ export type StrandMode = 'bootstrap' | 'networked';
440
+
441
+ /**
442
+ * Full strand configuration when adding a strand via the API
443
+ */
444
+ export interface StrandConfig {
445
+ /** Strand row from control network */
446
+ strandRow: StrandRow;
447
+ /** sApp configuration provided by the hosting application */
448
+ sAppConfig: SAppConfig;
449
+ /**
450
+ * Lifecycle mode for this strand instance. When omitted, CadreNode infers the
451
+ * mode from cohort membership (`bootstrap` for a solo/first node with no other
452
+ * `CadrePeer` rows, `networked` once the cohort has other members). An explicit
453
+ * value always wins.
454
+ */
455
+ mode?: StrandMode;
456
+ /**
457
+ * Whether THIS node is the strand's founder — the party that provisioned and
458
+ * published the strand (the responder in formation, the creator in host/solo
459
+ * paths; the same party that calls {@link CadreNode.publishStrand}). The founder
460
+ * runs the one-time membership bootstrap at bring-up (writes `Strand.Header`, and
461
+ * for a closed strand the founding `Member`+`Authority`). A joiner leaves this
462
+ * `false`/unset and writes nothing — it receives those rows via Optimystic sync.
463
+ * Defaults to `false`.
464
+ */
465
+ founder?: boolean;
466
+ }
467
+
468
+ /**
469
+ * Result of creating a new cadre peer
470
+ */
471
+ export interface CreatePeerResult {
472
+ peerId: PeerId;
473
+ privateKey: Uint8Array;
474
+ }
475
+
476
+ // ============================================================================
477
+ // Member Registration API Types (from api.md)
478
+ // ============================================================================
479
+
480
+ /**
481
+ * Registration data for joining a strand as a member.
482
+ * Sent from invited member to any cadre member to accept invitation.
483
+ */
484
+ export interface MemberRegistration {
485
+ /** The strand being joined */
486
+ strandId: string;
487
+ /** The member's public key for this strand */
488
+ key: string;
489
+ /** Peer IDs of the member's cadre nodes that will participate */
490
+ peerIds: string[];
491
+ }
492
+
493
+ /**
494
+ * Result of member registration
495
+ */
496
+ export interface MemberRegistrationResult {
497
+ success: boolean;
498
+ reason?: string;
499
+ }
500
+
501
+ // ============================================================================
502
+ // Strand Solicitation API Types (from api.md)
503
+ // ============================================================================
504
+
505
+ /**
506
+ * Open invitation for strand formation.
507
+ * Shared out-of-band to allow strangers to form strands.
508
+ */
509
+ export interface OpenInvitation {
510
+ /** Unique token identifying this invitation */
511
+ token: string;
512
+ /** The sApp that will be used for the strand */
513
+ sAppId: string;
514
+ /** When this invitation expires */
515
+ expiration: Date;
516
+ /** Bootstrap addresses to contact the inviter's cadre */
517
+ bootstrap: string[];
518
+ }
519
+
520
+ /**
521
+ * Result of forming a strand via open invitation
522
+ */
523
+ export interface FormStrandResult {
524
+ /** The member key assigned to the initiator for this strand */
525
+ memberKey: string;
526
+ /** Private key for the invitation (for signing future messages) */
527
+ invitePrivateKey: string;
528
+ /** The strand that was created */
529
+ strandId: string;
530
+ /**
531
+ * The strand's membership key (closed-strand read-gating secret), delivered through
532
+ * the formation protocol after consent (provision-then-record). Present only for a
533
+ * closed strand the responder returned with its key; undefined otherwise. Distinct
534
+ * from {@link invitePrivateKey} (the initiator's own generated signing key).
535
+ */
536
+ memberPrivateKey?: string;
537
+ }
538
+
539
+ /**
540
+ * Result of validating a strand formation request
541
+ */
542
+ export interface ValidateFormationResult {
543
+ /** Public key for validating this formation */
544
+ validationKey: string;
545
+ /** Signature over the formation approval */
546
+ validationSignature: string;
547
+ }
548
+
549
+ /**
550
+ * Disclosure object provided during strand formation.
551
+ * Contains identity and context information from the initiator.
552
+ */
553
+ export interface StrandFormationDisclosure {
554
+ /** Initiator's party identifier */
555
+ partyId?: string;
556
+ /** Additional identity bundle (app-specific) */
557
+ identityBundle?: unknown;
558
+ /** Human-readable purpose/reason for forming the strand */
559
+ purpose?: string;
560
+ /** Additional app-specific metadata */
561
+ metadata?: Record<string, unknown>;
562
+ }
563
+
564
+ /**
565
+ * Arachnode ring participation stub - will be implemented when arachnode is built
566
+ */
567
+ export interface ArachnodeConfig {
568
+ /** Enable Arachnode subsystem (Ring Zulu + storage rings). Storage-profile only. */
569
+ enableRingZulu: boolean;
570
+ /** Storage ring participation (storage profile only) */
571
+ storageRing?: {
572
+ /** Which ring to participate in based on capacity */
573
+ ring: number;
574
+ /** Partition within the ring */
575
+ partition?: number;
576
+ };
577
+ }
578
+
579
+ /**
580
+ * Events emitted by CadreNode
581
+ */
582
+ export interface CadreNodeEvents {
583
+ 'strand:started': { strandId: string };
584
+ 'strand:stopped': { strandId: string };
585
+ 'strand:error': { strandId: string; error: Error };
586
+ 'strand:idle': { strandId: string };
587
+ 'strand:hibernating': { strandId: string };
588
+ 'strand:waking': { strandId: string };
589
+ /**
590
+ * Emitted when the control network advertises a strand this node has no
591
+ * registered `sAppConfig` for — i.e. a strand created by another member. The
592
+ * hosting app decides whether to join it (register a config + `addStrand`,
593
+ * e.g. via a chat `joinChatStrand` helper). Carries the full {@link StrandRow}
594
+ * so the app can join without re-querying the control DB.
595
+ */
596
+ 'strand:discovered': { strandId: string; strand: StrandRow };
597
+ 'control:connected': void;
598
+ 'control:disconnected': void;
599
+ /** Emitted when a seed is received via the seed protocol */
600
+ 'seed:received': { partyId: string; peerId: string };
601
+ /** Emitted when a seed is successfully applied */
602
+ 'seed:applied': { partyId: string; peersAdded: number };
603
+ /** Emitted when seed application fails */
604
+ 'seed:error': { partyId: string; error: string };
605
+ }
606
+
607
+ // ============================================================================
608
+ // Peer Address Record / Resolution Types
609
+ // ============================================================================
610
+
611
+ /**
612
+ * A peer's self-published, freshness-stamped, self-signed address record.
613
+ *
614
+ * This is the logical shape of a `CadreControl.CadrePeer` row. A node publishes
615
+ * its own record (signing with the ed25519 key behind its PeerId); any node can
616
+ * resolve another member's current signaling/relay multiaddrs from only its
617
+ * PeerId by reading the record, re-verifying the self-signature against
618
+ * `publicKey`, checking freshness, and applying a trust gate.
619
+ *
620
+ * The signature covers `peerId`, the comma-joined `addrs`, and `updatedAt` — see
621
+ * `peerRecordSignedPayload`. `publicKey` is NOT inside the signed payload: it is
622
+ * the key the signature is verified *with*, and its libp2p-identity binding to
623
+ * `peerId` is checked separately at resolve time, so re-signing it would be
624
+ * redundant.
625
+ */
626
+ export interface PeerAddressRecord {
627
+ /** libp2p peer ID (base58btc) — the row key. */
628
+ peerId: string;
629
+ /** ed25519 public key (base64url) whose libp2p identity IS `peerId`. */
630
+ publicKey: string;
631
+ /** Current dialable multiaddrs, signaling (`/p2p-circuit`) first. */
632
+ addrs: string[];
633
+ /** epoch ms — freshness; strictly increasing per peer. */
634
+ updatedAt: number;
635
+ /** ed25519 self-signature over the signed payload, base64url. */
636
+ sig: string;
637
+ }
638
+
639
+ /**
640
+ * Outcome of {@link CadreNode.registerSelf}, surfaced so callers (e.g. the CLI
641
+ * `--authority` branch) can log what happened:
642
+ * - `inserted` — the first authority-signed INSERT of this node's `CadrePeer` row.
643
+ * - `refreshed` — a self-signed UPDATE of an existing row (heartbeat / addr change).
644
+ * - `skipped` — nothing written (no self-signing key, or not yet a member with no
645
+ * authority service to self-insert).
646
+ */
647
+ export type SelfRegistrationOutcome = 'inserted' | 'refreshed' | 'skipped';
648
+
649
+ /**
650
+ * Options for {@link CadreNode.resolvePeerAddrs}.
651
+ */
652
+ export interface ResolveOpts {
653
+ /**
654
+ * Freshness ceiling in ms. A record older than this is filtered out (a
655
+ * resolver must never hand back a dead relay reservation). Defaults to
656
+ * {@link DEFAULT_PEER_RECORD_MAX_AGE_MS}.
657
+ */
658
+ maxAgeMs?: number;
659
+ /** Return only `/p2p-circuit` signaling addrs (the WebRTC dial input). */
660
+ signalingOnly?: boolean;
661
+ /**
662
+ * Pluggable trust gate, evaluated after signature + freshness pass. Defaults
663
+ * to {@link currentMemberTrustPolicy} (any existing — therefore
664
+ * authority-vouched — CadrePeer member is trusted). Inject a stricter policy
665
+ * (e.g. from `seed-signerkey-trust-policy`) to reject otherwise-valid records
666
+ * before dialing.
667
+ */
668
+ trustPolicy?: PeerResolveTrustPolicy;
669
+ }
670
+
671
+ /**
672
+ * Context handed to a {@link PeerResolveTrustPolicy} when gating a resolution.
673
+ */
674
+ export interface PeerResolveContext {
675
+ /** The peer being resolved. */
676
+ peerId: string;
677
+ /** The record's ed25519 public key (already bound to `peerId` and verified). */
678
+ publicKey: string;
679
+ /** The party whose control network produced the record. */
680
+ partyId: string;
681
+ /** The full verified, fresh record. */
682
+ record: PeerAddressRecord;
683
+ }
684
+
685
+ /**
686
+ * Decides whether a signature-verified, fresh peer-address record should be
687
+ * trusted enough to dial. The seam through which a richer trust model
688
+ * (trust circle, pinned keys) composes with `resolvePeerAddrs`.
689
+ */
690
+ export interface PeerResolveTrustPolicy {
691
+ evaluate(ctx: PeerResolveContext): Promise<boolean> | boolean;
692
+ }
693
+
694
+ // ============================================================================
695
+ // Device Token Registry Types
696
+ // ============================================================================
697
+
698
+ /** Platform push channel a {@link DeviceTokenRecord} targets. */
699
+ export type PushPlatform = 'fcm' | 'apns';
700
+
701
+ /**
702
+ * A mobile cadre peer's self-published, freshness-stamped, self-signed platform
703
+ * push token.
704
+ *
705
+ * This is the logical shape of a `CadreControl.DeviceToken` row. A phone publishes
706
+ * its own record (signing with the ed25519 key behind its PeerId); a server peer
707
+ * resolves it from the phone's PeerId to deliver a push-wake over FCM/APNs when a
708
+ * control-network dial cannot reach the OS-suspended process. The signature covers
709
+ * `peerId`, `platform`, `token`, and `updatedAt` — see `deviceTokenSignedPayload`.
710
+ *
711
+ * Unlike {@link PeerAddressRecord}, the record carries NO public key: it is verified
712
+ * against the `CadrePeer.PublicKey` bound to the same `peerId`, so a `DeviceToken`
713
+ * with no backing `CadrePeer` row can never be resolved.
714
+ */
715
+ export interface DeviceTokenRecord {
716
+ /** libp2p peer ID (base58btc) — the row key and the CadrePeer this token belongs to. */
717
+ peerId: string;
718
+ /** Push channel: `'fcm'` (Android/Firebase) or `'apns'` (Apple). */
719
+ platform: PushPlatform;
720
+ /** Opaque platform device/registration token. */
721
+ token: string;
722
+ /** epoch ms — freshness; strictly increasing per self-update (replay guard). */
723
+ updatedAt: number;
724
+ /** ed25519 self-signature over the signed payload, base64url. */
725
+ sig: string;
726
+ }
727
+
728
+ /**
729
+ * Options for {@link CadreNode.resolveDeviceToken}.
730
+ */
731
+ export interface ResolveDeviceTokenOpts {
732
+ /**
733
+ * Optional freshness ceiling in ms. Defaults to NO ceiling (a record is fresh
734
+ * as long as its `updatedAt` is positive): unlike a relay reservation, a device
735
+ * push token stays valid until it rotates, so a long-suspended phone must remain
736
+ * resolvable for push-wake. Set this to bound staleness explicitly.
737
+ */
738
+ maxAgeMs?: number;
739
+ }
740
+
741
+ // ============================================================================
742
+ // Push Credentials (platform push delivery — see push-notifier.ts)
743
+ // ============================================================================
744
+
745
+ /**
746
+ * Platform push-delivery credentials injected into a participating node's config
747
+ * (`CadreNodeConfig.push`). Provisioned per spawned node by `cadre-host` and read
748
+ * by `createPushNotifier` to construct the FCM and/or APNs senders. A platform
749
+ * whose block is absent is simply not deliverable (the router returns a
750
+ * best-effort "no credentials" failure rather than throwing).
751
+ *
752
+ * Secret hygiene: every `privateKey` field is a secret — never logged (treated
753
+ * like the node's startup/seed tokens).
754
+ */
755
+ export interface PushCredentials {
756
+ /** FCM (Android / Firebase) service-account credentials. */
757
+ fcm?: FcmCredentials;
758
+ /** APNs (Apple) auth-key credentials. */
759
+ apns?: ApnsCredentials;
760
+ /**
761
+ * Per-`(peer, strand)` minimum gap between push-wakes the server fan-out emits —
762
+ * the anti-spam cooldown a chatty strand needs. Default
763
+ * {@link DEFAULT_PUSH_COOLDOWN_MS} (5 min). In-memory, acceptably lossy across
764
+ * restarts (`serviceWake` is idempotent, so a duplicate wake is harmless).
765
+ */
766
+ cooldownMs?: number;
767
+ /**
768
+ * Per-strand burst-coalescing window for the fan-out: a second activity trigger
769
+ * within this window is dropped (one wake is enough). Default
770
+ * {@link DEFAULT_PUSH_DEBOUNCE_MS} (10 s).
771
+ */
772
+ debounceMs?: number;
773
+ }
774
+
775
+ /** Google service-account fields needed for FCM HTTP v1 OAuth2. */
776
+ export interface FcmCredentials {
777
+ /** GCP project id → `POST https://fcm.googleapis.com/v1/projects/{projectId}/messages:send`. */
778
+ projectId: string;
779
+ /** Service-account email (JWT `iss`/`sub`). */
780
+ clientEmail: string;
781
+ /** Service-account RSA private key, PEM. Secret — never logged. */
782
+ privateKey: string;
783
+ }
784
+
785
+ /** Apple APNs auth-key (.p8) fields. */
786
+ export interface ApnsCredentials {
787
+ /** APNs key id (JWT header `kid`). */
788
+ keyId: string;
789
+ /** Apple team id (JWT `iss`). */
790
+ teamId: string;
791
+ /** App bundle id → `apns-topic` header. */
792
+ bundleId: string;
793
+ /** `.p8` ES256 private key, PEM. Secret — never logged. */
794
+ privateKey: string;
795
+ /** `true` → `api.push.apple.com`; false/undefined → `api.sandbox.push.apple.com`. */
796
+ production?: boolean;
797
+ }
798
+
799
+ // ============================================================================
800
+ // Seed Bootstrap API Types (from architecture.md)
801
+ // ============================================================================
802
+
803
+ /**
804
+ * A peer entry in a control network seed.
805
+ * Contains connection information for a cadre peer.
806
+ */
807
+ export interface SeedPeer {
808
+ /** Peer ID (libp2p identity) */
809
+ peerId: string;
810
+ /** Multiaddrs for connecting to this peer */
811
+ multiaddrs: string[];
812
+ /** Whether this peer holds an authority key */
813
+ isAuthority: boolean;
814
+ /** ed25519 public key (base64url) — present on authority peers for signerKey verification */
815
+ publicKey?: string;
816
+ }
817
+
818
+ /**
819
+ * Control network seed for bootstrapping new nodes.
820
+ * Pre-populates a new node's cache to solve the cold-start problem:
821
+ * new nodes need control data to validate connections, but can't get
822
+ * data without connecting first.
823
+ */
824
+ export interface ControlNetworkSeed {
825
+ /** Party ID this seed belongs to */
826
+ partyId: string;
827
+ /** Known peers in the cadre */
828
+ peers: SeedPeer[];
829
+ /** Signature over the seed by an authority key */
830
+ signature: string;
831
+ /** The authority key that signed this seed */
832
+ signerKey: string;
833
+ }
834
+
835
+ /**
836
+ * Message sent from instigator to new node during seed delivery.
837
+ * Delivered via /sereus/seed/1.0.0 libp2p protocol.
838
+ */
839
+ export interface SeedMessage {
840
+ /** Party ID for the cadre */
841
+ partyId: string;
842
+ /** Known peers in the cadre */
843
+ peers: SeedPeer[];
844
+ /** Signature by an authority key */
845
+ signature: string;
846
+ /** The authority key that signed this message */
847
+ signerKey: string;
848
+ }
849
+
850
+ /**
851
+ * Acknowledgment message from new node to instigator.
852
+ */
853
+ export interface SeedAckMessage {
854
+ /** Whether the seed was accepted */
855
+ accepted: boolean;
856
+ /** Reason for rejection (if not accepted) */
857
+ reason?: string;
858
+ }
859
+
860
+ // ============================================================================
861
+ // Strand Wake (control-network push-wake protocol) — see strand-wake-protocol.ts
862
+ // ============================================================================
863
+
864
+ /**
865
+ * Request sent over the control network's `WAKE_PROTOCOL` from a same-cadre peer
866
+ * to a hibernating peer, asking it to bring a strand online and pull pending
867
+ * activity. The receiver gates this on cadre membership before honoring it.
868
+ */
869
+ export interface WakeRequest {
870
+ /** The strand the sender knows has pending activity. */
871
+ strandId: string;
872
+ /** Optional cause hint, e.g. `"activity"` (a server saw new traffic) or `"manual"`. */
873
+ reason?: string;
874
+ }
875
+
876
+ /**
877
+ * Acknowledgment of a {@link WakeRequest}, returned on the same stream.
878
+ */
879
+ export interface WakeAck {
880
+ /** Whether the receiver honored the wake (member + participated strand). */
881
+ accepted: boolean;
882
+ /** The strand's status after the wake (present when `accepted`). */
883
+ status?: StrandStatus;
884
+ /** Reason for rejection (non-member, unknown strand, malformed frame). */
885
+ reason?: string;
886
+ }
887
+
888
+ // ============================================================================
889
+ // Strand Address (control-network strand-address RPC) — see strand-addr-protocol.ts
890
+ // ============================================================================
891
+
892
+ /**
893
+ * Control-network request from one of a party's own cadre nodes to a sibling,
894
+ * asking "what are your live multiaddrs for this strand's separate network?".
895
+ * Used to seed a strand mesh from co-cadre nodes that already run that strand.
896
+ * The receiver gates this on cadre membership before answering.
897
+ */
898
+ export interface StrandAddrRequest {
899
+ /** The strand whose strand-network address the requester wants to seed from. */
900
+ strandId: string;
901
+ }
902
+
903
+ /**
904
+ * Response to a {@link StrandAddrRequest}, carrying the responder's strand-node
905
+ * multiaddrs, returned on the same stream.
906
+ */
907
+ export interface StrandAddrResponse {
908
+ /** Echoes the requested strand id (empty on a reject/error reply). */
909
+ strandId: string;
910
+ /**
911
+ * Dialable strand-network multiaddr strings (signaling/`p2p-circuit` first);
912
+ * empty when the responder is a non-member, or does not currently run that
913
+ * strand, or the exchange failed.
914
+ */
915
+ multiaddrs: string[];
916
+ }
917
+
918
+ /**
919
+ * Options for authorizing a new peer in the cadre.
920
+ */
921
+ export interface AuthorizePeerOptions {
922
+ /** Peer ID to authorize */
923
+ peerId: string;
924
+ /** Optional multiaddrs for the peer */
925
+ multiaddrs?: string[];
926
+ }
927
+
928
+ /**
929
+ * Result of applying a seed to a node.
930
+ */
931
+ export interface ApplySeedResult {
932
+ /** Whether the seed was successfully applied */
933
+ success: boolean;
934
+ /** Number of peers added to the peer store */
935
+ peersAdded: number;
936
+ /** Error message if not successful */
937
+ error?: string;
938
+ }
939
+
940
+ // ============================================================================
941
+ // Seed Helper Types
942
+ // ============================================================================
943
+
944
+ /**
945
+ * Node network topology type for seed helper scenarios.
946
+ */
947
+ export type NodeTopology = 'public' | 'nat';
948
+
949
+ /**
950
+ * Invite code for phone-to-server enrollment.
951
+ * Used when a server invites a phone to join the cadre.
952
+ */
953
+ export interface CadreInvite {
954
+ /** Party ID of the cadre */
955
+ partyId: string;
956
+ /** Multiaddrs to dial the authority */
957
+ authorityAddrs: string[];
958
+ /**
959
+ * Authority ed25519 public keys (base64url) of the cadre, carried out-of-band
960
+ * so a cold-start invitee can pin the trusted authority set before applying
961
+ * any seed (the seed itself cannot vouch for its own signer). Populated by
962
+ * `createInvite` from the issuer's `AuthorityKey` table.
963
+ */
964
+ authorityKeys?: string[];
965
+ /** Optional invite token for validation */
966
+ token?: string;
967
+ /** Timestamp when invite was created */
968
+ createdAt: number;
969
+ /** Optional expiration timestamp */
970
+ expiresAt?: number;
971
+ }
972
+
973
+ /**
974
+ * Options for adding a drone via provider API.
975
+ */
976
+ export interface AddDroneOptions {
977
+ /** Peer ID of the new drone (returned from provider) */
978
+ dronePeerId: string;
979
+ /** Multiaddrs of the drone (returned from provider) */
980
+ droneMultiaddrs: string[];
981
+ }
982
+
983
+ /**
984
+ * Options for adding a phone via invite flow.
985
+ */
986
+ export interface AddPhoneOptions {
987
+ /** Peer ID of the phone (sent by phone when it connects) */
988
+ phonePeerId: string;
989
+ /** Invite token for validation (must match issued invite) */
990
+ token?: string;
991
+ }
992
+
993
+ /**
994
+ * Result of preparing a seed for drone initialization.
995
+ */
996
+ export interface DroneInitResult {
997
+ /** The seed to send to the provider API */
998
+ seed: ControlNetworkSeed;
999
+ /** Base64url encoded seed for transport */
1000
+ encodedSeed: string;
1001
+ }
1002
+
1003
+ /**
1004
+ * Result of creating an invite for a phone.
1005
+ */
1006
+ export interface InviteResult {
1007
+ /** The invite to share out-of-band */
1008
+ invite: CadreInvite;
1009
+ /** Base64url encoded invite for QR/link */
1010
+ encodedInvite: string;
1011
+ }
1012
+