uniwrtc 1.1.0 → 1.2.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/CLOUDFLARE_DEPLOYMENT.md +16 -110
- package/QUICKSTART_CLOUDFLARE.md +23 -33
- package/deploy-cloudflare.bat +17 -61
- package/deploy-cloudflare.sh +15 -58
- package/package.json +3 -1
- package/src/main.js +92 -17
- package/src/nostr/nostrClient.js +18 -48
- package/wrangler.toml +2 -27
package/CLOUDFLARE_DEPLOYMENT.md
CHANGED
|
@@ -1,126 +1,32 @@
|
|
|
1
1
|
# UniWRTC Cloudflare Deployment Guide
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Deploy Without Durable Objects (Cloudflare Pages)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
2. **Wrangler CLI** - Install: `npm install -g wrangler`
|
|
7
|
-
3. **Node.js** - v16 or higher
|
|
8
|
-
4. **Your Domain** - Domain must be on Cloudflare (e.g., `peer.ooo`)
|
|
9
|
-
|
|
10
|
-
## Setup Steps
|
|
11
|
-
|
|
12
|
-
### 1. Install Wrangler
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
npm install -g wrangler
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
### 2. Authenticate with Cloudflare
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
wrangler login
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
This will open your browser to authorize the CLI.
|
|
25
|
-
|
|
26
|
-
### 3. Update wrangler.toml
|
|
27
|
-
|
|
28
|
-
Replace the zone configuration with your domain:
|
|
29
|
-
|
|
30
|
-
```toml
|
|
31
|
-
[env.production]
|
|
32
|
-
routes = [
|
|
33
|
-
{ pattern = "signal.peer.ooo/*", zone_name = "peer.ooo" }
|
|
34
|
-
]
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### 4. Deploy to Cloudflare
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
# Deploy to production
|
|
41
|
-
wrangler publish --env production
|
|
42
|
-
|
|
43
|
-
# Or deploy to staging first
|
|
44
|
-
wrangler publish
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### 5. Access Your Signaling Server
|
|
48
|
-
|
|
49
|
-
Your server will be available at:
|
|
50
|
-
- **Production**: `https://signal.peer.ooo/`
|
|
51
|
-
- **Development**: `https://uniwrtc.<subdomain>.workers.dev/`
|
|
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.
|
|
52
6
|
|
|
53
|
-
|
|
7
|
+
### Prerequisites
|
|
54
8
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// In demo.html or client
|
|
59
|
-
const serverUrl = 'https://signal.peer.ooo/';
|
|
60
|
-
const roomId = 'my-room';
|
|
61
|
-
|
|
62
|
-
const client = new UniWRTCClient(serverUrl, {
|
|
63
|
-
roomId: roomId,
|
|
64
|
-
customPeerId: 'optional-id'
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
await client.connect();
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## How It Works
|
|
71
|
-
|
|
72
|
-
1. **Durable Objects** - One per room, manages peer discovery
|
|
73
|
-
2. **HTTP polling** - Browsers connect for signaling
|
|
74
|
-
3. **Signaling Only** - Offers/answers/ICE via Worker
|
|
75
|
-
4. **P2P Data** - WebRTC data channels bypass Cloudflare
|
|
76
|
-
5. **Free Tier** - Plenty of capacity for small deployments
|
|
77
|
-
|
|
78
|
-
## Cost
|
|
79
|
-
|
|
80
|
-
- **Requests**: 100,000 free per day (signaling only)
|
|
81
|
-
- **Compute**: Included in free tier
|
|
82
|
-
- **Durable Objects**: ~$0.15/million operations (minimal for signaling)
|
|
83
|
-
- **Total**: Free to very low cost
|
|
84
|
-
|
|
85
|
-
## Monitoring
|
|
86
|
-
|
|
87
|
-
Check deployment status:
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
wrangler tail --env production
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
View real-time logs from your Worker.
|
|
94
|
-
|
|
95
|
-
## Local Development
|
|
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
|
|
96
12
|
|
|
97
|
-
|
|
13
|
+
### Deploy
|
|
98
14
|
|
|
99
15
|
```bash
|
|
100
|
-
|
|
16
|
+
npm install
|
|
17
|
+
npm run deploy:cf:no-do
|
|
101
18
|
```
|
|
102
19
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
```javascript
|
|
107
|
-
const serverUrl = 'http://localhost:8787/';
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Troubleshooting
|
|
111
|
-
|
|
112
|
-
**Connection errors**: Ensure your domain is on Cloudflare with SSL enabled
|
|
113
|
-
|
|
114
|
-
**Connection refused**: Check the Worker route pattern in `wrangler.toml`
|
|
20
|
+
Notes:
|
|
21
|
+
- This deploys the `dist/` folder (static hosting).
|
|
22
|
+
- No server routes are deployed; Durable Objects are not used.
|
|
115
23
|
|
|
116
|
-
|
|
24
|
+
## Custom Domain (signal.peer.ooo)
|
|
117
25
|
|
|
118
|
-
|
|
26
|
+
To serve the Pages project at `https://signal.peer.ooo`:
|
|
119
27
|
|
|
120
|
-
1.
|
|
121
|
-
2.
|
|
122
|
-
3. Test with multiple browsers
|
|
123
|
-
4. Scale up!
|
|
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`
|
|
124
30
|
|
|
125
31
|
---
|
|
126
32
|
|
package/QUICKSTART_CLOUDFLARE.md
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
# Quick Start - Deploy to Cloudflare
|
|
1
|
+
# Quick Start - Deploy to Cloudflare
|
|
2
|
+
|
|
3
|
+
## Option A (No Durable Objects): Cloudflare Pages (static)
|
|
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
|
+
- Cloudflare account (free tier works)
|
|
9
|
+
- Node.js installed
|
|
10
|
+
- Wrangler CLI authenticated (`npx wrangler login`)
|
|
11
|
+
|
|
12
|
+
### Deploy
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
npm run deploy:cf:no-do
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Wrangler will prompt you to pick/create a Pages project the first time.
|
|
2
19
|
|
|
3
20
|
## Prerequisites
|
|
4
21
|
- Cloudflare account (free tier works)
|
|
5
|
-
- Your domain on Cloudflare
|
|
6
22
|
- Node.js installed
|
|
7
23
|
|
|
8
24
|
## Deploy
|
|
@@ -18,38 +34,12 @@ chmod +x deploy-cloudflare.sh
|
|
|
18
34
|
deploy-cloudflare.bat
|
|
19
35
|
```
|
|
20
36
|
|
|
21
|
-
## What
|
|
22
|
-
1. ✅
|
|
23
|
-
2. ✅
|
|
24
|
-
3. ✅ Asks for your domain (e.g., `signal.peer.ooo`)
|
|
25
|
-
4. ✅ Updates `wrangler.toml`
|
|
26
|
-
5. ✅ Deploys to Cloudflare Workers
|
|
27
|
-
6. ✅ Gives you the live URL
|
|
37
|
+
## What this does
|
|
38
|
+
1. ✅ Builds the Vite site into `dist/`
|
|
39
|
+
2. ✅ Deploys `dist/` to Cloudflare Pages
|
|
28
40
|
|
|
29
41
|
## After deployment:
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
```javascript
|
|
33
|
-
const serverUrl = 'https://signal.peer.ooo/'; // Your domain
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Then reload the demo and it will connect to your Cloudflare Workers signaling server! 🚀
|
|
37
|
-
|
|
38
|
-
## Testing
|
|
39
|
-
|
|
40
|
-
Test the server:
|
|
41
|
-
```bash
|
|
42
|
-
curl https://signal.peer.ooo/health
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
View logs:
|
|
46
|
-
```bash
|
|
47
|
-
wrangler tail --env production
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Local development:
|
|
51
|
-
```bash
|
|
52
|
-
wrangler dev
|
|
53
|
-
```
|
|
43
|
+
Then set your custom domain in Cloudflare Pages (and point `signal` to `<project>.pages.dev`).
|
|
54
44
|
|
|
55
|
-
That's it! Your
|
|
45
|
+
That's it! Your demo is now on Cloudflare Pages (no Durable Objects).
|
package/deploy-cloudflare.bat
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
@echo off
|
|
2
|
-
REM UniWRTC Cloudflare Automated Setup Script (Windows)
|
|
2
|
+
REM UniWRTC Cloudflare Automated Setup Script (Windows, NO Durable Objects)
|
|
3
|
+
REM Deploys the static demo to Cloudflare Pages.
|
|
3
4
|
|
|
4
5
|
setlocal enabledelayedexpansion
|
|
5
6
|
|
|
6
|
-
echo 🚀 UniWRTC Cloudflare Setup
|
|
7
|
+
echo 🚀 UniWRTC Cloudflare Setup (Pages / no Durable Objects)
|
|
7
8
|
echo ============================
|
|
8
9
|
echo.
|
|
9
10
|
|
|
@@ -40,74 +41,29 @@ if errorlevel 1 (
|
|
|
40
41
|
echo ✅ Authenticated with Cloudflare
|
|
41
42
|
echo.
|
|
42
43
|
|
|
43
|
-
REM
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
set /p DOMAIN="Enter your Cloudflare domain (e.g., peer.ooo): "
|
|
47
|
-
set /p SUBDOMAIN="Enter subdomain for signaling (e.g., signal): "
|
|
44
|
+
REM Project name
|
|
45
|
+
set PROJECT_NAME=signal-peer-ooo
|
|
46
|
+
if not "%~1"=="" set PROJECT_NAME=%~1
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
exit /b 1
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
if "!SUBDOMAIN!"=="" (
|
|
55
|
-
echo ❌ Subdomain required
|
|
56
|
-
exit /b 1
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
set FULL_DOMAIN=!SUBDOMAIN!.!DOMAIN!
|
|
60
|
-
|
|
61
|
-
REM Update wrangler.toml
|
|
62
|
-
echo 📝 Updating wrangler.toml...
|
|
63
|
-
(
|
|
64
|
-
echo name = "uniwrtc"
|
|
65
|
-
echo main = "src/index.js"
|
|
66
|
-
echo compatibility_date = "2024-12-20"
|
|
67
|
-
echo.
|
|
68
|
-
echo [env.production]
|
|
69
|
-
echo routes = [
|
|
70
|
-
echo { pattern = "!FULL_DOMAIN!/*", zone_name = "!DOMAIN!" }
|
|
71
|
-
echo ]
|
|
72
|
-
echo.
|
|
73
|
-
echo [[durable_objects.bindings]]
|
|
74
|
-
echo name = "ROOMS"
|
|
75
|
-
echo class_name = "Room"
|
|
76
|
-
echo.
|
|
77
|
-
echo [durable_objects]
|
|
78
|
-
echo migrations = [
|
|
79
|
-
echo { tag = "v1", new_classes = ["Room"] }
|
|
80
|
-
echo ]
|
|
81
|
-
echo.
|
|
82
|
-
echo [build]
|
|
83
|
-
echo command = "npm install"
|
|
84
|
-
) > wrangler.toml
|
|
48
|
+
echo 📦 Building static site...
|
|
49
|
+
call npm run build
|
|
85
50
|
|
|
86
|
-
echo
|
|
51
|
+
echo 🚀 Deploying to Cloudflare Pages project: %PROJECT_NAME%
|
|
87
52
|
echo.
|
|
88
53
|
|
|
54
|
+
REM Create project if needed (ignore errors)
|
|
55
|
+
call npx wrangler pages project create %PROJECT_NAME% --production-branch main >nul 2>nul
|
|
56
|
+
|
|
89
57
|
REM Deploy
|
|
90
|
-
|
|
91
|
-
echo.
|
|
92
|
-
call wrangler deploy --env production
|
|
58
|
+
call npx wrangler pages deploy dist --project-name %PROJECT_NAME%
|
|
93
59
|
|
|
94
60
|
echo.
|
|
95
61
|
echo ✅ Deployment Complete!
|
|
96
62
|
echo.
|
|
97
|
-
echo 🎉 Your
|
|
98
|
-
echo
|
|
99
|
-
echo.
|
|
100
|
-
echo
|
|
101
|
-
echo curl https://!FULL_DOMAIN!/health
|
|
102
|
-
echo.
|
|
103
|
-
echo 🧪 Local testing:
|
|
104
|
-
echo wrangler dev
|
|
105
|
-
echo.
|
|
106
|
-
echo 📊 View logs:
|
|
107
|
-
echo wrangler tail --env production
|
|
108
|
-
echo.
|
|
109
|
-
echo 🛠️ Next: Update demo.html to use:
|
|
110
|
-
echo const serverUrl = 'https://!FULL_DOMAIN!/';
|
|
63
|
+
echo 🎉 Your Pages site is deployed.
|
|
64
|
+
echo Next step for custom domain (manual in Cloudflare UI):
|
|
65
|
+
echo - Pages ^> %PROJECT_NAME% ^> Custom domains ^> add signal.peer.ooo
|
|
66
|
+
echo - DNS: CNAME signal ^> %PROJECT_NAME%.pages.dev
|
|
111
67
|
echo.
|
|
112
68
|
|
|
113
69
|
endlocal
|
package/deploy-cloudflare.sh
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
# UniWRTC Cloudflare Automated Setup Script
|
|
4
|
-
#
|
|
3
|
+
# UniWRTC Cloudflare Automated Setup Script (NO Durable Objects)
|
|
4
|
+
# Deploys the static demo to Cloudflare Pages.
|
|
5
5
|
|
|
6
6
|
set -e
|
|
7
7
|
|
|
8
|
-
echo "🚀 UniWRTC Cloudflare Setup"
|
|
8
|
+
echo "🚀 UniWRTC Cloudflare Setup (Pages / no Durable Objects)"
|
|
9
9
|
echo "============================"
|
|
10
10
|
echo ""
|
|
11
11
|
|
|
@@ -41,67 +41,24 @@ fi
|
|
|
41
41
|
echo "✅ Authenticated with Cloudflare"
|
|
42
42
|
echo ""
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
DOMAIN="peer.ooo"
|
|
46
|
-
SUBDOMAIN="signal"
|
|
44
|
+
PROJECT_NAME=${1:-"signal-peer-ooo"}
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
echo "📦 Building static site..."
|
|
47
|
+
npm run build
|
|
49
48
|
|
|
50
|
-
echo "
|
|
51
|
-
cat > wrangler.toml << EOF
|
|
52
|
-
name = "uniwrtc"
|
|
53
|
-
main = "src/index.js"
|
|
54
|
-
compatibility_date = "2024-12-20"
|
|
49
|
+
echo "🚀 Deploying to Cloudflare Pages project: ${PROJECT_NAME}"
|
|
55
50
|
|
|
56
|
-
|
|
51
|
+
# Create the project if it doesn't exist (ignore error if it already exists)
|
|
52
|
+
npx wrangler pages project create "${PROJECT_NAME}" --production-branch main 2>/dev/null || true
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
name
|
|
60
|
-
class_name = "Room"
|
|
61
|
-
|
|
62
|
-
[[migrations]]
|
|
63
|
-
tag = "v1"
|
|
64
|
-
new_classes = ["Room"]
|
|
65
|
-
|
|
66
|
-
[env.production]
|
|
67
|
-
routes = [
|
|
68
|
-
{ pattern = "${FULL_DOMAIN}/*", zone_name = "${DOMAIN}" }
|
|
69
|
-
]
|
|
70
|
-
|
|
71
|
-
assets = { directory = "./dist", binding = "ASSETS" }
|
|
72
|
-
|
|
73
|
-
[[env.production.durable_objects.bindings]]
|
|
74
|
-
name = "ROOMS"
|
|
75
|
-
class_name = "Room"
|
|
76
|
-
|
|
77
|
-
[build]
|
|
78
|
-
command = "npm install"
|
|
79
|
-
EOF
|
|
80
|
-
|
|
81
|
-
echo "✅ wrangler.toml updated"
|
|
82
|
-
echo ""
|
|
83
|
-
|
|
84
|
-
# Deploy
|
|
85
|
-
echo "🚀 Deploying to Cloudflare..."
|
|
86
|
-
echo ""
|
|
87
|
-
echo "Deploying to production..."
|
|
88
|
-
wrangler deploy --env production
|
|
54
|
+
# Deploy the built assets
|
|
55
|
+
npx wrangler pages deploy dist --project-name "${PROJECT_NAME}"
|
|
89
56
|
|
|
90
57
|
echo ""
|
|
91
58
|
echo "✅ Deployment Complete!"
|
|
92
59
|
echo ""
|
|
93
|
-
echo "🎉 Your
|
|
94
|
-
echo "
|
|
95
|
-
echo ""
|
|
96
|
-
echo "
|
|
97
|
-
echo " curl https://${FULL_DOMAIN}/health"
|
|
98
|
-
echo ""
|
|
99
|
-
echo "🧪 Local testing:"
|
|
100
|
-
echo " wrangler dev"
|
|
101
|
-
echo ""
|
|
102
|
-
echo "📊 View logs:"
|
|
103
|
-
echo " wrangler tail --env production"
|
|
104
|
-
echo ""
|
|
105
|
-
echo "🛠️ Next: Update demo.html to use:"
|
|
106
|
-
echo " const serverUrl = 'https://${FULL_DOMAIN}/';"
|
|
60
|
+
echo "🎉 Your Pages site is deployed."
|
|
61
|
+
echo "Next step for custom domain (manual in Cloudflare UI):"
|
|
62
|
+
echo " - Pages → ${PROJECT_NAME} → Custom domains → add signal.peer.ooo"
|
|
63
|
+
echo " - DNS: CNAME signal → ${PROJECT_NAME}.pages.dev"
|
|
107
64
|
echo ""
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniwrtc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A universal WebRTC signaling service",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"dev": "vite",
|
|
10
10
|
"build": "vite build",
|
|
11
11
|
"preview": "vite preview",
|
|
12
|
+
"deploy:cf:pages": "npm run build && npx wrangler pages deploy dist",
|
|
13
|
+
"deploy:cf:no-do": "npm run deploy:cf:pages",
|
|
12
14
|
"server": "node server.js",
|
|
13
15
|
"test": "node test.js"
|
|
14
16
|
},
|
package/src/main.js
CHANGED
|
@@ -7,6 +7,7 @@ window.UniWRTCClient = UniWRTCClient;
|
|
|
7
7
|
|
|
8
8
|
// Nostr is the default transport (no toggle)
|
|
9
9
|
let nostrClient = null;
|
|
10
|
+
let myPeerId = null;
|
|
10
11
|
|
|
11
12
|
let client = null;
|
|
12
13
|
const peerConnections = new Map();
|
|
@@ -154,6 +155,31 @@ window.connect = async function() {
|
|
|
154
155
|
await connectNostr();
|
|
155
156
|
};
|
|
156
157
|
|
|
158
|
+
function shouldInitiateWith(peerId) {
|
|
159
|
+
// Deterministic initiator to avoid offer glare
|
|
160
|
+
if (!myPeerId) return false;
|
|
161
|
+
return myPeerId.localeCompare(peerId) < 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sendSignal(to, payload) {
|
|
165
|
+
if (!nostrClient) throw new Error('Not connected to Nostr');
|
|
166
|
+
return nostrClient.send({
|
|
167
|
+
...payload,
|
|
168
|
+
to,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function ensurePeerConnection(peerId) {
|
|
173
|
+
if (!peerId || peerId === myPeerId) return null;
|
|
174
|
+
if (peerConnections.has(peerId) && peerConnections.get(peerId) instanceof RTCPeerConnection) {
|
|
175
|
+
return peerConnections.get(peerId);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const initiator = shouldInitiateWith(peerId);
|
|
179
|
+
const pc = await createPeerConnection(peerId, initiator);
|
|
180
|
+
return pc;
|
|
181
|
+
}
|
|
182
|
+
|
|
157
183
|
async function connectNostr() {
|
|
158
184
|
const relayUrl = document.getElementById('relayUrl').value.trim();
|
|
159
185
|
const roomIdInput = document.getElementById('roomId');
|
|
@@ -182,22 +208,65 @@ async function connectNostr() {
|
|
|
182
208
|
if (state === 'connected') updateStatus(true);
|
|
183
209
|
if (state === 'disconnected') updateStatus(false);
|
|
184
210
|
},
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
211
|
+
onPayload: async ({ from, payload }) => {
|
|
212
|
+
const peerId = from;
|
|
213
|
+
if (!peerId || peerId === myPeerId) return;
|
|
214
|
+
|
|
189
215
|
// Use the existing peer list UI as a simple "seen peers" list
|
|
190
216
|
if (!peerConnections.has(peerId)) {
|
|
191
217
|
peerConnections.set(peerId, null);
|
|
192
218
|
updatePeerList();
|
|
193
219
|
log(`Peer seen: ${peerId.substring(0, 6)}...`, 'success');
|
|
194
220
|
}
|
|
195
|
-
|
|
221
|
+
|
|
222
|
+
if (!payload || typeof payload !== 'object') return;
|
|
223
|
+
|
|
224
|
+
// Presence
|
|
225
|
+
if (payload.type === 'hello') {
|
|
226
|
+
await ensurePeerConnection(peerId);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Signaling messages are always targeted
|
|
231
|
+
if (payload.to && payload.to !== myPeerId) return;
|
|
232
|
+
|
|
233
|
+
if (payload.type === 'signal-offer' && payload.sdp) {
|
|
234
|
+
log(`Received offer from ${peerId.substring(0, 6)}...`, 'info');
|
|
235
|
+
const pc = await ensurePeerConnection(peerId);
|
|
236
|
+
await pc.setRemoteDescription(new RTCSessionDescription(payload.sdp));
|
|
237
|
+
const answer = await pc.createAnswer();
|
|
238
|
+
await pc.setLocalDescription(answer);
|
|
239
|
+
await sendSignal(peerId, { type: 'signal-answer', sdp: pc.localDescription });
|
|
240
|
+
log(`Sent answer to ${peerId.substring(0, 6)}...`, 'success');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (payload.type === 'signal-answer' && payload.sdp) {
|
|
245
|
+
log(`Received answer from ${peerId.substring(0, 6)}...`, 'info');
|
|
246
|
+
const pc = peerConnections.get(peerId);
|
|
247
|
+
if (pc instanceof RTCPeerConnection) {
|
|
248
|
+
await pc.setRemoteDescription(new RTCSessionDescription(payload.sdp));
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (payload.type === 'signal-ice' && payload.candidate) {
|
|
254
|
+
const pc = peerConnections.get(peerId);
|
|
255
|
+
if (pc instanceof RTCPeerConnection) {
|
|
256
|
+
try {
|
|
257
|
+
await pc.addIceCandidate(new RTCIceCandidate(payload.candidate));
|
|
258
|
+
} catch (e) {
|
|
259
|
+
log(`Failed to add ICE candidate: ${e?.message || e}`, 'warning');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
},
|
|
196
264
|
});
|
|
197
265
|
|
|
198
266
|
await nostrClient.connect();
|
|
199
267
|
|
|
200
268
|
const myPubkey = nostrClient.getPublicKey();
|
|
269
|
+
myPeerId = myPubkey;
|
|
201
270
|
document.getElementById('clientId').textContent = myPubkey.substring(0, 16) + '...';
|
|
202
271
|
document.getElementById('sessionId').textContent = effectiveRoom;
|
|
203
272
|
log(`Joined Nostr room: ${effectiveRoom}`, 'success');
|
|
@@ -336,6 +405,13 @@ window.disconnect = function() {
|
|
|
336
405
|
if (nostrClient) {
|
|
337
406
|
nostrClient.disconnect().catch(() => {});
|
|
338
407
|
nostrClient = null;
|
|
408
|
+
myPeerId = null;
|
|
409
|
+
peerConnections.forEach((pc) => {
|
|
410
|
+
if (pc instanceof RTCPeerConnection) pc.close();
|
|
411
|
+
});
|
|
412
|
+
peerConnections.clear();
|
|
413
|
+
dataChannels.clear();
|
|
414
|
+
updatePeerList();
|
|
339
415
|
}
|
|
340
416
|
if (client) {
|
|
341
417
|
client.disconnect();
|
|
@@ -366,7 +442,11 @@ async function createPeerConnection(peerId, shouldInitiate) {
|
|
|
366
442
|
pc.onicecandidate = (event) => {
|
|
367
443
|
if (event.candidate) {
|
|
368
444
|
log(`Sending ICE candidate to ${peerId.substring(0, 6)}...`, 'info');
|
|
369
|
-
|
|
445
|
+
if (nostrClient) {
|
|
446
|
+
sendSignal(peerId, { type: 'signal-ice', candidate: event.candidate.toJSON?.() || event.candidate });
|
|
447
|
+
} else if (client) {
|
|
448
|
+
client.sendIceCandidate(event.candidate, peerId);
|
|
449
|
+
}
|
|
370
450
|
}
|
|
371
451
|
};
|
|
372
452
|
|
|
@@ -381,7 +461,11 @@ async function createPeerConnection(peerId, shouldInitiate) {
|
|
|
381
461
|
|
|
382
462
|
const offer = await pc.createOffer();
|
|
383
463
|
await pc.setLocalDescription(offer);
|
|
384
|
-
|
|
464
|
+
if (nostrClient) {
|
|
465
|
+
await sendSignal(peerId, { type: 'signal-offer', sdp: pc.localDescription });
|
|
466
|
+
} else if (client) {
|
|
467
|
+
client.sendOffer(offer, peerId);
|
|
468
|
+
}
|
|
385
469
|
log(`Sent offer to ${peerId.substring(0, 6)}...`, 'success');
|
|
386
470
|
} else {
|
|
387
471
|
log(`Waiting for offer from ${peerId.substring(0, 6)}...`, 'info');
|
|
@@ -416,17 +500,8 @@ window.sendChatMessage = function() {
|
|
|
416
500
|
return;
|
|
417
501
|
}
|
|
418
502
|
|
|
419
|
-
if (nostrClient) {
|
|
420
|
-
nostrClient.sendMessage(message).catch((e) => {
|
|
421
|
-
log(`Failed to send via Nostr: ${e?.message || e}`, 'error');
|
|
422
|
-
});
|
|
423
|
-
displayChatMessage(message, 'You', true);
|
|
424
|
-
document.getElementById('chatMessage').value = '';
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
503
|
if (dataChannels.size === 0) {
|
|
429
|
-
log('No
|
|
504
|
+
log('No data channels yet. Open this room in another tab/browser and wait for WebRTC to connect.', 'error');
|
|
430
505
|
return;
|
|
431
506
|
}
|
|
432
507
|
|
package/src/nostr/nostrClient.js
CHANGED
|
@@ -15,7 +15,7 @@ function isHex64(s) {
|
|
|
15
15
|
* - Publishes kind:1 events tagged with ['t', room] and ['room', room]
|
|
16
16
|
* - Subscribes to kind:1 events filtered by #t
|
|
17
17
|
*/
|
|
18
|
-
export function createNostrClient({ relayUrl, room,
|
|
18
|
+
export function createNostrClient({ relayUrl, room, onPayload, onState } = {}) {
|
|
19
19
|
if (!relayUrl) throw new Error('relayUrl is required');
|
|
20
20
|
if (!room) throw new Error('room is required');
|
|
21
21
|
|
|
@@ -80,6 +80,9 @@ export function createNostrClient({ relayUrl, room, onMessage, onPeer, onState }
|
|
|
80
80
|
if (nostrEvent.id && state.seen.has(nostrEvent.id)) return;
|
|
81
81
|
if (nostrEvent.id) state.seen.add(nostrEvent.id);
|
|
82
82
|
|
|
83
|
+
// Ignore our own events
|
|
84
|
+
if (nostrEvent.pubkey && nostrEvent.pubkey === state.pubkey) return;
|
|
85
|
+
|
|
83
86
|
// Ensure it's for our room
|
|
84
87
|
const tags = Array.isArray(nostrEvent.tags) ? nostrEvent.tags : [];
|
|
85
88
|
const roomTag = tags.find((t) => Array.isArray(t) && t[0] === 'room');
|
|
@@ -93,22 +96,15 @@ export function createNostrClient({ relayUrl, room, onMessage, onPeer, onState }
|
|
|
93
96
|
return;
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (payload?.type === 'message' && typeof payload.message === 'string') {
|
|
106
|
-
const from = (payload.peerId || nostrEvent.pubkey || 'peer').substring(0, 8) + '...';
|
|
107
|
-
try {
|
|
108
|
-
onMessage?.({ from, text: payload.message });
|
|
109
|
-
} catch {
|
|
110
|
-
// ignore
|
|
111
|
-
}
|
|
99
|
+
try {
|
|
100
|
+
onPayload?.({
|
|
101
|
+
from: nostrEvent.pubkey,
|
|
102
|
+
payload,
|
|
103
|
+
eventId: nostrEvent.id,
|
|
104
|
+
createdAt: nostrEvent.created_at,
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
// ignore
|
|
112
108
|
}
|
|
113
109
|
}
|
|
114
110
|
}
|
|
@@ -160,37 +156,12 @@ export function createNostrClient({ relayUrl, room, onMessage, onPeer, onState }
|
|
|
160
156
|
sendRaw(['REQ', state.subId, filter]);
|
|
161
157
|
|
|
162
158
|
// Announce presence
|
|
163
|
-
await
|
|
159
|
+
await send({ type: 'hello' });
|
|
164
160
|
|
|
165
161
|
setState('connected');
|
|
166
162
|
}
|
|
167
163
|
|
|
168
|
-
async function
|
|
169
|
-
ensureKeys();
|
|
170
|
-
const created_at = Math.floor(Date.now() / 1000);
|
|
171
|
-
const tags = [
|
|
172
|
-
['room', state.room],
|
|
173
|
-
['t', state.room],
|
|
174
|
-
];
|
|
175
|
-
|
|
176
|
-
const eventTemplate = {
|
|
177
|
-
kind: 1,
|
|
178
|
-
created_at,
|
|
179
|
-
tags,
|
|
180
|
-
content: JSON.stringify({
|
|
181
|
-
type: 'peer-join',
|
|
182
|
-
peerId: state.pubkey,
|
|
183
|
-
room: state.room,
|
|
184
|
-
timestamp: Date.now(),
|
|
185
|
-
}),
|
|
186
|
-
pubkey: state.pubkey,
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const signed = finalizeEvent(eventTemplate, state.secretKeyHex);
|
|
190
|
-
sendRaw(['EVENT', signed]);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function sendMessage(text) {
|
|
164
|
+
async function send(payload) {
|
|
194
165
|
ensureKeys();
|
|
195
166
|
const created_at = Math.floor(Date.now() / 1000);
|
|
196
167
|
const tags = [
|
|
@@ -203,9 +174,8 @@ export function createNostrClient({ relayUrl, room, onMessage, onPeer, onState }
|
|
|
203
174
|
created_at,
|
|
204
175
|
tags,
|
|
205
176
|
content: JSON.stringify({
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
peerId: state.pubkey,
|
|
177
|
+
...payload,
|
|
178
|
+
from: state.pubkey,
|
|
209
179
|
room: state.room,
|
|
210
180
|
timestamp: Date.now(),
|
|
211
181
|
}),
|
|
@@ -237,7 +207,7 @@ export function createNostrClient({ relayUrl, room, onMessage, onPeer, onState }
|
|
|
237
207
|
return {
|
|
238
208
|
connect,
|
|
239
209
|
disconnect,
|
|
240
|
-
|
|
210
|
+
send,
|
|
241
211
|
getPublicKey: getPublicKeyHex,
|
|
242
212
|
};
|
|
243
213
|
}
|
package/wrangler.toml
CHANGED
|
@@ -1,27 +1,2 @@
|
|
|
1
|
-
name = "
|
|
2
|
-
|
|
3
|
-
compatibility_date = "2024-12-20"
|
|
4
|
-
|
|
5
|
-
assets = { directory = "./dist", binding = "ASSETS" }
|
|
6
|
-
|
|
7
|
-
[[durable_objects.bindings]]
|
|
8
|
-
name = "ROOMS"
|
|
9
|
-
class_name = "Room"
|
|
10
|
-
|
|
11
|
-
[[migrations]]
|
|
12
|
-
tag = "v1"
|
|
13
|
-
new_classes = ["Room"]
|
|
14
|
-
|
|
15
|
-
[env.production]
|
|
16
|
-
routes = [
|
|
17
|
-
{ pattern = "signal.peer.ooo/*", zone_name = "peer.ooo" }
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
assets = { directory = "./dist", binding = "ASSETS" }
|
|
21
|
-
|
|
22
|
-
[[env.production.durable_objects.bindings]]
|
|
23
|
-
name = "ROOMS"
|
|
24
|
-
class_name = "Room"
|
|
25
|
-
|
|
26
|
-
[build]
|
|
27
|
-
command = "npm install"
|
|
1
|
+
name = "signal-peer-ooo"
|
|
2
|
+
pages_build_output_dir = "dist"
|