UNPKG

peerpigeon

Version:

WebRTC-based peer-to-peer mesh networking library with intelligent routing and signaling server

1,475 lines (1,264 loc) 51.5 kB
import { EventEmitter } from './EventEmitter.js'; import { SignalingClient } from './SignalingClient.js'; import { PeerDiscovery } from './PeerDiscovery.js'; import { ConnectionManager } from './ConnectionManager.js'; import { SignalingHandler } from './SignalingHandler.js'; import { EvictionManager } from './EvictionManager.js'; import { MeshOptimizer } from './MeshOptimizer.js'; import { CleanupManager } from './CleanupManager.js'; import { StorageManager } from './StorageManager.js'; import { GossipManager } from './GossipManager.js'; import { MediaManager } from './MediaManager.js'; import { WebDHT } from './WebDHT.js'; import { CryptoManager } from './CryptoManager.js'; import { DistributedStorageManager } from './DistributedStorageManager.js'; import { environmentDetector } from './EnvironmentDetector.js'; import DebugLogger from './DebugLogger.js'; export class PeerPigeonMesh extends EventEmitter { constructor(options = {}) { super(); this.debug = DebugLogger.create('PeerPigeonMesh'); // Validate environment capabilities this.environmentReport = this.validateEnvironment(options); this.peerId = null; this.providedPeerId = options.peerId || null; this.signalingClient = null; this.peerDiscovery = null; // Configuration this.maxPeers = 3; this.minPeers = 2; this.autoDiscovery = true; this.evictionStrategy = true; this.xorRouting = true; this.enableWebDHT = options.enableWebDHT !== false; // Default to true, can be disabled by setting to false this.enableCrypto = options.enableCrypto === true; // Default to false, must be explicitly enabled // State this.connected = false; this.polling = false; // Only WebSocket is supported this.signalingUrl = null; this.discoveredPeers = new Map(); this.connectionMonitorInterval = null; // Initialize managers this.storageManager = new StorageManager(this); this.mediaManager = new MediaManager(); this.connectionManager = new ConnectionManager(this); this.evictionManager = new EvictionManager(this, this.connectionManager); this.meshOptimizer = new MeshOptimizer(this, this.connectionManager, this.evictionManager); this.cleanupManager = new CleanupManager(this); this.signalingHandler = new SignalingHandler(this, this.connectionManager); this.gossipManager = new GossipManager(this, this.connectionManager); this.webDHT = null; // Will be initialized after peerId is set this.distributedStorage = null; // Will be initialized after WebDHT is set // Initialize crypto manager if enabled this.cryptoManager = null; if (this.enableCrypto) { this.cryptoManager = new CryptoManager(); } // Set up inter-module event forwarding this.setupManagerEventHandlers(); // Set up unload handlers this.cleanupManager.setupUnloadHandlers(); // Load saved signaling URL immediately this.storageManager.loadSignalingUrlFromStorage(); } setupManagerEventHandlers() { // Forward events from managers to main mesh this.connectionManager.addEventListener('peersUpdated', () => { this.emit('peersUpdated'); }); // Handle peer disconnections and trigger keep-alive ping coordination this.addEventListener('peerDisconnected', (data) => { this.debug.log(`Peer ${data.peerId.substring(0, 8)}... disconnected: ${data.reason}`); // Trigger immediate keep-alive ping check in case the disconnected peer was the designated sender if (this.signalingClient && this.signalingClient.connected) { setTimeout(() => { this.signalingClient.triggerKeepAlivePingCheck(); }, 1000); // Small delay to let peer lists update } }); // Handle gossip messages and intercept mesh signaling this.gossipManager.addEventListener('messageReceived', (data) => { // Check if this is a mesh signaling message if (data.message && data.message.type === 'mesh_signaling') { this._handleMeshSignalingMessage(data.message, data.from); return; // Don't emit as regular message } // Handle crypto key exchange messages if (data.message && (data.message.type === 'key_exchange' || data.message.type === 'key_exchange_response')) { this._handleKeyExchange(data.message, data.from); return; // Don't emit as regular message } this.emit('messageReceived', data); }); // CRITICAL: Handle remote stream announcements from gossip // When we hear about a stream from an indirectly connected peer, // establish a direct connection to receive the media this.addEventListener('remoteStreamAnnouncement', (data) => { this._handleRemoteStreamAnnouncement(data); }); // Forward media events this.mediaManager.addEventListener('localStreamStarted', (data) => { this.emit('localStreamStarted', data); // Gossip stream start announcement to all peers in the mesh this.gossipManager.broadcastMessage({ event: 'streamStarted', peerId: this.peerId, hasVideo: data.hasVideo, hasAudio: data.hasAudio, timestamp: Date.now() }, 'mediaEvent'); }); this.mediaManager.addEventListener('localStreamStopped', () => { this.emit('localStreamStopped'); // Gossip stream stop announcement to all peers in the mesh this.gossipManager.broadcastMessage({ event: 'streamStopped', peerId: this.peerId, timestamp: Date.now() }, 'mediaEvent'); }); this.mediaManager.addEventListener('error', (data) => { this.emit('mediaError', data); }); // Forward remote stream events from ConnectionManager this.connectionManager.addEventListener('remoteStream', (data) => { this.emit('remoteStream', data); }); // Handle crypto events if crypto is enabled if (this.cryptoManager) { this.cryptoManager.addEventListener('cryptoReady', (data) => { this.emit('cryptoReady', data); }); this.cryptoManager.addEventListener('cryptoError', (data) => { this.emit('cryptoError', data); }); this.cryptoManager.addEventListener('peerKeyAdded', (data) => { this.emit('peerKeyAdded', data); }); this.cryptoManager.addEventListener('userAuthenticated', (data) => { this.emit('userAuthenticated', data); }); } } validateEnvironment(options = {}) { const report = environmentDetector.getEnvironmentReport(); const warnings = []; const errors = []; // Log environment info this.debug.log('🔍 PeerPigeon Environment Detection:', { runtime: `${report.runtime.isBrowser ? 'Browser' : ''}${report.runtime.isNodeJS ? 'Node.js' : ''}${report.runtime.isWorker ? 'Worker' : ''}${report.runtime.isNativeScript ? 'NativeScript' : ''}`, webrtc: report.capabilities.webrtc, websocket: report.capabilities.webSocket, browser: report.browser?.name || 'N/A', nativescript: report.nativescript?.platform || 'N/A' }); // Check WebRTC support (required for peer connections) if (!report.capabilities.webrtc) { if (report.runtime.isBrowser) { errors.push('WebRTC is not supported in this browser. PeerPigeon requires WebRTC for peer-to-peer connections.'); } else if (report.runtime.isNodeJS) { warnings.push('WebRTC support not detected in Node.js environment. Consider using a WebRTC library like node-webrtc.'); } else if (report.runtime.isNativeScript) { warnings.push('WebRTC support not detected in NativeScript environment. Consider using a native WebRTC plugin.'); } } // Check WebSocket support (required for signaling) if (!report.capabilities.webSocket) { if (report.runtime.isBrowser) { errors.push('WebSocket is not supported in this browser. PeerPigeon requires WebSocket for signaling.'); } else if (report.runtime.isNodeJS) { warnings.push('WebSocket support not detected. Install the "ws" package for WebSocket support in Node.js.'); } else if (report.runtime.isNativeScript) { warnings.push('WebSocket support not detected. Consider using a native WebSocket plugin or polyfill.'); } } // Check storage capabilities for persistent peer ID if ((report.runtime.isBrowser || report.runtime.isNativeScript) && !report.capabilities.localStorage && !report.capabilities.sessionStorage) { warnings.push('No storage mechanism available. Peer ID will not persist between sessions.'); } // Check crypto support for secure peer ID generation if (!report.capabilities.randomValues) { warnings.push('Crypto random values not available. Peer ID generation may be less secure.'); } // Network connectivity checks if (report.runtime.isBrowser && !report.network.online) { warnings.push('Browser reports offline status. Mesh networking may not function properly.'); } // Environment-specific warnings if (report.runtime.isBrowser) { // Browser-specific checks const browser = report.browser; if (browser && browser.name === 'ie') { errors.push('Internet Explorer is not supported. Please use a modern browser.'); } // Check for secure context in production if (typeof location !== 'undefined' && location.protocol === 'http:' && location.hostname !== 'localhost') { warnings.push('Running on HTTP in production. Some WebRTC features may be limited. Consider using HTTPS.'); } } if (report.runtime.isNativeScript) { // NativeScript-specific checks const nativeScript = report.nativescript; if (nativeScript && nativeScript.platform) { this.debug.log(`🔮 Running on NativeScript ${nativeScript.platform} platform`); // Platform-specific considerations if (nativeScript.platform === 'android') { warnings.push('Android WebRTC may require network permissions and appropriate security configurations.'); } else if (nativeScript.platform === 'ios' || nativeScript.platform === 'visionos') { warnings.push('iOS/visionOS WebRTC may require camera/microphone permissions for media features.'); } } } // Handle errors and warnings if (errors.length > 0) { const errorMessage = 'PeerPigeon environment validation failed:\n' + errors.join('\n'); this.debug.error(errorMessage); if (!options.ignoreEnvironmentErrors) { throw new Error(errorMessage); } } if (warnings.length > 0) { this.debug.warn('PeerPigeon environment warnings:\n' + warnings.join('\n')); } // Store capabilities for runtime checks this.capabilities = report.capabilities; this.runtimeInfo = report.runtime; return report; } async init() { try { // Use provided peer ID if valid, otherwise generate one if (this.providedPeerId) { if (this.storageManager.validatePeerId(this.providedPeerId)) { this.peerId = this.providedPeerId; this.debug.log(`Using provided peer ID: ${this.peerId}`); } else { this.debug.warn(`Invalid peer ID provided: ${this.providedPeerId}. Must be 40-character SHA-1 hex string. Generating new one.`); this.peerId = await this.storageManager.generatePeerId(); } } else { this.peerId = await this.storageManager.generatePeerId(); } // Initialize WebDHT now that we have a peerId (if enabled) if (this.enableWebDHT) { this.webDHT = new WebDHT(this); this.debug.log('WebDHT initialized and enabled'); // Setup WebDHT event handlers now that it's initialized this.setupWebDHTEventHandlers(); // Initialize DistributedStorageManager if WebDHT is enabled this.distributedStorage = new DistributedStorageManager(this); this.debug.log('DistributedStorageManager initialized'); // Setup DistributedStorageManager event handlers this.setupDistributedStorageEventHandlers(); } else { this.debug.log('WebDHT disabled by configuration'); } // Load signaling URL from query params or storage const savedUrl = this.storageManager.loadSignalingUrlFromQuery(); if (savedUrl) { this.signalingUrl = savedUrl; } this.signalingClient = new SignalingClient(this.peerId, this.maxPeers, this); this.setupSignalingHandlers(); this.peerDiscovery = new PeerDiscovery(this.peerId, { autoDiscovery: this.autoDiscovery, evictionStrategy: this.evictionStrategy, xorRouting: this.xorRouting, minPeers: this.minPeers, maxPeers: this.maxPeers }); this.setupDiscoveryHandlers(); // Initialize crypto manager if enabled if (this.cryptoManager) { try { this.debug.log('🔐 Initializing crypto manager...'); await this.cryptoManager.init({ generateKeypair: true }); this.debug.log('🔐 Crypto manager initialized successfully'); } catch (error) { this.debug.error('Failed to initialize crypto manager:', error); // Continue without crypto - don't fail the entire init this.enableCrypto = false; this.cryptoManager = null; } } this.emit('statusChanged', { type: 'initialized', peerId: this.peerId }); } catch (error) { this.debug.error('Failed to initialize mesh:', error); this.emit('statusChanged', { type: 'error', message: `Initialization failed: ${error.message}` }); throw error; } } setupSignalingHandlers() { this.signalingClient.addEventListener('connected', () => { this.connected = true; this.polling = false; this.peerDiscovery.start(); // Start periodic health monitoring this.startConnectionMonitoring(); this.emit('statusChanged', { type: 'connected' }); }); this.signalingClient.addEventListener('disconnected', () => { this.connected = false; this.polling = false; this.peerDiscovery.stop(); this.connectionManager.disconnectAllPeers(); this.stopConnectionMonitoring(); this.emit('statusChanged', { type: 'disconnected' }); }); this.signalingClient.addEventListener('signalingMessage', (message) => { this.signalingHandler.handleSignalingMessage(message); }); this.signalingClient.addEventListener('statusChanged', (data) => { this.emit('statusChanged', data); }); } setupDiscoveryHandlers() { this.peerDiscovery.addEventListener('peerDiscovered', (data) => { this.emit('peerDiscovered', data); }); this.peerDiscovery.addEventListener('connectToPeer', (data) => { this.debug.log(`PeerDiscovery requested connection to: ${data.peerId.substring(0, 8)}...`); this.connectionManager.connectToPeer(data.peerId); }); this.peerDiscovery.addEventListener('evictPeer', (data) => { this.evictionManager.evictPeer(data.peerId, data.reason); }); this.peerDiscovery.addEventListener('optimizeMesh', () => { this.peerDiscovery.optimizeMeshConnections(this.connectionManager.peers); }); this.peerDiscovery.addEventListener('optimizeConnections', (data) => { this.meshOptimizer.handleOptimizeConnections(data.unconnectedPeers); }); this.peerDiscovery.addEventListener('peersUpdated', (data) => { this.emit('statusChanged', { type: 'info', message: `Cleaned up ${data.removedCount} stale peer(s)` }); this.emit('peersUpdated'); }); // Handle capacity checks this.peerDiscovery.addEventListener('checkCapacity', () => { const canAccept = this.connectionManager.canAcceptMorePeers(); const currentConnectionCount = this.connectionManager.getConnectedPeerCount(); this.debug.log(`Capacity check: ${canAccept} (${currentConnectionCount}/${this.maxPeers} peers)`); this.peerDiscovery._canAcceptMorePeers = canAccept; this.peerDiscovery._currentConnectionCount = currentConnectionCount; }); // Handle eviction checks this.peerDiscovery.addEventListener('checkEviction', (data) => { const evictPeerId = this.evictionManager.shouldEvictForPeer(data.newPeerId); this.debug.log(`Eviction check for ${data.newPeerId.substring(0, 8)}...: ${evictPeerId ? evictPeerId.substring(0, 8) + '...' : 'none'}`); this.peerDiscovery._shouldEvictForPeer = evictPeerId; }); } async connect(signalingUrl) { this.signalingUrl = signalingUrl; this.storageManager.saveSignalingUrlToStorage(signalingUrl); this.polling = false; // Only WebSocket is supported // Don't emit connecting here - SignalingClient will handle it with more detail try { await this.signalingClient.connect(signalingUrl); } catch (error) { this.debug.error('Connection failed:', error); this.polling = false; this.emit('statusChanged', { type: 'error', message: `Connection failed: ${error.message}` }); throw error; } } disconnect() { if (this.connected) { this.cleanupManager.sendGoodbyeMessage(); } this.connected = false; this.polling = false; // Stop connection monitoring this.stopConnectionMonitoring(); if (this.signalingClient) { this.signalingClient.disconnect(); } if (this.peerDiscovery) { this.peerDiscovery.stop(); } this.connectionManager.disconnectAllPeers(); this.connectionManager.cleanup(); // Cleanup WebDHT if (this.webDHT) { this.webDHT.cleanup(); } this.evictionManager.cleanup(); this.cleanupManager.cleanup(); this.gossipManager.cleanup(); this.emit('statusChanged', { type: 'disconnected' }); } // Configuration methods setMaxPeers(maxPeers) { this.maxPeers = Math.max(1, Math.min(50, maxPeers)); if (this.connectionManager.peers.size > this.maxPeers) { this.evictionManager.disconnectExcessPeers(); } return this.maxPeers; } setMinPeers(minPeers) { this.minPeers = Math.max(0, Math.min(49, minPeers)); // If we're below minimum and auto-discovery is enabled, trigger optimization if (this.connectionManager.getConnectedPeerCount() < this.minPeers && this.autoDiscovery && this.connected) { this.peerDiscovery.optimizeMeshConnections(this.connectionManager.peers); } return this.minPeers; } setXorRouting(enabled) { this.xorRouting = enabled; this.emit('statusChanged', { type: 'setting', setting: 'xorRouting', value: enabled }); // If XOR routing is disabled, we might need to adjust our connection strategy if (!enabled && this.evictionStrategy) { this.emit('statusChanged', { type: 'warning', message: 'XOR routing disabled - eviction strategy effectiveness reduced' }); } } setAutoDiscovery(enabled) { this.autoDiscovery = enabled; this.emit('statusChanged', { type: 'setting', setting: 'autoDiscovery', value: enabled }); } setEvictionStrategy(enabled) { this.evictionStrategy = enabled; this.emit('statusChanged', { type: 'setting', setting: 'evictionStrategy', value: enabled }); } // Status and information methods getStatus() { const connectedCount = this.connectionManager.getConnectedPeerCount(); const totalCount = this.connectionManager.peers.size; return { peerId: this.peerId, connected: this.connected, polling: false, // Only WebSocket is supported connectedCount, totalPeerCount: totalCount, // Include total count for debugging minPeers: this.minPeers, maxPeers: this.maxPeers, discoveredCount: this.discoveredPeers.size, autoDiscovery: this.autoDiscovery, evictionStrategy: this.evictionStrategy, xorRouting: this.xorRouting }; } getPeers() { return this.connectionManager.getPeers(); } getPeerStatus(peerConnection) { return peerConnection.getStatus(); } getDiscoveredPeers() { if (!this.peerDiscovery) { return []; } const discoveredPeers = this.peerDiscovery.getDiscoveredPeers(); // Enrich with connection state from the actual peer connections return discoveredPeers.map(peer => { const peerConnection = this.connectionManager.getPeer(peer.peerId); let isConnected = false; if (peerConnection) { const status = peerConnection.getStatus(); // Consider peer connected if WebRTC connection is established isConnected = status === 'connected' || status === 'channel-connecting'; } return { ...peer, isConnected }; }); } /** * Send a direct message to a specific peer via gossip routing * @param {string} targetPeerId - The destination peer's ID * @param {string|object} content - The message content * @returns {string|null} The message ID if sent, or null on error */ sendDirectMessage(targetPeerId, content) { if (!targetPeerId || typeof targetPeerId !== 'string') { this.debug.error('Invalid targetPeerId for direct message'); return null; } return this.gossipManager.sendDirectMessage(targetPeerId, content); } /** * Send a broadcast (gossip) message to all peers * @param {string|object} content - The message content * @returns {string|null} The message ID if sent, or null on error */ sendMessage(content) { // For clarity, this is a broadcast/gossip message return this.gossipManager.broadcastMessage(content, 'chat'); } // Helper methods for backward compatibility canAcceptMorePeers() { return this.connectionManager.canAcceptMorePeers(); } getConnectedPeerCount() { return this.connectionManager.getConnectedPeerCount(); } // Expose peers Map for backward compatibility get peers() { return this.connectionManager.peers; } // Get peer status method for UI compatibility getPeerUIStatus(peer) { if (!peer) return 'unknown'; return peer.getStatus ? peer.getStatus() : 'unknown'; } // Get connected peer IDs as array for UI compatibility getConnectedPeerIds() { return this.connectionManager.getPeers() .filter(peer => peer.status === 'connected') .map(peer => peer.peerId); } // Advanced methods async cleanupStaleSignalingData() { return this.cleanupManager.cleanupStaleSignalingData(); } forceConnectToAllPeers() { return this.meshOptimizer.forceConnectToAllPeers(); } // Debugging and maintenance methods forceCleanupInvalidPeers() { this.debug.log('Force cleaning up peers not in connected state...'); return this.connectionManager.forceCleanupInvalidPeers(); } cleanupStalePeers() { this.debug.log('Manually cleaning up stale peers...'); return this.connectionManager.cleanupStalePeers(); } getPeerStateSummary() { return this.connectionManager.getPeerStateSummary(); } debugConnectivity() { return this.meshOptimizer.debugConnectivity(); } // Connection monitoring methods startConnectionMonitoring() { // Monitor connection health every 2 minutes if (this.connectionMonitorInterval) { clearInterval(this.connectionMonitorInterval); } // Environment-aware timer const intervalCallback = () => { if (this.signalingClient) { const stats = this.signalingClient.getConnectionStats(); this.debug.log('Connection monitoring:', stats); // Force health check if connection seems problematic if (stats.connected && stats.lastPingTime && stats.lastPongTime) { const timeSinceLastPong = Date.now() - stats.lastPongTime; if (timeSinceLastPong > 60000) { // 1 minute without pong this.debug.warn('Connection may be unhealthy, forcing health check'); this.signalingClient.forceHealthCheck(); } } // Emit connection status for UI this.emit('connectionStats', stats); } }; if (environmentDetector.isBrowser) { this.connectionMonitorInterval = window.setInterval(intervalCallback, 120000); } else { this.connectionMonitorInterval = setInterval(intervalCallback, 120000); } this.debug.log('Started connection monitoring'); } stopConnectionMonitoring() { if (this.connectionMonitorInterval) { clearInterval(this.connectionMonitorInterval); this.connectionMonitorInterval = null; this.debug.log('Stopped connection monitoring'); } } // Media management methods async initializeMedia() { return await this.mediaManager.init(); } async startMedia(options = {}) { const { video = false, audio = false, deviceIds = {} } = options; try { const stream = await this.mediaManager.startLocalStream({ video, audio, deviceIds }); // Update all existing peer connections with the new stream const connections = this.connectionManager.getAllConnections(); this.debug.log(`📡 MEDIA START: Applying stream to ${connections.length} connections`); for (const connection of connections) { this.debug.log(`📡 MEDIA START: Setting stream for peer ${connection.peerId.substring(0, 8)}...`); await connection.setLocalStream(stream); // FORCE IMMEDIATE RENEGOTIATION: Don't wait for the automatic trigger this.debug.log(`📡 MEDIA START: Forcing immediate renegotiation for peer ${connection.peerId.substring(0, 8)}...`); setTimeout(async () => { try { if (connection.connection && connection.connection.signalingState === 'stable') { // Create and send a renegotiation offer immediately const offer = await connection.createOffer(); await this.sendSignalingMessage({ type: 'renegotiation-offer', data: offer, timestamp: Date.now(), forced: true // Mark as forced renegotiation }, connection.peerId); this.debug.log(`✅ MEDIA START: Forced renegotiation offer sent to ${connection.peerId.substring(0, 8)}...`); } } catch (error) { this.debug.error(`❌ MEDIA START: Failed to force renegotiation for ${connection.peerId.substring(0, 8)}...`, error); } }, 500); // Small delay to ensure setLocalStream completes } return stream; } catch (error) { this.debug.error('Failed to start media:', error); throw error; } } async stopMedia() { this.mediaManager.stopLocalStream(); // Update all existing peer connections to remove the stream const connections = this.connectionManager.getAllConnections(); for (const connection of connections) { await connection.setLocalStream(null); } } toggleVideo() { return this.mediaManager.toggleVideo(); } toggleAudio() { return this.mediaManager.toggleAudio(); } getMediaState() { return this.mediaManager.getMediaState(); } getMediaDevices() { return this.mediaManager.devices; } async enumerateMediaDevices() { return await this.mediaManager.enumerateDevices(); } getLocalStream() { return this.mediaManager.localStream; } // Get remote streams from all connected peers getRemoteStreams() { const streams = new Map(); const connections = this.connectionManager.getAllConnections(); for (const connection of connections) { const remoteStream = connection.getRemoteStream(); if (remoteStream) { streams.set(connection.peerId, remoteStream); } } return streams; } // Static utility methods static validatePeerId(peerId) { return typeof peerId === 'string' && /^[a-fA-F0-9]{40}$/.test(peerId); } static async generatePeerId() { const array = new Uint8Array(20); crypto.getRandomValues(array); return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); } // WebDHT methods - Distributed Hash Table /** * Store a key-value pair in the distributed hash table * @param {string} key - The key to store * @param {any} value - The value to store * @param {object} options - Storage options (ttl, etc.) * @returns {Promise<boolean>} True if stored successfully */ async dhtPut(key, value, options = {}) { if (!this.enableWebDHT) { throw new Error('WebDHT is disabled. Enable it by setting enableWebDHT: true in constructor options.'); } if (!this.webDHT) { throw new Error('WebDHT not initialized'); } return this.webDHT.put(key, value, options); } /** * Retrieve a value from the distributed hash table * @param {string} key - The key to retrieve * @param {object} options - Retrieval options (subscribe, etc.) * @returns {Promise<any>} The stored value or null if not found */ async dhtGet(key, options = {}) { if (!this.enableWebDHT) { throw new Error('WebDHT is disabled. Enable it by setting enableWebDHT: true in constructor options.'); } if (!this.webDHT) { throw new Error('WebDHT not initialized'); } return this.webDHT.get(key, options); } /** * Subscribe to changes for a key in the DHT * @param {string} key - The key to subscribe to * @returns {Promise<any>} The current value */ async dhtSubscribe(key) { if (!this.enableWebDHT) { throw new Error('WebDHT is disabled. Enable it by setting enableWebDHT: true in constructor options.'); } if (!this.webDHT) { throw new Error('WebDHT not initialized'); } return this.webDHT.subscribe(key); } /** * Unsubscribe from changes for a key in the DHT * @param {string} key - The key to unsubscribe from */ async dhtUnsubscribe(key) { if (!this.enableWebDHT) { throw new Error('WebDHT is disabled. Enable it by setting enableWebDHT: true in constructor options.'); } if (!this.webDHT) { throw new Error('WebDHT not initialized'); } return this.webDHT.unsubscribe(key); } /** * Update a key's value and notify subscribers * @param {string} key - The key to update * @param {any} newValue - The new value * @param {object} options - Update options * @returns {Promise<boolean>} True if updated successfully */ async dhtUpdate(key, newValue, options = {}) { if (!this.enableWebDHT) { throw new Error('WebDHT is disabled. Enable it by setting enableWebDHT: true in constructor options.'); } if (!this.webDHT) { throw new Error('WebDHT not initialized'); } return this.webDHT.update(key, newValue, options); } /** * Get DHT statistics * @returns {object} DHT statistics */ getDHTStats() { if (!this.enableWebDHT) { return { error: 'WebDHT is disabled. Enable it by setting enableWebDHT: true in constructor options.' }; } if (!this.webDHT) { return { error: 'WebDHT not initialized' }; } return this.webDHT.getStats(); } /** * Check if WebDHT is enabled * @returns {boolean} True if WebDHT is enabled */ isDHTEnabled() { return this.enableWebDHT; } /** * Setup WebDHT event handlers */ setupWebDHTEventHandlers() { // Only set up if webDHT exists if (this.webDHT) { this.webDHT.addEventListener('valueChanged', (data) => { this.emit('dhtValueChanged', data); }); } } /** * Setup event handlers for DistributedStorageManager */ setupDistributedStorageEventHandlers() { // Only set up if distributedStorage exists if (this.distributedStorage) { this.distributedStorage.addEventListener('dataStored', (data) => { this.emit('storageDataStored', data); }); this.distributedStorage.addEventListener('dataRetrieved', (data) => { this.emit('storageDataRetrieved', data); }); this.distributedStorage.addEventListener('dataUpdated', (data) => { this.emit('storageDataUpdated', data); }); this.distributedStorage.addEventListener('dataDeleted', (data) => { this.emit('storageDataDeleted', data); }); this.distributedStorage.addEventListener('accessGranted', (data) => { this.emit('storageAccessGranted', data); }); this.distributedStorage.addEventListener('accessRevoked', (data) => { this.emit('storageAccessRevoked', data); }); } } /** * Connect to a specific peer by ID */ connectToPeer(peerId) { if (!peerId || typeof peerId !== 'string') { throw new Error('Valid peer ID is required'); } if (peerId === this.peerId) { throw new Error('Cannot connect to yourself'); } return this.connectionManager.connectToPeer(peerId); } /** * Get the current environment report * @returns {object} Complete environment detection report */ getEnvironmentReport() { return this.environmentReport; } /** * Get runtime capabilities * @returns {object} Capabilities detected during initialization */ getCapabilities() { return this.capabilities; } /** * Get runtime information * @returns {object} Runtime environment information */ getRuntimeInfo() { return this.runtimeInfo; } /** * Check if a specific feature is supported * @param {string} feature - The feature to check (e.g., 'webrtc', 'websocket', 'localstorage') * @returns {boolean} True if the feature is supported */ hasFeature(feature) { return environmentDetector.hasFeature(feature); } /** * Get environment-specific recommendations * @returns {object} Recommendations based on current environment */ getEnvironmentRecommendations() { const recommendations = []; const report = this.environmentReport; if (report.runtime.isBrowser) { if (!report.network.online) { recommendations.push({ type: 'warning', message: 'Browser is offline. Enable network connectivity for mesh functionality.' }); } if (typeof location !== 'undefined' && location.protocol === 'http:' && location.hostname !== 'localhost') { recommendations.push({ type: 'security', message: 'Consider using HTTPS for better WebRTC compatibility and security.' }); } if (report.browser && report.browser.name === 'safari') { recommendations.push({ type: 'compatibility', message: 'Safari has some WebRTC limitations. Test thoroughly for production use.' }); } } if (report.runtime.isNodeJS) { if (!report.capabilities.webSocket) { recommendations.push({ type: 'dependency', message: 'Install the "ws" package for WebSocket support: npm install ws' }); } if (!report.capabilities.webrtc) { recommendations.push({ type: 'dependency', message: 'Install "node-webrtc" or similar for WebRTC support in Node.js: npm install node-webrtc' }); } } if (!report.capabilities.localStorage && !report.capabilities.sessionStorage) { recommendations.push({ type: 'feature', message: 'No persistent storage available. Peer ID will change on restart.' }); } return { environment: report.runtime, recommendations }; } // ============================================ // Cryptographic Methods (unsea integration) // ============================================ /** * Initialize crypto with user credentials * @param {Object} options - Crypto initialization options * @param {string} options.alias - User alias for persistent identity * @param {string} options.password - User password * @returns {Promise<boolean>} True if crypto was initialized successfully */ async initCrypto(options = {}) { if (!this.enableCrypto) { this.enableCrypto = true; this.cryptoManager = new CryptoManager(); } if (!this.cryptoManager) { throw new Error('Crypto manager not available'); } try { await this.cryptoManager.init(options); return true; } catch (error) { this.debug.error('Failed to initialize crypto:', error); throw error; } } /** * Get crypto status and information * @returns {Object} Crypto status information */ getCryptoStatus() { if (!this.cryptoManager) { return { enabled: false, initialized: false, error: 'Crypto not enabled. Enable with enableCrypto: true in constructor' }; } return this.cryptoManager.getStatus(); } /** * Enable/disable crypto functionality * @param {boolean} enabled - Whether to enable crypto */ setCrypto(enabled) { if (enabled && !this.cryptoManager) { this.cryptoManager = new CryptoManager(); } this.enableCrypto = enabled; } /** * Encrypt a message without sending it * @param {any} content - Message content to encrypt * @param {string} peerId - Optional target peer ID for peer-to-peer encryption * @param {string} groupId - Optional group ID for group encryption * @returns {Promise<Object>} Encrypted message object */ async encryptMessage(content, peerId = null, groupId = null) { if (!this.enableCrypto || !this.cryptoManager) { throw new Error('Crypto not enabled or initialized'); } try { if (groupId) { return await this.cryptoManager.encryptForGroup(content, groupId); } else if (peerId) { return await this.cryptoManager.encryptForPeer(content, peerId); } else { // For benchmark/testing purposes, encrypt with our own public key const ourPeerId = this.peerId; const ourKeypair = this.cryptoManager.keypair; if (ourKeypair && ourKeypair.pub && ourKeypair.epub) { // Add our own key temporarily for testing (include both pub and epub) this.cryptoManager.addPeerKey(ourPeerId, { pub: ourKeypair.pub, epub: ourKeypair.epub }); return await this.cryptoManager.encryptForPeer(content, ourPeerId); } else { throw new Error('No complete keypair available for encryption (need both pub and epub)'); } } } catch (error) { this.debug.error('Failed to encrypt message:', error); throw error; } } /** * Send a signaling message over the mesh (peer-to-peer) instead of the signaling server * This allows peers to coordinate without the signaling server after initial connection * @param {Object} message - The signaling message * @param {string} targetPeerId - Target peer ID (optional, for direct signaling) * @returns {Promise<boolean>} Success status */ async sendMeshSignalingMessage(message, targetPeerId = null) { if (!this.connected) { this.debug.warn('Cannot send mesh signaling message - mesh not connected'); return false; } const signalingMessage = { type: 'mesh_signaling', meshSignalingType: message.type, data: message.data, fromPeerId: this.peerId, targetPeerId, timestamp: Date.now(), messageId: this.generateMessageId() }; this.debug.log(`📡 Sending mesh signaling message: ${message.type} ${targetPeerId ? `to ${targetPeerId.substring(0, 8)}...` : '(broadcast)'}`); try { if (targetPeerId) { // Send directly to target peer return this.sendDirectMessage(targetPeerId, signalingMessage); } else { // Broadcast to all connected peers return this.broadcast(signalingMessage); } } catch (error) { this.debug.error('Failed to send mesh signaling message:', error); return false; } } /** * Handle incoming mesh signaling messages * @param {Object} message - The mesh signaling message * @param {string} from - Sender peer ID * @private */ _handleMeshSignalingMessage(message, from) { if (!message.meshSignalingType || !message.data) { this.debug.warn('Invalid mesh signaling message format'); return; } // Only process messages intended for us or broadcasts if (message.targetPeerId && message.targetPeerId !== this.peerId) { this.debug.log(`Ignoring mesh signaling message not intended for us (target: ${message.targetPeerId?.substring(0, 8)}...)`); return; } this.debug.log(`📡 Received mesh signaling message: ${message.meshSignalingType} from ${from.substring(0, 8)}...`); // Create a signaling message format that our existing handler can process const reconstitutedMessage = { type: message.meshSignalingType, data: message.data, fromPeerId: from, targetPeerId: message.targetPeerId, timestamp: message.timestamp, messageId: message.messageId, viaWebSocket: false, // Mark as coming from mesh, not WebSocket viaMesh: true }; // Forward to our existing signaling handler this.signalingHandler.handleSignalingMessage(reconstitutedMessage); } /** * Send a signaling message, using mesh connections for renegotiation * @param {Object} message - The signaling message * @param {string} targetPeerId - Target peer ID (optional) * @returns {Promise<boolean>} Success status */ async sendSignalingMessage(message, targetPeerId = null) { // CRITICAL FIX: Use mesh for renegotiation, WebSocket only for initial handshake const isRenegotiation = message.type === 'renegotiation-offer' || message.type === 'renegotiation-answer'; if (isRenegotiation && targetPeerId) { // Use existing mesh connection for renegotiation this.debug.log(`🔄 MESH RENEGOTIATION: Sending ${message.type} via mesh to ${targetPeerId.substring(0, 8)}...`); const peerConnection = this.connectionManager.getPeer(targetPeerId); if (peerConnection && peerConnection.sendMessage) { const success = peerConnection.sendMessage({ type: 'signaling', data: message, fromPeerId: this.peerId, timestamp: Date.now() }); if (success) { this.debug.log(`✅ MESH RENEGOTIATION: ${message.type} sent via mesh to ${targetPeerId.substring(0, 8)}...`); return true; } else { this.debug.error(`❌ MESH RENEGOTIATION: Failed to send ${message.type} via mesh to ${targetPeerId.substring(0, 8)}...`); } } else { this.debug.error(`❌ MESH RENEGOTIATION: No mesh connection to ${targetPeerId.substring(0, 8)}... for ${message.type}`); } // Fall back to WebSocket if mesh fails this.debug.log(`🔄 FALLBACK: Using WebSocket for ${message.type} to ${targetPeerId.substring(0, 8)}...`); } // Use WebSocket for initial offers/answers and fallback if (this.signalingClient && this.signalingClient.isConnected()) { this.debug.log(`📡 Using WebSocket signaling for ${message.type} to ${targetPeerId?.substring(0, 8) || 'broadcast'}`); // Include targetPeerId in the message if provided const messageWithTarget = { ...message }; if (targetPeerId) { messageWithTarget.targetPeerId = targetPeerId; } return await this.signalingClient.sendSignalingMessage(messageWithTarget); } this.debug.warn(`📡 Cannot send signaling message ${message.type} - WebSocket not connected and mesh failed`); return false; } /** * Send an encrypted message to a specific peer * @param {string} peerId - Target peer ID * @param {any} content - Message content * @param {Object} options - Message options * @returns {Promise<string|null>} Message ID if sent successfully */ async sendEncryptedMessage(peerId, content, _options = {}) { if (!this.enableCrypto || !this.cryptoManager) { throw new Error('Crypto not enabled or initialized'); } try { const encryptedContent = await this.cryptoManager.encryptForPeer(content, peerId); return this.sendDirectMessage(peerId, encryptedContent); } catch (error) { this.debug.error('Failed to send encrypted message:', error); throw error; } } /** * Send an encrypted broadcast message * @param {any} content - Message content * @param {string} groupId - Optional group ID for group encryption * @returns {Promise<string|null>} Message ID if sent successfully */ async sendEncryptedBroadcast(content, groupId = null) { if (!this.enableCrypto || !this.cryptoManager) { throw new Error('Crypto not enabled or initialized'); } try { let encryptedContent; if (groupId) { encryptedContent = await this.cryptoManager.encryptForGroup(content, groupId); } else { // For broadcast without group, we'll need to encrypt for each peer individually // This is a simplified approach - in practice, you'd use group keys encryptedContent = { encrypted: true, broadcast: true, data: content, from: this.cryptoManager.getPublicKey(), timestamp: Date.now() }; } // Use 'encrypted' message type instead of 'chat' for encrypted broadcasts return this.gossipManager.broadcastMessage(encryptedContent, 'encrypted'); } catch (error) { this.debug.error('Failed to send encrypted broadcast:', error); throw error; } } /** * Decrypt a received message * @param {Object} encryptedData - Encrypted message data * @returns {Promise<any>} Decrypted content */ async decryptMessage(encryptedData) { if (!this.enableCrypto || !this.cryptoManager) { return encryptedData; // Return as-is if crypto not enabled } try { if (encryptedData.group) { return await this.cryptoManager.decryptFromGroup(encryptedData); } else { return await this.cryptoManager.decryptFromPeer(encryptedData); } } catch (error) { this.debug.error('Failed to decrypt message:', error); throw error; } } /** * Exchange public keys with a peer * @param {string} peerId - Peer ID to exchange keys with */ async exchangeKeysWithPeer(peerId) { if (!this.enableCrypto || !this.cryptoManager) { throw new Error('Crypto not enabled or initialized'); } const keypair = this.cryptoManager.keypair; if (!keypair || !keypair.pub || !keypair.epub) { throw new Error('No complete keypair available (need both pub and epub)'); } // Send both our public key (pub) and encryption public key (epub) to the peer return this.gossipManager.sendDirectMessage(peerId, { type: 'key_exchange', publicKey: { pub: keypair.pub, epub: keypair.epub }, timestamp: Date.now() }, 'key_exchange'); } /** * Add a peer's public key * @param {string} peerId - Peer ID * @param {string} publicKey - Peer's public key * @returns {boolean} True if key was added successfully */ addPeerPublicKey(peerId, publicKey) { if (!this.enableCrypto || !this.cryptoManager) { return false; } return this.cryptoManager.addPeerKey(peerId, publicKey); } /** * Generate a group key for encrypted group communications * @param {string} groupId - Group identifier * @returns {Promise<Object>} Generated group key */ async generateGroupKey(groupId) { if (!this.enableCrypto || !this.cryptoManager) { throw new Error('Crypto not enabled or initialized'); } return await this.cryptoManager.generateGroupKey(groupId); } /** * Add a group key for encrypted group communications * @param {string} groupId - Group identifier * @param {Object} groupKey - Group key object */ addGroupKey(groupId, groupKey) { if (!this.enableCrypto || !this.cryptoManager) { throw new Error('Crypto not enabled or initialized'); } this.cryptoManager.addGroupKey(groupId, groupKey); } /** * Sign data with our private key * @param {any} data - Data to sign * @returns {Promise<string>} Digital signature */ async signData(data) { if (!this.enableCrypto || !this.cryptoManager) { throw new Error('Crypto not enabled or initialized'); } return await this.cryptoManager.sign(data); } /** * Verify a signature * @param {string} signature - Signature to verify * @param {any} data - Original data * @param {string} publicKey - Signer's public key * @returns {Promise<boolean>} True if signature is valid */ async verifySignature(signature, data, publicKey) { if (!this.enableCrypto || !this.cryptoManager) { return true; // If crypto disabled, assume valid } return await this.cryptoManager.verify(signature, data, publicKey); } /** * Export our public key for sharing * @returns {Object|null} Public key export data */ exportPublicKey() { if (!this.enableCrypto || !this.cryptoManager) { return null; } return this.cryptoManager.exportPublicKey(); } /** * Run crypto self-tests * @returns {Promise<Object>} Test results */ async runCryptoTests() { if (!this.enableCrypto || !this.cryptoManager) { throw new Error('Crypto not enabled or initialized'); } return await this.cryptoManager.runSelfTest(); } /** * Reset crypto state and clear all keys */ resetCrypto() { if (this.cryptoManager) { this.cryptoManager.reset(); } } /** * Handle incoming key exchange messages * @param {Object} data - Key exchange message data * @param {string} from - Sender's peer ID * @private */ _handleKeyExchange(data, from) { if ((data.type === 'key_exchange' || da