@ruvector/edge-net 0.1.2 → 0.1.4

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.
package/sync.js ADDED
@@ -0,0 +1,799 @@
1
+ /**
2
+ * @ruvector/edge-net Hybrid Sync Service
3
+ *
4
+ * Multi-device identity and ledger synchronization using:
5
+ * - P2P sync via WebRTC (fast, direct when devices online together)
6
+ * - Firestore sync (persistent fallback, cross-session)
7
+ * - Identity linking via PiKey signatures
8
+ *
9
+ * @module @ruvector/edge-net/sync
10
+ */
11
+
12
+ import { EventEmitter } from 'events';
13
+ import { createHash, randomBytes } from 'crypto';
14
+
15
+ // ============================================
16
+ // SYNC CONFIGURATION
17
+ // ============================================
18
+
19
+ export const SYNC_CONFIG = {
20
+ // Firestore endpoints (Genesis nodes)
21
+ firestore: {
22
+ projectId: 'ruvector-edge-net',
23
+ collection: 'ledger-sync',
24
+ identityCollection: 'identity-links',
25
+ },
26
+ // Sync intervals
27
+ intervals: {
28
+ p2pHeartbeat: 5000, // 5s P2P sync check
29
+ firestoreSync: 30000, // 30s Firestore sync
30
+ staleThreshold: 60000, // 1min before considering state stale
31
+ },
32
+ // CRDT merge settings
33
+ crdt: {
34
+ maxBatchSize: 1000, // Max entries per merge
35
+ conflictResolution: 'lww', // Last-write-wins
36
+ },
37
+ // Genesis node endpoints
38
+ genesisNodes: [
39
+ { region: 'us-central1', url: 'https://edge-net-genesis-us.ruvector.dev' },
40
+ { region: 'europe-west1', url: 'https://edge-net-genesis-eu.ruvector.dev' },
41
+ { region: 'asia-east1', url: 'https://edge-net-genesis-asia.ruvector.dev' },
42
+ ],
43
+ };
44
+
45
+ // ============================================
46
+ // IDENTITY LINKER
47
+ // ============================================
48
+
49
+ /**
50
+ * Links a PiKey identity across multiple devices
51
+ * Uses cryptographic challenge-response to prove ownership
52
+ */
53
+ export class IdentityLinker extends EventEmitter {
54
+ constructor(piKey, options = {}) {
55
+ super();
56
+ this.piKey = piKey;
57
+ this.publicKeyHex = this.toHex(piKey.getPublicKey());
58
+ this.shortId = piKey.getShortId();
59
+ this.options = {
60
+ genesisUrl: options.genesisUrl || SYNC_CONFIG.genesisNodes[0].url,
61
+ ...options,
62
+ };
63
+ this.linkedDevices = new Map();
64
+ this.authToken = null;
65
+ this.deviceId = this.generateDeviceId();
66
+ }
67
+
68
+ /**
69
+ * Generate unique device ID
70
+ */
71
+ generateDeviceId() {
72
+ const platform = typeof window !== 'undefined' ? 'browser' : 'node';
73
+ const random = randomBytes(8).toString('hex');
74
+ const timestamp = Date.now().toString(36);
75
+ return `${platform}-${timestamp}-${random}`;
76
+ }
77
+
78
+ /**
79
+ * Authenticate with genesis node using PiKey signature
80
+ */
81
+ async authenticate() {
82
+ try {
83
+ // Step 1: Request challenge
84
+ const challengeRes = await this.fetchWithTimeout(
85
+ `${this.options.genesisUrl}/api/v1/identity/challenge`,
86
+ {
87
+ method: 'POST',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify({
90
+ publicKey: this.publicKeyHex,
91
+ deviceId: this.deviceId,
92
+ }),
93
+ }
94
+ );
95
+
96
+ if (!challengeRes.ok) {
97
+ throw new Error(`Challenge request failed: ${challengeRes.status}`);
98
+ }
99
+
100
+ const { challenge, nonce } = await challengeRes.json();
101
+
102
+ // Step 2: Sign challenge with PiKey
103
+ const challengeBytes = this.fromHex(challenge);
104
+ const signature = this.piKey.sign(challengeBytes);
105
+
106
+ // Step 3: Submit signature for verification
107
+ const authRes = await this.fetchWithTimeout(
108
+ `${this.options.genesisUrl}/api/v1/identity/verify`,
109
+ {
110
+ method: 'POST',
111
+ headers: { 'Content-Type': 'application/json' },
112
+ body: JSON.stringify({
113
+ publicKey: this.publicKeyHex,
114
+ deviceId: this.deviceId,
115
+ nonce,
116
+ signature: this.toHex(signature),
117
+ }),
118
+ }
119
+ );
120
+
121
+ if (!authRes.ok) {
122
+ throw new Error(`Authentication failed: ${authRes.status}`);
123
+ }
124
+
125
+ const { token, expiresAt, linkedDevices } = await authRes.json();
126
+
127
+ this.authToken = token;
128
+ this.tokenExpiry = new Date(expiresAt);
129
+
130
+ // Update linked devices
131
+ for (const device of linkedDevices || []) {
132
+ this.linkedDevices.set(device.deviceId, device);
133
+ }
134
+
135
+ this.emit('authenticated', {
136
+ deviceId: this.deviceId,
137
+ linkedDevices: this.linkedDevices.size,
138
+ });
139
+
140
+ return { success: true, token, linkedDevices: this.linkedDevices.size };
141
+
142
+ } catch (error) {
143
+ // Fallback: Generate local-only token for P2P sync
144
+ console.warn('[Sync] Genesis authentication failed, using local mode:', error.message);
145
+ this.authToken = this.generateLocalToken();
146
+ this.emit('authenticated', { deviceId: this.deviceId, mode: 'local' });
147
+ return { success: true, mode: 'local' };
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Generate local token for P2P-only mode
153
+ */
154
+ generateLocalToken() {
155
+ const payload = {
156
+ sub: this.publicKeyHex,
157
+ dev: this.deviceId,
158
+ iat: Date.now(),
159
+ mode: 'local',
160
+ };
161
+ return Buffer.from(JSON.stringify(payload)).toString('base64');
162
+ }
163
+
164
+ /**
165
+ * Link a new device to this identity
166
+ */
167
+ async linkDevice(deviceInfo) {
168
+ if (!this.authToken) {
169
+ await this.authenticate();
170
+ }
171
+
172
+ try {
173
+ const res = await this.fetchWithTimeout(
174
+ `${this.options.genesisUrl}/api/v1/identity/link`,
175
+ {
176
+ method: 'POST',
177
+ headers: {
178
+ 'Content-Type': 'application/json',
179
+ 'Authorization': `Bearer ${this.authToken}`,
180
+ },
181
+ body: JSON.stringify({
182
+ publicKey: this.publicKeyHex,
183
+ newDevice: deviceInfo,
184
+ }),
185
+ }
186
+ );
187
+
188
+ if (!res.ok) {
189
+ throw new Error(`Link failed: ${res.status}`);
190
+ }
191
+
192
+ const result = await res.json();
193
+ this.linkedDevices.set(deviceInfo.deviceId, deviceInfo);
194
+
195
+ this.emit('device_linked', { deviceId: deviceInfo.deviceId });
196
+ return result;
197
+
198
+ } catch (error) {
199
+ // P2P fallback: Store in local linked devices for gossip
200
+ this.linkedDevices.set(deviceInfo.deviceId, {
201
+ ...deviceInfo,
202
+ linkedAt: Date.now(),
203
+ mode: 'p2p',
204
+ });
205
+ return { success: true, mode: 'p2p' };
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Get all linked devices
211
+ */
212
+ getLinkedDevices() {
213
+ return Array.from(this.linkedDevices.values());
214
+ }
215
+
216
+ /**
217
+ * Check if a device is linked to this identity
218
+ */
219
+ isDeviceLinked(deviceId) {
220
+ return this.linkedDevices.has(deviceId);
221
+ }
222
+
223
+ // Utility methods
224
+ toHex(bytes) {
225
+ return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
226
+ }
227
+
228
+ fromHex(hex) {
229
+ const bytes = new Uint8Array(hex.length / 2);
230
+ for (let i = 0; i < hex.length; i += 2) {
231
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
232
+ }
233
+ return bytes;
234
+ }
235
+
236
+ async fetchWithTimeout(url, options, timeout = 10000) {
237
+ const controller = new AbortController();
238
+ const id = setTimeout(() => controller.abort(), timeout);
239
+ try {
240
+ const response = await fetch(url, { ...options, signal: controller.signal });
241
+ clearTimeout(id);
242
+ return response;
243
+ } catch (error) {
244
+ clearTimeout(id);
245
+ throw error;
246
+ }
247
+ }
248
+ }
249
+
250
+ // ============================================
251
+ // LEDGER SYNC SERVICE
252
+ // ============================================
253
+
254
+ /**
255
+ * Hybrid sync service for credit ledger
256
+ * Combines P2P (fast) and Firestore (persistent) sync
257
+ */
258
+ export class LedgerSyncService extends EventEmitter {
259
+ constructor(identityLinker, ledger, options = {}) {
260
+ super();
261
+ this.identity = identityLinker;
262
+ this.ledger = ledger;
263
+ this.options = {
264
+ enableP2P: true,
265
+ enableFirestore: true,
266
+ syncInterval: SYNC_CONFIG.intervals.firestoreSync,
267
+ ...options,
268
+ };
269
+
270
+ // Sync state
271
+ this.lastSyncTime = 0;
272
+ this.syncInProgress = false;
273
+ this.pendingChanges = [];
274
+ this.peerStates = new Map(); // deviceId -> { earned, spent, timestamp }
275
+ this.vectorClock = new Map(); // deviceId -> counter
276
+
277
+ // P2P connections
278
+ this.p2pPeers = new Map();
279
+
280
+ // Intervals
281
+ this.syncIntervalId = null;
282
+ this.heartbeatId = null;
283
+ }
284
+
285
+ /**
286
+ * Start sync service
287
+ */
288
+ async start() {
289
+ // Authenticate first
290
+ await this.identity.authenticate();
291
+
292
+ // Start periodic sync
293
+ if (this.options.enableFirestore) {
294
+ this.syncIntervalId = setInterval(
295
+ () => this.syncWithFirestore(),
296
+ this.options.syncInterval
297
+ );
298
+ }
299
+
300
+ // Start P2P heartbeat
301
+ if (this.options.enableP2P) {
302
+ this.heartbeatId = setInterval(
303
+ () => this.p2pHeartbeat(),
304
+ SYNC_CONFIG.intervals.p2pHeartbeat
305
+ );
306
+ }
307
+
308
+ // Initial sync
309
+ await this.fullSync();
310
+
311
+ this.emit('started', { deviceId: this.identity.deviceId });
312
+ return this;
313
+ }
314
+
315
+ /**
316
+ * Stop sync service
317
+ */
318
+ stop() {
319
+ if (this.syncIntervalId) {
320
+ clearInterval(this.syncIntervalId);
321
+ this.syncIntervalId = null;
322
+ }
323
+ if (this.heartbeatId) {
324
+ clearInterval(this.heartbeatId);
325
+ this.heartbeatId = null;
326
+ }
327
+ this.emit('stopped');
328
+ }
329
+
330
+ /**
331
+ * Full sync - fetch from all sources and merge
332
+ */
333
+ async fullSync() {
334
+ if (this.syncInProgress) return;
335
+ this.syncInProgress = true;
336
+
337
+ try {
338
+ const results = await Promise.allSettled([
339
+ this.options.enableFirestore ? this.fetchFromFirestore() : null,
340
+ this.options.enableP2P ? this.fetchFromP2PPeers() : null,
341
+ ]);
342
+
343
+ // Merge all fetched states
344
+ for (const result of results) {
345
+ if (result.status === 'fulfilled' && result.value) {
346
+ await this.mergeState(result.value);
347
+ }
348
+ }
349
+
350
+ // Push our state
351
+ await this.pushState();
352
+
353
+ this.lastSyncTime = Date.now();
354
+ this.emit('synced', {
355
+ timestamp: this.lastSyncTime,
356
+ balance: this.ledger.balance(),
357
+ });
358
+
359
+ } catch (error) {
360
+ this.emit('sync_error', { error: error.message });
361
+ } finally {
362
+ this.syncInProgress = false;
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Fetch ledger state from Firestore
368
+ */
369
+ async fetchFromFirestore() {
370
+ if (!this.identity.authToken) return null;
371
+
372
+ try {
373
+ const res = await this.identity.fetchWithTimeout(
374
+ `${this.identity.options.genesisUrl}/api/v1/ledger/${this.identity.publicKeyHex}`,
375
+ {
376
+ method: 'GET',
377
+ headers: {
378
+ 'Authorization': `Bearer ${this.identity.authToken}`,
379
+ },
380
+ }
381
+ );
382
+
383
+ if (!res.ok) {
384
+ if (res.status === 404) return null; // No state yet
385
+ throw new Error(`Firestore fetch failed: ${res.status}`);
386
+ }
387
+
388
+ const { states } = await res.json();
389
+ return states; // Array of { deviceId, earned, spent, timestamp }
390
+
391
+ } catch (error) {
392
+ console.warn('[Sync] Firestore fetch failed:', error.message);
393
+ return null;
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Fetch ledger state from P2P peers
399
+ */
400
+ async fetchFromP2PPeers() {
401
+ const states = [];
402
+
403
+ for (const [peerId, peer] of this.p2pPeers) {
404
+ try {
405
+ if (peer.dataChannel?.readyState === 'open') {
406
+ const state = await this.requestStateFromPeer(peer);
407
+ if (state) {
408
+ states.push({ deviceId: peerId, ...state });
409
+ }
410
+ }
411
+ } catch (error) {
412
+ console.warn(`[Sync] P2P fetch from ${peerId} failed:`, error.message);
413
+ }
414
+ }
415
+
416
+ return states.length > 0 ? states : null;
417
+ }
418
+
419
+ /**
420
+ * Request state from a P2P peer
421
+ */
422
+ requestStateFromPeer(peer) {
423
+ return new Promise((resolve, reject) => {
424
+ const requestId = randomBytes(8).toString('hex');
425
+ const timeout = setTimeout(() => {
426
+ reject(new Error('P2P state request timeout'));
427
+ }, 5000);
428
+
429
+ const handler = (event) => {
430
+ try {
431
+ const msg = JSON.parse(event.data);
432
+ if (msg.type === 'ledger_state' && msg.requestId === requestId) {
433
+ clearTimeout(timeout);
434
+ peer.dataChannel.removeEventListener('message', handler);
435
+ resolve(msg.state);
436
+ }
437
+ } catch (e) { /* ignore */ }
438
+ };
439
+
440
+ peer.dataChannel.addEventListener('message', handler);
441
+ peer.dataChannel.send(JSON.stringify({
442
+ type: 'ledger_state_request',
443
+ requestId,
444
+ from: this.identity.deviceId,
445
+ }));
446
+ });
447
+ }
448
+
449
+ /**
450
+ * Merge remote state into local ledger (CRDT)
451
+ */
452
+ async mergeState(states) {
453
+ if (!states || !Array.isArray(states)) return;
454
+
455
+ for (const state of states) {
456
+ // Skip our own state
457
+ if (state.deviceId === this.identity.deviceId) continue;
458
+
459
+ // Check vector clock for freshness
460
+ const lastSeen = this.vectorClock.get(state.deviceId) || 0;
461
+ if (state.timestamp <= lastSeen) continue;
462
+
463
+ // CRDT merge
464
+ try {
465
+ if (state.earned && state.spent) {
466
+ const earned = typeof state.earned === 'string'
467
+ ? JSON.parse(state.earned)
468
+ : state.earned;
469
+ const spent = typeof state.spent === 'string'
470
+ ? JSON.parse(state.spent)
471
+ : state.spent;
472
+
473
+ this.ledger.merge(
474
+ JSON.stringify(earned),
475
+ JSON.stringify(spent)
476
+ );
477
+ }
478
+
479
+ // Update vector clock
480
+ this.vectorClock.set(state.deviceId, state.timestamp);
481
+ this.peerStates.set(state.deviceId, state);
482
+
483
+ this.emit('state_merged', {
484
+ deviceId: state.deviceId,
485
+ newBalance: this.ledger.balance(),
486
+ });
487
+
488
+ } catch (error) {
489
+ console.warn(`[Sync] Merge failed for ${state.deviceId}:`, error.message);
490
+ }
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Push local state to sync destinations
496
+ */
497
+ async pushState() {
498
+ const state = this.exportState();
499
+
500
+ // Push to Firestore
501
+ if (this.options.enableFirestore && this.identity.authToken) {
502
+ await this.pushToFirestore(state);
503
+ }
504
+
505
+ // Broadcast to P2P peers
506
+ if (this.options.enableP2P) {
507
+ this.broadcastToP2P(state);
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Export current ledger state
513
+ */
514
+ exportState() {
515
+ return {
516
+ deviceId: this.identity.deviceId,
517
+ publicKey: this.identity.publicKeyHex,
518
+ earned: this.ledger.exportEarned(),
519
+ spent: this.ledger.exportSpent(),
520
+ balance: this.ledger.balance(),
521
+ totalEarned: this.ledger.totalEarned(),
522
+ totalSpent: this.ledger.totalSpent(),
523
+ timestamp: Date.now(),
524
+ };
525
+ }
526
+
527
+ /**
528
+ * Push state to Firestore
529
+ */
530
+ async pushToFirestore(state) {
531
+ try {
532
+ const res = await this.identity.fetchWithTimeout(
533
+ `${this.identity.options.genesisUrl}/api/v1/ledger/${this.identity.publicKeyHex}`,
534
+ {
535
+ method: 'PUT',
536
+ headers: {
537
+ 'Content-Type': 'application/json',
538
+ 'Authorization': `Bearer ${this.identity.authToken}`,
539
+ },
540
+ body: JSON.stringify({
541
+ deviceId: state.deviceId,
542
+ earned: state.earned,
543
+ spent: state.spent,
544
+ timestamp: state.timestamp,
545
+ }),
546
+ }
547
+ );
548
+
549
+ if (!res.ok) {
550
+ throw new Error(`Firestore push failed: ${res.status}`);
551
+ }
552
+
553
+ return true;
554
+
555
+ } catch (error) {
556
+ console.warn('[Sync] Firestore push failed:', error.message);
557
+ return false;
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Broadcast state to P2P peers
563
+ */
564
+ broadcastToP2P(state) {
565
+ const message = JSON.stringify({
566
+ type: 'ledger_state_broadcast',
567
+ state: {
568
+ deviceId: state.deviceId,
569
+ earned: state.earned,
570
+ spent: state.spent,
571
+ timestamp: state.timestamp,
572
+ },
573
+ });
574
+
575
+ for (const [peerId, peer] of this.p2pPeers) {
576
+ try {
577
+ if (peer.dataChannel?.readyState === 'open') {
578
+ peer.dataChannel.send(message);
579
+ }
580
+ } catch (error) {
581
+ console.warn(`[Sync] P2P broadcast to ${peerId} failed:`, error.message);
582
+ }
583
+ }
584
+ }
585
+
586
+ /**
587
+ * P2P heartbeat - discover and sync with nearby devices
588
+ */
589
+ async p2pHeartbeat() {
590
+ // Broadcast presence to linked devices
591
+ const presence = {
592
+ type: 'presence',
593
+ deviceId: this.identity.deviceId,
594
+ publicKey: this.identity.publicKeyHex,
595
+ balance: this.ledger.balance(),
596
+ timestamp: Date.now(),
597
+ };
598
+
599
+ for (const [peerId, peer] of this.p2pPeers) {
600
+ try {
601
+ if (peer.dataChannel?.readyState === 'open') {
602
+ peer.dataChannel.send(JSON.stringify(presence));
603
+ }
604
+ } catch (error) {
605
+ // Remove stale peer
606
+ this.p2pPeers.delete(peerId);
607
+ }
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Register a P2P peer for sync
613
+ */
614
+ registerP2PPeer(peerId, dataChannel) {
615
+ this.p2pPeers.set(peerId, { dataChannel, connectedAt: Date.now() });
616
+
617
+ // Handle incoming messages
618
+ dataChannel.addEventListener('message', (event) => {
619
+ this.handleP2PMessage(peerId, event.data);
620
+ });
621
+
622
+ this.emit('peer_registered', { peerId });
623
+ }
624
+
625
+ /**
626
+ * Handle incoming P2P message
627
+ */
628
+ async handleP2PMessage(peerId, data) {
629
+ try {
630
+ const msg = JSON.parse(data);
631
+
632
+ switch (msg.type) {
633
+ case 'ledger_state_request':
634
+ // Respond with our state
635
+ const state = this.exportState();
636
+ const peer = this.p2pPeers.get(peerId);
637
+ if (peer?.dataChannel?.readyState === 'open') {
638
+ peer.dataChannel.send(JSON.stringify({
639
+ type: 'ledger_state',
640
+ requestId: msg.requestId,
641
+ state: {
642
+ earned: state.earned,
643
+ spent: state.spent,
644
+ timestamp: state.timestamp,
645
+ },
646
+ }));
647
+ }
648
+ break;
649
+
650
+ case 'ledger_state_broadcast':
651
+ // Merge incoming state
652
+ if (msg.state) {
653
+ await this.mergeState([{ deviceId: peerId, ...msg.state }]);
654
+ }
655
+ break;
656
+
657
+ case 'presence':
658
+ // Update peer info
659
+ const existingPeer = this.p2pPeers.get(peerId);
660
+ if (existingPeer) {
661
+ existingPeer.lastSeen = Date.now();
662
+ existingPeer.balance = msg.balance;
663
+ }
664
+ break;
665
+ }
666
+
667
+ } catch (error) {
668
+ console.warn(`[Sync] P2P message handling failed:`, error.message);
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Sync with Firestore (called periodically)
674
+ */
675
+ async syncWithFirestore() {
676
+ if (this.syncInProgress) return;
677
+
678
+ try {
679
+ const states = await this.fetchFromFirestore();
680
+ if (states) {
681
+ await this.mergeState(states);
682
+ }
683
+ await this.pushToFirestore(this.exportState());
684
+ } catch (error) {
685
+ console.warn('[Sync] Periodic Firestore sync failed:', error.message);
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Force immediate sync
691
+ */
692
+ async forceSync() {
693
+ return this.fullSync();
694
+ }
695
+
696
+ /**
697
+ * Get sync status
698
+ */
699
+ getStatus() {
700
+ return {
701
+ deviceId: this.identity.deviceId,
702
+ publicKey: this.identity.publicKeyHex,
703
+ shortId: this.identity.shortId,
704
+ linkedDevices: this.identity.getLinkedDevices().length,
705
+ p2pPeers: this.p2pPeers.size,
706
+ lastSyncTime: this.lastSyncTime,
707
+ balance: this.ledger.balance(),
708
+ totalEarned: this.ledger.totalEarned(),
709
+ totalSpent: this.ledger.totalSpent(),
710
+ syncEnabled: {
711
+ p2p: this.options.enableP2P,
712
+ firestore: this.options.enableFirestore,
713
+ },
714
+ };
715
+ }
716
+ }
717
+
718
+ // ============================================
719
+ // SYNC MANAGER (CONVENIENCE WRAPPER)
720
+ // ============================================
721
+
722
+ /**
723
+ * High-level sync manager for easy integration
724
+ */
725
+ export class SyncManager extends EventEmitter {
726
+ constructor(piKey, ledger, options = {}) {
727
+ super();
728
+ this.identityLinker = new IdentityLinker(piKey, options);
729
+ this.syncService = new LedgerSyncService(this.identityLinker, ledger, options);
730
+
731
+ // Forward events
732
+ this.syncService.on('synced', (data) => this.emit('synced', data));
733
+ this.syncService.on('state_merged', (data) => this.emit('state_merged', data));
734
+ this.syncService.on('sync_error', (data) => this.emit('sync_error', data));
735
+ this.identityLinker.on('authenticated', (data) => this.emit('authenticated', data));
736
+ this.identityLinker.on('device_linked', (data) => this.emit('device_linked', data));
737
+ }
738
+
739
+ /**
740
+ * Start sync
741
+ */
742
+ async start() {
743
+ await this.syncService.start();
744
+ return this;
745
+ }
746
+
747
+ /**
748
+ * Stop sync
749
+ */
750
+ stop() {
751
+ this.syncService.stop();
752
+ }
753
+
754
+ /**
755
+ * Force sync
756
+ */
757
+ async sync() {
758
+ return this.syncService.forceSync();
759
+ }
760
+
761
+ /**
762
+ * Register P2P peer
763
+ */
764
+ registerPeer(peerId, dataChannel) {
765
+ this.syncService.registerP2PPeer(peerId, dataChannel);
766
+ }
767
+
768
+ /**
769
+ * Get status
770
+ */
771
+ getStatus() {
772
+ return this.syncService.getStatus();
773
+ }
774
+
775
+ /**
776
+ * Export identity for another device
777
+ */
778
+ exportIdentity(password) {
779
+ return this.identityLinker.piKey.createEncryptedBackup(password);
780
+ }
781
+
782
+ /**
783
+ * Link devices via QR code data
784
+ */
785
+ generateLinkData() {
786
+ return {
787
+ publicKey: this.identityLinker.publicKeyHex,
788
+ shortId: this.identityLinker.shortId,
789
+ genesisUrl: this.identityLinker.options.genesisUrl,
790
+ timestamp: Date.now(),
791
+ };
792
+ }
793
+ }
794
+
795
+ // ============================================
796
+ // EXPORTS
797
+ // ============================================
798
+
799
+ export default SyncManager;