@ruvector/edge-net 0.1.1 → 0.1.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.
package/join.html ADDED
@@ -0,0 +1,985 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Join Edge-Net | RuVector Distributed Compute</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0a0a0f;
10
+ --surface: #12121a;
11
+ --border: #2a2a3a;
12
+ --primary: #6366f1;
13
+ --primary-hover: #818cf8;
14
+ --success: #22c55e;
15
+ --warning: #f59e0b;
16
+ --text: #e2e8f0;
17
+ --text-muted: #94a3b8;
18
+ }
19
+ * { box-sizing: border-box; margin: 0; padding: 0; }
20
+ body {
21
+ font-family: 'SF Mono', 'Fira Code', monospace;
22
+ background: var(--bg);
23
+ color: var(--text);
24
+ min-height: 100vh;
25
+ padding: 2rem;
26
+ }
27
+ .container {
28
+ max-width: 800px;
29
+ margin: 0 auto;
30
+ }
31
+ header {
32
+ text-align: center;
33
+ margin-bottom: 2rem;
34
+ }
35
+ h1 {
36
+ font-size: 2rem;
37
+ background: linear-gradient(135deg, var(--primary), var(--success));
38
+ -webkit-background-clip: text;
39
+ -webkit-text-fill-color: transparent;
40
+ margin-bottom: 0.5rem;
41
+ }
42
+ .subtitle {
43
+ color: var(--text-muted);
44
+ font-size: 0.9rem;
45
+ }
46
+ .card {
47
+ background: var(--surface);
48
+ border: 1px solid var(--border);
49
+ border-radius: 12px;
50
+ padding: 1.5rem;
51
+ margin-bottom: 1.5rem;
52
+ }
53
+ .card h2 {
54
+ font-size: 1rem;
55
+ color: var(--primary);
56
+ margin-bottom: 1rem;
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 0.5rem;
60
+ }
61
+ .form-group {
62
+ margin-bottom: 1rem;
63
+ }
64
+ label {
65
+ display: block;
66
+ font-size: 0.85rem;
67
+ color: var(--text-muted);
68
+ margin-bottom: 0.5rem;
69
+ }
70
+ input[type="text"], input[type="password"] {
71
+ width: 100%;
72
+ padding: 0.75rem;
73
+ background: var(--bg);
74
+ border: 1px solid var(--border);
75
+ border-radius: 8px;
76
+ color: var(--text);
77
+ font-family: inherit;
78
+ font-size: 0.9rem;
79
+ }
80
+ input:focus {
81
+ outline: none;
82
+ border-color: var(--primary);
83
+ }
84
+ .btn {
85
+ display: inline-flex;
86
+ align-items: center;
87
+ gap: 0.5rem;
88
+ padding: 0.75rem 1.5rem;
89
+ background: var(--primary);
90
+ color: white;
91
+ border: none;
92
+ border-radius: 8px;
93
+ font-family: inherit;
94
+ font-size: 0.9rem;
95
+ cursor: pointer;
96
+ transition: background 0.2s;
97
+ }
98
+ .btn:hover { background: var(--primary-hover); }
99
+ .btn:disabled {
100
+ opacity: 0.5;
101
+ cursor: not-allowed;
102
+ }
103
+ .btn-secondary {
104
+ background: transparent;
105
+ border: 1px solid var(--border);
106
+ }
107
+ .btn-secondary:hover {
108
+ background: var(--surface);
109
+ }
110
+ .identity-display {
111
+ background: var(--bg);
112
+ border: 1px solid var(--border);
113
+ border-radius: 8px;
114
+ padding: 1rem;
115
+ font-size: 0.85rem;
116
+ }
117
+ .identity-row {
118
+ display: flex;
119
+ justify-content: space-between;
120
+ padding: 0.5rem 0;
121
+ border-bottom: 1px solid var(--border);
122
+ }
123
+ .identity-row:last-child { border-bottom: none; }
124
+ .identity-label { color: var(--text-muted); }
125
+ .identity-value {
126
+ font-weight: 600;
127
+ word-break: break-all;
128
+ }
129
+ .pi-key { color: var(--success); }
130
+ .status {
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 0.5rem;
134
+ padding: 0.75rem 1rem;
135
+ border-radius: 8px;
136
+ margin-bottom: 1rem;
137
+ font-size: 0.85rem;
138
+ }
139
+ .status.info { background: rgba(99, 102, 241, 0.1); border: 1px solid var(--primary); }
140
+ .status.success { background: rgba(34, 197, 94, 0.1); border: 1px solid var(--success); }
141
+ .status.warning { background: rgba(245, 158, 11, 0.1); border: 1px solid var(--warning); }
142
+ .network-stats {
143
+ display: grid;
144
+ grid-template-columns: repeat(3, 1fr);
145
+ gap: 1rem;
146
+ margin-top: 1rem;
147
+ }
148
+ .stat {
149
+ text-align: center;
150
+ padding: 1rem;
151
+ background: var(--bg);
152
+ border-radius: 8px;
153
+ }
154
+ .stat-value {
155
+ font-size: 1.5rem;
156
+ font-weight: bold;
157
+ color: var(--primary);
158
+ }
159
+ .stat-label {
160
+ font-size: 0.75rem;
161
+ color: var(--text-muted);
162
+ margin-top: 0.25rem;
163
+ }
164
+ .contribution-log {
165
+ max-height: 200px;
166
+ overflow-y: auto;
167
+ background: var(--bg);
168
+ border-radius: 8px;
169
+ padding: 1rem;
170
+ font-size: 0.8rem;
171
+ }
172
+ .log-entry {
173
+ padding: 0.25rem 0;
174
+ color: var(--text-muted);
175
+ }
176
+ .log-entry.success { color: var(--success); }
177
+ .log-entry.highlight { color: var(--primary); }
178
+ .hidden { display: none; }
179
+ .actions {
180
+ display: flex;
181
+ gap: 1rem;
182
+ flex-wrap: wrap;
183
+ }
184
+ #qr-code {
185
+ display: flex;
186
+ justify-content: center;
187
+ padding: 1rem;
188
+ background: white;
189
+ border-radius: 8px;
190
+ margin-top: 1rem;
191
+ }
192
+ .crypto-badge {
193
+ display: inline-flex;
194
+ align-items: center;
195
+ gap: 0.25rem;
196
+ padding: 0.25rem 0.5rem;
197
+ background: rgba(34, 197, 94, 0.1);
198
+ border: 1px solid var(--success);
199
+ border-radius: 4px;
200
+ font-size: 0.7rem;
201
+ color: var(--success);
202
+ }
203
+ </style>
204
+ </head>
205
+ <body>
206
+ <div class="container">
207
+ <header>
208
+ <h1>🌐 Edge-Net Join</h1>
209
+ <p class="subtitle">Contribute browser compute, earn credits</p>
210
+ <div style="margin-top: 0.5rem;">
211
+ <span class="crypto-badge">🔐 Ed25519</span>
212
+ <span class="crypto-badge">🛡️ Argon2id</span>
213
+ <span class="crypto-badge">🔒 AES-256-GCM</span>
214
+ </div>
215
+ </header>
216
+
217
+ <!-- Step 1: Generate or Restore Identity -->
218
+ <div class="card" id="identity-section">
219
+ <h2>🔑 Your Identity</h2>
220
+
221
+ <div id="no-identity">
222
+ <div class="status info">
223
+ <span>ℹ️</span>
224
+ <span>Create a new identity or restore an existing one to join the network.</span>
225
+ </div>
226
+
227
+ <div class="form-group">
228
+ <label for="site-id">Site ID (your unique identifier)</label>
229
+ <input type="text" id="site-id" placeholder="e.g., alice, bob, node-42" />
230
+ </div>
231
+
232
+ <div class="form-group">
233
+ <label for="password">Password (for encrypted backup)</label>
234
+ <input type="password" id="password" placeholder="Strong password for identity encryption" />
235
+ </div>
236
+
237
+ <div class="actions">
238
+ <button class="btn" id="generate-btn" onclick="generateIdentity()">
239
+ <span>✨</span> Generate New Identity
240
+ </button>
241
+ <button class="btn btn-secondary" onclick="document.getElementById('restore-file').click()">
242
+ <span>📥</span> Restore from Backup
243
+ </button>
244
+ <input type="file" id="restore-file" class="hidden" accept=".identity" onchange="restoreIdentity(event)" />
245
+ </div>
246
+ </div>
247
+
248
+ <div id="has-identity" class="hidden">
249
+ <div class="status success">
250
+ <span>✅</span>
251
+ <span>Identity active and connected to network</span>
252
+ </div>
253
+
254
+ <div class="identity-display">
255
+ <div class="identity-row">
256
+ <span class="identity-label">Site ID</span>
257
+ <span class="identity-value" id="display-site-id">-</span>
258
+ </div>
259
+ <div class="identity-row">
260
+ <span class="identity-label">Pi-Key</span>
261
+ <span class="identity-value pi-key" id="display-pi-key">-</span>
262
+ </div>
263
+ <div class="identity-row">
264
+ <span class="identity-label">Public Key</span>
265
+ <span class="identity-value" id="display-pubkey">-</span>
266
+ </div>
267
+ <div class="identity-row">
268
+ <span class="identity-label">Created</span>
269
+ <span class="identity-value" id="display-created">-</span>
270
+ </div>
271
+ </div>
272
+
273
+ <div class="actions" style="margin-top: 1rem;">
274
+ <button class="btn btn-secondary" onclick="exportIdentity()">
275
+ <span>📤</span> Export Backup
276
+ </button>
277
+ <button class="btn btn-secondary" onclick="copyPublicKey()">
278
+ <span>📋</span> Copy Public Key
279
+ </button>
280
+ <button class="btn btn-secondary" onclick="showQR()">
281
+ <span>📱</span> Show QR
282
+ </button>
283
+ </div>
284
+ <div id="qr-code" class="hidden"></div>
285
+ </div>
286
+ </div>
287
+
288
+ <!-- Step 2: Network Status -->
289
+ <div class="card" id="network-section">
290
+ <h2>📡 Network Status</h2>
291
+
292
+ <div class="network-stats">
293
+ <div class="stat">
294
+ <div class="stat-value" id="stat-peers">0</div>
295
+ <div class="stat-label">Connected Peers</div>
296
+ </div>
297
+ <div class="stat">
298
+ <div class="stat-value" id="stat-contributions">0</div>
299
+ <div class="stat-label">Contributions</div>
300
+ </div>
301
+ <div class="stat">
302
+ <div class="stat-value" id="stat-credits">0</div>
303
+ <div class="stat-label">Credits Earned</div>
304
+ </div>
305
+ </div>
306
+
307
+ <div class="contribution-log" id="contribution-log">
308
+ <div class="log-entry">Waiting for identity...</div>
309
+ </div>
310
+ </div>
311
+
312
+ <!-- Step 3: Contribute -->
313
+ <div class="card" id="contribute-section">
314
+ <h2>⚡ Contribute Compute</h2>
315
+
316
+ <div class="status warning" id="contribute-status">
317
+ <span>⏳</span>
318
+ <span>Generate or restore identity to start contributing</span>
319
+ </div>
320
+
321
+ <div class="actions">
322
+ <button class="btn" id="start-btn" disabled onclick="startContributing()">
323
+ <span>▶️</span> Start Contributing
324
+ </button>
325
+ <button class="btn btn-secondary" id="stop-btn" disabled onclick="stopContributing()">
326
+ <span>⏹️</span> Stop
327
+ </button>
328
+ </div>
329
+ </div>
330
+ </div>
331
+
332
+ <script type="module">
333
+ // Import WASM module
334
+ import init, * as wasm from './ruvector_edge_net.js';
335
+
336
+ let wasmModule = null;
337
+ let identity = null;
338
+ let contributing = false;
339
+ let contributionCount = 0;
340
+ let creditsEarned = 0;
341
+ let peerCount = 0;
342
+
343
+ // Initialize WASM
344
+ async function initWasm() {
345
+ try {
346
+ await init();
347
+ wasmModule = wasm;
348
+ log('WASM module loaded', 'success');
349
+ checkStoredIdentity();
350
+ } catch (err) {
351
+ log('Failed to load WASM: ' + err.message, 'error');
352
+ }
353
+ }
354
+
355
+ // Check for stored identity in localStorage
356
+ function checkStoredIdentity() {
357
+ const stored = localStorage.getItem('edge-net-identity');
358
+ if (stored) {
359
+ try {
360
+ identity = JSON.parse(stored);
361
+ showIdentity();
362
+ log('Identity restored from storage', 'success');
363
+ } catch (e) {
364
+ log('Stored identity corrupted', 'error');
365
+ }
366
+ }
367
+ }
368
+
369
+ // Generate new identity
370
+ window.generateIdentity = async function() {
371
+ const siteId = document.getElementById('site-id').value.trim();
372
+ const password = document.getElementById('password').value;
373
+
374
+ if (!siteId) {
375
+ alert('Please enter a Site ID');
376
+ return;
377
+ }
378
+ if (password.length < 8) {
379
+ alert('Password must be at least 8 characters');
380
+ return;
381
+ }
382
+
383
+ document.getElementById('generate-btn').disabled = true;
384
+ log('Generating identity...', 'highlight');
385
+
386
+ try {
387
+ // Generate Pi-Key identity using WASM
388
+ const piKeyData = wasmModule.generate_pi_key();
389
+
390
+ // Create identity object
391
+ identity = {
392
+ siteId: siteId,
393
+ piKey: arrayToHex(piKeyData.pi_key).slice(0, 20),
394
+ publicKey: arrayToHex(piKeyData.public_key),
395
+ created: new Date().toISOString(),
396
+ sessions: 1,
397
+ contributions: [],
398
+ // Store encrypted private key for backup
399
+ encryptedPrivateKey: await encryptData(piKeyData.private_key, password)
400
+ };
401
+
402
+ // Save to localStorage
403
+ localStorage.setItem('edge-net-identity', JSON.stringify(identity));
404
+ localStorage.setItem('edge-net-password-hint', password.length.toString());
405
+
406
+ showIdentity();
407
+ log('Identity generated: π:' + identity.piKey, 'success');
408
+
409
+ // Announce to network
410
+ announceToNetwork();
411
+
412
+ } catch (err) {
413
+ log('Generation failed: ' + err.message, 'error');
414
+ }
415
+
416
+ document.getElementById('generate-btn').disabled = false;
417
+ };
418
+
419
+ // Show identity UI
420
+ function showIdentity() {
421
+ document.getElementById('no-identity').classList.add('hidden');
422
+ document.getElementById('has-identity').classList.remove('hidden');
423
+
424
+ document.getElementById('display-site-id').textContent = identity.siteId;
425
+ document.getElementById('display-pi-key').textContent = 'π:' + identity.piKey;
426
+ document.getElementById('display-pubkey').textContent = identity.publicKey.slice(0, 16) + '...';
427
+ document.getElementById('display-created').textContent = new Date(identity.created).toLocaleDateString();
428
+
429
+ document.getElementById('start-btn').disabled = false;
430
+ document.getElementById('contribute-status').innerHTML = '<span>✅</span><span>Ready to contribute compute to the network</span>';
431
+ document.getElementById('contribute-status').className = 'status success';
432
+ }
433
+
434
+ // Export encrypted identity backup
435
+ window.exportIdentity = async function() {
436
+ const password = prompt('Enter password to encrypt backup:');
437
+ if (!password) return;
438
+
439
+ const backup = {
440
+ version: 1,
441
+ identity: identity,
442
+ exported: new Date().toISOString()
443
+ };
444
+
445
+ const encrypted = await encryptData(JSON.stringify(backup), password);
446
+ const blob = new Blob([encrypted], { type: 'application/octet-stream' });
447
+ const url = URL.createObjectURL(blob);
448
+
449
+ const a = document.createElement('a');
450
+ a.href = url;
451
+ a.download = `${identity.siteId}.identity`;
452
+ a.click();
453
+
454
+ URL.revokeObjectURL(url);
455
+ log('Identity exported to ' + identity.siteId + '.identity', 'success');
456
+ };
457
+
458
+ // Restore identity from backup
459
+ window.restoreIdentity = async function(event) {
460
+ const file = event.target.files[0];
461
+ if (!file) return;
462
+
463
+ const password = prompt('Enter backup password:');
464
+ if (!password) return;
465
+
466
+ try {
467
+ const encrypted = await file.text();
468
+ const decrypted = await decryptData(encrypted, password);
469
+ const backup = JSON.parse(decrypted);
470
+
471
+ identity = backup.identity;
472
+ identity.sessions = (identity.sessions || 0) + 1;
473
+
474
+ localStorage.setItem('edge-net-identity', JSON.stringify(identity));
475
+ showIdentity();
476
+
477
+ log('Identity restored: π:' + identity.piKey, 'success');
478
+ announceToNetwork();
479
+
480
+ } catch (err) {
481
+ alert('Failed to restore: ' + err.message);
482
+ }
483
+ };
484
+
485
+ // Copy public key
486
+ window.copyPublicKey = function() {
487
+ navigator.clipboard.writeText(identity.publicKey);
488
+ log('Public key copied to clipboard', 'success');
489
+ };
490
+
491
+ // Show QR code
492
+ window.showQR = function() {
493
+ const qrDiv = document.getElementById('qr-code');
494
+ if (qrDiv.classList.contains('hidden')) {
495
+ // Simple text QR representation (in production, use a QR library)
496
+ qrDiv.innerHTML = `<div style="text-align: center; color: #000;">
497
+ <div style="font-size: 0.8rem; margin-bottom: 0.5rem;">Scan to verify</div>
498
+ <div style="font-family: monospace; font-size: 0.7rem; word-break: break-all; max-width: 200px;">
499
+ ${identity.publicKey}
500
+ </div>
501
+ </div>`;
502
+ qrDiv.classList.remove('hidden');
503
+ } else {
504
+ qrDiv.classList.add('hidden');
505
+ }
506
+ };
507
+
508
+ // Start contributing compute
509
+ window.startContributing = function() {
510
+ if (!identity) return;
511
+
512
+ contributing = true;
513
+ document.getElementById('start-btn').disabled = true;
514
+ document.getElementById('stop-btn').disabled = false;
515
+
516
+ log('Starting compute contribution...', 'highlight');
517
+ contributeLoop();
518
+ };
519
+
520
+ // Stop contributing
521
+ window.stopContributing = function() {
522
+ contributing = false;
523
+ document.getElementById('start-btn').disabled = false;
524
+ document.getElementById('stop-btn').disabled = true;
525
+ log('Compute contribution stopped', 'warning');
526
+ };
527
+
528
+ // Contribution loop
529
+ async function contributeLoop() {
530
+ while (contributing) {
531
+ try {
532
+ // Simulate compute task
533
+ const taskId = Math.random().toString(36).slice(2, 10);
534
+ log(`Processing task ${taskId}...`);
535
+
536
+ // Do actual WASM computation
537
+ const start = performance.now();
538
+
539
+ // Vector computation task
540
+ const vectors = [];
541
+ for (let i = 0; i < 100; i++) {
542
+ vectors.push(new Float32Array(128).map(() => Math.random()));
543
+ }
544
+
545
+ // Compute similarities (actual work)
546
+ let computed = 0;
547
+ for (let i = 0; i < vectors.length; i++) {
548
+ for (let j = i + 1; j < vectors.length; j++) {
549
+ dotProduct(vectors[i], vectors[j]);
550
+ computed++;
551
+ }
552
+ }
553
+
554
+ const elapsed = performance.now() - start;
555
+
556
+ // Record contribution
557
+ contributionCount++;
558
+ const credits = Math.floor(computed / 100);
559
+ creditsEarned += credits;
560
+
561
+ // Update stats
562
+ document.getElementById('stat-contributions').textContent = contributionCount;
563
+ document.getElementById('stat-credits').textContent = creditsEarned;
564
+
565
+ // Save contribution
566
+ identity.contributions.push({
567
+ taskId,
568
+ computed,
569
+ credits,
570
+ timestamp: Date.now()
571
+ });
572
+ localStorage.setItem('edge-net-identity', JSON.stringify(identity));
573
+
574
+ log(`Task ${taskId} complete: ${computed} ops, +${credits} credits (${elapsed.toFixed(1)}ms)`, 'success');
575
+
576
+ // Wait before next task
577
+ await sleep(2000);
578
+
579
+ } catch (err) {
580
+ log('Task error: ' + err.message, 'error');
581
+ await sleep(5000);
582
+ }
583
+ }
584
+ }
585
+
586
+ // ========================================
587
+ // Real WebRTC P2P Implementation
588
+ // ========================================
589
+
590
+ // WebRTC Configuration
591
+ const WEBRTC_CONFIG = {
592
+ iceServers: [
593
+ { urls: 'stun:stun.l.google.com:19302' },
594
+ { urls: 'stun:stun1.l.google.com:19302' },
595
+ ]
596
+ };
597
+
598
+ // Relay server for signaling
599
+ const RELAY_URL = window.location.hostname === 'localhost'
600
+ ? 'ws://localhost:8080'
601
+ : 'wss://edge-net-relay.ruvector.dev';
602
+
603
+ let signalingSocket = null;
604
+ const peerConnections = new Map(); // peerId -> RTCPeerConnection
605
+ const dataChannels = new Map(); // peerId -> RTCDataChannel
606
+
607
+ // Connect to signaling server and announce
608
+ async function announceToNetwork() {
609
+ log('Connecting to network...', 'highlight');
610
+
611
+ try {
612
+ signalingSocket = new WebSocket(RELAY_URL);
613
+
614
+ signalingSocket.onopen = () => {
615
+ log('Connected to relay server', 'success');
616
+
617
+ // Register with network
618
+ signalingSocket.send(JSON.stringify({
619
+ type: 'register',
620
+ nodeId: identity.piKey,
621
+ publicKey: identity.publicKey,
622
+ siteId: identity.siteId,
623
+ }));
624
+ };
625
+
626
+ signalingSocket.onmessage = async (event) => {
627
+ try {
628
+ const message = JSON.parse(event.data);
629
+ await handleSignalingMessage(message);
630
+ } catch (err) {
631
+ console.error('Message error:', err);
632
+ }
633
+ };
634
+
635
+ signalingSocket.onclose = () => {
636
+ log('Disconnected from relay', 'warning');
637
+ // Attempt reconnection after delay
638
+ setTimeout(announceToNetwork, 5000);
639
+ };
640
+
641
+ signalingSocket.onerror = (err) => {
642
+ log('Relay connection error', 'error');
643
+ console.error('WebSocket error:', err);
644
+ };
645
+
646
+ } catch (err) {
647
+ log('Failed to connect: ' + err.message, 'error');
648
+ // Fallback to simulation
649
+ simulatePeers();
650
+ }
651
+ }
652
+
653
+ // Handle signaling messages
654
+ async function handleSignalingMessage(message) {
655
+ switch (message.type) {
656
+ case 'welcome':
657
+ log(`Registered as ${message.nodeId}`, 'success');
658
+ peerCount = message.peers?.length || 0;
659
+ document.getElementById('stat-peers').textContent = peerCount;
660
+
661
+ // Connect to existing peers
662
+ if (message.peers) {
663
+ for (const peerId of message.peers) {
664
+ if (identity.piKey > peerId) {
665
+ await initiatePeerConnection(peerId);
666
+ }
667
+ }
668
+ }
669
+ break;
670
+
671
+ case 'node_joined':
672
+ log(`Peer joined: ${message.nodeId.slice(0, 8)}...`, 'success');
673
+ peerCount++;
674
+ document.getElementById('stat-peers').textContent = peerCount;
675
+
676
+ // Initiate connection if we have higher ID
677
+ if (identity.piKey > message.nodeId) {
678
+ await initiatePeerConnection(message.nodeId);
679
+ }
680
+ break;
681
+
682
+ case 'node_left':
683
+ log(`Peer left: ${message.nodeId.slice(0, 8)}...`);
684
+ closePeerConnection(message.nodeId);
685
+ peerCount = Math.max(0, peerCount - 1);
686
+ document.getElementById('stat-peers').textContent = peerCount;
687
+ break;
688
+
689
+ case 'webrtc_offer':
690
+ await handleOffer(message.from, message.offer);
691
+ break;
692
+
693
+ case 'webrtc_answer':
694
+ await handleAnswer(message.from, message.answer);
695
+ break;
696
+
697
+ case 'webrtc_ice':
698
+ await handleIceCandidate(message.from, message.candidate);
699
+ break;
700
+
701
+ case 'webrtc_disconnect':
702
+ closePeerConnection(message.from);
703
+ break;
704
+
705
+ case 'time_crystal_sync':
706
+ // Update network phase
707
+ break;
708
+ }
709
+ }
710
+
711
+ // Initiate WebRTC connection to peer
712
+ async function initiatePeerConnection(peerId) {
713
+ if (peerConnections.has(peerId)) return;
714
+
715
+ log(`Connecting to ${peerId.slice(0, 8)}...`);
716
+
717
+ const pc = new RTCPeerConnection(WEBRTC_CONFIG);
718
+ peerConnections.set(peerId, pc);
719
+
720
+ setupPeerConnection(pc, peerId);
721
+
722
+ // Create data channel
723
+ const channel = pc.createDataChannel('edge-net', {
724
+ ordered: true,
725
+ maxRetransmits: 3,
726
+ });
727
+ setupDataChannel(channel, peerId);
728
+
729
+ // Create and send offer
730
+ const offer = await pc.createOffer();
731
+ await pc.setLocalDescription(offer);
732
+
733
+ signalingSocket.send(JSON.stringify({
734
+ type: 'webrtc_offer',
735
+ targetId: peerId,
736
+ offer: offer,
737
+ }));
738
+ }
739
+
740
+ // Handle incoming offer
741
+ async function handleOffer(peerId, offer) {
742
+ log(`Offer from ${peerId.slice(0, 8)}...`);
743
+
744
+ const pc = new RTCPeerConnection(WEBRTC_CONFIG);
745
+ peerConnections.set(peerId, pc);
746
+
747
+ setupPeerConnection(pc, peerId);
748
+
749
+ // Handle incoming data channel
750
+ pc.ondatachannel = (event) => {
751
+ setupDataChannel(event.channel, peerId);
752
+ };
753
+
754
+ await pc.setRemoteDescription(new RTCSessionDescription(offer));
755
+ const answer = await pc.createAnswer();
756
+ await pc.setLocalDescription(answer);
757
+
758
+ signalingSocket.send(JSON.stringify({
759
+ type: 'webrtc_answer',
760
+ targetId: peerId,
761
+ answer: answer,
762
+ }));
763
+ }
764
+
765
+ // Handle incoming answer
766
+ async function handleAnswer(peerId, answer) {
767
+ const pc = peerConnections.get(peerId);
768
+ if (pc) {
769
+ await pc.setRemoteDescription(new RTCSessionDescription(answer));
770
+ }
771
+ }
772
+
773
+ // Handle ICE candidate
774
+ async function handleIceCandidate(peerId, candidate) {
775
+ const pc = peerConnections.get(peerId);
776
+ if (pc && candidate) {
777
+ try {
778
+ await pc.addIceCandidate(new RTCIceCandidate(candidate));
779
+ } catch (err) {
780
+ console.warn('ICE candidate error:', err);
781
+ }
782
+ }
783
+ }
784
+
785
+ // Setup peer connection event handlers
786
+ function setupPeerConnection(pc, peerId) {
787
+ pc.onicecandidate = (event) => {
788
+ if (event.candidate && signalingSocket?.readyState === WebSocket.OPEN) {
789
+ signalingSocket.send(JSON.stringify({
790
+ type: 'webrtc_ice',
791
+ targetId: peerId,
792
+ candidate: event.candidate,
793
+ }));
794
+ }
795
+ };
796
+
797
+ pc.oniceconnectionstatechange = () => {
798
+ const state = pc.iceConnectionState;
799
+ if (state === 'connected') {
800
+ log(`P2P connected: ${peerId.slice(0, 8)}...`, 'success');
801
+ } else if (state === 'disconnected' || state === 'failed') {
802
+ closePeerConnection(peerId);
803
+ }
804
+ };
805
+ }
806
+
807
+ // Setup data channel event handlers
808
+ function setupDataChannel(channel, peerId) {
809
+ dataChannels.set(peerId, channel);
810
+
811
+ channel.onopen = () => {
812
+ log(`Data channel open: ${peerId.slice(0, 8)}...`, 'success');
813
+ };
814
+
815
+ channel.onmessage = (event) => {
816
+ try {
817
+ const message = JSON.parse(event.data);
818
+ handleP2PMessage(peerId, message);
819
+ } catch (err) {
820
+ console.error('P2P message error:', err);
821
+ }
822
+ };
823
+
824
+ channel.onclose = () => {
825
+ dataChannels.delete(peerId);
826
+ };
827
+ }
828
+
829
+ // Handle P2P messages over data channel
830
+ function handleP2PMessage(peerId, message) {
831
+ if (message.type === 'task') {
832
+ log(`Task from ${peerId.slice(0, 8)}: ${message.taskId}`);
833
+ } else if (message.type === 'result') {
834
+ log(`Result from ${peerId.slice(0, 8)}: ${message.taskId}`, 'success');
835
+ }
836
+ }
837
+
838
+ // Send message to peer via data channel
839
+ function sendToPeer(peerId, message) {
840
+ const channel = dataChannels.get(peerId);
841
+ if (channel && channel.readyState === 'open') {
842
+ channel.send(JSON.stringify(message));
843
+ return true;
844
+ }
845
+ return false;
846
+ }
847
+
848
+ // Broadcast to all connected peers
849
+ function broadcastToPeers(message) {
850
+ let sent = 0;
851
+ for (const [peerId, channel] of dataChannels) {
852
+ if (channel.readyState === 'open') {
853
+ channel.send(JSON.stringify(message));
854
+ sent++;
855
+ }
856
+ }
857
+ return sent;
858
+ }
859
+
860
+ // Close peer connection
861
+ function closePeerConnection(peerId) {
862
+ const channel = dataChannels.get(peerId);
863
+ if (channel) {
864
+ channel.close();
865
+ dataChannels.delete(peerId);
866
+ }
867
+
868
+ const pc = peerConnections.get(peerId);
869
+ if (pc) {
870
+ pc.close();
871
+ peerConnections.delete(peerId);
872
+ }
873
+ }
874
+
875
+ // Fallback simulation mode
876
+ function simulatePeers() {
877
+ log('Using simulation mode (offline)', 'warning');
878
+ peerCount = 3;
879
+ document.getElementById('stat-peers').textContent = peerCount;
880
+ }
881
+
882
+ // Utility functions
883
+ function log(message, type = '') {
884
+ const logDiv = document.getElementById('contribution-log');
885
+ const entry = document.createElement('div');
886
+ entry.className = 'log-entry ' + type;
887
+ entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
888
+ logDiv.insertBefore(entry, logDiv.firstChild);
889
+
890
+ // Keep only last 50 entries
891
+ while (logDiv.children.length > 50) {
892
+ logDiv.removeChild(logDiv.lastChild);
893
+ }
894
+ }
895
+
896
+ function arrayToHex(arr) {
897
+ return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
898
+ }
899
+
900
+ function dotProduct(a, b) {
901
+ let sum = 0;
902
+ for (let i = 0; i < a.length; i++) sum += a[i] * b[i];
903
+ return sum;
904
+ }
905
+
906
+ function sleep(ms) {
907
+ return new Promise(resolve => setTimeout(resolve, ms));
908
+ }
909
+
910
+ // Simple encryption (in production, use Web Crypto API with Argon2)
911
+ async function encryptData(data, password) {
912
+ const encoder = new TextEncoder();
913
+ const dataBytes = typeof data === 'string' ? encoder.encode(data) : data;
914
+ const keyMaterial = await crypto.subtle.importKey(
915
+ 'raw',
916
+ encoder.encode(password),
917
+ 'PBKDF2',
918
+ false,
919
+ ['deriveBits', 'deriveKey']
920
+ );
921
+
922
+ const salt = crypto.getRandomValues(new Uint8Array(16));
923
+ const iv = crypto.getRandomValues(new Uint8Array(12));
924
+
925
+ const key = await crypto.subtle.deriveKey(
926
+ { name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
927
+ keyMaterial,
928
+ { name: 'AES-GCM', length: 256 },
929
+ false,
930
+ ['encrypt']
931
+ );
932
+
933
+ const encrypted = await crypto.subtle.encrypt(
934
+ { name: 'AES-GCM', iv },
935
+ key,
936
+ dataBytes
937
+ );
938
+
939
+ // Combine salt + iv + encrypted
940
+ const result = new Uint8Array(salt.length + iv.length + encrypted.byteLength);
941
+ result.set(salt, 0);
942
+ result.set(iv, salt.length);
943
+ result.set(new Uint8Array(encrypted), salt.length + iv.length);
944
+
945
+ return btoa(String.fromCharCode(...result));
946
+ }
947
+
948
+ async function decryptData(encryptedBase64, password) {
949
+ const encoder = new TextEncoder();
950
+ const encrypted = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0));
951
+
952
+ const salt = encrypted.slice(0, 16);
953
+ const iv = encrypted.slice(16, 28);
954
+ const data = encrypted.slice(28);
955
+
956
+ const keyMaterial = await crypto.subtle.importKey(
957
+ 'raw',
958
+ encoder.encode(password),
959
+ 'PBKDF2',
960
+ false,
961
+ ['deriveBits', 'deriveKey']
962
+ );
963
+
964
+ const key = await crypto.subtle.deriveKey(
965
+ { name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
966
+ keyMaterial,
967
+ { name: 'AES-GCM', length: 256 },
968
+ false,
969
+ ['decrypt']
970
+ );
971
+
972
+ const decrypted = await crypto.subtle.decrypt(
973
+ { name: 'AES-GCM', iv },
974
+ key,
975
+ data
976
+ );
977
+
978
+ return new TextDecoder().decode(decrypted);
979
+ }
980
+
981
+ // Initialize on load
982
+ initWasm();
983
+ </script>
984
+ </body>
985
+ </html>