@ruvector/edge-net 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/dht.js +790 -0
  2. package/genesis.js +858 -0
  3. package/p2p.js +577 -0
  4. package/package.json +24 -3
  5. package/webrtc.js +37 -5
package/genesis.js ADDED
@@ -0,0 +1,858 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @ruvector/edge-net Genesis Node
4
+ *
5
+ * Bootstrap node for the edge-net P2P network.
6
+ * Provides signaling, peer discovery, and ledger sync.
7
+ *
8
+ * Run: node genesis.js [--port 8787] [--data ~/.ruvector/genesis]
9
+ *
10
+ * @module @ruvector/edge-net/genesis
11
+ */
12
+
13
+ import { EventEmitter } from 'events';
14
+ import { createHash, randomBytes } from 'crypto';
15
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
16
+ import { join } from 'path';
17
+
18
+ // ============================================
19
+ // GENESIS NODE CONFIGURATION
20
+ // ============================================
21
+
22
+ export const GENESIS_CONFIG = {
23
+ port: parseInt(process.env.GENESIS_PORT || '8787'),
24
+ host: process.env.GENESIS_HOST || '0.0.0.0',
25
+ dataDir: process.env.GENESIS_DATA || join(process.env.HOME || '/tmp', '.ruvector', 'genesis'),
26
+ // Rate limiting
27
+ rateLimit: {
28
+ maxConnectionsPerIp: 50,
29
+ maxMessagesPerSecond: 100,
30
+ challengeExpiry: 60000, // 1 minute
31
+ },
32
+ // Cleanup
33
+ cleanup: {
34
+ staleConnectionTimeout: 300000, // 5 minutes
35
+ cleanupInterval: 60000, // 1 minute
36
+ },
37
+ };
38
+
39
+ // ============================================
40
+ // PEER REGISTRY
41
+ // ============================================
42
+
43
+ export class PeerRegistry {
44
+ constructor() {
45
+ this.peers = new Map(); // peerId -> peer info
46
+ this.byPublicKey = new Map(); // publicKey -> peerId
47
+ this.byRoom = new Map(); // room -> Set<peerId>
48
+ this.connections = new Map(); // connectionId -> peerId
49
+ }
50
+
51
+ register(peerId, info) {
52
+ this.peers.set(peerId, {
53
+ ...info,
54
+ peerId,
55
+ registeredAt: Date.now(),
56
+ lastSeen: Date.now(),
57
+ });
58
+
59
+ if (info.publicKey) {
60
+ this.byPublicKey.set(info.publicKey, peerId);
61
+ }
62
+
63
+ return this.peers.get(peerId);
64
+ }
65
+
66
+ update(peerId, updates) {
67
+ const peer = this.peers.get(peerId);
68
+ if (peer) {
69
+ Object.assign(peer, updates, { lastSeen: Date.now() });
70
+ }
71
+ return peer;
72
+ }
73
+
74
+ get(peerId) {
75
+ return this.peers.get(peerId);
76
+ }
77
+
78
+ getByPublicKey(publicKey) {
79
+ const peerId = this.byPublicKey.get(publicKey);
80
+ return peerId ? this.peers.get(peerId) : null;
81
+ }
82
+
83
+ remove(peerId) {
84
+ const peer = this.peers.get(peerId);
85
+ if (peer) {
86
+ if (peer.publicKey) {
87
+ this.byPublicKey.delete(peer.publicKey);
88
+ }
89
+ if (peer.room) {
90
+ const room = this.byRoom.get(peer.room);
91
+ if (room) room.delete(peerId);
92
+ }
93
+ this.peers.delete(peerId);
94
+ return true;
95
+ }
96
+ return false;
97
+ }
98
+
99
+ joinRoom(peerId, room) {
100
+ const peer = this.peers.get(peerId);
101
+ if (!peer) return false;
102
+
103
+ // Leave old room
104
+ if (peer.room && peer.room !== room) {
105
+ const oldRoom = this.byRoom.get(peer.room);
106
+ if (oldRoom) oldRoom.delete(peerId);
107
+ }
108
+
109
+ // Join new room
110
+ if (!this.byRoom.has(room)) {
111
+ this.byRoom.set(room, new Set());
112
+ }
113
+ this.byRoom.get(room).add(peerId);
114
+ peer.room = room;
115
+
116
+ return true;
117
+ }
118
+
119
+ getRoomPeers(room) {
120
+ const peerIds = this.byRoom.get(room) || new Set();
121
+ return Array.from(peerIds).map(id => this.peers.get(id)).filter(Boolean);
122
+ }
123
+
124
+ getAllPeers() {
125
+ return Array.from(this.peers.values());
126
+ }
127
+
128
+ pruneStale(maxAge = GENESIS_CONFIG.cleanup.staleConnectionTimeout) {
129
+ const cutoff = Date.now() - maxAge;
130
+ const removed = [];
131
+
132
+ for (const [peerId, peer] of this.peers) {
133
+ if (peer.lastSeen < cutoff) {
134
+ this.remove(peerId);
135
+ removed.push(peerId);
136
+ }
137
+ }
138
+
139
+ return removed;
140
+ }
141
+
142
+ getStats() {
143
+ return {
144
+ totalPeers: this.peers.size,
145
+ rooms: this.byRoom.size,
146
+ roomSizes: Object.fromEntries(
147
+ Array.from(this.byRoom.entries()).map(([room, peers]) => [room, peers.size])
148
+ ),
149
+ };
150
+ }
151
+ }
152
+
153
+ // ============================================
154
+ // LEDGER STORE
155
+ // ============================================
156
+
157
+ export class LedgerStore {
158
+ constructor(dataDir) {
159
+ this.dataDir = dataDir;
160
+ this.ledgers = new Map();
161
+ this.pendingWrites = new Map();
162
+
163
+ // Ensure data directory exists
164
+ if (!existsSync(dataDir)) {
165
+ mkdirSync(dataDir, { recursive: true });
166
+ }
167
+
168
+ // Load existing ledgers
169
+ this.loadAll();
170
+ }
171
+
172
+ loadAll() {
173
+ try {
174
+ const indexPath = join(this.dataDir, 'index.json');
175
+ if (existsSync(indexPath)) {
176
+ const index = JSON.parse(readFileSync(indexPath, 'utf8'));
177
+ for (const publicKey of index.keys || []) {
178
+ this.load(publicKey);
179
+ }
180
+ }
181
+ } catch (err) {
182
+ console.warn('[Genesis] Failed to load ledger index:', err.message);
183
+ }
184
+ }
185
+
186
+ load(publicKey) {
187
+ try {
188
+ const path = join(this.dataDir, `ledger-${publicKey.slice(0, 16)}.json`);
189
+ if (existsSync(path)) {
190
+ const data = JSON.parse(readFileSync(path, 'utf8'));
191
+ this.ledgers.set(publicKey, data);
192
+ return data;
193
+ }
194
+ } catch (err) {
195
+ console.warn(`[Genesis] Failed to load ledger ${publicKey.slice(0, 8)}:`, err.message);
196
+ }
197
+ return null;
198
+ }
199
+
200
+ save(publicKey) {
201
+ try {
202
+ const data = this.ledgers.get(publicKey);
203
+ if (!data) return false;
204
+
205
+ const path = join(this.dataDir, `ledger-${publicKey.slice(0, 16)}.json`);
206
+ writeFileSync(path, JSON.stringify(data, null, 2));
207
+
208
+ // Update index
209
+ this.saveIndex();
210
+ return true;
211
+ } catch (err) {
212
+ console.warn(`[Genesis] Failed to save ledger ${publicKey.slice(0, 8)}:`, err.message);
213
+ return false;
214
+ }
215
+ }
216
+
217
+ saveIndex() {
218
+ try {
219
+ const indexPath = join(this.dataDir, 'index.json');
220
+ writeFileSync(indexPath, JSON.stringify({
221
+ keys: Array.from(this.ledgers.keys()),
222
+ updatedAt: Date.now(),
223
+ }, null, 2));
224
+ } catch (err) {
225
+ console.warn('[Genesis] Failed to save index:', err.message);
226
+ }
227
+ }
228
+
229
+ get(publicKey) {
230
+ return this.ledgers.get(publicKey);
231
+ }
232
+
233
+ getStates(publicKey) {
234
+ const ledger = this.ledgers.get(publicKey);
235
+ if (!ledger) return [];
236
+
237
+ return Object.values(ledger.devices || {});
238
+ }
239
+
240
+ update(publicKey, deviceId, state) {
241
+ if (!this.ledgers.has(publicKey)) {
242
+ this.ledgers.set(publicKey, {
243
+ publicKey,
244
+ createdAt: Date.now(),
245
+ devices: {},
246
+ });
247
+ }
248
+
249
+ const ledger = this.ledgers.get(publicKey);
250
+
251
+ // Merge state
252
+ const existing = ledger.devices[deviceId] || {};
253
+ const merged = this.mergeCRDT(existing, state);
254
+
255
+ ledger.devices[deviceId] = {
256
+ ...merged,
257
+ deviceId,
258
+ updatedAt: Date.now(),
259
+ };
260
+
261
+ // Schedule write
262
+ this.scheduleSave(publicKey);
263
+
264
+ return ledger.devices[deviceId];
265
+ }
266
+
267
+ mergeCRDT(existing, incoming) {
268
+ // Simple LWW merge for now
269
+ if (!existing.timestamp || incoming.timestamp > existing.timestamp) {
270
+ return { ...incoming };
271
+ }
272
+
273
+ // If same timestamp, merge counters
274
+ return {
275
+ earned: Math.max(existing.earned || 0, incoming.earned || 0),
276
+ spent: Math.max(existing.spent || 0, incoming.spent || 0),
277
+ timestamp: Math.max(existing.timestamp || 0, incoming.timestamp || 0),
278
+ };
279
+ }
280
+
281
+ scheduleSave(publicKey) {
282
+ if (this.pendingWrites.has(publicKey)) return;
283
+
284
+ this.pendingWrites.set(publicKey, setTimeout(() => {
285
+ this.save(publicKey);
286
+ this.pendingWrites.delete(publicKey);
287
+ }, 1000));
288
+ }
289
+
290
+ flush() {
291
+ for (const [publicKey, timeout] of this.pendingWrites) {
292
+ clearTimeout(timeout);
293
+ this.save(publicKey);
294
+ }
295
+ this.pendingWrites.clear();
296
+ }
297
+
298
+ getStats() {
299
+ return {
300
+ totalLedgers: this.ledgers.size,
301
+ totalDevices: Array.from(this.ledgers.values())
302
+ .reduce((sum, l) => sum + Object.keys(l.devices || {}).length, 0),
303
+ };
304
+ }
305
+ }
306
+
307
+ // ============================================
308
+ // AUTHENTICATION SERVICE
309
+ // ============================================
310
+
311
+ export class AuthService {
312
+ constructor() {
313
+ this.challenges = new Map(); // nonce -> { challenge, publicKey, expiresAt }
314
+ this.tokens = new Map(); // token -> { publicKey, deviceId, expiresAt }
315
+ }
316
+
317
+ createChallenge(publicKey, deviceId) {
318
+ const nonce = randomBytes(32).toString('hex');
319
+ const challenge = randomBytes(32).toString('hex');
320
+
321
+ this.challenges.set(nonce, {
322
+ challenge,
323
+ publicKey,
324
+ deviceId,
325
+ expiresAt: Date.now() + GENESIS_CONFIG.rateLimit.challengeExpiry,
326
+ });
327
+
328
+ return { nonce, challenge };
329
+ }
330
+
331
+ verifyChallenge(nonce, publicKey, signature) {
332
+ const challengeData = this.challenges.get(nonce);
333
+ if (!challengeData) {
334
+ return { valid: false, error: 'Invalid nonce' };
335
+ }
336
+
337
+ if (Date.now() > challengeData.expiresAt) {
338
+ this.challenges.delete(nonce);
339
+ return { valid: false, error: 'Challenge expired' };
340
+ }
341
+
342
+ if (challengeData.publicKey !== publicKey) {
343
+ return { valid: false, error: 'Public key mismatch' };
344
+ }
345
+
346
+ // Simple signature verification (in production, use proper Ed25519)
347
+ const expectedSig = createHash('sha256')
348
+ .update(challengeData.challenge + publicKey)
349
+ .digest('hex');
350
+
351
+ // For now, accept any signature (real impl would verify Ed25519)
352
+ // In production: verify Ed25519 signature
353
+
354
+ this.challenges.delete(nonce);
355
+
356
+ // Generate token
357
+ const token = randomBytes(32).toString('hex');
358
+ const tokenData = {
359
+ publicKey,
360
+ deviceId: challengeData.deviceId,
361
+ createdAt: Date.now(),
362
+ expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
363
+ };
364
+
365
+ this.tokens.set(token, tokenData);
366
+
367
+ return { valid: true, token, expiresAt: tokenData.expiresAt };
368
+ }
369
+
370
+ validateToken(token) {
371
+ const tokenData = this.tokens.get(token);
372
+ if (!tokenData) return null;
373
+
374
+ if (Date.now() > tokenData.expiresAt) {
375
+ this.tokens.delete(token);
376
+ return null;
377
+ }
378
+
379
+ return tokenData;
380
+ }
381
+
382
+ cleanup() {
383
+ const now = Date.now();
384
+
385
+ for (const [nonce, data] of this.challenges) {
386
+ if (now > data.expiresAt) {
387
+ this.challenges.delete(nonce);
388
+ }
389
+ }
390
+
391
+ for (const [token, data] of this.tokens) {
392
+ if (now > data.expiresAt) {
393
+ this.tokens.delete(token);
394
+ }
395
+ }
396
+ }
397
+ }
398
+
399
+ // ============================================
400
+ // GENESIS NODE SERVER
401
+ // ============================================
402
+
403
+ export class GenesisNode extends EventEmitter {
404
+ constructor(options = {}) {
405
+ super();
406
+ this.config = { ...GENESIS_CONFIG, ...options };
407
+ this.peerRegistry = new PeerRegistry();
408
+ this.ledgerStore = new LedgerStore(this.config.dataDir);
409
+ this.authService = new AuthService();
410
+
411
+ this.wss = null;
412
+ this.connections = new Map();
413
+ this.cleanupInterval = null;
414
+
415
+ this.stats = {
416
+ startedAt: null,
417
+ totalConnections: 0,
418
+ totalMessages: 0,
419
+ signalsRelayed: 0,
420
+ };
421
+ }
422
+
423
+ async start() {
424
+ console.log('\n🌐 Starting Edge-Net Genesis Node...');
425
+ console.log(` Port: ${this.config.port}`);
426
+ console.log(` Data: ${this.config.dataDir}`);
427
+
428
+ // Import ws dynamically
429
+ const { WebSocketServer } = await import('ws');
430
+
431
+ this.wss = new WebSocketServer({
432
+ port: this.config.port,
433
+ host: this.config.host,
434
+ });
435
+
436
+ this.wss.on('connection', (ws, req) => this.handleConnection(ws, req));
437
+ this.wss.on('error', (err) => this.emit('error', err));
438
+
439
+ // Start cleanup interval
440
+ this.cleanupInterval = setInterval(() => this.cleanup(), this.config.cleanup.cleanupInterval);
441
+
442
+ this.stats.startedAt = Date.now();
443
+
444
+ console.log(`\nāœ… Genesis Node running on ws://${this.config.host}:${this.config.port}`);
445
+ console.log(` API: http://${this.config.host}:${this.config.port}/api/v1/`);
446
+
447
+ this.emit('started', { port: this.config.port });
448
+
449
+ return this;
450
+ }
451
+
452
+ stop() {
453
+ if (this.cleanupInterval) {
454
+ clearInterval(this.cleanupInterval);
455
+ }
456
+
457
+ if (this.wss) {
458
+ this.wss.close();
459
+ }
460
+
461
+ this.ledgerStore.flush();
462
+
463
+ this.emit('stopped');
464
+ }
465
+
466
+ handleConnection(ws, req) {
467
+ const connectionId = randomBytes(16).toString('hex');
468
+ const ip = req.socket.remoteAddress;
469
+
470
+ this.stats.totalConnections++;
471
+
472
+ this.connections.set(connectionId, {
473
+ ws,
474
+ ip,
475
+ peerId: null,
476
+ connectedAt: Date.now(),
477
+ });
478
+
479
+ ws.on('message', (data) => {
480
+ try {
481
+ const message = JSON.parse(data.toString());
482
+ this.handleMessage(connectionId, message);
483
+ } catch (err) {
484
+ console.warn(`[Genesis] Invalid message from ${connectionId}:`, err.message);
485
+ }
486
+ });
487
+
488
+ ws.on('close', () => {
489
+ this.handleDisconnect(connectionId);
490
+ });
491
+
492
+ ws.on('error', (err) => {
493
+ console.warn(`[Genesis] Connection error ${connectionId}:`, err.message);
494
+ });
495
+
496
+ // Send welcome
497
+ this.send(connectionId, {
498
+ type: 'welcome',
499
+ connectionId,
500
+ serverTime: Date.now(),
501
+ });
502
+ }
503
+
504
+ handleDisconnect(connectionId) {
505
+ const conn = this.connections.get(connectionId);
506
+ if (conn?.peerId) {
507
+ const peer = this.peerRegistry.get(conn.peerId);
508
+ if (peer?.room) {
509
+ // Notify room peers
510
+ this.broadcastToRoom(peer.room, {
511
+ type: 'peer-left',
512
+ peerId: conn.peerId,
513
+ }, conn.peerId);
514
+ }
515
+ this.peerRegistry.remove(conn.peerId);
516
+ }
517
+ this.connections.delete(connectionId);
518
+ }
519
+
520
+ handleMessage(connectionId, message) {
521
+ this.stats.totalMessages++;
522
+
523
+ const conn = this.connections.get(connectionId);
524
+ if (!conn) return;
525
+
526
+ switch (message.type) {
527
+ // Signaling messages
528
+ case 'announce':
529
+ this.handleAnnounce(connectionId, message);
530
+ break;
531
+
532
+ case 'join':
533
+ this.handleJoinRoom(connectionId, message);
534
+ break;
535
+
536
+ case 'offer':
537
+ case 'answer':
538
+ case 'ice-candidate':
539
+ this.relaySignal(connectionId, message);
540
+ break;
541
+
542
+ // Auth messages
543
+ case 'auth-challenge':
544
+ this.handleAuthChallenge(connectionId, message);
545
+ break;
546
+
547
+ case 'auth-verify':
548
+ this.handleAuthVerify(connectionId, message);
549
+ break;
550
+
551
+ // Ledger messages
552
+ case 'ledger-get':
553
+ this.handleLedgerGet(connectionId, message);
554
+ break;
555
+
556
+ case 'ledger-put':
557
+ this.handleLedgerPut(connectionId, message);
558
+ break;
559
+
560
+ // DHT bootstrap
561
+ case 'dht-bootstrap':
562
+ this.handleDHTBootstrap(connectionId, message);
563
+ break;
564
+
565
+ default:
566
+ console.warn(`[Genesis] Unknown message type: ${message.type}`);
567
+ }
568
+ }
569
+
570
+ handleAnnounce(connectionId, message) {
571
+ const conn = this.connections.get(connectionId);
572
+ const peerId = message.piKey || message.peerId || randomBytes(16).toString('hex');
573
+
574
+ conn.peerId = peerId;
575
+
576
+ this.peerRegistry.register(peerId, {
577
+ publicKey: message.publicKey,
578
+ siteId: message.siteId,
579
+ capabilities: message.capabilities || [],
580
+ connectionId,
581
+ });
582
+
583
+ // Send current peer list
584
+ const peers = this.peerRegistry.getAllPeers()
585
+ .filter(p => p.peerId !== peerId)
586
+ .map(p => ({
587
+ piKey: p.peerId,
588
+ siteId: p.siteId,
589
+ capabilities: p.capabilities,
590
+ }));
591
+
592
+ this.send(connectionId, {
593
+ type: 'peer-list',
594
+ peers,
595
+ });
596
+
597
+ // Notify other peers
598
+ for (const peer of this.peerRegistry.getAllPeers()) {
599
+ if (peer.peerId !== peerId && peer.connectionId) {
600
+ this.send(peer.connectionId, {
601
+ type: 'peer-joined',
602
+ peerId,
603
+ siteId: message.siteId,
604
+ capabilities: message.capabilities,
605
+ });
606
+ }
607
+ }
608
+ }
609
+
610
+ handleJoinRoom(connectionId, message) {
611
+ const conn = this.connections.get(connectionId);
612
+ if (!conn?.peerId) return;
613
+
614
+ const room = message.room || 'default';
615
+ this.peerRegistry.joinRoom(conn.peerId, room);
616
+
617
+ // Send room peers
618
+ const roomPeers = this.peerRegistry.getRoomPeers(room)
619
+ .filter(p => p.peerId !== conn.peerId)
620
+ .map(p => ({
621
+ piKey: p.peerId,
622
+ siteId: p.siteId,
623
+ }));
624
+
625
+ this.send(connectionId, {
626
+ type: 'room-joined',
627
+ room,
628
+ peers: roomPeers,
629
+ });
630
+
631
+ // Notify room peers
632
+ this.broadcastToRoom(room, {
633
+ type: 'peer-joined',
634
+ peerId: conn.peerId,
635
+ siteId: this.peerRegistry.get(conn.peerId)?.siteId,
636
+ }, conn.peerId);
637
+ }
638
+
639
+ relaySignal(connectionId, message) {
640
+ this.stats.signalsRelayed++;
641
+
642
+ const conn = this.connections.get(connectionId);
643
+ if (!conn?.peerId) return;
644
+
645
+ const targetPeer = this.peerRegistry.get(message.to);
646
+ if (!targetPeer?.connectionId) {
647
+ this.send(connectionId, {
648
+ type: 'error',
649
+ error: 'Target peer not found',
650
+ originalType: message.type,
651
+ });
652
+ return;
653
+ }
654
+
655
+ // Relay the signal
656
+ this.send(targetPeer.connectionId, {
657
+ ...message,
658
+ from: conn.peerId,
659
+ });
660
+ }
661
+
662
+ handleAuthChallenge(connectionId, message) {
663
+ const { nonce, challenge } = this.authService.createChallenge(
664
+ message.publicKey,
665
+ message.deviceId
666
+ );
667
+
668
+ this.send(connectionId, {
669
+ type: 'auth-challenge-response',
670
+ nonce,
671
+ challenge,
672
+ });
673
+ }
674
+
675
+ handleAuthVerify(connectionId, message) {
676
+ const result = this.authService.verifyChallenge(
677
+ message.nonce,
678
+ message.publicKey,
679
+ message.signature
680
+ );
681
+
682
+ this.send(connectionId, {
683
+ type: 'auth-verify-response',
684
+ ...result,
685
+ });
686
+ }
687
+
688
+ handleLedgerGet(connectionId, message) {
689
+ const tokenData = this.authService.validateToken(message.token);
690
+ if (!tokenData) {
691
+ this.send(connectionId, {
692
+ type: 'ledger-response',
693
+ error: 'Invalid or expired token',
694
+ });
695
+ return;
696
+ }
697
+
698
+ const states = this.ledgerStore.getStates(message.publicKey || tokenData.publicKey);
699
+
700
+ this.send(connectionId, {
701
+ type: 'ledger-response',
702
+ states,
703
+ });
704
+ }
705
+
706
+ handleLedgerPut(connectionId, message) {
707
+ const tokenData = this.authService.validateToken(message.token);
708
+ if (!tokenData) {
709
+ this.send(connectionId, {
710
+ type: 'ledger-put-response',
711
+ error: 'Invalid or expired token',
712
+ });
713
+ return;
714
+ }
715
+
716
+ const updated = this.ledgerStore.update(
717
+ tokenData.publicKey,
718
+ message.deviceId || tokenData.deviceId,
719
+ message.state
720
+ );
721
+
722
+ this.send(connectionId, {
723
+ type: 'ledger-put-response',
724
+ success: true,
725
+ state: updated,
726
+ });
727
+ }
728
+
729
+ handleDHTBootstrap(connectionId, message) {
730
+ // Return known peers for DHT bootstrap
731
+ const peers = this.peerRegistry.getAllPeers()
732
+ .slice(0, 20)
733
+ .map(p => ({
734
+ id: p.peerId,
735
+ address: p.connectionId,
736
+ lastSeen: p.lastSeen,
737
+ }));
738
+
739
+ this.send(connectionId, {
740
+ type: 'dht-bootstrap-response',
741
+ peers,
742
+ });
743
+ }
744
+
745
+ send(connectionId, message) {
746
+ const conn = this.connections.get(connectionId);
747
+ if (conn?.ws?.readyState === 1) {
748
+ conn.ws.send(JSON.stringify(message));
749
+ }
750
+ }
751
+
752
+ broadcastToRoom(room, message, excludePeerId = null) {
753
+ const peers = this.peerRegistry.getRoomPeers(room);
754
+ for (const peer of peers) {
755
+ if (peer.peerId !== excludePeerId && peer.connectionId) {
756
+ this.send(peer.connectionId, message);
757
+ }
758
+ }
759
+ }
760
+
761
+ cleanup() {
762
+ // Prune stale peers
763
+ const removed = this.peerRegistry.pruneStale();
764
+ if (removed.length > 0) {
765
+ console.log(`[Genesis] Pruned ${removed.length} stale peers`);
766
+ }
767
+
768
+ // Cleanup auth
769
+ this.authService.cleanup();
770
+ }
771
+
772
+ getStats() {
773
+ return {
774
+ ...this.stats,
775
+ uptime: this.stats.startedAt ? Date.now() - this.stats.startedAt : 0,
776
+ ...this.peerRegistry.getStats(),
777
+ ...this.ledgerStore.getStats(),
778
+ activeConnections: this.connections.size,
779
+ };
780
+ }
781
+ }
782
+
783
+ // ============================================
784
+ // CLI
785
+ // ============================================
786
+
787
+ async function main() {
788
+ const args = process.argv.slice(2);
789
+
790
+ // Parse args
791
+ let port = GENESIS_CONFIG.port;
792
+ let dataDir = GENESIS_CONFIG.dataDir;
793
+
794
+ for (let i = 0; i < args.length; i++) {
795
+ if (args[i] === '--port' && args[i + 1]) {
796
+ port = parseInt(args[i + 1]);
797
+ i++;
798
+ } else if (args[i] === '--data' && args[i + 1]) {
799
+ dataDir = args[i + 1];
800
+ i++;
801
+ } else if (args[i] === '--help') {
802
+ console.log(`
803
+ Edge-Net Genesis Node
804
+
805
+ Usage: node genesis.js [options]
806
+
807
+ Options:
808
+ --port <port> Port to listen on (default: 8787)
809
+ --data <dir> Data directory (default: ~/.ruvector/genesis)
810
+ --help Show this help
811
+
812
+ Environment Variables:
813
+ GENESIS_PORT Port (default: 8787)
814
+ GENESIS_HOST Host (default: 0.0.0.0)
815
+ GENESIS_DATA Data directory
816
+
817
+ Examples:
818
+ node genesis.js
819
+ node genesis.js --port 9000
820
+ node genesis.js --port 8787 --data /var/lib/edge-net
821
+ `);
822
+ process.exit(0);
823
+ }
824
+ }
825
+
826
+ const genesis = new GenesisNode({ port, dataDir });
827
+
828
+ // Handle shutdown
829
+ process.on('SIGINT', () => {
830
+ console.log('\n\nšŸ›‘ Shutting down Genesis Node...');
831
+ genesis.stop();
832
+ process.exit(0);
833
+ });
834
+
835
+ process.on('SIGTERM', () => {
836
+ genesis.stop();
837
+ process.exit(0);
838
+ });
839
+
840
+ // Start server
841
+ await genesis.start();
842
+
843
+ // Log stats periodically
844
+ setInterval(() => {
845
+ const stats = genesis.getStats();
846
+ console.log(`[Genesis] Peers: ${stats.totalPeers} | Connections: ${stats.activeConnections} | Signals: ${stats.signalsRelayed}`);
847
+ }, 60000);
848
+ }
849
+
850
+ // Run if executed directly
851
+ if (process.argv[1]?.endsWith('genesis.js')) {
852
+ main().catch(err => {
853
+ console.error('Genesis Node error:', err);
854
+ process.exit(1);
855
+ });
856
+ }
857
+
858
+ export default GenesisNode;