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 +1 -1
- package/src/main.js +65 -14
- package/src/nostr/nostrClient.js +4 -18
- package/CLOUDFLARE_DEPLOYMENT.md +0 -33
package/package.json
CHANGED
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/src/nostr/nostrClient.js
CHANGED
|
@@ -55,24 +55,10 @@ export function createNostrClient({ relayUrl, room, onPayload, onState, onNotice
|
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
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
|
|
package/CLOUDFLARE_DEPLOYMENT.md
DELETED
|
@@ -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/)
|