@ruvector/edge-net 0.4.1 → 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
  /**
@@ -137,6 +151,11 @@ export class FirebaseSignaling extends EventEmitter {
137
151
  this.db = null;
138
152
  this.rtdb = null;
139
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
+
140
159
  // State
141
160
  this.isConnected = false;
142
161
  this.peers = new Map();
@@ -150,27 +169,49 @@ export class FirebaseSignaling extends EventEmitter {
150
169
  firebaseSignals: 0,
151
170
  dhtSignals: 0,
152
171
  p2pSignals: 0,
172
+ verifiedSignals: 0,
173
+ rejectedSignals: 0,
153
174
  };
154
175
  }
155
176
 
156
177
  /**
157
- * Initialize Firebase connection
178
+ * Initialize Firebase connection with WASM cryptographic security
158
179
  */
159
180
  async connect() {
160
- // SECURITY: Require valid config
181
+ // Use built-in config if not provided
182
+ if (!this.config) {
183
+ this.config = getFirebaseConfig();
184
+ }
185
+
161
186
  if (!this.config || !this.config.apiKey || !this.config.projectId) {
162
- console.log(' ⚠️ Firebase not configured (no credentials)');
163
- console.log(' 💡 Set environment variables: FIREBASE_API_KEY, FIREBASE_PROJECT_ID');
187
+ console.log(' ⚠️ Firebase not configured');
164
188
  this.emit('not-configured');
165
189
  return false;
166
190
  }
167
191
 
168
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
+
169
210
  // Dynamic import Firebase (tree-shakeable)
170
211
  const { initializeApp, getApps } = await import('firebase/app');
171
212
  const { getFirestore, collection, doc, setDoc, onSnapshot, deleteDoc, query, where, orderBy, limit, serverTimestamp } = await import('firebase/firestore');
172
213
 
173
- // Store Firebase methods for later use (Firestore only - no RTDB)
214
+ // Store Firebase methods for later use
174
215
  this.firebase = {
175
216
  collection, doc, setDoc, onSnapshot, deleteDoc, query, where, orderBy, limit, serverTimestamp
176
217
  };
@@ -179,9 +220,19 @@ export class FirebaseSignaling extends EventEmitter {
179
220
  const apps = getApps();
180
221
  this.app = apps.length ? apps[0] : initializeApp(this.config);
181
222
 
182
- // Initialize Firestore (for signaling AND presence - no RTDB needed)
223
+ // Initialize Firestore
183
224
  this.db = getFirestore(this.app);
184
225
 
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
+ }
235
+
185
236
  // Register presence in Firestore
186
237
  await this.registerPresence();
187
238
 
@@ -192,7 +243,7 @@ export class FirebaseSignaling extends EventEmitter {
192
243
  this.subscribeToSignals();
193
244
 
194
245
  this.isConnected = true;
195
- console.log(' ✅ Firebase signaling connected (Firestore)');
246
+ console.log(' ✅ Firebase connected with WASM security');
196
247
 
197
248
  this.emit('connected');
198
249
  return true;
@@ -205,32 +256,54 @@ export class FirebaseSignaling extends EventEmitter {
205
256
  }
206
257
 
207
258
  /**
208
- * Register this peer's presence in Firestore
259
+ * Register this peer's presence in Firestore with WASM-signed data
209
260
  */
210
261
  async registerPresence() {
211
262
  const { doc, setDoc, serverTimestamp } = this.firebase;
212
263
 
213
264
  const presenceRef = doc(this.db, SIGNALING_PATHS.peers, this.peerId);
214
265
 
215
- // Set online status in Firestore
216
- await setDoc(presenceRef, {
266
+ // Build presence data
267
+ const presenceData = {
217
268
  peerId: this.peerId,
218
269
  room: this.room,
219
270
  online: true,
220
271
  lastSeen: serverTimestamp(),
221
272
  capabilities: ['compute', 'storage', 'verify'],
222
- }, { merge: true });
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
+ }
287
+
288
+ // Set online status in Firestore
289
+ await setDoc(presenceRef, presenceData, { merge: true });
223
290
 
224
291
  // Set up heartbeat to maintain presence (Firestore doesn't have onDisconnect)
225
292
  this._heartbeatInterval = setInterval(async () => {
226
293
  try {
227
- await setDoc(presenceRef, { lastSeen: serverTimestamp() }, { merge: true });
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 });
228
301
  } catch (e) {
229
302
  // Ignore heartbeat errors
230
303
  }
231
304
  }, 30000);
232
305
 
233
- console.log(` 📡 Registered presence: ${this.peerId.slice(0, 8)}...`);
306
+ console.log(` 📡 Registered presence: ${this.peerId.slice(0, 8)}... (WASM-signed)`);
234
307
  }
235
308
 
236
309
  /**
@@ -303,7 +376,7 @@ export class FirebaseSignaling extends EventEmitter {
303
376
  }
304
377
 
305
378
  /**
306
- * Handle incoming signal
379
+ * Handle incoming signal with WASM signature verification
307
380
  */
308
381
  async handleSignal(signal, docId) {
309
382
  this.stats.firebaseSignals++;
@@ -312,19 +385,46 @@ export class FirebaseSignaling extends EventEmitter {
312
385
  const { doc, deleteDoc } = this.firebase;
313
386
  await deleteDoc(doc(this.db, SIGNALING_PATHS.signals, docId));
314
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
+
315
415
  // Emit appropriate event
316
416
  switch (signal.type) {
317
417
  case 'offer':
318
- this.emit('offer', { from: signal.from, offer: signal.data });
418
+ this.emit('offer', { from: signal.from, offer: signal.data, verified: !!signal.signature });
319
419
  break;
320
420
  case 'answer':
321
- this.emit('answer', { from: signal.from, answer: signal.data });
421
+ this.emit('answer', { from: signal.from, answer: signal.data, verified: !!signal.signature });
322
422
  break;
323
423
  case 'ice-candidate':
324
- this.emit('ice-candidate', { from: signal.from, candidate: signal.data });
424
+ this.emit('ice-candidate', { from: signal.from, candidate: signal.data, verified: !!signal.signature });
325
425
  break;
326
426
  default:
327
- this.emit('signal', signal);
427
+ this.emit('signal', { ...signal, verified: !!signal.signature });
328
428
  }
329
429
  }
330
430
 
@@ -350,7 +450,7 @@ export class FirebaseSignaling extends EventEmitter {
350
450
  }
351
451
 
352
452
  /**
353
- * Send signal via Firebase
453
+ * Send signal via Firebase with WASM signature
354
454
  */
355
455
  async sendSignal(toPeerId, type, data) {
356
456
  if (!this.isConnected) {
@@ -362,14 +462,30 @@ export class FirebaseSignaling extends EventEmitter {
362
462
  const signalId = `${this.peerId}-${toPeerId}-${Date.now()}`;
363
463
  const signalRef = doc(this.db, SIGNALING_PATHS.signals, signalId);
364
464
 
365
- await setDoc(signalRef, {
465
+ const timestamp = Date.now();
466
+ const signalData = {
366
467
  from: this.peerId,
367
468
  to: toPeerId,
368
469
  type,
369
470
  data,
370
- timestamp: Date.now(),
471
+ timestamp,
371
472
  room: this.room,
372
- });
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);
373
489
 
374
490
  return true;
375
491
  }
@@ -597,6 +713,10 @@ export class HybridBootstrap extends EventEmitter {
597
713
  this.peerId = options.peerId;
598
714
  this.config = options.firebaseConfig || DEFAULT_FIREBASE_CONFIG;
599
715
 
716
+ // WASM Security
717
+ /** @type {import('./secure-access.js').SecureAccessManager|null} */
718
+ this.secureAccess = options.secureAccess || null;
719
+
600
720
  // Components
601
721
  this.firebase = null;
602
722
  this.dht = null;
@@ -614,25 +734,63 @@ export class HybridBootstrap extends EventEmitter {
614
734
  directConnections: 0,
615
735
  firebaseSignals: 0,
616
736
  p2pSignals: 0,
737
+ verifiedPeers: 0,
617
738
  };
618
739
  }
619
740
 
620
741
  /**
621
- * Start hybrid bootstrap
742
+ * Start hybrid bootstrap with WASM security
622
743
  */
623
744
  async start(webrtc, dht) {
624
745
  this.webrtc = webrtc;
625
746
  this.dht = dht;
626
747
 
627
- // 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
628
766
  this.firebase = new FirebaseSignaling({
629
767
  peerId: this.peerId,
630
768
  firebaseConfig: this.config,
769
+ secureAccess: this.secureAccess,
631
770
  });
632
771
 
633
772
  // Wire up events
634
773
  this.setupFirebaseEvents();
635
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
+
636
794
  // Connect to Firebase
637
795
  const connected = await this.firebase.connect();
638
796
 
@@ -693,18 +851,10 @@ export class HybridBootstrap extends EventEmitter {
693
851
  if (!this.webrtc) return;
694
852
 
695
853
  try {
696
- // Create offer
697
- const offer = await this.webrtc.createOffer(peerId);
698
-
699
- // Try P2P signaling first (if peer is directly connected)
700
- if (this.mode === 'p2p' && this.webrtc.isConnected(peerId)) {
701
- this.webrtc.sendToPeer(peerId, { type: 'offer', offer });
702
- this.stats.p2pSignals++;
703
- } else {
704
- // Fall back to Firebase
705
- await this.firebase.sendOffer(peerId, offer);
706
- this.stats.firebaseSignals++;
707
- }
854
+ // Use WebRTCPeerManager's connectToPeer method
855
+ // This handles offer creation and signaling internally
856
+ await this.webrtc.connectToPeer(peerId);
857
+ this.stats.directConnections++;
708
858
 
709
859
  } catch (error) {
710
860
  console.warn(`[HybridBootstrap] Connect to ${peerId.slice(0, 8)} failed:`, error.message);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@ruvector/edge-net",
3
- "version": "0.4.1",
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": [
@@ -190,6 +198,7 @@
190
198
  "@ruvector/ruvllm": "^0.2.3",
191
199
  "@xenova/transformers": "^2.17.2",
192
200
  "firebase": "^10.14.1",
201
+ "wrtc": "^0.4.7",
193
202
  "ws": "^8.18.3"
194
203
  }
195
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());