uniwrtc 1.8.0 → 2.0.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniwrtc",
3
- "version": "1.8.0",
3
+ "version": "2.0.0",
4
4
  "description": "A universal WebRTC signaling service",
5
5
  "main": "server.js",
6
6
  "type": "module",
package/src/main.js CHANGED
@@ -12,6 +12,7 @@ let myPeerId = null;
12
12
  let mySessionNonce = null;
13
13
  const peerSessions = new Map();
14
14
  const peerProbeState = new Map();
15
+ const peerResyncState = new Map();
15
16
  const readyPeers = new Set();
16
17
  const deferredHelloPeers = new Set();
17
18
 
@@ -42,23 +43,17 @@ function isHex64(s) {
42
43
  return typeof s === 'string' && /^[0-9a-fA-F]{64}$/.test(s);
43
44
  }
44
45
 
45
- function ensureIdentity() {
46
- // Must match the key used in createNostrClient() so the pubkey stays consistent.
47
- const storageKey = 'nostr-secret-key-tab';
48
- let stored = sessionStorage.getItem(storageKey);
49
-
50
- if (stored && stored.includes(',')) {
51
- sessionStorage.removeItem(storageKey);
52
- stored = null;
53
- }
46
+ // Store the secret key for use with all Nostr clients
47
+ let mySecretKeyHex = null;
54
48
 
55
- if (!isHex64(stored)) {
49
+ function ensureIdentity() {
50
+ // Generate a fresh unique peer ID on every page load
51
+ // This ensures each browser tab/reload gets a new identity
52
+ if (!mySecretKeyHex) {
56
53
  const secretBytes = generateSecretKey();
57
- stored = bytesToHex(secretBytes);
58
- sessionStorage.setItem(storageKey, stored);
54
+ mySecretKeyHex = bytesToHex(secretBytes);
59
55
  }
60
-
61
- return getPublicKey(stored);
56
+ return getPublicKey(mySecretKeyHex);
62
57
  }
63
58
 
64
59
  // Initialize app
@@ -102,6 +97,11 @@ document.getElementById('app').innerHTML = `
102
97
  </div>
103
98
  </div>
104
99
 
100
+ <div class="card">
101
+ <h2>Role</h2>
102
+ <div id="roleBadge" data-testid="roleBadge" class="status-badge status-disconnected">Not assigned</div>
103
+ </div>
104
+
105
105
  <div class="card">
106
106
  <h2>Connected Peers</h2>
107
107
  <div id="peerList" data-testid="peerList" class="peer-list">
@@ -230,6 +230,27 @@ function updateStatus(connected) {
230
230
  }
231
231
  }
232
232
 
233
+ function updateRole(role) {
234
+ const badge = document.getElementById('roleBadge');
235
+ if (!badge) return;
236
+
237
+ if (role === 'coordinator') {
238
+ badge.textContent = 'Coordinator';
239
+ badge.className = 'status-badge status-connected';
240
+ // If assigned a role, we must be connected
241
+ updateStatus(true);
242
+ } else if (role === 'peer') {
243
+ badge.textContent = 'Peer';
244
+ badge.className = 'status-badge status-info';
245
+ // If assigned a role, we must be connected
246
+ updateStatus(true);
247
+ } else {
248
+ badge.textContent = 'Not assigned';
249
+ badge.className = 'status-badge status-disconnected';
250
+ updateStatus(false);
251
+ }
252
+ }
253
+
233
254
  function updatePeerList() {
234
255
  const peerList = document.getElementById('peerList');
235
256
  if (!peerList) return;
@@ -328,6 +349,28 @@ async function maybeProbePeer(peerId) {
328
349
  }
329
350
  }
330
351
 
352
+ // Force a probe to resync sessions when we detect mismatched toSession
353
+ async function resyncPeerSession(peerId, reason = 'session mismatch') {
354
+ if (!nostrClient) return;
355
+
356
+ const now = Date.now();
357
+ const last = peerResyncState.get(peerId) || 0;
358
+ if (now - last < 3000) return; // throttle resync attempts
359
+ peerResyncState.set(peerId, now);
360
+
361
+ const probeId = Math.random().toString(36).slice(2, 10) + now.toString(36);
362
+ // Track probe so probe-ack can update readiness/session
363
+ const prevSession = peerSessions.get(peerId) || null;
364
+ peerProbeState.set(peerId, { session: prevSession, ts: now, probeId });
365
+
366
+ try {
367
+ await sendSignal(peerId, { type: 'probe', probeId });
368
+ log(`Resyncing session with ${peerId.substring(0, 6)}... (${reason})`, 'info');
369
+ } catch (e) {
370
+ log(`Resync probe failed: ${e?.message || e}`, 'warning');
371
+ }
372
+ }
373
+
331
374
  function logDrop(peerId, payload, reason) {
332
375
  const t = payload?.type || 'unknown';
333
376
  if (t !== 'signal-offer' && t !== 'signal-answer' && t !== 'signal-ice' && t !== 'signal-ice-batch' && t !== 'probe' && t !== 'probe-ack') return;
@@ -488,6 +531,7 @@ async function connectNostr() {
488
531
  const makeClient = (relayUrl) => createNostrClient({
489
532
  relayUrl,
490
533
  room: effectiveRoom,
534
+ secretKeyHex: mySecretKeyHex,
491
535
  onState: (state) => {
492
536
  if (state === 'connected') updateStatus(true);
493
537
  if (state === 'disconnected') updateStatus(false);
@@ -561,6 +605,7 @@ async function connectNostr() {
561
605
  // Only accept signaling intended for THIS browser session
562
606
  if (payload.toSession && payload.toSession !== mySessionNonce) {
563
607
  logDrop(peerId, payload, 'toSession mismatch');
608
+ await resyncPeerSession(peerId, 'toSession mismatch');
564
609
  return;
565
610
  }
566
611
 
@@ -666,6 +711,11 @@ async function connectNostr() {
666
711
  log(`Ignoring non-answer in signal-answer from ${peerId.substring(0, 6)}...`, 'warning');
667
712
  return;
668
713
  }
714
+ // Perfect negotiation guard: only accept an answer when we have a local offer
715
+ if (pc.signalingState !== 'have-local-offer') {
716
+ log(`Ignoring answer; unexpected signaling state: ${pc.signalingState}`, 'warning');
717
+ return;
718
+ }
669
719
  await pc.setRemoteDescription(new RTCSessionDescription(payload.sdp));
670
720
  await flushPendingIce(peerId);
671
721
  }
@@ -879,6 +929,7 @@ async function connectWebRTC() {
879
929
  }
880
930
 
881
931
  window.disconnect = function() {
932
+ updateRole(null);
882
933
  if (nostrClient) {
883
934
  nostrClient.disconnect().catch(() => {});
884
935
  nostrClient = null;
@@ -55,24 +55,10 @@ export function createNostrClient({ relayUrl, room, onPayload, onState, onNotice
55
55
  return;
56
56
  }
57
57
 
58
- // IMPORTANT: For this demo we want each browser tab to have a distinct peer ID.
59
- // sessionStorage is per-tab, while localStorage is shared across tabs.
60
- let stored = effectiveStorage.getItem(storageKey);
61
-
62
- // If stored value looks like an array string from prior buggy storage, clear it
63
- if (stored && stored.includes(',')) {
64
- effectiveStorage.removeItem(storageKey);
65
- stored = null;
66
- }
67
-
68
- if (!isHex64(stored)) {
69
- const secretBytes = generateSecretKey();
70
- state.secretKeyHex = bytesToHex(secretBytes);
71
- effectiveStorage.setItem(storageKey, state.secretKeyHex);
72
- } else {
73
- state.secretKeyHex = stored;
74
- }
75
-
58
+ // Generate a fresh unique peer ID on every page load
59
+ // This ensures each browser tab/reload gets a new identity
60
+ const secretBytes = generateSecretKey();
61
+ state.secretKeyHex = bytesToHex(secretBytes);
76
62
  state.pubkey = getPublicKey(state.secretKeyHex);
77
63
  }
78
64
 
@@ -1,33 +0,0 @@
1
- # UniWRTC Cloudflare Deployment Guide
2
-
3
- ## Deploy (Cloudflare Pages)
4
-
5
- This repo’s current demo works client-side (Nostr), so you can deploy just the static site to Cloudflare Pages.
6
-
7
- ### Prerequisites
8
-
9
- 1. **Cloudflare Account** - Free tier is sufficient
10
- 2. **Wrangler CLI** - Install: `npm install -g wrangler` (or use `npx`)
11
- 3. **Node.js** - v16 or higher
12
-
13
- ### Deploy
14
-
15
- ```bash
16
- npm install
17
- npm run deploy:cf:pages
18
- ```
19
-
20
- Notes:
21
- - This deploys the `dist/` folder (static hosting).
22
- - No server routes are deployed.
23
-
24
- ## Custom Domain (signal.peer.ooo)
25
-
26
- To serve the Pages project at `https://signal.peer.ooo`:
27
-
28
- 1. Cloudflare Dashboard → Pages → your project → **Custom domains** → add `signal.peer.ooo`
29
- 2. Cloudflare DNS → set `signal` as a CNAME to `<your-pages-project>.pages.dev`
30
-
31
- ---
32
-
33
- For more info: [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)