@serfab/cadre-core 0.0.2

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 (54) hide show
  1. package/README.md +252 -0
  2. package/dist/arachnode-stub.d.ts +60 -0
  3. package/dist/arachnode-stub.js +120 -0
  4. package/dist/arachnode-stub.js.map +1 -0
  5. package/dist/cadre-node.d.ts +252 -0
  6. package/dist/cadre-node.js +753 -0
  7. package/dist/cadre-node.js.map +1 -0
  8. package/dist/control-database.d.ts +62 -0
  9. package/dist/control-database.js +295 -0
  10. package/dist/control-database.js.map +1 -0
  11. package/dist/enrollment.d.ts +76 -0
  12. package/dist/enrollment.js +126 -0
  13. package/dist/enrollment.js.map +1 -0
  14. package/dist/hibernation-manager.d.ts +68 -0
  15. package/dist/hibernation-manager.js +212 -0
  16. package/dist/hibernation-manager.js.map +1 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.js +24 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/seed-bootstrap.d.ts +164 -0
  21. package/dist/seed-bootstrap.js +554 -0
  22. package/dist/seed-bootstrap.js.map +1 -0
  23. package/dist/strand-database.d.ts +39 -0
  24. package/dist/strand-database.js +108 -0
  25. package/dist/strand-database.js.map +1 -0
  26. package/dist/strand-formation-manager.d.ts +87 -0
  27. package/dist/strand-formation-manager.js +168 -0
  28. package/dist/strand-formation-manager.js.map +1 -0
  29. package/dist/strand-instance-manager.d.ts +60 -0
  30. package/dist/strand-instance-manager.js +209 -0
  31. package/dist/strand-instance-manager.js.map +1 -0
  32. package/dist/strand-solicitation.d.ts +145 -0
  33. package/dist/strand-solicitation.js +180 -0
  34. package/dist/strand-solicitation.js.map +1 -0
  35. package/dist/strand-watcher.d.ts +70 -0
  36. package/dist/strand-watcher.js +136 -0
  37. package/dist/strand-watcher.js.map +1 -0
  38. package/dist/types.d.ts +484 -0
  39. package/dist/types.js +26 -0
  40. package/dist/types.js.map +1 -0
  41. package/package.json +67 -0
  42. package/src/arachnode-stub.ts +160 -0
  43. package/src/cadre-node.ts +923 -0
  44. package/src/control-database.ts +376 -0
  45. package/src/enrollment.ts +199 -0
  46. package/src/hibernation-manager.ts +268 -0
  47. package/src/index.ts +71 -0
  48. package/src/seed-bootstrap.ts +753 -0
  49. package/src/strand-database.ts +168 -0
  50. package/src/strand-formation-manager.ts +277 -0
  51. package/src/strand-instance-manager.ts +281 -0
  52. package/src/strand-solicitation.ts +318 -0
  53. package/src/strand-watcher.ts +197 -0
  54. package/src/types.ts +553 -0
@@ -0,0 +1,753 @@
1
+ import debug from 'debug';
2
+ import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
3
+ import { createLibp2pNode } from '@optimystic/db-p2p';
4
+ import { StrandWatcher } from './strand-watcher.js';
5
+ import { StrandInstanceManager } from './strand-instance-manager.js';
6
+ import { EnrollmentService } from './enrollment.js';
7
+ import { HibernationManager } from './hibernation-manager.js';
8
+ import { ControlDatabase } from './control-database.js';
9
+ import { SeedBootstrapService } from './seed-bootstrap.js';
10
+ import { StrandSolicitationService } from './strand-solicitation.js';
11
+ const log = debug('sereus:cadre:node');
12
+ /**
13
+ * CadreNode is the main entry point for a cadre member.
14
+ * It manages:
15
+ * - Connection to the control network
16
+ * - Watching for strand changes
17
+ * - Starting/stopping strand instances
18
+ * - Strand hibernation lifecycle
19
+ * - Peer enrollment
20
+ */
21
+ export class CadreNode {
22
+ constructor(config) {
23
+ this.controlNode = null;
24
+ this.controlDatabase = null;
25
+ this.strandWatcher = null;
26
+ this.seedBootstrapService = null;
27
+ this.strandSolicitationService = null;
28
+ this.running = false;
29
+ this.eventHandlers = new Map();
30
+ /** Map of strandId -> sAppConfig for sAppId filtering and management */
31
+ this.sAppConfigs = new Map();
32
+ this.config = config;
33
+ this.strandManager = new StrandInstanceManager();
34
+ this.enrollmentService = new EnrollmentService();
35
+ // Create hibernation manager with callbacks
36
+ const hibernationCallbacks = {
37
+ onIdle: async (strandId) => this.handleStrandIdle(strandId),
38
+ onHibernate: async (strandId) => this.handleStrandHibernate(strandId),
39
+ onWake: async (strandId) => this.handleStrandWake(strandId)
40
+ };
41
+ this.hibernationManager = new HibernationManager(config.hibernation ?? { enabled: false }, hibernationCallbacks);
42
+ log('CadreNode created for party: %s', config.controlNetwork.partyId);
43
+ }
44
+ /**
45
+ * SAppIdLookup implementation - get sAppId for a strand
46
+ */
47
+ getSAppId(strandId) {
48
+ return this.sAppConfigs.get(strandId)?.id;
49
+ }
50
+ /**
51
+ * Get the peer ID of this node (available after start)
52
+ */
53
+ get peerId() {
54
+ return this.controlNode?.peerId;
55
+ }
56
+ /**
57
+ * Get the multiaddrs of this node (available after start)
58
+ */
59
+ getMultiaddrs() {
60
+ if (!this.controlNode)
61
+ return [];
62
+ return this.controlNode.getMultiaddrs().map(ma => ma.toString());
63
+ }
64
+ /**
65
+ * Check if the node is running
66
+ */
67
+ get isRunning() {
68
+ return this.running;
69
+ }
70
+ /**
71
+ * Get all strand instances
72
+ */
73
+ getStrands() {
74
+ return this.strandManager.getInstances();
75
+ }
76
+ /**
77
+ * Get a specific strand instance
78
+ */
79
+ getStrand(strandId) {
80
+ return this.strandManager.getInstance(strandId);
81
+ }
82
+ /**
83
+ * Get the enrollment service for adding new peers
84
+ */
85
+ getEnrollmentService() {
86
+ return this.enrollmentService;
87
+ }
88
+ /**
89
+ * Start the cadre node
90
+ */
91
+ async start() {
92
+ if (this.running) {
93
+ log('CadreNode already running');
94
+ return;
95
+ }
96
+ log('Starting CadreNode for party: %s', this.config.controlNetwork.partyId);
97
+ try {
98
+ // Create the control network libp2p node
99
+ this.controlNode = await this.createControlNode();
100
+ log('Control node started with ID: %s', this.controlNode.peerId.toString());
101
+ // Extract coordinatedRepo from the node (attached by createLibp2pNode)
102
+ const coordinatedRepo = this.controlNode.coordinatedRepo;
103
+ if (!coordinatedRepo) {
104
+ throw new Error('coordinatedRepo not available on control node');
105
+ }
106
+ // Initialize the control database with the libp2p node
107
+ this.controlDatabase = new ControlDatabase({
108
+ partyId: this.config.controlNetwork.partyId,
109
+ libp2pNode: this.controlNode,
110
+ coordinatedRepo,
111
+ schemaPath: this.config.controlNetwork.schemaPath,
112
+ });
113
+ await this.controlDatabase.initialize();
114
+ log('Control database initialized');
115
+ // Create strand queryable using the control database
116
+ const queryable = this.createStrandQueryable();
117
+ // Create and start the strand watcher with sAppId lookup
118
+ this.strandWatcher = new StrandWatcher(queryable, {
119
+ onStrandAdded: async (strand) => this.handleStrandAdded(strand),
120
+ onStrandRemoved: async (strandId) => this.handleStrandRemoved(strandId)
121
+ }, this.config.strandFilter ?? { mode: 'all' }, this.config.strandWatchInterval ?? 5000, this // CadreNode implements SAppIdLookup
122
+ );
123
+ await this.strandWatcher.start();
124
+ // Start hibernation manager
125
+ this.hibernationManager.start();
126
+ this.running = true;
127
+ this.emit('control:connected', undefined);
128
+ log('CadreNode started successfully');
129
+ // Schedule self-registration in background
130
+ this.scheduleSelfRegistration();
131
+ }
132
+ catch (error) {
133
+ log('Failed to start CadreNode: %o', error);
134
+ await this.cleanup();
135
+ throw error;
136
+ }
137
+ }
138
+ /**
139
+ * Stop the cadre node
140
+ */
141
+ async stop() {
142
+ if (!this.running) {
143
+ log('CadreNode not running');
144
+ return;
145
+ }
146
+ log('Stopping CadreNode');
147
+ await this.cleanup();
148
+ this.running = false;
149
+ this.emit('control:disconnected', undefined);
150
+ log('CadreNode stopped');
151
+ }
152
+ /**
153
+ * Subscribe to events
154
+ */
155
+ on(event, handler) {
156
+ if (!this.eventHandlers.has(event)) {
157
+ this.eventHandlers.set(event, new Set());
158
+ }
159
+ this.eventHandlers.get(event).add(handler);
160
+ }
161
+ /**
162
+ * Unsubscribe from events
163
+ */
164
+ off(event, handler) {
165
+ this.eventHandlers.get(event)?.delete(handler);
166
+ }
167
+ emit(event, data) {
168
+ this.eventHandlers.get(event)?.forEach(handler => {
169
+ try {
170
+ handler(data);
171
+ }
172
+ catch (e) {
173
+ log('Event handler error: %o', e);
174
+ }
175
+ });
176
+ }
177
+ async createControlNode() {
178
+ const { controlNetwork, network, storage, profile, privateKey } = this.config;
179
+ // Determine relay mode: if explicitly set in config, use that;
180
+ // otherwise default to true for storage profile nodes (better connectivity/uptime)
181
+ const enableRelay = network?.enableRelay ?? (profile === 'storage');
182
+ // Create storage provider for control network
183
+ // Uses the factory function pattern if provided to create control-network-specific storage
184
+ const controlStorageProvider = storage?.provider
185
+ ? (typeof storage.provider === 'function'
186
+ ? storage.provider('control')
187
+ : storage.provider)
188
+ : undefined;
189
+ const nodeOptions = {
190
+ port: 0,
191
+ bootstrapNodes: controlNetwork.bootstrapNodes,
192
+ networkName: `control-${controlNetwork.partyId}`,
193
+ storage: controlStorageProvider,
194
+ fretProfile: profile === 'storage' ? 'core' : 'edge',
195
+ relay: enableRelay,
196
+ clusterSize: 3,
197
+ clusterPolicy: { allowDownsize: true, sizeTolerance: 0.5 },
198
+ arachnode: { enableRingZulu: true },
199
+ ...(network?.transports && { transports: network.transports }),
200
+ ...(network?.listenAddrs && { listenAddrs: network.listenAddrs })
201
+ };
202
+ // If private key provided, we need to load it
203
+ // Note: createLibp2pNode doesn't support privateKey directly yet
204
+ // For now we create the node without it
205
+ if (privateKey) {
206
+ log('Private key provided - loading identity');
207
+ // This would need to be integrated into createLibp2pNode
208
+ }
209
+ return await createLibp2pNode(nodeOptions);
210
+ }
211
+ createStrandQueryable() {
212
+ return {
213
+ queryStrands: async () => {
214
+ if (!this.controlDatabase) {
215
+ log('Control database not initialized, returning empty strands');
216
+ return [];
217
+ }
218
+ log('Querying strands from control database');
219
+ return await this.controlDatabase.queryStrands();
220
+ }
221
+ };
222
+ }
223
+ /**
224
+ * Schedule self-registration in the CadrePeer table.
225
+ * This runs as a background task after the node is started.
226
+ */
227
+ scheduleSelfRegistration() {
228
+ // Run in background - don't block start()
229
+ setTimeout(async () => {
230
+ try {
231
+ await this.registerSelf();
232
+ }
233
+ catch (error) {
234
+ log('Self-registration failed: %o', error);
235
+ // Don't emit error event - this is a background task
236
+ // The node can still function without self-registration
237
+ }
238
+ }, 1000); // Small delay to ensure node is fully started
239
+ }
240
+ /**
241
+ * Register this peer in the CadrePeer table.
242
+ * This allows other cadre members to discover this node.
243
+ */
244
+ async registerSelf() {
245
+ if (!this.controlNode || !this.controlDatabase) {
246
+ log('Cannot register self - node or database not initialized');
247
+ return;
248
+ }
249
+ const peerId = this.controlNode.peerId.toString();
250
+ const multiaddrs = this.controlNode.getMultiaddrs();
251
+ if (multiaddrs.length === 0) {
252
+ log('No multiaddrs available for self-registration');
253
+ return;
254
+ }
255
+ // Use the first available multiaddr (typically the one with the peer ID)
256
+ const multiaddr = multiaddrs[0].toString();
257
+ log('Registering self: peerId=%s, multiaddr=%s', peerId, multiaddr);
258
+ // TODO: This requires signing the update to satisfy the AuthorizedUpdate constraint
259
+ // For now, we log what would happen. Full implementation requires:
260
+ // 1. Either an authority key to authorize the insert
261
+ // 2. Or the peer's private key to sign the update
262
+ log('Self-registration requires authorization - skipping for now');
263
+ // const db = this.controlDatabase.getDatabase();
264
+ // await db.exec(
265
+ // 'insert or replace into CadrePeer (PeerId, Multiaddr) values (?, ?)',
266
+ // [peerId, multiaddr]
267
+ // );
268
+ }
269
+ async handleStrandAdded(strand) {
270
+ log('Handling strand added from control network: %s', strand.Id);
271
+ // Check if we have sApp config for this strand
272
+ const sAppConfig = this.sAppConfigs.get(strand.Id);
273
+ if (!sAppConfig) {
274
+ log('No sAppConfig registered for strand %s - skipping auto-start', strand.Id);
275
+ // Strand detected in control network but not yet configured via addStrand
276
+ // This is normal - the app will call addStrand with the config
277
+ return;
278
+ }
279
+ try {
280
+ const instance = await this.strandManager.startStrand({
281
+ strandRow: strand,
282
+ sAppConfig,
283
+ storage: this.config.storage,
284
+ network: this.config.network,
285
+ profile: this.config.profile,
286
+ defaultLatencyHint: this.config.hibernation?.defaultLatencyHint ?? 'interactive'
287
+ });
288
+ // Register with hibernation manager
289
+ this.hibernationManager.trackStrand(instance);
290
+ this.emit('strand:started', { strandId: strand.Id });
291
+ }
292
+ catch (error) {
293
+ log('Error starting strand %s: %o', strand.Id, error);
294
+ this.emit('strand:error', {
295
+ strandId: strand.Id,
296
+ error: error instanceof Error ? error : new Error(String(error))
297
+ });
298
+ }
299
+ }
300
+ async handleStrandRemoved(strandId) {
301
+ log('Handling strand removed: %s', strandId);
302
+ try {
303
+ // Untrack from hibernation
304
+ this.hibernationManager.untrackStrand(strandId);
305
+ // Remove sApp config
306
+ this.sAppConfigs.delete(strandId);
307
+ await this.strandManager.stopStrand(strandId);
308
+ this.emit('strand:stopped', { strandId });
309
+ }
310
+ catch (error) {
311
+ log('Error stopping strand %s: %o', strandId, error);
312
+ this.emit('strand:error', {
313
+ strandId,
314
+ error: error instanceof Error ? error : new Error(String(error))
315
+ });
316
+ }
317
+ }
318
+ async cleanup() {
319
+ // Stop hibernation manager
320
+ this.hibernationManager.stop();
321
+ // Stop seed bootstrap service
322
+ if (this.seedBootstrapService) {
323
+ await this.seedBootstrapService.shutdown();
324
+ this.seedBootstrapService = null;
325
+ }
326
+ // Unregister strand solicitation service
327
+ if (this.strandSolicitationService && this.controlNode) {
328
+ this.strandSolicitationService.unregisterResponder(this.controlNode);
329
+ this.strandSolicitationService = null;
330
+ }
331
+ // Stop strand watcher
332
+ if (this.strandWatcher) {
333
+ await this.strandWatcher.stop();
334
+ this.strandWatcher = null;
335
+ }
336
+ // Stop all strand instances
337
+ await this.strandManager.stopAll();
338
+ // Clear sApp configs
339
+ this.sAppConfigs.clear();
340
+ // Close control database (this also shuts down the collection factory)
341
+ if (this.controlDatabase) {
342
+ await this.controlDatabase.close();
343
+ this.controlDatabase = null;
344
+ }
345
+ // Stop control node
346
+ if (this.controlNode) {
347
+ await this.controlNode.stop();
348
+ this.controlNode = null;
349
+ }
350
+ }
351
+ // Hibernation callbacks
352
+ async handleStrandIdle(strandId) {
353
+ const instance = this.strandManager.getInstance(strandId);
354
+ if (instance) {
355
+ instance.status = 'idle';
356
+ log('Strand %s transitioned to idle', strandId);
357
+ this.emit('strand:idle', { strandId });
358
+ }
359
+ }
360
+ async handleStrandHibernate(strandId) {
361
+ const instance = this.strandManager.getInstance(strandId);
362
+ if (instance) {
363
+ instance.status = 'hibernating';
364
+ // Optionally disconnect libp2p to save resources
365
+ // For now we keep it connected but could stop it here
366
+ log('Strand %s transitioned to hibernating', strandId);
367
+ this.emit('strand:hibernating', { strandId });
368
+ }
369
+ }
370
+ async handleStrandWake(strandId) {
371
+ const instance = this.strandManager.getInstance(strandId);
372
+ if (instance) {
373
+ instance.status = 'active';
374
+ instance.lastActivity = new Date();
375
+ log('Strand %s woke up', strandId);
376
+ this.emit('strand:waking', { strandId });
377
+ }
378
+ }
379
+ /**
380
+ * Add a strand with its sApp configuration.
381
+ * The hosting application must provide the sApp schema when creating a strand.
382
+ */
383
+ async addStrand(config) {
384
+ if (!this.running) {
385
+ throw new Error('CadreNode not running');
386
+ }
387
+ const { strandRow, sAppConfig } = config;
388
+ // Store sApp config for this strand
389
+ this.sAppConfigs.set(strandRow.Id, sAppConfig);
390
+ log('Registered sAppConfig for strand %s (sApp: %s)', strandRow.Id, sAppConfig.id);
391
+ const instance = await this.strandManager.startStrand({
392
+ strandRow,
393
+ sAppConfig,
394
+ storage: this.config.storage,
395
+ network: this.config.network,
396
+ profile: this.config.profile,
397
+ defaultLatencyHint: this.config.hibernation?.defaultLatencyHint ?? 'interactive'
398
+ });
399
+ // Register with hibernation manager
400
+ this.hibernationManager.trackStrand(instance);
401
+ this.emit('strand:started', { strandId: strandRow.Id });
402
+ return instance;
403
+ }
404
+ /**
405
+ * Remove a strand
406
+ */
407
+ async removeStrand(strandId) {
408
+ if (!this.running) {
409
+ throw new Error('CadreNode not running');
410
+ }
411
+ // Untrack from hibernation
412
+ this.hibernationManager.untrackStrand(strandId);
413
+ // Remove sApp config
414
+ this.sAppConfigs.delete(strandId);
415
+ await this.strandManager.stopStrand(strandId);
416
+ this.emit('strand:stopped', { strandId });
417
+ }
418
+ /**
419
+ * Record activity on a strand (resets hibernation timer)
420
+ */
421
+ recordStrandActivity(strandId) {
422
+ const instance = this.strandManager.getInstance(strandId);
423
+ if (instance) {
424
+ this.hibernationManager.recordActivity(instance);
425
+ }
426
+ }
427
+ /**
428
+ * Force wake a hibernating strand
429
+ */
430
+ async wakeStrand(strandId) {
431
+ await this.hibernationManager.wakeStrand(strandId);
432
+ }
433
+ /**
434
+ * Get the control network node (for advanced use)
435
+ */
436
+ getControlNode() {
437
+ return this.controlNode;
438
+ }
439
+ /**
440
+ * Get the control database (for advanced queries)
441
+ */
442
+ getControlDatabase() {
443
+ return this.controlDatabase;
444
+ }
445
+ /**
446
+ * Force a poll of the strand watcher (for testing)
447
+ */
448
+ async forceStrandPoll() {
449
+ await this.strandWatcher?.forcePoll();
450
+ }
451
+ /**
452
+ * Get the sApp configuration for a strand
453
+ */
454
+ getSAppConfig(strandId) {
455
+ return this.sAppConfigs.get(strandId);
456
+ }
457
+ // ============================================================================
458
+ // Seed Bootstrap API
459
+ // ============================================================================
460
+ /**
461
+ * Initialize the seed bootstrap service with an authority key.
462
+ * Must be called before using seed-related methods that require signing.
463
+ *
464
+ * @param authorityPrivateKey - The authority's private key (base64url encoded)
465
+ */
466
+ initializeSeedBootstrap(authorityPrivateKey) {
467
+ if (!this.controlNode || !this.controlDatabase) {
468
+ throw new Error('CadreNode must be started before initializing seed bootstrap');
469
+ }
470
+ this.seedBootstrapService = new SeedBootstrapService({
471
+ partyId: this.config.controlNetwork.partyId,
472
+ authorityPrivateKey,
473
+ });
474
+ this.seedBootstrapService.setEventCallbacks({
475
+ onSeedReceived: (partyId, peerId) => this.emit('seed:received', { partyId, peerId }),
476
+ onSeedApplied: (partyId, peersAdded) => this.emit('seed:applied', { partyId, peersAdded }),
477
+ onSeedError: (partyId, error) => this.emit('seed:error', { partyId, error }),
478
+ });
479
+ this.seedBootstrapService.initialize(this.controlNode, this.controlDatabase);
480
+ log('Seed bootstrap service initialized');
481
+ }
482
+ /**
483
+ * Enable the seed listener for receiving seeds via the /sereus/seed/1.0.0 protocol.
484
+ * This is for drone nodes that need to receive seeds without being an authority.
485
+ * Does not require an authority key.
486
+ */
487
+ enableSeedListener() {
488
+ if (!this.controlNode || !this.controlDatabase) {
489
+ throw new Error('CadreNode must be started before enabling seed listener');
490
+ }
491
+ // Don't re-initialize if already has a service
492
+ if (this.seedBootstrapService) {
493
+ log('Seed bootstrap service already initialized');
494
+ return;
495
+ }
496
+ this.seedBootstrapService = new SeedBootstrapService({
497
+ partyId: this.config.controlNetwork.partyId,
498
+ // No authority key - this node only receives seeds
499
+ });
500
+ this.seedBootstrapService.setEventCallbacks({
501
+ onSeedReceived: (partyId, peerId) => this.emit('seed:received', { partyId, peerId }),
502
+ onSeedApplied: (partyId, peersAdded) => this.emit('seed:applied', { partyId, peersAdded }),
503
+ onSeedError: (partyId, error) => this.emit('seed:error', { partyId, error }),
504
+ });
505
+ this.seedBootstrapService.initialize(this.controlNode, this.controlDatabase);
506
+ log('Seed listener enabled');
507
+ }
508
+ /**
509
+ * Get the seed bootstrap service (for advanced use)
510
+ */
511
+ getSeedBootstrapService() {
512
+ return this.seedBootstrapService;
513
+ }
514
+ /**
515
+ * Authorize a new peer to join the cadre.
516
+ * Signs the peer ID with the authority key and inserts into CadrePeer table.
517
+ *
518
+ * @param peerId - The peer ID to authorize
519
+ * @param multiaddrs - Optional multiaddrs for the peer
520
+ */
521
+ async authorizePeer(peerId, multiaddrs) {
522
+ if (!this.seedBootstrapService) {
523
+ throw new Error('Seed bootstrap service not initialized. Call initializeSeedBootstrap() first.');
524
+ }
525
+ await this.seedBootstrapService.authorizePeer({ peerId, multiaddrs });
526
+ }
527
+ /**
528
+ * Create a seed from the current control network state.
529
+ * The seed contains peer information and is signed by an authority.
530
+ */
531
+ async createSeed() {
532
+ if (!this.seedBootstrapService) {
533
+ throw new Error('Seed bootstrap service not initialized. Call initializeSeedBootstrap() first.');
534
+ }
535
+ return await this.seedBootstrapService.createSeed();
536
+ }
537
+ /**
538
+ * Apply a seed to populate the peer cache and enable connections.
539
+ * Validates the seed signature before applying.
540
+ */
541
+ async applySeed(seed) {
542
+ if (!this.seedBootstrapService) {
543
+ // Create a temporary service for applying seeds (doesn't need authority key)
544
+ const tempService = new SeedBootstrapService({
545
+ partyId: seed.partyId,
546
+ });
547
+ if (this.controlNode && this.controlDatabase) {
548
+ tempService.initialize(this.controlNode, this.controlDatabase);
549
+ }
550
+ return await tempService.applySeed(seed);
551
+ }
552
+ return await this.seedBootstrapService.applySeed(seed);
553
+ }
554
+ /**
555
+ * Deliver a seed directly to a peer via the /sereus/seed/1.0.0 protocol.
556
+ */
557
+ async deliverSeed(targetMultiaddr, seed) {
558
+ if (!this.seedBootstrapService) {
559
+ throw new Error('Seed bootstrap service not initialized. Call initializeSeedBootstrap() first.');
560
+ }
561
+ return await this.seedBootstrapService.deliverSeed(targetMultiaddr, seed);
562
+ }
563
+ /**
564
+ * Encode a seed for out-of-band delivery (e.g., QR code, copy/paste).
565
+ */
566
+ encodeSeed(seed) {
567
+ // Static method - doesn't need service initialization
568
+ const json = JSON.stringify(seed);
569
+ return uint8ArrayToString(new TextEncoder().encode(json), 'base64url');
570
+ }
571
+ /**
572
+ * Decode a seed from base64url encoding.
573
+ */
574
+ decodeSeed(encoded) {
575
+ // Static method - doesn't need service initialization
576
+ const bytes = uint8ArrayFromString(encoded, 'base64url');
577
+ const json = new TextDecoder().decode(bytes);
578
+ return JSON.parse(json);
579
+ }
580
+ /**
581
+ * Get this node's circuit relay address for inclusion in seeds.
582
+ * Returns null if no relay address is available.
583
+ */
584
+ async getRelayAddress() {
585
+ if (!this.controlNode) {
586
+ return null;
587
+ }
588
+ const addrs = this.controlNode.getMultiaddrs();
589
+ const relayAddr = addrs.find(addr => addr.toString().includes('/p2p-circuit/'));
590
+ return relayAddr?.toString() ?? null;
591
+ }
592
+ // ============================================================================
593
+ // Seed Bootstrap Helper Methods
594
+ // ============================================================================
595
+ /**
596
+ * Add a drone to the cadre (for phone/server adding provider-hosted node).
597
+ * Creates authorization and seed for drone initialization.
598
+ */
599
+ async addDrone(options) {
600
+ if (!this.seedBootstrapService) {
601
+ throw new Error('Seed bootstrap service not initialized. Call initializeSeedBootstrap() first.');
602
+ }
603
+ return await this.seedBootstrapService.addDrone(options);
604
+ }
605
+ /**
606
+ * Create an invite for a phone to join the cadre.
607
+ * Use when a server wants to invite a NAT'd phone.
608
+ */
609
+ async createInvite(token, expiresIn) {
610
+ if (!this.seedBootstrapService) {
611
+ throw new Error('Seed bootstrap service not initialized. Call initializeSeedBootstrap() first.');
612
+ }
613
+ return await this.seedBootstrapService.createInvite(token, expiresIn);
614
+ }
615
+ /**
616
+ * Accept a phone connection using an invite.
617
+ * Call this when a phone dials in with an invite token.
618
+ */
619
+ async acceptPhone(options, issuedInvite) {
620
+ if (!this.seedBootstrapService) {
621
+ throw new Error('Seed bootstrap service not initialized. Call initializeSeedBootstrap() first.');
622
+ }
623
+ await this.seedBootstrapService.acceptPhone(options, issuedInvite);
624
+ }
625
+ /**
626
+ * Add a phone to the cadre with relay support.
627
+ * Use when both nodes are NAT'd (phone-to-phone).
628
+ */
629
+ async addPhoneWithRelay(phonePeerId) {
630
+ if (!this.seedBootstrapService) {
631
+ throw new Error('Seed bootstrap service not initialized. Call initializeSeedBootstrap() first.');
632
+ }
633
+ return await this.seedBootstrapService.addPhoneWithRelay(phonePeerId);
634
+ }
635
+ /**
636
+ * Encode an invite for out-of-band delivery (QR, link, etc.).
637
+ */
638
+ encodeInvite(invite) {
639
+ const json = JSON.stringify(invite);
640
+ return uint8ArrayToString(new TextEncoder().encode(json), 'base64url');
641
+ }
642
+ /**
643
+ * Decode an invite from base64url encoding.
644
+ */
645
+ decodeInvite(encoded) {
646
+ const bytes = uint8ArrayFromString(encoded, 'base64url');
647
+ const json = new TextDecoder().decode(bytes);
648
+ return JSON.parse(json);
649
+ }
650
+ /**
651
+ * Dial an authority from an invite (for phone joining via invite).
652
+ */
653
+ async dialInvite(invite) {
654
+ if (!this.seedBootstrapService) {
655
+ // Create a temporary service for dialing
656
+ const tempService = new SeedBootstrapService({
657
+ partyId: invite.partyId,
658
+ });
659
+ if (this.controlNode && this.controlDatabase) {
660
+ tempService.initialize(this.controlNode, this.controlDatabase);
661
+ }
662
+ await tempService.dialInvite(invite);
663
+ return;
664
+ }
665
+ await this.seedBootstrapService.dialInvite(invite);
666
+ }
667
+ // ============================================================================
668
+ // Strand Solicitation API (strand-proto integration)
669
+ // ============================================================================
670
+ /**
671
+ * Initialize the strand solicitation service.
672
+ * This enables forming strands with other parties via open invitations.
673
+ *
674
+ * @param options - Configuration for the solicitation service
675
+ */
676
+ initializeStrandSolicitation(options) {
677
+ if (!this.controlNode) {
678
+ throw new Error('CadreNode must be started before initializing strand solicitation');
679
+ }
680
+ this.strandSolicitationService = new StrandSolicitationService({
681
+ ...options,
682
+ partyId: this.config.controlNetwork.partyId,
683
+ cadrePeerAddrs: this.getMultiaddrs()
684
+ });
685
+ // Register as responder on the control node
686
+ this.strandSolicitationService.registerResponder(this.controlNode);
687
+ log('Strand solicitation service initialized');
688
+ }
689
+ /**
690
+ * Get the strand solicitation service (for advanced use)
691
+ */
692
+ getStrandSolicitationService() {
693
+ return this.strandSolicitationService;
694
+ }
695
+ /**
696
+ * Create an open invitation for others to form strands with this party.
697
+ *
698
+ * @param sAppId - The sApp to use for formed strands
699
+ * @param expirationMs - How long the invitation is valid (ms from now)
700
+ * @returns The open invitation to share out-of-band
701
+ */
702
+ async createOpenInvitation(sAppId, expirationMs = 24 * 60 * 60 * 1000 // 24 hours default
703
+ ) {
704
+ if (!this.strandSolicitationService) {
705
+ // Create a temporary service for creating invitations
706
+ this.initializeStrandSolicitation();
707
+ }
708
+ const bootstrap = this.getMultiaddrs();
709
+ if (bootstrap.length === 0) {
710
+ throw new Error('No multiaddrs available for invitation');
711
+ }
712
+ return await this.strandSolicitationService.createOpenInvitation(sAppId, expirationMs, bootstrap);
713
+ }
714
+ /**
715
+ * Form a strand with a responder via an open invitation.
716
+ *
717
+ * @param invitation - The open invitation received out-of-band
718
+ * @param disclosure - Identity/context information to share with the responder
719
+ * @returns The member key and strand info if successful
720
+ */
721
+ async formStrand(invitation, disclosure = {}) {
722
+ if (!this.controlNode) {
723
+ throw new Error('CadreNode must be started before forming strands');
724
+ }
725
+ if (!this.strandSolicitationService) {
726
+ this.initializeStrandSolicitation();
727
+ }
728
+ return await this.strandSolicitationService.formStrand(invitation, disclosure, this.controlNode);
729
+ }
730
+ /**
731
+ * Encode an open invitation for out-of-band delivery (QR, link, etc.).
732
+ */
733
+ encodeInvitation(invitation) {
734
+ const json = JSON.stringify({
735
+ ...invitation,
736
+ expiration: invitation.expiration.toISOString()
737
+ });
738
+ return uint8ArrayToString(new TextEncoder().encode(json), 'base64url');
739
+ }
740
+ /**
741
+ * Decode an open invitation from base64url encoding.
742
+ */
743
+ decodeInvitation(encoded) {
744
+ const bytes = uint8ArrayFromString(encoded, 'base64url');
745
+ const json = new TextDecoder().decode(bytes);
746
+ const parsed = JSON.parse(json);
747
+ return {
748
+ ...parsed,
749
+ expiration: new Date(parsed.expiration)
750
+ };
751
+ }
752
+ }
753
+ //# sourceMappingURL=cadre-node.js.map