uniwrtc 1.3.0 → 1.5.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.
@@ -1,8 +1,8 @@
1
1
  # UniWRTC Cloudflare Deployment Guide
2
2
 
3
- ## Deploy Without Durable Objects (Cloudflare Pages)
3
+ ## Deploy (Cloudflare Pages)
4
4
 
5
- If you only want to host the demo UI (static assets) and do NOT want Durable Objects, deploy the Vite build to Cloudflare Pages.
5
+ This repo’s current demo works client-side (Nostr), so you can deploy just the static site to Cloudflare Pages.
6
6
 
7
7
  ### Prerequisites
8
8
 
@@ -14,12 +14,12 @@ If you only want to host the demo UI (static assets) and do NOT want Durable Obj
14
14
 
15
15
  ```bash
16
16
  npm install
17
- npm run deploy:cf:no-do
17
+ npm run deploy:cf:pages
18
18
  ```
19
19
 
20
20
  Notes:
21
21
  - This deploys the `dist/` folder (static hosting).
22
- - No server routes are deployed; Durable Objects are not used.
22
+ - No server routes are deployed.
23
23
 
24
24
  ## Custom Domain (signal.peer.ooo)
25
25
 
@@ -1,6 +1,6 @@
1
1
  # Quick Start - Deploy to Cloudflare
2
2
 
3
- ## Option A (No Durable Objects): Cloudflare Pages (static)
3
+ ## Cloudflare Pages (static)
4
4
 
5
5
  This repo’s current demo works client-side (Nostr), so you can deploy just the static site to Cloudflare Pages.
6
6
 
@@ -12,28 +12,11 @@ This repo’s current demo works client-side (Nostr), so you can deploy just the
12
12
  ### Deploy
13
13
  ```bash
14
14
  npm install
15
- npm run deploy:cf:no-do
15
+ npm run deploy:cf:pages
16
16
  ```
17
17
 
18
18
  Wrangler will prompt you to pick/create a Pages project the first time.
19
19
 
20
- ## Prerequisites
21
- - Cloudflare account (free tier works)
22
- - Node.js installed
23
-
24
- ## Deploy
25
-
26
- ### macOS / Linux
27
- ```bash
28
- chmod +x deploy-cloudflare.sh
29
- ./deploy-cloudflare.sh
30
- ```
31
-
32
- ### Windows
33
- ```bash
34
- deploy-cloudflare.bat
35
- ```
36
-
37
20
  ## What this does
38
21
  1. ✅ Builds the Vite site into `dist/`
39
22
  2. ✅ Deploys `dist/` to Cloudflare Pages
@@ -42,4 +25,4 @@ deploy-cloudflare.bat
42
25
 
43
26
  Then set your custom domain in Cloudflare Pages (and point `signal` to `<project>.pages.dev`).
44
27
 
45
- That's it! Your demo is now on Cloudflare Pages (no Durable Objects).
28
+ That's it! Your demo is now on Cloudflare Pages.
package/README.md CHANGED
@@ -1,53 +1,88 @@
1
1
  # UniWRTC
2
2
 
3
- A universal WebRTC signaling service that provides a simple and flexible **HTTP polling** signaling server for WebRTC applications.
3
+ A WebRTC demo + signaling utilities.
4
+
5
+ The default demo flow uses **Nostr relays for WebRTC signaling** (offer/answer/ICE + presence) and a **WebRTC data channel for app data**.
6
+
7
+ This repo also includes an optional **legacy HTTP polling signaling server** (useful for local development), but the live demo is now fully static/serverless.
4
8
 
5
9
  Available on npm: https://www.npmjs.com/package/uniwrtc
6
10
 
7
11
  ## Features
8
12
 
9
- - 🚀 **Simple signaling** - HTTP polling (works locally and on Cloudflare Durable Objects)
10
- - 🏠 **Session-based architecture** - Support for multiple sessions with isolated peer groups
11
- - 🔌 **Flexible client library** - Ready-to-use JavaScript client for browser and Node.js
12
- - 📡 **Real-time messaging** - Efficient message routing between peers
13
- - 🔄 **Auto-reconnection** - Built-in reconnection logic for reliable connections
14
- - 📊 **Health monitoring** - HTTP health check endpoint for monitoring
15
- - 🎯 **Minimal dependencies** - Lightweight implementation with minimal runtime deps
13
+ - 🤝 **Nostr signaling (demo default)** - WebRTC offer/answer/ICE + presence over Nostr relays
14
+ - 📦 **WebRTC for data** - App/chat data rides the data channel (not the relay)
15
+ - 🏠 **Session-based rooms** - Multiple sessions with isolated peer groups
16
+ - 🔌 **Optional legacy server** - HTTP polling signaling server (local/dev)
17
+ - 🧊 **STUN-only** - No TURN in the default demo
16
18
 
17
19
  ## Quick Start
18
20
 
19
- ## Using with `simple-peer` (SDP text-only)
21
+ ## Using with `simple-peer` (Nostr signaling)
20
22
 
21
- This repo's signaling format sends **SDP as plain text** for offers/answers.
22
- `simple-peer` uses `{ type, sdp }` objects, so use the adapter in [simple-peer-adapter.js](simple-peer-adapter.js).
23
+ The repos default demo uses Nostr relays for signaling. Here’s a minimal `simple-peer` example that uses the built-in Nostr client for signaling messages.
23
24
 
24
- Example (browser):
25
+ Example (browser, two tabs):
25
26
 
26
27
  ```js
27
28
  import Peer from 'simple-peer';
28
- import UniWRTCClient from './client-browser.js';
29
- import { sendSimplePeerSignal, attachUniWRTCToSimplePeer, chooseDeterministicInitiator } from './simple-peer-adapter.js';
29
+ import { createNostrClient } from './src/nostr/nostrClient.js';
30
+
31
+ const relayUrl = 'wss://relay.damus.io';
32
+ const room = 'my-room';
33
+
34
+ const nostr = createNostrClient({
35
+ relayUrl,
36
+ room,
37
+ onPayload: ({ from, payload }) => {
38
+ // Only accept signals intended for us
39
+ if (payload?.type !== 'sp-signal') return;
40
+ if (payload?.to !== myId) return;
41
+ if (targetId && from !== targetId) return;
42
+
43
+ try {
44
+ peer.signal(payload.signal);
45
+ } catch (e) {
46
+ console.warn('Failed to apply signal:', e);
47
+ }
48
+ },
49
+ });
30
50
 
31
- const client = new UniWRTCClient('https://your-signal-server', { roomId: 'my-room' });
32
- await client.connect();
51
+ await nostr.connect();
52
+
53
+ const myId = nostr.getPublicKey();
54
+ console.log('My Peer ID:', myId);
33
55
 
34
- // Join a session (peers in the same session can connect)
35
- await client.joinSession('my-room');
56
+ // Set this in each tab (copy/paste from the other tab)
57
+ const targetId = prompt('Paste the other Peer ID:')?.trim();
58
+ if (!targetId) throw new Error('Missing targetId');
59
+
60
+ // Deterministic initiator prevents offer collisions
61
+ const initiator = myId.localeCompare(targetId) < 0;
36
62
 
37
- // Ensure exactly ONE side initiates for a given pair
38
- const initiator = chooseDeterministicInitiator(client.clientId, targetId);
39
63
  const peer = new Peer({ initiator, trickle: true });
40
- const cleanup = attachUniWRTCToSimplePeer(client, peer);
41
64
 
42
- peer.on('signal', (signal) => {
43
- // targetId must be the other peer's UniWRTC client id
44
- sendSimplePeerSignal(client, signal, targetId);
65
+ peer.on('signal', async (signal) => {
66
+ // Send signaling via Nostr; WebRTC data stays P2P.
67
+ await nostr.send({
68
+ type: 'sp-signal',
69
+ to: targetId,
70
+ signal,
71
+ });
72
+ });
73
+
74
+ peer.on('connect', () => {
75
+ console.log('WebRTC connected');
76
+ peer.send('hello over datachannel');
45
77
  });
46
78
 
47
- // When done:
48
- // cleanup();
79
+ peer.on('data', (data) => {
80
+ console.log('Got data:', data.toString());
81
+ });
49
82
  ```
50
83
 
84
+ Note: Nostr relays are generally public. Don’t send secrets in signaling payloads.
85
+
51
86
  ### Installation
52
87
 
53
88
  #### From npm (recommended)
@@ -85,7 +120,11 @@ PORT=8080
85
120
 
86
121
  ### Try the Demo
87
122
 
88
- The interactive demo is available live at **https://signal.peer.ooo/** (Cloudflare Workers deployment) or run locally:
123
+ The interactive demo is available live at **https://signal.peer.ooo/** (Cloudflare Pages static site) or run locally.
124
+
125
+ The demo uses:
126
+ - Nostr relays for signaling
127
+ - WebRTC data channels for data/chat
89
128
 
90
129
  **Using the deployed demo (recommended):**
91
130
  1. Open https://signal.peer.ooo/ in two browser tabs
@@ -95,15 +134,15 @@ The interactive demo is available live at **https://signal.peer.ooo/** (Cloudfla
95
134
  5. Open the P2P chat and send messages between tabs
96
135
 
97
136
  **Or run locally:**
98
- 1. Start the server: `npm start` (signaling at `http://localhost:8080`)
99
- 2. Start the Vite dev server: `npm run dev` (demo at `http://localhost:5173/`)
137
+ 1. Install deps: `npm install`
138
+ 2. Start Vite: `npm run dev` (demo at `http://localhost:5173/`)
100
139
  3. Open the demo in two browser tabs
101
140
  4. Enter the same session ID in both, then Connect
102
141
  5. Chat P2P once data channels open
103
142
 
104
143
  ## Usage
105
144
 
106
- ### Server API
145
+ ### Legacy HTTP Signaling Server API (optional)
107
146
 
108
147
  The signaling server supports:
109
148
  - HTTP polling signaling (no WebSockets)
@@ -342,10 +381,8 @@ client.on('ice-candidate', async (data) => {
342
381
  await client.connect();
343
382
  client.joinSession('my-video-session');
344
383
 
345
- // Or use Cloudflare Durable Objects deployment (HTTP polling; no WebSockets)
346
- const cfClient = new UniWRTCClient('https://signal.peer.ooo');
347
- await cfClient.connect();
348
- cfClient.joinSession('my-session');
384
+ // Note: https://signal.peer.ooo is the static demo site (Nostr signaling),
385
+ // not an HTTP polling signaling server endpoint.
349
386
  ```
350
387
 
351
388
  ## API Reference
@@ -407,25 +444,33 @@ Response:
407
444
 
408
445
  ## Architecture
409
446
 
447
+ ### Nostr Signaling (Demo Default)
448
+
449
+ - Peers publish signaling messages (offer/answer/ICE/presence) to a Nostr relay.
450
+ - Messages are scoped to a room/session and targeted to a specific peer session.
451
+ - Once the WebRTC data channel opens, application data/chat is sent P2P.
452
+ - This is designed to work without running your own signaling server.
453
+
410
454
  ### Session-based Peer Isolation
411
455
 
412
456
  - **Sessions**: Each session is identified by a unique string ID (also called "room" in the UI)
413
457
  - **Peer routing**: Each peer gets a unique client ID; signaling messages are routed only to intended targets
414
458
  - **Session isolation**: Peers in different sessions cannot see or communicate with each other
415
- - **Cloudflare Durable Objects**: Uses DO state to isolate sessions; routing by `?room=` query param per session
416
459
  - Clients join with `joinSession(sessionId)` and receive notifications when other peers join the same session
417
460
 
418
461
  ### Message Flow
419
462
 
420
- 1. Client connects via HTTPS (Cloudflare DO HTTP polling)
421
- 2. Server/Durable Object assigns a unique client ID
463
+ This section applies to the legacy HTTP polling server:
464
+
465
+ 1. Client connects via HTTP(S)
466
+ 2. Server assigns a unique client ID
422
467
  3. Client sends join message with session ID
423
468
  4. Server broadcasts `peer-joined` to other peers in the same session only
424
469
  5. Peers exchange WebRTC offers/answers/ICE candidates via the server
425
470
  6. Server routes signaling messages to specific peers by target ID (unicast, not broadcast)
426
471
 
427
472
  Notes:
428
- - Cloudflare signaling uses JSON over HTTPS requests to `/api` (polling).
473
+ - Server signaling uses JSON over HTTPS requests to `/api` (polling).
429
474
  - Offers/answers are transmitted as SDP strings (text-only) in the `offer`/`answer` fields.
430
475
  - ICE candidates are transmitted as a compact text string: `candidate|sdpMid|sdpMLineIndex`.
431
476
 
@@ -1,10 +1,10 @@
1
1
  @echo off
2
- REM UniWRTC Cloudflare Automated Setup Script (Windows, NO Durable Objects)
2
+ REM UniWRTC Cloudflare Automated Setup Script (Windows)
3
3
  REM Deploys the static demo to Cloudflare Pages.
4
4
 
5
5
  setlocal enabledelayedexpansion
6
6
 
7
- echo 🚀 UniWRTC Cloudflare Setup (Pages / no Durable Objects)
7
+ echo 🚀 UniWRTC Cloudflare Setup (Cloudflare Pages)
8
8
  echo ============================
9
9
  echo.
10
10
 
@@ -1,11 +1,11 @@
1
1
  #!/bin/bash
2
2
 
3
- # UniWRTC Cloudflare Automated Setup Script (NO Durable Objects)
3
+ # UniWRTC Cloudflare Automated Setup Script
4
4
  # Deploys the static demo to Cloudflare Pages.
5
5
 
6
6
  set -e
7
7
 
8
- echo "🚀 UniWRTC Cloudflare Setup (Pages / no Durable Objects)"
8
+ echo "🚀 UniWRTC Cloudflare Setup (Cloudflare Pages)"
9
9
  echo "============================"
10
10
  echo ""
11
11
 
package/package-cf.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "uniwrtc-cloudflare",
3
3
  "version": "1.0.0",
4
- "description": "UniWRTC Cloudflare Workers deployment",
4
+ "description": "UniWRTC Cloudflare deployment (legacy worker config)",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "wrangler dev",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniwrtc",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "A universal WebRTC signaling service",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -10,9 +10,8 @@
10
10
  "build": "vite build",
11
11
  "preview": "vite preview",
12
12
  "deploy:cf:pages": "npm run build && npx wrangler pages deploy dist",
13
- "deploy:cf:no-do": "npm run deploy:cf:pages",
14
13
  "server": "node server.js",
15
- "test": "node test.js"
14
+ "test": "playwright test"
16
15
  },
17
16
  "keywords": [
18
17
  "webrtc",
@@ -3,17 +3,24 @@ import { defineConfig, devices } from '@playwright/test';
3
3
  export default defineConfig({
4
4
  testDir: './tests',
5
5
  // Run tests IN PARALLEL - all browsers at the same time
6
- fullyParallel: true,
6
+ fullyParallel: false,
7
7
  forbidOnly: !!process.env.CI,
8
8
  retries: process.env.CI ? 2 : 0,
9
- workers: 4, // Multiple workers - run browsers in parallel
9
+ workers: process.env.CI ? 2 : 1,
10
10
  reporter: 'html',
11
11
  use: {
12
- baseURL: 'https://signal.peer.ooo',
12
+ baseURL: 'http://127.0.0.1:5173',
13
13
  trace: 'on-first-retry',
14
14
  screenshot: 'only-on-failure',
15
15
  },
16
16
 
17
+ webServer: {
18
+ command: 'npm run dev -- --host 127.0.0.1 --port 5173',
19
+ url: 'http://127.0.0.1:5173',
20
+ reuseExistingServer: !process.env.CI,
21
+ timeout: 120_000,
22
+ },
23
+
17
24
  projects: [
18
25
  {
19
26
  name: 'chromium',
package/server.js CHANGED
@@ -10,7 +10,7 @@ const __dirname = path.dirname(__filename);
10
10
 
11
11
  const CLIENT_TTL_MS = 60_000;
12
12
 
13
- // Top-level rooms are keyed by ?room= (mirrors Cloudflare DO routing)
13
+ // Top-level rooms are keyed by ?room=
14
14
  // Each room contains multiple sessions keyed by sessionId.
15
15
  const rooms = new Map();
16
16
 
@@ -111,7 +111,7 @@ class UniWRTCClient {
111
111
 
112
112
  joinSession(sessionId) {
113
113
  this.sessionId = sessionId;
114
- // Durable Objects handle session joining automatically via room parameter
114
+ // Session is provided via the room query param on connection
115
115
  }
116
116
 
117
117
  leaveSession() {
@@ -153,8 +153,7 @@ class UniWRTCClient {
153
153
  }
154
154
 
155
155
  listRooms() {
156
- // Durable Objects don't expose room listing
157
- console.log('Room listing not available with Durable Objects');
156
+ console.log('Room listing is not available for this transport');
158
157
  }
159
158
 
160
159
  on(event, handler) {
package/src/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * UniWRTC Cloudflare Worker
3
- * WebRTC Signaling Service using Durable Objects
4
- * Serves static assets and HTTP polling signaling
3
+ * Legacy HTTP polling signaling worker
5
4
  */
6
5
 
7
6
  import { Room } from './room.js';
package/src/main.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import './style.css';
2
2
  import UniWRTCClient from '../client-browser.js';
3
3
  import { createNostrClient } from './nostr/nostrClient.js';
4
+ import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
4
5
 
5
6
  // Make UniWRTCClient available globally for backwards compatibility
6
7
  window.UniWRTCClient = UniWRTCClient;
@@ -31,6 +32,35 @@ const dataChannels = new Map();
31
32
  const pendingIce = new Map();
32
33
  const outboundIceBatches = new Map();
33
34
 
35
+ function bytesToHex(bytes) {
36
+ return Array.from(bytes)
37
+ .map((b) => b.toString(16).padStart(2, '0'))
38
+ .join('');
39
+ }
40
+
41
+ function isHex64(s) {
42
+ return typeof s === 'string' && /^[0-9a-fA-F]{64}$/.test(s);
43
+ }
44
+
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
+ }
54
+
55
+ if (!isHex64(stored)) {
56
+ const secretBytes = generateSecretKey();
57
+ stored = bytesToHex(secretBytes);
58
+ sessionStorage.setItem(storageKey, stored);
59
+ }
60
+
61
+ return getPublicKey(stored);
62
+ }
63
+
34
64
  // Initialize app
35
65
  document.getElementById('app').innerHTML = `
36
66
  <div class="container">
@@ -44,7 +74,7 @@ document.getElementById('app').innerHTML = `
44
74
  <div class="connection-controls">
45
75
  <div>
46
76
  <label style="display: block; margin-bottom: 5px; color: #64748b; font-size: 13px;">Relay URL(s)</label>
47
- <input type="text" id="relayUrl" placeholder="wss://relay.damus.io" value="${DEFAULT_RELAYS.join(', ')}" style="width: 100%; padding: 8px; border: 1px solid #e2e8f0; border-radius: 4px; font-family: monospace; font-size: 12px;">
77
+ <input type="text" id="relayUrl" data-testid="relayUrl" placeholder="wss://relay.damus.io" value="${DEFAULT_RELAYS.join(', ')}" style="width: 100%; padding: 8px; border: 1px solid #e2e8f0; border-radius: 4px; font-family: monospace; font-size: 12px;">
48
78
  </div>
49
79
  <div>
50
80
  <label style="display: block; margin-bottom: 5px; color: #64748b; font-size: 13px;">Room / Session ID</label>
@@ -112,12 +142,33 @@ if (urlRoom) {
112
142
  log(`Using default room ID: ${defaultRoom}`, 'info');
113
143
  }
114
144
 
115
- // STUN-only ICE servers (no TURN)
116
- const ICE_SERVERS = [
117
- { urls: ['stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302'] },
118
- { urls: ['stun:stun2.l.google.com:19302', 'stun:stun3.l.google.com:19302'] },
119
- { urls: ['stun:stun.cloudflare.com:3478'] },
120
- ];
145
+ // Client/session identity should not depend on relay connectivity.
146
+ try {
147
+ myPeerId = ensureIdentity();
148
+ document.getElementById('clientId').textContent = myPeerId.substring(0, 16) + '...';
149
+ } catch {
150
+ // Leave as-is if identity init fails.
151
+ }
152
+
153
+ document.getElementById('sessionId').textContent = roomInput.value || 'Not joined';
154
+ roomInput.addEventListener('input', () => {
155
+ document.getElementById('sessionId').textContent = roomInput.value.trim() || 'Not joined';
156
+ });
157
+
158
+ // ICE servers: STUN-only by default (no TURN). For deterministic local testing,
159
+ // support host-only ICE via URL flag: ?ice=host (or ?ice=none)
160
+ const iceMode = (params.get('ice') || '').toLowerCase();
161
+ const ICE_SERVERS = iceMode === 'host' || iceMode === 'none'
162
+ ? []
163
+ : [
164
+ { urls: ['stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302'] },
165
+ { urls: ['stun:stun2.l.google.com:19302', 'stun:stun3.l.google.com:19302'] },
166
+ { urls: ['stun:stun.cloudflare.com:3478'] },
167
+ ];
168
+
169
+ if (ICE_SERVERS.length === 0) {
170
+ log('Using host-only ICE candidates (no STUN)', 'info');
171
+ }
121
172
 
122
173
  function log(message, type = 'info') {
123
174
  const logContainer = document.getElementById('logContainer');
@@ -127,11 +178,11 @@ function log(message, type = 'info') {
127
178
  entry.textContent = `[${timestamp}] ${message}`;
128
179
 
129
180
  // Add testid for specific log messages
130
- if (message.includes('Connected with client ID')) {
181
+ if (message.includes('Connected with client ID') || message.includes('Nostr connection established')) {
131
182
  entry.setAttribute('data-testid', 'log-connected');
132
- } else if (message.includes('Joined session')) {
183
+ } else if (message.includes('Joined session') || message.includes('Joined Nostr room')) {
133
184
  entry.setAttribute('data-testid', 'log-joined');
134
- } else if (message.includes('Peer joined')) {
185
+ } else if (message.includes('Peer joined') || message.includes('Peer seen')) {
135
186
  entry.setAttribute('data-testid', 'log-peer-joined');
136
187
  } else if (message.includes('Data channel open')) {
137
188
  entry.setAttribute('data-testid', 'log-data-channel');
@@ -156,8 +207,7 @@ function updateStatus(connected) {
156
207
  badge.className = 'status-badge status-disconnected';
157
208
  connectBtn.disabled = false;
158
209
  disconnectBtn.disabled = true;
159
- document.getElementById('clientId').textContent = 'Not connected';
160
- document.getElementById('sessionId').textContent = 'Not joined';
210
+ // Keep client/session labels stable; identity and room are local state.
161
211
  }
162
212
  }
163
213
 
@@ -323,6 +373,11 @@ async function connectNostr() {
323
373
  const relayUrlRaw = document.getElementById('relayUrl').value.trim();
324
374
  const roomIdInput = document.getElementById('roomId');
325
375
  const roomId = roomIdInput.value.trim();
376
+
377
+ if (!relayUrlRaw) {
378
+ log('Please enter a relay URL', 'error');
379
+ return;
380
+ }
326
381
 
327
382
  const relayCandidatesRaw = relayUrlRaw.toLowerCase() === 'auto'
328
383
  ? DEFAULT_RELAYS
@@ -337,11 +392,6 @@ async function connectNostr() {
337
392
 
338
393
  const relayCandidates = relayCandidatesRaw.length ? relayCandidatesRaw : DEFAULT_RELAYS;
339
394
 
340
- if (relayCandidates.length === 0) {
341
- log('Please enter a relay URL', 'error');
342
- return;
343
- }
344
-
345
395
  const effectiveRoom = roomId || `room-${Math.random().toString(36).substring(2, 10)}`;
346
396
  if (!roomId) roomIdInput.value = effectiveRoom;
347
397
 
@@ -354,7 +404,7 @@ async function connectNostr() {
354
404
  }
355
405
 
356
406
  // Reset local peer state to avoid stale sessions targeting the wrong browser tab.
357
- myPeerId = null;
407
+ myPeerId = myPeerId || ensureIdentity();
358
408
  mySessionNonce = null;
359
409
  peerSessions.clear();
360
410
  peerProbeState.clear();
@@ -389,8 +439,7 @@ async function connectNostr() {
389
439
 
390
440
  // Identity must be ready before we connect/subscribe; inbound events can arrive immediately.
391
441
  // Any client instance will derive the same per-tab keypair.
392
- const identityClient = createNostrClient({ relayUrl: relayCandidates[0], room: effectiveRoom });
393
- const myPubkey = identityClient.getPublicKey();
442
+ const myPubkey = myPeerId || ensureIdentity();
394
443
  myPeerId = myPubkey;
395
444
  mySessionNonce = Math.random().toString(36).slice(2) + Date.now().toString(36);
396
445
  document.getElementById('clientId').textContent = myPubkey.substring(0, 16) + '...';
@@ -689,7 +738,7 @@ async function connectWebRTC() {
689
738
  let finalUrl = serverUrl;
690
739
  if (serverUrl.includes('signal.peer.ooo')) {
691
740
  finalUrl = `${serverUrl.replace(/\/$/, '')}/ws?room=${roomId}`;
692
- log(`Using Cloudflare Durable Objects with session: ${roomId}`, 'info');
741
+ log(`Using hosted endpoint with session: ${roomId}`, 'info');
693
742
  }
694
743
 
695
744
  client = new UniWRTCClient(finalUrl, { autoReconnect: false });
@@ -793,7 +842,6 @@ window.disconnect = function() {
793
842
  if (nostrClient) {
794
843
  nostrClient.disconnect().catch(() => {});
795
844
  nostrClient = null;
796
- myPeerId = null;
797
845
  mySessionNonce = null;
798
846
  peerSessions.clear();
799
847
  peerProbeState.clear();
package/src/room.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Durable Object for WebRTC signaling (HTTP polling only)
2
+ * Legacy room state for HTTP polling signaling (no WebSockets)
3
3
  */
4
4
  export class Room {
5
5
  constructor(state, env) {
package/tests/e2e.spec.js CHANGED
@@ -1,38 +1,30 @@
1
1
  import { test, expect } from '@playwright/test';
2
2
 
3
- function failOnWebSocket(page) {
4
- page.on('websocket', (ws) => {
5
- throw new Error(`WebSockets are disabled, but a WebSocket was opened: ${ws.url()}`);
6
- });
7
- }
8
-
9
3
  test.describe('UniWRTC Demo - Full Integration Tests', () => {
10
- const BASE_URL = 'https://signal.peer.ooo';
11
4
  const ROOM_ID = 'test';
12
5
 
13
6
  test.describe('Connection and Session Management', () => {
14
7
  test('should load demo page and display UI', async ({ page }) => {
15
- failOnWebSocket(page);
16
- await page.goto(BASE_URL);
8
+ await page.goto('/');
17
9
 
18
10
  // Check main elements exist
19
11
  await expect(page.locator('h1')).toContainText('UniWRTC Demo');
20
12
  await expect(page.locator('text=Connection')).toBeVisible();
21
- await expect(page.getByTestId('serverUrl')).toHaveValue('https://signal.peer.ooo');
13
+ await expect(page.getByTestId('relayUrl')).toBeVisible();
22
14
  await expect(page.getByTestId('roomId')).toHaveValue('demo-room');
23
15
  await expect(page.getByTestId('connectBtn')).toBeVisible();
24
16
  });
25
17
 
26
- test('should connect to signaling server and join session', async ({ page }) => {
27
- failOnWebSocket(page);
28
- await page.goto(BASE_URL);
18
+ test('should connect to relay and join room', async ({ page }) => {
19
+ test.setTimeout(60_000);
20
+ await page.goto('/');
29
21
 
30
22
  // Click connect
31
23
  await page.getByTestId('connectBtn').click();
32
24
 
33
25
  // Wait for connection success log
34
- await expect(page.getByTestId('log-connected')).toBeVisible({ timeout: 20000 });
35
- await expect(page.getByTestId('log-joined')).toBeVisible({ timeout: 20000 });
26
+ await expect(page.getByTestId('log-connected')).toBeVisible({ timeout: 30000 });
27
+ await expect(page.getByTestId('log-joined')).toBeVisible({ timeout: 30000 });
36
28
 
37
29
  // Check status changed to connected
38
30
  const badge = page.getByTestId('statusBadge');
@@ -46,12 +38,12 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
46
38
  });
47
39
 
48
40
  test('should handle disconnect', async ({ page }) => {
49
- failOnWebSocket(page);
50
- await page.goto(BASE_URL);
41
+ test.setTimeout(60_000);
42
+ await page.goto('/');
51
43
 
52
44
  // Connect first
53
45
  await page.getByTestId('connectBtn').click();
54
- await expect(page.getByTestId('log-connected')).toBeVisible({ timeout: 20000 });
46
+ await expect(page.getByTestId('log-connected')).toBeVisible({ timeout: 30000 });
55
47
 
56
48
  // Now disconnect
57
49
  await page.getByTestId('disconnectBtn').click();
@@ -59,7 +51,6 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
59
51
  // Check status changed back to disconnected
60
52
  const badge = page.getByTestId('statusBadge');
61
53
  await expect(badge).toContainText('Disconnected');
62
- await expect(page.getByTestId('clientId')).toContainText('Not connected');
63
54
  });
64
55
  });
65
56
 
@@ -68,21 +59,18 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
68
59
  // Open three browser contexts (simulating three users)
69
60
  const context1 = await browser.newContext();
70
61
  const page1 = await context1.newPage();
71
- failOnWebSocket(page1);
72
62
 
73
63
  const context2 = await browser.newContext();
74
64
  const page2 = await context2.newPage();
75
- failOnWebSocket(page2);
76
65
 
77
66
  const context3 = await browser.newContext();
78
67
  const page3 = await context3.newPage();
79
- failOnWebSocket(page3);
80
68
 
81
69
  try {
82
70
  // Connect all three peers to same room
83
- await page1.goto(BASE_URL);
84
- await page2.goto(BASE_URL);
85
- await page3.goto(BASE_URL);
71
+ await page1.goto('/?ice=host');
72
+ await page2.goto('/?ice=host');
73
+ await page3.goto('/?ice=host');
86
74
 
87
75
  // Use shared room ID for all three peers
88
76
  await page1.getByTestId('roomId').fill(ROOM_ID);
@@ -127,21 +115,18 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
127
115
  test.setTimeout(90_000);
128
116
  const context1 = await browser.newContext();
129
117
  const page1 = await context1.newPage();
130
- failOnWebSocket(page1);
131
118
 
132
119
  const context2 = await browser.newContext();
133
120
  const page2 = await context2.newPage();
134
- failOnWebSocket(page2);
135
121
 
136
122
  const context3 = await browser.newContext();
137
123
  const page3 = await context3.newPage();
138
- failOnWebSocket(page3);
139
124
 
140
125
  try {
141
126
  // Connect all three peers
142
- await page1.goto(BASE_URL);
143
- await page2.goto(BASE_URL);
144
- await page3.goto(BASE_URL);
127
+ await page1.goto('/?ice=host');
128
+ await page2.goto('/?ice=host');
129
+ await page3.goto('/?ice=host');
145
130
 
146
131
  await page1.getByTestId('roomId').fill(ROOM_ID);
147
132
  await page2.getByTestId('roomId').fill(ROOM_ID);
@@ -172,9 +157,9 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
172
157
  }
173
158
 
174
159
  // Wait for data channels to open - wait for at least one data channel log on each peer with extended timeout
175
- await expect(page1.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 40000 });
176
- await expect(page2.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 40000 });
177
- await expect(page3.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 40000 });
160
+ await expect(page1.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 60000 });
161
+ await expect(page2.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 60000 });
162
+ await expect(page3.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 60000 });
178
163
 
179
164
  // Wait longer to ensure all data channels are fully established
180
165
  await page1.waitForTimeout(3000);
@@ -199,21 +184,18 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
199
184
  test.setTimeout(90_000);
200
185
  const context1 = await browser.newContext();
201
186
  const page1 = await context1.newPage();
202
- failOnWebSocket(page1);
203
187
 
204
188
  const context2 = await browser.newContext();
205
189
  const page2 = await context2.newPage();
206
- failOnWebSocket(page2);
207
190
 
208
191
  const context3 = await browser.newContext();
209
192
  const page3 = await context3.newPage();
210
- failOnWebSocket(page3);
211
193
 
212
194
  try {
213
195
  // Connect all three peers
214
- await page1.goto(BASE_URL);
215
- await page2.goto(BASE_URL);
216
- await page3.goto(BASE_URL);
196
+ await page1.goto('/?ice=host');
197
+ await page2.goto('/?ice=host');
198
+ await page3.goto('/?ice=host');
217
199
 
218
200
  await page1.getByTestId('roomId').fill(ROOM_ID);
219
201
  await page2.getByTestId('roomId').fill(ROOM_ID);
@@ -229,9 +211,9 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
229
211
  await expect(page3.getByTestId('log-connected')).toBeVisible({ timeout: 10000 });
230
212
 
231
213
  // Wait for data channels to open on all peers first with extended timeout
232
- await expect(page1.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 25000 });
233
- await expect(page2.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 25000 });
234
- await expect(page3.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 25000 });
214
+ await expect(page1.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 60000 });
215
+ await expect(page2.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 60000 });
216
+ await expect(page3.getByTestId('log-data-channel').first()).toBeVisible({ timeout: 60000 });
235
217
 
236
218
  // Give the browser a moment to fully wire up data channel handlers.
237
219
  await page1.waitForTimeout(3000);
@@ -287,16 +269,14 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
287
269
  test('should not connect peers from different sessions', async ({ browser }) => {
288
270
  const context1 = await browser.newContext();
289
271
  const page1 = await context1.newPage();
290
- failOnWebSocket(page1);
291
272
 
292
273
  const context2 = await browser.newContext();
293
274
  const page2 = await context2.newPage();
294
- failOnWebSocket(page2);
295
275
 
296
276
  try {
297
277
  // Connect to different rooms
298
- await page1.goto(BASE_URL);
299
- await page2.goto(BASE_URL);
278
+ await page1.goto('/');
279
+ await page2.goto('/');
300
280
 
301
281
  // Use different unique room IDs to ensure isolation
302
282
  const roomA = `iso-a-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
@@ -328,24 +308,21 @@ test.describe('UniWRTC Demo - Full Integration Tests', () => {
328
308
  });
329
309
 
330
310
  test.describe('Error Handling', () => {
331
- test('should show error when server URL is empty', async ({ page }) => {
332
- failOnWebSocket(page);
333
- await page.goto(BASE_URL);
334
-
335
- await page.getByTestId('serverUrl').fill('');
311
+ test('should show error when relay URL is empty', async ({ page }) => {
312
+ await page.goto('/');
313
+ await page.getByTestId('relayUrl').fill('');
336
314
  await page.getByTestId('connectBtn').click();
337
-
338
- await expect(page.locator('text=Please enter a server URL')).toBeVisible({ timeout: 5000 });
315
+ await expect(page.locator('text=Please enter a relay URL')).toBeVisible({ timeout: 5000 });
339
316
  });
340
317
 
341
- test('should show error when room ID is empty', async ({ page }) => {
342
- failOnWebSocket(page);
343
- await page.goto(BASE_URL);
344
-
318
+ test('should auto-fill a room when room ID is empty', async ({ page }) => {
319
+ test.setTimeout(60_000);
320
+ await page.goto('/');
345
321
  await page.getByTestId('roomId').fill('');
346
322
  await page.getByTestId('connectBtn').click();
347
-
348
- await expect(page.locator('text=Please enter a room ID')).toBeVisible({ timeout: 5000 });
323
+ await expect(page.getByTestId('log-connected')).toBeVisible({ timeout: 30000 });
324
+ await expect(page.getByTestId('roomId')).not.toHaveValue('');
325
+ await expect(page.getByTestId('sessionId')).not.toContainText('Not joined');
349
326
  });
350
327
  });
351
328
  });