@ruvector/edge-net 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deploy/.env.example +97 -0
- package/deploy/DEPLOY.md +481 -0
- package/deploy/Dockerfile +99 -0
- package/deploy/docker-compose.yml +162 -0
- package/deploy/genesis-prod.js +1536 -0
- package/deploy/health-check.js +187 -0
- package/deploy/prometheus.yml +38 -0
- package/firebase-signaling.js +242 -53
- package/package.json +19 -3
- package/real-workers.js +9 -4
- package/scheduler.js +8 -4
- package/secure-access.js +595 -0
- package/tests/distributed-workers-test.js +1609 -0
- package/tests/p2p-migration-test.js +1102 -0
- package/tests/webrtc-peer-test.js +686 -0
- package/webrtc.js +727 -50
package/firebase-signaling.js
CHANGED
|
@@ -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.
|
|
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
|
-
*
|
|
28
|
+
* Edge-Net Public Firebase Configuration
|
|
23
29
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
*
|
|
35
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
53
|
-
return
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
216
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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,43 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
350
450
|
}
|
|
351
451
|
|
|
352
452
|
/**
|
|
353
|
-
*
|
|
453
|
+
* Serialize WebRTC objects to plain JSON for Firebase storage
|
|
454
|
+
* RTCIceCandidate and RTCSessionDescription are not directly storable
|
|
455
|
+
*/
|
|
456
|
+
_serializeWebRTCData(data) {
|
|
457
|
+
if (!data || typeof data !== 'object') {
|
|
458
|
+
return data;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Handle RTCIceCandidate
|
|
462
|
+
if (data.candidate !== undefined && data.sdpMid !== undefined) {
|
|
463
|
+
return {
|
|
464
|
+
candidate: data.candidate,
|
|
465
|
+
sdpMid: data.sdpMid,
|
|
466
|
+
sdpMLineIndex: data.sdpMLineIndex,
|
|
467
|
+
usernameFragment: data.usernameFragment || null,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Handle RTCSessionDescription (offer/answer)
|
|
472
|
+
if (data.type !== undefined && data.sdp !== undefined) {
|
|
473
|
+
return {
|
|
474
|
+
type: data.type,
|
|
475
|
+
sdp: data.sdp,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Try to convert any object with toJSON method
|
|
480
|
+
if (typeof data.toJSON === 'function') {
|
|
481
|
+
return data.toJSON();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Return as-is if already plain object
|
|
485
|
+
return data;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Send signal via Firebase with WASM signature
|
|
354
490
|
*/
|
|
355
491
|
async sendSignal(toPeerId, type, data) {
|
|
356
492
|
if (!this.isConnected) {
|
|
@@ -362,14 +498,33 @@ export class FirebaseSignaling extends EventEmitter {
|
|
|
362
498
|
const signalId = `${this.peerId}-${toPeerId}-${Date.now()}`;
|
|
363
499
|
const signalRef = doc(this.db, SIGNALING_PATHS.signals, signalId);
|
|
364
500
|
|
|
365
|
-
|
|
501
|
+
// Serialize WebRTC objects to plain JSON
|
|
502
|
+
const serializedData = this._serializeWebRTCData(data);
|
|
503
|
+
|
|
504
|
+
const timestamp = Date.now();
|
|
505
|
+
const signalData = {
|
|
366
506
|
from: this.peerId,
|
|
367
507
|
to: toPeerId,
|
|
368
508
|
type,
|
|
369
|
-
data,
|
|
370
|
-
timestamp
|
|
509
|
+
data: serializedData,
|
|
510
|
+
timestamp,
|
|
371
511
|
room: this.room,
|
|
372
|
-
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// Sign the signal with WASM cryptography
|
|
515
|
+
if (this.secureAccess) {
|
|
516
|
+
const signed = this.secureAccess.signMessage({
|
|
517
|
+
from: this.peerId,
|
|
518
|
+
to: toPeerId,
|
|
519
|
+
type,
|
|
520
|
+
data: typeof serializedData === 'object' ? JSON.stringify(serializedData) : serializedData,
|
|
521
|
+
timestamp
|
|
522
|
+
});
|
|
523
|
+
signalData.signature = signed.signature;
|
|
524
|
+
signalData.publicKey = signed.publicKey;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
await setDoc(signalRef, signalData);
|
|
373
528
|
|
|
374
529
|
return true;
|
|
375
530
|
}
|
|
@@ -597,6 +752,10 @@ export class HybridBootstrap extends EventEmitter {
|
|
|
597
752
|
this.peerId = options.peerId;
|
|
598
753
|
this.config = options.firebaseConfig || DEFAULT_FIREBASE_CONFIG;
|
|
599
754
|
|
|
755
|
+
// WASM Security
|
|
756
|
+
/** @type {import('./secure-access.js').SecureAccessManager|null} */
|
|
757
|
+
this.secureAccess = options.secureAccess || null;
|
|
758
|
+
|
|
600
759
|
// Components
|
|
601
760
|
this.firebase = null;
|
|
602
761
|
this.dht = null;
|
|
@@ -614,25 +773,63 @@ export class HybridBootstrap extends EventEmitter {
|
|
|
614
773
|
directConnections: 0,
|
|
615
774
|
firebaseSignals: 0,
|
|
616
775
|
p2pSignals: 0,
|
|
776
|
+
verifiedPeers: 0,
|
|
617
777
|
};
|
|
618
778
|
}
|
|
619
779
|
|
|
620
780
|
/**
|
|
621
|
-
* Start hybrid bootstrap
|
|
781
|
+
* Start hybrid bootstrap with WASM security
|
|
622
782
|
*/
|
|
623
783
|
async start(webrtc, dht) {
|
|
624
784
|
this.webrtc = webrtc;
|
|
625
785
|
this.dht = dht;
|
|
626
786
|
|
|
627
|
-
//
|
|
787
|
+
// Initialize WASM security if not provided
|
|
788
|
+
if (!this.secureAccess) {
|
|
789
|
+
try {
|
|
790
|
+
const { createSecureAccess } = await import('./secure-access.js');
|
|
791
|
+
this.secureAccess = await createSecureAccess({
|
|
792
|
+
siteId: 'edge-net',
|
|
793
|
+
persistIdentity: true
|
|
794
|
+
});
|
|
795
|
+
// Use WASM node ID if peerId not set
|
|
796
|
+
if (!this.peerId) {
|
|
797
|
+
this.peerId = this.secureAccess.getShortId();
|
|
798
|
+
}
|
|
799
|
+
} catch (err) {
|
|
800
|
+
console.log(' ⚠️ WASM security unavailable for bootstrap');
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Start with Firebase, passing WASM security
|
|
628
805
|
this.firebase = new FirebaseSignaling({
|
|
629
806
|
peerId: this.peerId,
|
|
630
807
|
firebaseConfig: this.config,
|
|
808
|
+
secureAccess: this.secureAccess,
|
|
631
809
|
});
|
|
632
810
|
|
|
633
811
|
// Wire up events
|
|
634
812
|
this.setupFirebaseEvents();
|
|
635
813
|
|
|
814
|
+
// Set up WebRTC to use Firebase for signaling
|
|
815
|
+
if (this.webrtc) {
|
|
816
|
+
this.webrtc.setExternalSignaling(async (type, toPeerId, data) => {
|
|
817
|
+
// Route signaling through Firebase
|
|
818
|
+
switch (type) {
|
|
819
|
+
case 'offer':
|
|
820
|
+
await this.firebase.sendOffer(toPeerId, data);
|
|
821
|
+
break;
|
|
822
|
+
case 'answer':
|
|
823
|
+
await this.firebase.sendAnswer(toPeerId, data);
|
|
824
|
+
break;
|
|
825
|
+
case 'ice-candidate':
|
|
826
|
+
await this.firebase.sendIceCandidate(toPeerId, data);
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
this.stats.firebaseSignals++;
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
636
833
|
// Connect to Firebase
|
|
637
834
|
const connected = await this.firebase.connect();
|
|
638
835
|
|
|
@@ -693,18 +890,10 @@ export class HybridBootstrap extends EventEmitter {
|
|
|
693
890
|
if (!this.webrtc) return;
|
|
694
891
|
|
|
695
892
|
try {
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
}
|
|
893
|
+
// Use WebRTCPeerManager's connectToPeer method
|
|
894
|
+
// This handles offer creation and signaling internally
|
|
895
|
+
await this.webrtc.connectToPeer(peerId);
|
|
896
|
+
this.stats.directConnections++;
|
|
708
897
|
|
|
709
898
|
} catch (error) {
|
|
710
899
|
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.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Distributed compute intelligence network with
|
|
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",
|
|
@@ -73,6 +77,8 @@
|
|
|
73
77
|
"ruvector_edge_net.d.ts",
|
|
74
78
|
"ruvector_edge_net_bg.wasm.d.ts",
|
|
75
79
|
"node/",
|
|
80
|
+
"deploy/",
|
|
81
|
+
"tests/",
|
|
76
82
|
"index.js",
|
|
77
83
|
"cli.js",
|
|
78
84
|
"join.js",
|
|
@@ -96,6 +102,7 @@
|
|
|
96
102
|
"p2p.js",
|
|
97
103
|
"firebase-signaling.js",
|
|
98
104
|
"firebase-setup.js",
|
|
105
|
+
"secure-access.js",
|
|
99
106
|
"README.md",
|
|
100
107
|
"LICENSE"
|
|
101
108
|
],
|
|
@@ -160,6 +167,9 @@
|
|
|
160
167
|
},
|
|
161
168
|
"./firebase-setup": {
|
|
162
169
|
"import": "./firebase-setup.js"
|
|
170
|
+
},
|
|
171
|
+
"./secure-access": {
|
|
172
|
+
"import": "./secure-access.js"
|
|
163
173
|
}
|
|
164
174
|
},
|
|
165
175
|
"sideEffects": [
|
|
@@ -181,6 +191,11 @@
|
|
|
181
191
|
"signaling": "node -e \"import('./signaling.js').then(m => new m.SignalingServer().start())\"",
|
|
182
192
|
"genesis": "node genesis.js",
|
|
183
193
|
"genesis:start": "node genesis.js --port 8787",
|
|
194
|
+
"genesis:prod": "NODE_ENV=production node deploy/genesis-prod.js",
|
|
195
|
+
"genesis:docker": "docker-compose -f deploy/docker-compose.yml up -d",
|
|
196
|
+
"genesis:docker:logs": "docker-compose -f deploy/docker-compose.yml logs -f genesis",
|
|
197
|
+
"genesis:docker:stop": "docker-compose -f deploy/docker-compose.yml down",
|
|
198
|
+
"genesis:docker:build": "docker build -t ruvector/edge-net-genesis:latest -f deploy/Dockerfile .",
|
|
184
199
|
"p2p": "node -e \"import('./p2p.js').then(m => m.createP2PNetwork({ nodeId: 'test' }))\"",
|
|
185
200
|
"monitor": "node -e \"import('./monitor.js').then(m => { const mon = new m.Monitor(); mon.start(); setInterval(() => console.log(JSON.stringify(mon.generateReport(), null, 2)), 5000); })\"",
|
|
186
201
|
"firebase:setup": "node firebase-setup.js",
|
|
@@ -190,6 +205,7 @@
|
|
|
190
205
|
"@ruvector/ruvllm": "^0.2.3",
|
|
191
206
|
"@xenova/transformers": "^2.17.2",
|
|
192
207
|
"firebase": "^10.14.1",
|
|
208
|
+
"wrtc": "^0.4.7",
|
|
193
209
|
"ws": "^8.18.3"
|
|
194
210
|
}
|
|
195
211
|
}
|
package/real-workers.js
CHANGED
|
@@ -295,6 +295,7 @@ export class RealWorkerPool extends EventEmitter {
|
|
|
295
295
|
status: 'idle',
|
|
296
296
|
tasksCompleted: 0,
|
|
297
297
|
currentTask: null,
|
|
298
|
+
terminated: false, // Track intentional termination
|
|
298
299
|
};
|
|
299
300
|
|
|
300
301
|
worker.on('message', (msg) => {
|
|
@@ -307,13 +308,16 @@ export class RealWorkerPool extends EventEmitter {
|
|
|
307
308
|
});
|
|
308
309
|
|
|
309
310
|
worker.on('exit', (code) => {
|
|
310
|
-
if (
|
|
311
|
-
|
|
311
|
+
// Only respawn if worker crashed unexpectedly (not terminated intentionally)
|
|
312
|
+
if (!workerInfo.terminated && this.status === 'ready') {
|
|
313
|
+
console.error(`[Worker ${index}] Exited unexpectedly with code ${code}, respawning...`);
|
|
312
314
|
// Respawn worker
|
|
313
315
|
const idx = this.workers.indexOf(workerInfo);
|
|
314
|
-
if (idx >= 0
|
|
316
|
+
if (idx >= 0) {
|
|
315
317
|
this.spawnWorker(index).then(w => {
|
|
316
318
|
this.workers[idx] = w;
|
|
319
|
+
}).catch(err => {
|
|
320
|
+
console.error(`[Worker ${index}] Failed to respawn:`, err.message);
|
|
317
321
|
});
|
|
318
322
|
}
|
|
319
323
|
}
|
|
@@ -537,8 +541,9 @@ export class RealWorkerPool extends EventEmitter {
|
|
|
537
541
|
await new Promise(r => setTimeout(r, 100));
|
|
538
542
|
}
|
|
539
543
|
|
|
540
|
-
// Terminate workers
|
|
544
|
+
// Terminate workers (mark as intentionally terminated first)
|
|
541
545
|
for (const workerInfo of this.workers) {
|
|
546
|
+
workerInfo.terminated = true;
|
|
542
547
|
await workerInfo.worker.terminate();
|
|
543
548
|
}
|
|
544
549
|
|
package/scheduler.js
CHANGED
|
@@ -596,11 +596,15 @@ export class TaskScheduler extends EventEmitter {
|
|
|
596
596
|
worker.allocate(task);
|
|
597
597
|
this.running.set(task.id, task);
|
|
598
598
|
|
|
599
|
-
// Calculate wait time
|
|
599
|
+
// Calculate wait time using running average
|
|
600
600
|
const waitTime = task.startedAt - task.queuedAt;
|
|
601
|
-
this.stats.
|
|
602
|
-
|
|
603
|
-
|
|
601
|
+
const assignedCount = this.stats.completed + this.running.size;
|
|
602
|
+
if (assignedCount <= 1) {
|
|
603
|
+
this.stats.avgWaitTime = waitTime;
|
|
604
|
+
} else {
|
|
605
|
+
this.stats.avgWaitTime =
|
|
606
|
+
(this.stats.avgWaitTime * (assignedCount - 1) + waitTime) / assignedCount;
|
|
607
|
+
}
|
|
604
608
|
|
|
605
609
|
this.emit('task-assigned', {
|
|
606
610
|
taskId: task.id,
|