@ruvector/edge-net 0.4.0 → 0.4.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.
@@ -4,9 +4,15 @@
4
4
  * Uses Google Firebase as bootstrap infrastructure for WebRTC signaling
5
5
  * with migration path to full P2P DHT network.
6
6
  *
7
+ * Security Model (WASM-based, no Firebase Auth needed):
8
+ * 1. Each node generates cryptographic identity in WASM (PiKey)
9
+ * 2. All messages are signed with Ed25519 keys
10
+ * 3. Peers verify signatures before accepting connections
11
+ * 4. AdaptiveSecurity provides self-learning attack detection
12
+ *
7
13
  * Architecture:
8
14
  * 1. Firebase Firestore for signaling (offer/answer/ICE)
9
- * 2. Firebase Realtime DB for presence (who's online)
15
+ * 2. WASM cryptographic identity (no Firebase Auth)
10
16
  * 3. Gradual migration to DHT as network grows
11
17
  *
12
18
  * @module @ruvector/edge-net/firebase-signaling
@@ -19,23 +25,32 @@ import { EventEmitter } from 'events';
19
25
  // ============================================
20
26
 
21
27
  /**
22
- * Get Firebase config from environment variables or saved config
28
+ * Edge-Net Public Firebase Configuration
23
29
  *
24
- * Configuration sources (in order of priority):
25
- * 1. Environment variables: FIREBASE_API_KEY, FIREBASE_PROJECT_ID
26
- * 2. Saved config from: ~/.edge-net/firebase.json (via firebase-setup.js)
27
- * 3. Application Default Credentials (for server-side via gcloud)
30
+ * This is the PUBLIC Firebase project for edge-net P2P network.
31
+ * API keys for Firebase web apps are designed to be public - security is via:
32
+ * 1. Firestore Security Rules (only authenticated users can write)
33
+ * 2. Anonymous Authentication (anyone can join, tracked by UID)
34
+ * 3. API restrictions in Google Cloud Console
28
35
  *
29
- * SECURITY:
30
- * - Never hardcode API keys
31
- * - Use `gcloud auth application-default login` for server-side
32
- * - Restrict API keys by domain in Google Cloud Console for browser-side
36
+ * Contributors automatically join the network - no setup required!
37
+ */
38
+ export const EDGE_NET_FIREBASE_CONFIG = {
39
+ apiKey: "AIzaSyAZAJhathdnKZGzBQ8iDBFG8_OQsvb2QvA",
40
+ projectId: "ruv-dev",
41
+ authDomain: "ruv-dev.firebaseapp.com",
42
+ storageBucket: "ruv-dev.appspot.com",
43
+ };
44
+
45
+ /**
46
+ * Get Firebase config
33
47
  *
34
- * Setup:
35
- * npx edge-net-firebase-setup --project YOUR_PROJECT_ID
48
+ * Priority:
49
+ * 1. Environment variables (for custom Firebase projects)
50
+ * 2. Built-in edge-net public config (no setup required)
36
51
  */
37
52
  export function getFirebaseConfig() {
38
- // Try environment variables first (highest priority)
53
+ // Allow override via environment variables for custom projects
39
54
  const apiKey = process.env.FIREBASE_API_KEY;
40
55
  const projectId = process.env.FIREBASE_PROJECT_ID;
41
56
 
@@ -44,13 +59,12 @@ export function getFirebaseConfig() {
44
59
  apiKey,
45
60
  projectId,
46
61
  authDomain: process.env.FIREBASE_AUTH_DOMAIN || `${projectId}.firebaseapp.com`,
47
- databaseURL: process.env.FIREBASE_DATABASE_URL || `https://${projectId}-default-rtdb.firebaseio.com`,
48
62
  storageBucket: process.env.FIREBASE_STORAGE_BUCKET || `${projectId}.appspot.com`,
49
63
  };
50
64
  }
51
65
 
52
- // Sync version only uses env vars - use getFirebaseConfigAsync for file config
53
- return null;
66
+ // Use built-in public edge-net config (no setup required!)
67
+ return EDGE_NET_FIREBASE_CONFIG;
54
68
  }
55
69
 
56
70
  /**
@@ -95,13 +109,14 @@ export async function getFirebaseConfigAsync() {
95
109
  export const DEFAULT_FIREBASE_CONFIG = null;
96
110
 
97
111
  /**
98
- * Signaling paths in Firestore
112
+ * Signaling collection names in Firestore
113
+ * Each is a top-level collection (Firestore requires collection/document pairs)
99
114
  */
100
115
  export const SIGNALING_PATHS = {
101
- peers: 'edge-net/peers',
102
- signals: 'edge-net/signals',
103
- rooms: 'edge-net/rooms',
104
- ledger: 'edge-net/ledger',
116
+ peers: 'edgenet_peers',
117
+ signals: 'edgenet_signals',
118
+ rooms: 'edgenet_rooms',
119
+ ledger: 'edgenet_ledger',
105
120
  };
106
121
 
107
122
  // ============================================
@@ -136,6 +151,11 @@ export class FirebaseSignaling extends EventEmitter {
136
151
  this.db = null;
137
152
  this.rtdb = null;
138
153
 
154
+ // WASM Security (replaces Firebase Auth)
155
+ /** @type {import('./secure-access.js').SecureAccessManager|null} */
156
+ this.secureAccess = options.secureAccess || null;
157
+ this.verifySignatures = options.verifySignatures !== false;
158
+
139
159
  // State
140
160
  this.isConnected = false;
141
161
  this.peers = new Map();
@@ -149,44 +169,71 @@ export class FirebaseSignaling extends EventEmitter {
149
169
  firebaseSignals: 0,
150
170
  dhtSignals: 0,
151
171
  p2pSignals: 0,
172
+ verifiedSignals: 0,
173
+ rejectedSignals: 0,
152
174
  };
153
175
  }
154
176
 
155
177
  /**
156
- * Initialize Firebase connection
178
+ * Initialize Firebase connection with WASM cryptographic security
157
179
  */
158
180
  async connect() {
159
- // SECURITY: Require valid config
181
+ // Use built-in config if not provided
182
+ if (!this.config) {
183
+ this.config = getFirebaseConfig();
184
+ }
185
+
160
186
  if (!this.config || !this.config.apiKey || !this.config.projectId) {
161
- console.log(' ⚠️ Firebase not configured (no credentials)');
162
- console.log(' 💡 Set environment variables: FIREBASE_API_KEY, FIREBASE_PROJECT_ID');
187
+ console.log(' ⚠️ Firebase not configured');
163
188
  this.emit('not-configured');
164
189
  return false;
165
190
  }
166
191
 
167
192
  try {
193
+ // Initialize WASM security if not provided
194
+ if (!this.secureAccess) {
195
+ try {
196
+ const { createSecureAccess } = await import('./secure-access.js');
197
+ this.secureAccess = await createSecureAccess({
198
+ siteId: this.room,
199
+ persistIdentity: true
200
+ });
201
+ // Use WASM-generated node ID if peerId not set
202
+ if (!this.peerId) {
203
+ this.peerId = this.secureAccess.getShortId();
204
+ }
205
+ } catch (err) {
206
+ console.log(' ⚠️ WASM security unavailable, using basic mode');
207
+ }
208
+ }
209
+
168
210
  // Dynamic import Firebase (tree-shakeable)
169
211
  const { initializeApp, getApps } = await import('firebase/app');
170
- const { getFirestore, collection, doc, setDoc, onSnapshot, deleteDoc, query, where, orderBy, limit } = await import('firebase/firestore');
171
- const { getDatabase, ref, set, onValue, onDisconnect, serverTimestamp } = await import('firebase/database');
212
+ const { getFirestore, collection, doc, setDoc, onSnapshot, deleteDoc, query, where, orderBy, limit, serverTimestamp } = await import('firebase/firestore');
172
213
 
173
214
  // Store Firebase methods for later use
174
215
  this.firebase = {
175
- collection, doc, setDoc, onSnapshot, deleteDoc, query, where, orderBy, limit,
176
- ref, set, onValue, onDisconnect, serverTimestamp
216
+ collection, doc, setDoc, onSnapshot, deleteDoc, query, where, orderBy, limit, serverTimestamp
177
217
  };
178
218
 
179
219
  // Initialize or reuse existing app
180
220
  const apps = getApps();
181
221
  this.app = apps.length ? apps[0] : initializeApp(this.config);
182
222
 
183
- // Initialize Firestore (for signaling)
223
+ // Initialize Firestore
184
224
  this.db = getFirestore(this.app);
185
225
 
186
- // Initialize Realtime Database (for presence)
187
- this.rtdb = getDatabase(this.app);
226
+ // WASM cryptographic identity (replaces Firebase Auth)
227
+ if (this.secureAccess) {
228
+ this.uid = this.secureAccess.getNodeId();
229
+ console.log(` 🔐 WASM crypto identity: ${this.secureAccess.getShortId()}`);
230
+ console.log(` 📦 Public key: ${this.secureAccess.getPublicKeyHex().slice(0, 16)}...`);
231
+ } else {
232
+ this.uid = this.peerId;
233
+ console.log(` ⚠️ No WASM security, using peerId: ${this.peerId?.slice(0, 8)}...`);
234
+ }
188
235
 
189
- // Register presence
236
+ // Register presence in Firestore
190
237
  await this.registerPresence();
191
238
 
192
239
  // Listen for peers
@@ -196,7 +243,7 @@ export class FirebaseSignaling extends EventEmitter {
196
243
  this.subscribeToSignals();
197
244
 
198
245
  this.isConnected = true;
199
- console.log(' ✅ Firebase signaling connected');
246
+ console.log(' ✅ Firebase connected with WASM security');
200
247
 
201
248
  this.emit('connected');
202
249
  return true;
@@ -209,54 +256,98 @@ export class FirebaseSignaling extends EventEmitter {
209
256
  }
210
257
 
211
258
  /**
212
- * Register this peer's presence in Firebase
259
+ * Register this peer's presence in Firestore with WASM-signed data
213
260
  */
214
261
  async registerPresence() {
215
- const { ref, set, onDisconnect, serverTimestamp } = this.firebase;
262
+ const { doc, setDoc, serverTimestamp } = this.firebase;
216
263
 
217
- const presenceRef = ref(this.rtdb, `presence/${this.room}/${this.peerId}`);
264
+ const presenceRef = doc(this.db, SIGNALING_PATHS.peers, this.peerId);
218
265
 
219
- // Set online status
220
- await set(presenceRef, {
266
+ // Build presence data
267
+ const presenceData = {
221
268
  peerId: this.peerId,
222
269
  room: this.room,
223
270
  online: true,
224
271
  lastSeen: serverTimestamp(),
225
272
  capabilities: ['compute', 'storage', 'verify'],
226
- });
273
+ };
274
+
275
+ // Add WASM cryptographic identity if available
276
+ if (this.secureAccess) {
277
+ presenceData.publicKey = this.secureAccess.getPublicKeyHex();
278
+ // Sign the presence announcement
279
+ const signed = this.secureAccess.signMessage({
280
+ peerId: this.peerId,
281
+ room: this.room,
282
+ capabilities: presenceData.capabilities
283
+ });
284
+ presenceData.signature = signed.signature;
285
+ presenceData.signedAt = signed.timestamp;
286
+ }
227
287
 
228
- // Remove on disconnect
229
- onDisconnect(presenceRef).remove();
288
+ // Set online status in Firestore
289
+ await setDoc(presenceRef, presenceData, { merge: true });
290
+
291
+ // Set up heartbeat to maintain presence (Firestore doesn't have onDisconnect)
292
+ this._heartbeatInterval = setInterval(async () => {
293
+ try {
294
+ const heartbeat = { lastSeen: serverTimestamp() };
295
+ // Sign heartbeat if security available
296
+ if (this.secureAccess) {
297
+ const signed = this.secureAccess.signMessage({ heartbeat: Date.now() });
298
+ heartbeat.heartbeatSig = signed.signature;
299
+ }
300
+ await setDoc(presenceRef, heartbeat, { merge: true });
301
+ } catch (e) {
302
+ // Ignore heartbeat errors
303
+ }
304
+ }, 30000);
230
305
 
231
- console.log(` 📡 Registered presence: ${this.peerId.slice(0, 8)}...`);
306
+ console.log(` 📡 Registered presence: ${this.peerId.slice(0, 8)}... (WASM-signed)`);
232
307
  }
233
308
 
234
309
  /**
235
- * Subscribe to peer presence updates
310
+ * Subscribe to peer presence updates (using Firestore)
236
311
  */
237
312
  subscribeToPeers() {
238
- const { ref, onValue } = this.firebase;
239
-
240
- const roomRef = ref(this.rtdb, `presence/${this.room}`);
313
+ const { collection, query, where, onSnapshot } = this.firebase;
241
314
 
242
- const unsubscribe = onValue(roomRef, (snapshot) => {
243
- const peers = snapshot.val() || {};
315
+ // Query peers in same room that were active in last 2 minutes
316
+ const peersRef = collection(this.db, SIGNALING_PATHS.peers);
317
+ const q = query(peersRef, where('room', '==', this.room));
244
318
 
245
- // Track new peers
246
- for (const [peerId, data] of Object.entries(peers)) {
247
- if (peerId !== this.peerId && !this.peers.has(peerId)) {
248
- this.peers.set(peerId, data);
249
- this.emit('peer-discovered', { peerId, ...data });
250
- }
251
- }
252
-
253
- // Track disconnected peers
254
- for (const peerId of this.peers.keys()) {
255
- if (!peers[peerId]) {
256
- this.peers.delete(peerId);
257
- this.emit('peer-left', { peerId });
319
+ const unsubscribe = onSnapshot(q, (snapshot) => {
320
+ const now = Date.now();
321
+ const staleThreshold = 2 * 60 * 1000; // 2 minutes
322
+
323
+ snapshot.docChanges().forEach((change) => {
324
+ const data = change.doc.data();
325
+ const peerId = change.doc.id;
326
+
327
+ if (peerId === this.peerId) return; // Skip self
328
+
329
+ if (change.type === 'added' || change.type === 'modified') {
330
+ // Check if peer is still active (lastSeen within threshold)
331
+ const lastSeen = data.lastSeen?.toMillis?.() || 0;
332
+ if (now - lastSeen < staleThreshold) {
333
+ if (!this.peers.has(peerId)) {
334
+ this.peers.set(peerId, data);
335
+ this.emit('peer-discovered', { peerId, ...data });
336
+ }
337
+ } else {
338
+ // Peer is stale
339
+ if (this.peers.has(peerId)) {
340
+ this.peers.delete(peerId);
341
+ this.emit('peer-left', { peerId });
342
+ }
343
+ }
344
+ } else if (change.type === 'removed') {
345
+ if (this.peers.has(peerId)) {
346
+ this.peers.delete(peerId);
347
+ this.emit('peer-left', { peerId });
348
+ }
258
349
  }
259
- }
350
+ });
260
351
  });
261
352
 
262
353
  this.unsubscribers.push(unsubscribe);
@@ -285,7 +376,7 @@ export class FirebaseSignaling extends EventEmitter {
285
376
  }
286
377
 
287
378
  /**
288
- * Handle incoming signal
379
+ * Handle incoming signal with WASM signature verification
289
380
  */
290
381
  async handleSignal(signal, docId) {
291
382
  this.stats.firebaseSignals++;
@@ -294,19 +385,46 @@ export class FirebaseSignaling extends EventEmitter {
294
385
  const { doc, deleteDoc } = this.firebase;
295
386
  await deleteDoc(doc(this.db, SIGNALING_PATHS.signals, docId));
296
387
 
388
+ // Verify signature if WASM security is enabled
389
+ if (this.verifySignatures && this.secureAccess && signal.signature && signal.publicKey) {
390
+ const isValid = this.secureAccess.verifyMessage({
391
+ payload: JSON.stringify({
392
+ from: signal.from,
393
+ to: signal.to,
394
+ type: signal.type,
395
+ data: typeof signal.data === 'object' ? JSON.stringify(signal.data) : signal.data,
396
+ timestamp: signal.timestamp
397
+ }),
398
+ signature: signal.signature,
399
+ publicKey: signal.publicKey,
400
+ timestamp: signal.timestamp
401
+ });
402
+
403
+ if (!isValid) {
404
+ console.warn(` ⚠️ Invalid signature from ${signal.from?.slice(0, 8)}...`);
405
+ this.stats.rejectedSignals++;
406
+ this.emit('invalid-signature', { from: signal.from, type: signal.type });
407
+ return; // Reject the signal
408
+ }
409
+
410
+ // Register verified peer
411
+ this.secureAccess.registerPeer(signal.from, signal.publicKey);
412
+ this.stats.verifiedSignals++;
413
+ }
414
+
297
415
  // Emit appropriate event
298
416
  switch (signal.type) {
299
417
  case 'offer':
300
- this.emit('offer', { from: signal.from, offer: signal.data });
418
+ this.emit('offer', { from: signal.from, offer: signal.data, verified: !!signal.signature });
301
419
  break;
302
420
  case 'answer':
303
- this.emit('answer', { from: signal.from, answer: signal.data });
421
+ this.emit('answer', { from: signal.from, answer: signal.data, verified: !!signal.signature });
304
422
  break;
305
423
  case 'ice-candidate':
306
- this.emit('ice-candidate', { from: signal.from, candidate: signal.data });
424
+ this.emit('ice-candidate', { from: signal.from, candidate: signal.data, verified: !!signal.signature });
307
425
  break;
308
426
  default:
309
- this.emit('signal', signal);
427
+ this.emit('signal', { ...signal, verified: !!signal.signature });
310
428
  }
311
429
  }
312
430
 
@@ -332,7 +450,7 @@ export class FirebaseSignaling extends EventEmitter {
332
450
  }
333
451
 
334
452
  /**
335
- * Send signal via Firebase
453
+ * Send signal via Firebase with WASM signature
336
454
  */
337
455
  async sendSignal(toPeerId, type, data) {
338
456
  if (!this.isConnected) {
@@ -344,14 +462,30 @@ export class FirebaseSignaling extends EventEmitter {
344
462
  const signalId = `${this.peerId}-${toPeerId}-${Date.now()}`;
345
463
  const signalRef = doc(this.db, SIGNALING_PATHS.signals, signalId);
346
464
 
347
- await setDoc(signalRef, {
465
+ const timestamp = Date.now();
466
+ const signalData = {
348
467
  from: this.peerId,
349
468
  to: toPeerId,
350
469
  type,
351
470
  data,
352
- timestamp: Date.now(),
471
+ timestamp,
353
472
  room: this.room,
354
- });
473
+ };
474
+
475
+ // Sign the signal with WASM cryptography
476
+ if (this.secureAccess) {
477
+ const signed = this.secureAccess.signMessage({
478
+ from: this.peerId,
479
+ to: toPeerId,
480
+ type,
481
+ data: typeof data === 'object' ? JSON.stringify(data) : data,
482
+ timestamp
483
+ });
484
+ signalData.signature = signed.signature;
485
+ signalData.publicKey = signed.publicKey;
486
+ }
487
+
488
+ await setDoc(signalRef, signalData);
355
489
 
356
490
  return true;
357
491
  }
@@ -370,17 +504,27 @@ export class FirebaseSignaling extends EventEmitter {
370
504
  * Disconnect and cleanup
371
505
  */
372
506
  async disconnect() {
507
+ // Stop heartbeat
508
+ if (this._heartbeatInterval) {
509
+ clearInterval(this._heartbeatInterval);
510
+ this._heartbeatInterval = null;
511
+ }
512
+
373
513
  // Unsubscribe from all listeners
374
514
  for (const unsub of this.unsubscribers) {
375
515
  if (typeof unsub === 'function') unsub();
376
516
  }
377
517
  this.unsubscribers = [];
378
518
 
379
- // Remove presence
380
- if (this.rtdb && this.firebase) {
381
- const { ref, set } = this.firebase;
382
- const presenceRef = ref(this.rtdb, `presence/${this.room}/${this.peerId}`);
383
- await set(presenceRef, null);
519
+ // Remove presence from Firestore
520
+ if (this.db && this.firebase) {
521
+ try {
522
+ const { doc, deleteDoc } = this.firebase;
523
+ const presenceRef = doc(this.db, SIGNALING_PATHS.peers, this.peerId);
524
+ await deleteDoc(presenceRef);
525
+ } catch (e) {
526
+ // Ignore cleanup errors
527
+ }
384
528
  }
385
529
 
386
530
  this.isConnected = false;
@@ -569,6 +713,10 @@ export class HybridBootstrap extends EventEmitter {
569
713
  this.peerId = options.peerId;
570
714
  this.config = options.firebaseConfig || DEFAULT_FIREBASE_CONFIG;
571
715
 
716
+ // WASM Security
717
+ /** @type {import('./secure-access.js').SecureAccessManager|null} */
718
+ this.secureAccess = options.secureAccess || null;
719
+
572
720
  // Components
573
721
  this.firebase = null;
574
722
  this.dht = null;
@@ -586,25 +734,63 @@ export class HybridBootstrap extends EventEmitter {
586
734
  directConnections: 0,
587
735
  firebaseSignals: 0,
588
736
  p2pSignals: 0,
737
+ verifiedPeers: 0,
589
738
  };
590
739
  }
591
740
 
592
741
  /**
593
- * Start hybrid bootstrap
742
+ * Start hybrid bootstrap with WASM security
594
743
  */
595
744
  async start(webrtc, dht) {
596
745
  this.webrtc = webrtc;
597
746
  this.dht = dht;
598
747
 
599
- // Start with Firebase
748
+ // Initialize WASM security if not provided
749
+ if (!this.secureAccess) {
750
+ try {
751
+ const { createSecureAccess } = await import('./secure-access.js');
752
+ this.secureAccess = await createSecureAccess({
753
+ siteId: 'edge-net',
754
+ persistIdentity: true
755
+ });
756
+ // Use WASM node ID if peerId not set
757
+ if (!this.peerId) {
758
+ this.peerId = this.secureAccess.getShortId();
759
+ }
760
+ } catch (err) {
761
+ console.log(' ⚠️ WASM security unavailable for bootstrap');
762
+ }
763
+ }
764
+
765
+ // Start with Firebase, passing WASM security
600
766
  this.firebase = new FirebaseSignaling({
601
767
  peerId: this.peerId,
602
768
  firebaseConfig: this.config,
769
+ secureAccess: this.secureAccess,
603
770
  });
604
771
 
605
772
  // Wire up events
606
773
  this.setupFirebaseEvents();
607
774
 
775
+ // Set up WebRTC to use Firebase for signaling
776
+ if (this.webrtc) {
777
+ this.webrtc.setExternalSignaling(async (type, toPeerId, data) => {
778
+ // Route signaling through Firebase
779
+ switch (type) {
780
+ case 'offer':
781
+ await this.firebase.sendOffer(toPeerId, data);
782
+ break;
783
+ case 'answer':
784
+ await this.firebase.sendAnswer(toPeerId, data);
785
+ break;
786
+ case 'ice-candidate':
787
+ await this.firebase.sendIceCandidate(toPeerId, data);
788
+ break;
789
+ }
790
+ this.stats.firebaseSignals++;
791
+ });
792
+ }
793
+
608
794
  // Connect to Firebase
609
795
  const connected = await this.firebase.connect();
610
796
 
@@ -665,18 +851,10 @@ export class HybridBootstrap extends EventEmitter {
665
851
  if (!this.webrtc) return;
666
852
 
667
853
  try {
668
- // Create offer
669
- const offer = await this.webrtc.createOffer(peerId);
670
-
671
- // Try P2P signaling first (if peer is directly connected)
672
- if (this.mode === 'p2p' && this.webrtc.isConnected(peerId)) {
673
- this.webrtc.sendToPeer(peerId, { type: 'offer', offer });
674
- this.stats.p2pSignals++;
675
- } else {
676
- // Fall back to Firebase
677
- await this.firebase.sendOffer(peerId, offer);
678
- this.stats.firebaseSignals++;
679
- }
854
+ // Use WebRTCPeerManager's connectToPeer method
855
+ // This handles offer creation and signaling internally
856
+ await this.webrtc.connectToPeer(peerId);
857
+ this.stats.directConnections++;
680
858
 
681
859
  } catch (error) {
682
860
  console.warn(`[HybridBootstrap] Connect to ${peerId.slice(0, 8)} failed:`, error.message);
package/p2p.js CHANGED
@@ -614,7 +614,7 @@ export class P2PNetwork extends EventEmitter {
614
614
  * Get current balance
615
615
  */
616
616
  getBalance() {
617
- return this.ledger?.getBalance() || 0;
617
+ return this.ledger?.balance?.() ?? this.ledger?.getBalance?.() ?? 0;
618
618
  }
619
619
 
620
620
  /**
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@ruvector/edge-net",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
- "description": "Distributed compute intelligence network with AI agents and workers - contribute browser compute, spawn distributed AI agents, earn credits. Features Time Crystal coordination, Neural DAG attention, P2P swarm intelligence, ONNX inference, WebRTC signaling, CRDT ledger, and multi-agent workflows.",
5
+ "description": "Distributed compute intelligence network with WASM cryptographic security - contribute browser compute, spawn distributed AI agents, earn credits. Features Ed25519 signing, PiKey identity, Time Crystal coordination, Neural DAG attention, P2P swarm intelligence, ONNX inference, WebRTC signaling, CRDT ledger, and multi-agent workflows.",
6
6
  "main": "ruvector_edge_net.js",
7
7
  "module": "ruvector_edge_net.js",
8
8
  "types": "ruvector_edge_net.d.ts",
@@ -55,7 +55,11 @@
55
55
  "firebase",
56
56
  "gcloud",
57
57
  "firestore",
58
- "realtime-database"
58
+ "realtime-database",
59
+ "ed25519",
60
+ "wasm-cryptography",
61
+ "pikey",
62
+ "secure-access"
59
63
  ],
60
64
  "author": "RuVector Team <team@ruvector.dev>",
61
65
  "license": "MIT",
@@ -96,6 +100,7 @@
96
100
  "p2p.js",
97
101
  "firebase-signaling.js",
98
102
  "firebase-setup.js",
103
+ "secure-access.js",
99
104
  "README.md",
100
105
  "LICENSE"
101
106
  ],
@@ -160,6 +165,9 @@
160
165
  },
161
166
  "./firebase-setup": {
162
167
  "import": "./firebase-setup.js"
168
+ },
169
+ "./secure-access": {
170
+ "import": "./secure-access.js"
163
171
  }
164
172
  },
165
173
  "sideEffects": [
@@ -189,14 +197,8 @@
189
197
  "dependencies": {
190
198
  "@ruvector/ruvllm": "^0.2.3",
191
199
  "@xenova/transformers": "^2.17.2",
200
+ "firebase": "^10.14.1",
201
+ "wrtc": "^0.4.7",
192
202
  "ws": "^8.18.3"
193
- },
194
- "peerDependencies": {
195
- "firebase": "^10.0.0"
196
- },
197
- "peerDependenciesMeta": {
198
- "firebase": {
199
- "optional": true
200
- }
201
203
  }
202
204
  }
@@ -0,0 +1,595 @@
1
+ /**
2
+ * @ruvector/edge-net Secure Access Layer
3
+ *
4
+ * Uses WASM cryptographic primitives for secure network access.
5
+ * No external authentication needed - cryptographic proof of identity.
6
+ *
7
+ * Security Model:
8
+ * 1. Each node generates a PiKey (Ed25519-based) in WASM
9
+ * 2. All messages are signed with the node's private key
10
+ * 3. Other nodes verify signatures with public keys
11
+ * 4. AdaptiveSecurity provides self-learning attack detection
12
+ *
13
+ * @module @ruvector/edge-net/secure-access
14
+ */
15
+
16
+ import { EventEmitter } from 'events';
17
+
18
+ /**
19
+ * Secure Access Manager
20
+ *
21
+ * Provides WASM-based cryptographic identity and message signing
22
+ * for secure P2P network access without external auth providers.
23
+ */
24
+ export class SecureAccessManager extends EventEmitter {
25
+ constructor(options = {}) {
26
+ super();
27
+
28
+ /** @type {import('./ruvector_edge_net').PiKey|null} */
29
+ this.piKey = null;
30
+
31
+ /** @type {import('./ruvector_edge_net').SessionKey|null} */
32
+ this.sessionKey = null;
33
+
34
+ /** @type {import('./ruvector_edge_net').WasmNodeIdentity|null} */
35
+ this.nodeIdentity = null;
36
+
37
+ /** @type {import('./ruvector_edge_net').AdaptiveSecurity|null} */
38
+ this.security = null;
39
+
40
+ /** @type {Map<string, Uint8Array>} Known peer public keys */
41
+ this.knownPeers = new Map();
42
+
43
+ /** @type {Map<string, number>} Peer reputation scores */
44
+ this.peerReputation = new Map();
45
+
46
+ this.options = {
47
+ siteId: options.siteId || 'edge-net',
48
+ sessionTTL: options.sessionTTL || 3600, // 1 hour
49
+ backupPassword: options.backupPassword || null,
50
+ persistIdentity: options.persistIdentity !== false,
51
+ ...options
52
+ };
53
+
54
+ this.wasm = null;
55
+ this.initialized = false;
56
+ }
57
+
58
+ /**
59
+ * Initialize secure access with WASM cryptography
60
+ */
61
+ async initialize() {
62
+ if (this.initialized) return this;
63
+
64
+ console.log('🔐 Initializing WASM Secure Access...');
65
+
66
+ // Load WASM module
67
+ try {
68
+ // For Node.js, use the node-specific CJS module which auto-loads WASM
69
+ const isNode = typeof process !== 'undefined' && process.versions?.node;
70
+ if (isNode) {
71
+ // Node.js: CJS module loads WASM synchronously on import
72
+ this.wasm = await import('./node/ruvector_edge_net.cjs');
73
+ } else {
74
+ // Browser: Use ES module with WASM init
75
+ const wasmModule = await import('./ruvector_edge_net.js');
76
+ // Call default init to load WASM binary
77
+ if (wasmModule.default && typeof wasmModule.default === 'function') {
78
+ await wasmModule.default();
79
+ }
80
+ this.wasm = wasmModule;
81
+ }
82
+ } catch (err) {
83
+ console.error(' ❌ WASM load error:', err.message);
84
+ throw err;
85
+ }
86
+
87
+ // Try to restore existing identity
88
+ const restored = await this._tryRestoreIdentity();
89
+
90
+ if (!restored) {
91
+ // Generate new cryptographic identity
92
+ await this._generateIdentity();
93
+ }
94
+
95
+ // Initialize adaptive security
96
+ this.security = new this.wasm.AdaptiveSecurity();
97
+
98
+ // Create session key for encrypted communications
99
+ this.sessionKey = new this.wasm.SessionKey(this.piKey, this.options.sessionTTL);
100
+
101
+ this.initialized = true;
102
+
103
+ console.log(` 🔑 Node ID: ${this.getShortId()}`);
104
+ console.log(` 📦 Public Key: ${this.getPublicKeyHex().slice(0, 16)}...`);
105
+ console.log(` ⏱️ Session expires: ${new Date(Date.now() + this.options.sessionTTL * 1000).toISOString()}`);
106
+
107
+ this.emit('initialized', {
108
+ nodeId: this.getNodeId(),
109
+ publicKey: this.getPublicKeyHex()
110
+ });
111
+
112
+ return this;
113
+ }
114
+
115
+ /**
116
+ * Try to restore identity from localStorage or backup
117
+ */
118
+ async _tryRestoreIdentity() {
119
+ if (!this.options.persistIdentity) return false;
120
+
121
+ try {
122
+ // Check localStorage (browser) or file (Node.js)
123
+ let stored = null;
124
+
125
+ if (typeof localStorage !== 'undefined') {
126
+ stored = localStorage.getItem('edge-net-identity');
127
+ } else if (typeof process !== 'undefined') {
128
+ const fs = await import('fs');
129
+ const path = await import('path');
130
+ const identityPath = path.join(process.cwd(), '.edge-net-identity');
131
+ if (fs.existsSync(identityPath)) {
132
+ stored = fs.readFileSync(identityPath, 'utf8');
133
+ }
134
+ }
135
+
136
+ if (stored) {
137
+ const data = JSON.parse(stored);
138
+ const encrypted = new Uint8Array(data.encrypted);
139
+
140
+ // Use default password if none provided
141
+ const password = this.options.backupPassword || 'edge-net-default-key';
142
+
143
+ this.piKey = this.wasm.PiKey.restoreFromBackup(encrypted, password);
144
+ this.nodeIdentity = this.wasm.WasmNodeIdentity.fromSecretKey(
145
+ encrypted, // Same key derivation
146
+ this.options.siteId
147
+ );
148
+
149
+ console.log(' ♻️ Restored existing identity');
150
+ return true;
151
+ }
152
+ } catch (err) {
153
+ console.log(' ⚡ Creating new identity (no backup found)');
154
+ }
155
+
156
+ return false;
157
+ }
158
+
159
+ /**
160
+ * Generate new cryptographic identity
161
+ */
162
+ async _generateIdentity() {
163
+ // Generate Pi-Key (Ed25519-based with Pi magic)
164
+ // Constructor takes optional genesis_seed (Uint8Array or null)
165
+ const genesisSeed = this.options.genesisSeed || null;
166
+ this.piKey = new this.wasm.PiKey(genesisSeed);
167
+
168
+ // Create node identity from same site
169
+ this.nodeIdentity = new this.wasm.WasmNodeIdentity(this.options.siteId);
170
+
171
+ // Persist identity if enabled
172
+ if (this.options.persistIdentity) {
173
+ await this._persistIdentity();
174
+ }
175
+
176
+ console.log(' ✨ Generated new cryptographic identity');
177
+ }
178
+
179
+ /**
180
+ * Persist identity to storage
181
+ */
182
+ async _persistIdentity() {
183
+ const password = this.options.backupPassword || 'edge-net-default-key';
184
+ const backup = this.piKey.createEncryptedBackup(password);
185
+ const data = JSON.stringify({
186
+ encrypted: Array.from(backup),
187
+ created: Date.now(),
188
+ siteId: this.options.siteId
189
+ });
190
+
191
+ try {
192
+ if (typeof localStorage !== 'undefined') {
193
+ localStorage.setItem('edge-net-identity', data);
194
+ } else if (typeof process !== 'undefined') {
195
+ const fs = await import('fs');
196
+ const path = await import('path');
197
+ const identityPath = path.join(process.cwd(), '.edge-net-identity');
198
+ fs.writeFileSync(identityPath, data);
199
+ }
200
+ } catch (err) {
201
+ console.warn(' ⚠️ Could not persist identity:', err.message);
202
+ }
203
+ }
204
+
205
+ // ============================================
206
+ // IDENTITY & KEYS
207
+ // ============================================
208
+
209
+ /**
210
+ * Get node ID (full)
211
+ */
212
+ getNodeId() {
213
+ return this.piKey?.getIdentityHex() || this.nodeIdentity?.getId?.() || 'unknown';
214
+ }
215
+
216
+ /**
217
+ * Get short node ID for display
218
+ */
219
+ getShortId() {
220
+ return this.piKey?.getShortId() || this.getNodeId().slice(0, 8);
221
+ }
222
+
223
+ /**
224
+ * Get public key as hex string
225
+ */
226
+ getPublicKeyHex() {
227
+ return Array.from(this.piKey?.getPublicKey() || new Uint8Array(32))
228
+ .map(b => b.toString(16).padStart(2, '0'))
229
+ .join('');
230
+ }
231
+
232
+ /**
233
+ * Get public key as bytes
234
+ */
235
+ getPublicKeyBytes() {
236
+ return this.piKey?.getPublicKey() || new Uint8Array(32);
237
+ }
238
+
239
+ // ============================================
240
+ // MESSAGE SIGNING & VERIFICATION
241
+ // ============================================
242
+
243
+ /**
244
+ * Sign a message/object
245
+ * @param {object|string|Uint8Array} message - Message to sign
246
+ * @returns {{ payload: string, signature: string, publicKey: string, timestamp: number }}
247
+ */
248
+ signMessage(message) {
249
+ const payload = typeof message === 'string' ? message :
250
+ message instanceof Uint8Array ? new TextDecoder().decode(message) :
251
+ JSON.stringify(message);
252
+
253
+ const timestamp = Date.now();
254
+ const dataToSign = `${payload}|${timestamp}`;
255
+ const dataBytes = new TextEncoder().encode(dataToSign);
256
+
257
+ const signature = this.piKey.sign(dataBytes);
258
+
259
+ return {
260
+ payload,
261
+ signature: Array.from(signature).map(b => b.toString(16).padStart(2, '0')).join(''),
262
+ publicKey: this.getPublicKeyHex(),
263
+ timestamp,
264
+ nodeId: this.getShortId()
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Verify a signed message
270
+ * @param {object} signed - Signed message object
271
+ * @returns {boolean} Whether signature is valid
272
+ */
273
+ verifyMessage(signed) {
274
+ try {
275
+ const { payload, signature, publicKey, timestamp } = signed;
276
+
277
+ // Check timestamp (reject messages older than 5 minutes)
278
+ const age = Date.now() - timestamp;
279
+ if (age > 5 * 60 * 1000) {
280
+ console.warn('⚠️ Message too old:', age, 'ms');
281
+ return false;
282
+ }
283
+
284
+ // Convert hex strings back to bytes
285
+ const dataToVerify = `${payload}|${timestamp}`;
286
+ const dataBytes = new TextEncoder().encode(dataToVerify);
287
+ const sigBytes = new Uint8Array(signature.match(/.{2}/g).map(h => parseInt(h, 16)));
288
+ const pubKeyBytes = new Uint8Array(publicKey.match(/.{2}/g).map(h => parseInt(h, 16)));
289
+
290
+ // Verify using WASM
291
+ const valid = this.piKey.verify(dataBytes, sigBytes, pubKeyBytes);
292
+
293
+ // Update peer reputation based on verification
294
+ if (valid) {
295
+ this._updateReputation(signed.nodeId || publicKey.slice(0, 16), 0.01);
296
+ } else {
297
+ this._updateReputation(signed.nodeId || publicKey.slice(0, 16), -0.1);
298
+ this._recordSuspicious(signed.nodeId, 'invalid_signature');
299
+ }
300
+
301
+ return valid;
302
+ } catch (err) {
303
+ console.warn('⚠️ Signature verification error:', err.message);
304
+ return false;
305
+ }
306
+ }
307
+
308
+ // ============================================
309
+ // PEER MANAGEMENT
310
+ // ============================================
311
+
312
+ /**
313
+ * Register a known peer's public key
314
+ */
315
+ registerPeer(peerId, publicKey) {
316
+ const pubKeyBytes = typeof publicKey === 'string' ?
317
+ new Uint8Array(publicKey.match(/.{2}/g).map(h => parseInt(h, 16))) :
318
+ publicKey;
319
+
320
+ this.knownPeers.set(peerId, pubKeyBytes);
321
+ this.peerReputation.set(peerId, this.peerReputation.get(peerId) || 0.5);
322
+
323
+ this.emit('peer-registered', { peerId, publicKey: this.getPublicKeyHex() });
324
+ }
325
+
326
+ /**
327
+ * Get reputation score for a peer (0-1)
328
+ */
329
+ getPeerReputation(peerId) {
330
+ return this.peerReputation.get(peerId) || 0.5;
331
+ }
332
+
333
+ /**
334
+ * Update peer reputation
335
+ */
336
+ _updateReputation(peerId, delta) {
337
+ const current = this.peerReputation.get(peerId) || 0.5;
338
+ const newScore = Math.max(0, Math.min(1, current + delta));
339
+ this.peerReputation.set(peerId, newScore);
340
+
341
+ // Emit warning if reputation drops too low
342
+ if (newScore < 0.2) {
343
+ this.emit('peer-suspicious', { peerId, reputation: newScore });
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Record suspicious activity for learning
349
+ */
350
+ _recordSuspicious(peerId, reason) {
351
+ if (this.security) {
352
+ // Record for adaptive security learning
353
+ const features = new Float32Array([
354
+ Date.now() / 1e12,
355
+ this.getPeerReputation(peerId),
356
+ reason === 'invalid_signature' ? 1 : 0,
357
+ reason === 'replay_attack' ? 1 : 0,
358
+ 0, 0, 0, 0 // Padding
359
+ ]);
360
+ this.security.recordAttackPattern(reason, features, 0.5);
361
+ }
362
+ }
363
+
364
+ // ============================================
365
+ // ENCRYPTION (SESSION-BASED)
366
+ // ============================================
367
+
368
+ /**
369
+ * Encrypt data for secure transmission
370
+ */
371
+ encrypt(data) {
372
+ if (!this.sessionKey || this.sessionKey.isExpired()) {
373
+ // Refresh session key
374
+ this.sessionKey = new this.wasm.SessionKey(this.piKey, this.options.sessionTTL);
375
+ }
376
+
377
+ const dataBytes = typeof data === 'string' ?
378
+ new TextEncoder().encode(data) :
379
+ data instanceof Uint8Array ? data :
380
+ new TextEncoder().encode(JSON.stringify(data));
381
+
382
+ return this.sessionKey.encrypt(dataBytes);
383
+ }
384
+
385
+ /**
386
+ * Decrypt received data
387
+ */
388
+ decrypt(encrypted) {
389
+ if (!this.sessionKey) {
390
+ throw new Error('No session key available');
391
+ }
392
+
393
+ return this.sessionKey.decrypt(encrypted);
394
+ }
395
+
396
+ // ============================================
397
+ // SECURITY ANALYSIS
398
+ // ============================================
399
+
400
+ /**
401
+ * Analyze request for potential attacks
402
+ * @returns {number} Threat score (0-1, higher = more suspicious)
403
+ */
404
+ analyzeRequest(features) {
405
+ if (!this.security) return 0;
406
+
407
+ const featureArray = features instanceof Float32Array ?
408
+ features :
409
+ new Float32Array(Array.isArray(features) ? features : Object.values(features));
410
+
411
+ return this.security.detectAttack(featureArray);
412
+ }
413
+
414
+ /**
415
+ * Get security statistics
416
+ */
417
+ getSecurityStats() {
418
+ if (!this.security) return null;
419
+
420
+ return JSON.parse(this.security.getStats());
421
+ }
422
+
423
+ /**
424
+ * Export security patterns for persistence
425
+ */
426
+ exportSecurityPatterns() {
427
+ if (!this.security) return null;
428
+ return this.security.exportPatterns();
429
+ }
430
+
431
+ /**
432
+ * Import previously learned security patterns
433
+ */
434
+ importSecurityPatterns(patterns) {
435
+ if (!this.security) return;
436
+ this.security.importPatterns(patterns);
437
+ }
438
+
439
+ // ============================================
440
+ // CHALLENGE-RESPONSE
441
+ // ============================================
442
+
443
+ /**
444
+ * Create a challenge for peer verification
445
+ */
446
+ createChallenge() {
447
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
448
+ const timestamp = Date.now();
449
+
450
+ return {
451
+ challenge: Array.from(challenge).map(b => b.toString(16).padStart(2, '0')).join(''),
452
+ timestamp,
453
+ issuer: this.getShortId()
454
+ };
455
+ }
456
+
457
+ /**
458
+ * Respond to a challenge (proves identity)
459
+ */
460
+ respondToChallenge(challengeData) {
461
+ const challengeBytes = new Uint8Array(
462
+ challengeData.challenge.match(/.{2}/g).map(h => parseInt(h, 16))
463
+ );
464
+
465
+ const responseData = new Uint8Array([
466
+ ...challengeBytes,
467
+ ...new TextEncoder().encode(`|${challengeData.timestamp}|${this.getShortId()}`)
468
+ ]);
469
+
470
+ const signature = this.piKey.sign(responseData);
471
+
472
+ return {
473
+ ...challengeData,
474
+ response: Array.from(signature).map(b => b.toString(16).padStart(2, '0')).join(''),
475
+ responder: this.getShortId(),
476
+ publicKey: this.getPublicKeyHex()
477
+ };
478
+ }
479
+
480
+ /**
481
+ * Verify a challenge response
482
+ */
483
+ verifyChallengeResponse(response) {
484
+ try {
485
+ const challengeBytes = new Uint8Array(
486
+ response.challenge.match(/.{2}/g).map(h => parseInt(h, 16))
487
+ );
488
+
489
+ const responseData = new Uint8Array([
490
+ ...challengeBytes,
491
+ ...new TextEncoder().encode(`|${response.timestamp}|${response.responder}`)
492
+ ]);
493
+
494
+ const sigBytes = new Uint8Array(
495
+ response.response.match(/.{2}/g).map(h => parseInt(h, 16))
496
+ );
497
+ const pubKeyBytes = new Uint8Array(
498
+ response.publicKey.match(/.{2}/g).map(h => parseInt(h, 16))
499
+ );
500
+
501
+ const valid = this.piKey.verify(responseData, sigBytes, pubKeyBytes);
502
+
503
+ if (valid) {
504
+ // Register this peer as verified
505
+ this.registerPeer(response.responder, response.publicKey);
506
+ this._updateReputation(response.responder, 0.05);
507
+ }
508
+
509
+ return valid;
510
+ } catch (err) {
511
+ console.warn('Challenge verification failed:', err.message);
512
+ return false;
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Clean up resources
518
+ */
519
+ dispose() {
520
+ try { this.piKey?.free?.(); } catch (e) { /* already freed */ }
521
+ try { this.sessionKey?.free?.(); } catch (e) { /* already freed */ }
522
+ try { this.nodeIdentity?.free?.(); } catch (e) { /* already freed */ }
523
+ try { this.security?.free?.(); } catch (e) { /* already freed */ }
524
+ this.piKey = null;
525
+ this.sessionKey = null;
526
+ this.nodeIdentity = null;
527
+ this.security = null;
528
+ this.knownPeers.clear();
529
+ this.peerReputation.clear();
530
+ this.initialized = false;
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Create a secure access manager
536
+ */
537
+ export async function createSecureAccess(options = {}) {
538
+ const manager = new SecureAccessManager(options);
539
+ await manager.initialize();
540
+ return manager;
541
+ }
542
+
543
+ /**
544
+ * Wrap Firebase signaling with WASM security
545
+ */
546
+ export function wrapWithSecurity(firebaseSignaling, secureAccess) {
547
+ const originalAnnounce = firebaseSignaling.announcePeer?.bind(firebaseSignaling);
548
+ const originalSendOffer = firebaseSignaling.sendOffer?.bind(firebaseSignaling);
549
+ const originalSendAnswer = firebaseSignaling.sendAnswer?.bind(firebaseSignaling);
550
+ const originalSendIceCandidate = firebaseSignaling.sendIceCandidate?.bind(firebaseSignaling);
551
+
552
+ // Wrap peer announcement with signature
553
+ if (originalAnnounce) {
554
+ firebaseSignaling.announcePeer = async (peerId, metadata = {}) => {
555
+ const signedMetadata = secureAccess.signMessage({
556
+ ...metadata,
557
+ publicKey: secureAccess.getPublicKeyHex()
558
+ });
559
+ return originalAnnounce(peerId, signedMetadata);
560
+ };
561
+ }
562
+
563
+ // Wrap signaling messages with signatures
564
+ if (originalSendOffer) {
565
+ firebaseSignaling.sendOffer = async (toPeerId, offer) => {
566
+ const signed = secureAccess.signMessage({ type: 'offer', offer });
567
+ return originalSendOffer(toPeerId, signed);
568
+ };
569
+ }
570
+
571
+ if (originalSendAnswer) {
572
+ firebaseSignaling.sendAnswer = async (toPeerId, answer) => {
573
+ const signed = secureAccess.signMessage({ type: 'answer', answer });
574
+ return originalSendAnswer(toPeerId, signed);
575
+ };
576
+ }
577
+
578
+ if (originalSendIceCandidate) {
579
+ firebaseSignaling.sendIceCandidate = async (toPeerId, candidate) => {
580
+ const signed = secureAccess.signMessage({ type: 'ice', candidate });
581
+ return originalSendIceCandidate(toPeerId, signed);
582
+ };
583
+ }
584
+
585
+ // Add verification method
586
+ firebaseSignaling.verifySignedMessage = (signed) => {
587
+ return secureAccess.verifyMessage(signed);
588
+ };
589
+
590
+ firebaseSignaling.secureAccess = secureAccess;
591
+
592
+ return firebaseSignaling;
593
+ }
594
+
595
+ export default SecureAccessManager;
package/webrtc.js CHANGED
@@ -408,6 +408,15 @@ export class WebRTCPeerManager extends EventEmitter {
408
408
  failedConnections: 0,
409
409
  messagesRouted: 0,
410
410
  };
411
+ // External signaling callback (e.g., Firebase)
412
+ this.externalSignaling = null;
413
+ }
414
+
415
+ /**
416
+ * Set external signaling callback for Firebase or other signaling
417
+ */
418
+ setExternalSignaling(callback) {
419
+ this.externalSignaling = callback;
411
420
  }
412
421
 
413
422
  /**
@@ -639,13 +648,23 @@ export class WebRTCPeerManager extends EventEmitter {
639
648
 
640
649
  const offer = await peerConnection.createOffer();
641
650
 
642
- // Send offer via signaling
643
- this.signalingSocket.send(JSON.stringify({
651
+ // Send offer via available signaling method
652
+ const signalData = {
644
653
  type: 'offer',
645
654
  to: peerId,
646
655
  from: this.localIdentity.piKey,
647
656
  offer,
648
- }));
657
+ };
658
+
659
+ if (this.signalingSocket?.readyState === 1) {
660
+ // WebSocket signaling
661
+ this.signalingSocket.send(JSON.stringify(signalData));
662
+ } else if (this.externalSignaling) {
663
+ // External signaling (Firebase, etc.)
664
+ await this.externalSignaling('offer', peerId, offer);
665
+ } else {
666
+ throw new Error('No signaling method available');
667
+ }
649
668
 
650
669
  this.peers.set(peerId, peerConnection);
651
670
  this.emit('peers-updated', this.getPeerList());
@@ -653,6 +672,7 @@ export class WebRTCPeerManager extends EventEmitter {
653
672
  } catch (err) {
654
673
  this.stats.failedConnections++;
655
674
  console.error(`Failed to connect to ${peerId}:`, err.message);
675
+ throw err;
656
676
  }
657
677
  }
658
678
 
@@ -678,13 +698,17 @@ export class WebRTCPeerManager extends EventEmitter {
678
698
 
679
699
  const answer = await peerConnection.handleOffer(offer);
680
700
 
681
- // Send answer via signaling
682
- this.signalingSocket.send(JSON.stringify({
683
- type: 'answer',
684
- to: from,
685
- from: this.localIdentity.piKey,
686
- answer,
687
- }));
701
+ // Send answer via available signaling method
702
+ if (this.signalingSocket?.readyState === 1) {
703
+ this.signalingSocket.send(JSON.stringify({
704
+ type: 'answer',
705
+ to: from,
706
+ from: this.localIdentity.piKey,
707
+ answer,
708
+ }));
709
+ } else if (this.externalSignaling) {
710
+ await this.externalSignaling('answer', from, answer);
711
+ }
688
712
 
689
713
  this.peers.set(from, peerConnection);
690
714
  this.emit('peers-updated', this.getPeerList());