@xtr-dev/rondevu-server 0.1.5 → 0.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/README.md +217 -69
- package/dist/index.js +1067 -368
- package/dist/index.js.map +4 -4
- package/package.json +3 -2
- package/src/app.ts +339 -271
- package/src/crypto.ts +164 -0
- package/src/storage/d1.ts +295 -119
- package/src/storage/sqlite.ts +309 -107
- package/src/storage/types.ts +159 -29
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@xtr-dev/rondevu-server)
|
|
4
4
|
|
|
5
|
-
🌐 **
|
|
5
|
+
🌐 **DNS-like WebRTC signaling with username claiming and service discovery**
|
|
6
6
|
|
|
7
|
-
Scalable
|
|
7
|
+
Scalable WebRTC signaling server with cryptographic username claiming, service publishing, and privacy-preserving discovery.
|
|
8
8
|
|
|
9
9
|
**Related repositories:**
|
|
10
10
|
- [@xtr-dev/rondevu-client](https://github.com/xtr-dev/rondevu-client) - TypeScript client library ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-client))
|
|
@@ -15,14 +15,28 @@ Scalable peer-to-peer connection establishment with topic-based discovery, state
|
|
|
15
15
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
|
-
- **
|
|
18
|
+
- **Username Claiming**: Cryptographic username ownership with Ed25519 signatures (365-day validity, auto-renewed on use)
|
|
19
|
+
- **Service Publishing**: Package-style naming with semantic versioning (com.example.chat@1.0.0)
|
|
20
|
+
- **Privacy-Preserving Discovery**: UUID-based service index prevents enumeration
|
|
21
|
+
- **Public/Private Services**: Control service visibility
|
|
19
22
|
- **Stateless Authentication**: AES-256-GCM encrypted credentials, no server-side sessions
|
|
20
|
-
- **Protected Offers**: Optional secret field for access-controlled peer connections
|
|
21
|
-
- **Bloom Filters**: Client-side peer exclusion for efficient discovery
|
|
22
|
-
- **Multi-Offer Support**: Create multiple offers per peer simultaneously
|
|
23
23
|
- **Complete WebRTC Signaling**: Offer/answer exchange and ICE candidate relay
|
|
24
24
|
- **Dual Storage**: SQLite (Node.js/Docker) and Cloudflare D1 (Workers) backends
|
|
25
25
|
|
|
26
|
+
## Architecture
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Username Claiming → Service Publishing → Service Discovery → WebRTC Connection
|
|
30
|
+
|
|
31
|
+
alice claims "alice" with Ed25519 signature
|
|
32
|
+
↓
|
|
33
|
+
alice publishes com.example.chat@1.0.0 → receives UUID abc123
|
|
34
|
+
↓
|
|
35
|
+
bob queries alice's services → gets UUID abc123
|
|
36
|
+
↓
|
|
37
|
+
bob connects to UUID abc123 → WebRTC connection established
|
|
38
|
+
```
|
|
39
|
+
|
|
26
40
|
## Quick Start
|
|
27
41
|
|
|
28
42
|
**Node.js:**
|
|
@@ -32,7 +46,7 @@ npm install && npm start
|
|
|
32
46
|
|
|
33
47
|
**Docker:**
|
|
34
48
|
```bash
|
|
35
|
-
docker build -t rondevu . && docker run -p 3000:3000 -e STORAGE_PATH=:memory: rondevu
|
|
49
|
+
docker build -t rondevu . && docker run -p 3000:3000 -e STORAGE_PATH=:memory: -e AUTH_SECRET=$(openssl rand -hex 32) rondevu
|
|
36
50
|
```
|
|
37
51
|
|
|
38
52
|
**Cloudflare Workers:**
|
|
@@ -53,86 +67,182 @@ Health check endpoint with version
|
|
|
53
67
|
#### `POST /register`
|
|
54
68
|
Register a new peer and receive credentials (peerId + secret)
|
|
55
69
|
|
|
56
|
-
|
|
70
|
+
Generates a cryptographically random 128-bit peer ID.
|
|
71
|
+
|
|
72
|
+
**Response:**
|
|
57
73
|
```json
|
|
58
74
|
{
|
|
59
|
-
"peerId": "
|
|
75
|
+
"peerId": "f17c195f067255e357232e34cf0735d9",
|
|
76
|
+
"secret": "DdorTR8QgSn9yngn+4qqR8cs1aMijvX..."
|
|
60
77
|
}
|
|
61
78
|
```
|
|
62
79
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
### Username Management
|
|
81
|
+
|
|
82
|
+
#### `POST /usernames/claim`
|
|
83
|
+
Claim a username with cryptographic proof
|
|
84
|
+
|
|
85
|
+
**Request:**
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"username": "alice",
|
|
89
|
+
"publicKey": "base64-encoded-ed25519-public-key",
|
|
90
|
+
"signature": "base64-encoded-signature",
|
|
91
|
+
"message": "claim:alice:1733404800000"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
66
94
|
|
|
67
95
|
**Response:**
|
|
68
96
|
```json
|
|
69
97
|
{
|
|
70
|
-
"
|
|
71
|
-
"
|
|
98
|
+
"username": "alice",
|
|
99
|
+
"claimedAt": 1733404800000,
|
|
100
|
+
"expiresAt": 1765027200000
|
|
72
101
|
}
|
|
73
102
|
```
|
|
74
103
|
|
|
75
|
-
|
|
76
|
-
|
|
104
|
+
**Validation:**
|
|
105
|
+
- Username format: `^[a-z0-9][a-z0-9-]*[a-z0-9]$` (3-32 characters)
|
|
106
|
+
- Signature must be valid Ed25519 signature
|
|
107
|
+
- Timestamp must be within 5 minutes (replay protection)
|
|
108
|
+
- Expires after 365 days, auto-renewed on use
|
|
77
109
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
- `offset` (optional): Number of topics to skip (default: 0)
|
|
110
|
+
#### `GET /usernames/:username`
|
|
111
|
+
Check username availability and claim status
|
|
81
112
|
|
|
82
113
|
**Response:**
|
|
83
114
|
```json
|
|
84
115
|
{
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"
|
|
90
|
-
"limit": 50,
|
|
91
|
-
"offset": 0
|
|
116
|
+
"username": "alice",
|
|
117
|
+
"available": false,
|
|
118
|
+
"claimedAt": 1733404800000,
|
|
119
|
+
"expiresAt": 1765027200000,
|
|
120
|
+
"publicKey": "..."
|
|
92
121
|
}
|
|
93
122
|
```
|
|
94
123
|
|
|
95
|
-
#### `GET /
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
**Query Parameters:**
|
|
99
|
-
- `limit` (optional): Maximum offers to return (default: 50, max: 200)
|
|
100
|
-
- `bloom` (optional): Base64-encoded bloom filter to exclude known peers
|
|
124
|
+
#### `GET /usernames/:username/services`
|
|
125
|
+
List all services for a username (privacy-preserving)
|
|
101
126
|
|
|
102
127
|
**Response:**
|
|
103
128
|
```json
|
|
104
129
|
{
|
|
105
|
-
"
|
|
106
|
-
"
|
|
130
|
+
"username": "alice",
|
|
131
|
+
"services": [
|
|
107
132
|
{
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
133
|
+
"uuid": "abc123",
|
|
134
|
+
"isPublic": false
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"uuid": "def456",
|
|
138
|
+
"isPublic": true,
|
|
139
|
+
"serviceFqn": "com.example.public@1.0.0",
|
|
140
|
+
"metadata": { "description": "Public service" }
|
|
116
141
|
}
|
|
117
|
-
]
|
|
118
|
-
|
|
119
|
-
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Service Management
|
|
147
|
+
|
|
148
|
+
#### `POST /services`
|
|
149
|
+
Publish a service (requires authentication and username signature)
|
|
150
|
+
|
|
151
|
+
**Headers:**
|
|
152
|
+
- `Authorization: Bearer {peerId}:{secret}`
|
|
153
|
+
|
|
154
|
+
**Request:**
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"username": "alice",
|
|
158
|
+
"serviceFqn": "com.example.chat@1.0.0",
|
|
159
|
+
"sdp": "v=0...",
|
|
160
|
+
"ttl": 300000,
|
|
161
|
+
"isPublic": false,
|
|
162
|
+
"metadata": { "description": "Chat service" },
|
|
163
|
+
"signature": "base64-encoded-signature",
|
|
164
|
+
"message": "publish:alice:com.example.chat@1.0.0:1733404800000"
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Response:**
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"serviceId": "uuid-v4",
|
|
172
|
+
"uuid": "uuid-v4-for-index",
|
|
173
|
+
"offerId": "offer-hash-id",
|
|
174
|
+
"expiresAt": 1733405100000
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Service FQN Format:**
|
|
179
|
+
- Service name: Reverse domain notation (e.g., `com.example.chat`)
|
|
180
|
+
- Version: Semantic versioning (e.g., `1.0.0`, `2.1.3-beta`)
|
|
181
|
+
- Complete FQN: `service-name@version` (e.g., `com.example.chat@1.0.0`)
|
|
182
|
+
|
|
183
|
+
**Validation:**
|
|
184
|
+
- Service name pattern: `^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$`
|
|
185
|
+
- Length: 3-128 characters
|
|
186
|
+
- Version pattern: `^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9.-]+)?$`
|
|
187
|
+
|
|
188
|
+
#### `GET /services/:uuid`
|
|
189
|
+
Get service details by UUID
|
|
190
|
+
|
|
191
|
+
**Response:**
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"serviceId": "...",
|
|
195
|
+
"username": "alice",
|
|
196
|
+
"serviceFqn": "com.example.chat@1.0.0",
|
|
197
|
+
"offerId": "...",
|
|
198
|
+
"sdp": "v=0...",
|
|
199
|
+
"isPublic": false,
|
|
200
|
+
"metadata": { ... },
|
|
201
|
+
"createdAt": 1733404800000,
|
|
202
|
+
"expiresAt": 1733405100000
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `DELETE /services/:serviceId`
|
|
207
|
+
Unpublish a service (requires authentication and ownership)
|
|
208
|
+
|
|
209
|
+
**Headers:**
|
|
210
|
+
- `Authorization: Bearer {peerId}:{secret}`
|
|
211
|
+
|
|
212
|
+
**Request:**
|
|
213
|
+
```json
|
|
214
|
+
{
|
|
215
|
+
"username": "alice"
|
|
120
216
|
}
|
|
121
217
|
```
|
|
122
218
|
|
|
123
|
-
|
|
124
|
-
- `hasSecret`: Boolean flag indicating whether a secret is required to answer this offer. The actual secret is never exposed in public endpoints.
|
|
125
|
-
- `info`: Optional public metadata field (max 128 characters) visible to all peers.
|
|
219
|
+
### Service Discovery
|
|
126
220
|
|
|
127
|
-
#### `
|
|
128
|
-
|
|
221
|
+
#### `POST /index/:username/query`
|
|
222
|
+
Query a service by FQN
|
|
129
223
|
|
|
130
|
-
|
|
224
|
+
**Request:**
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"serviceFqn": "com.example.chat@1.0.0"
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Response:**
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"uuid": "abc123",
|
|
235
|
+
"allowed": true
|
|
236
|
+
}
|
|
237
|
+
```
|
|
131
238
|
|
|
132
|
-
|
|
239
|
+
### Offer Management (Low-level)
|
|
133
240
|
|
|
134
241
|
#### `POST /offers`
|
|
135
|
-
Create one or more offers
|
|
242
|
+
Create one or more offers (requires authentication)
|
|
243
|
+
|
|
244
|
+
**Headers:**
|
|
245
|
+
- `Authorization: Bearer {peerId}:{secret}`
|
|
136
246
|
|
|
137
247
|
**Request:**
|
|
138
248
|
```json
|
|
@@ -140,19 +250,12 @@ Create one or more offers
|
|
|
140
250
|
"offers": [
|
|
141
251
|
{
|
|
142
252
|
"sdp": "v=0...",
|
|
143
|
-
"
|
|
144
|
-
"ttl": 300000,
|
|
145
|
-
"secret": "my-secret-password", // Optional: protect offer (max 128 chars)
|
|
146
|
-
"info": "Looking for peers in EU region" // Optional: public info (max 128 chars)
|
|
253
|
+
"ttl": 300000
|
|
147
254
|
}
|
|
148
255
|
]
|
|
149
256
|
}
|
|
150
257
|
```
|
|
151
258
|
|
|
152
|
-
**Notes:**
|
|
153
|
-
- `secret` (optional): Protect the offer with a secret. Answerers must provide the correct secret to connect.
|
|
154
|
-
- `info` (optional): Public metadata visible to all peers (max 128 characters). Useful for describing the offer or connection requirements.
|
|
155
|
-
|
|
156
259
|
#### `GET /offers/mine`
|
|
157
260
|
List all offers owned by authenticated peer
|
|
158
261
|
|
|
@@ -168,14 +271,10 @@ Answer an offer (locks it to answerer)
|
|
|
168
271
|
**Request:**
|
|
169
272
|
```json
|
|
170
273
|
{
|
|
171
|
-
"sdp": "v=0..."
|
|
172
|
-
"secret": "my-secret-password" // Required if offer is protected
|
|
274
|
+
"sdp": "v=0..."
|
|
173
275
|
}
|
|
174
276
|
```
|
|
175
277
|
|
|
176
|
-
**Notes:**
|
|
177
|
-
- `secret` (optional): Required if the offer was created with a secret. Must match the offer's secret.
|
|
178
|
-
|
|
179
278
|
#### `GET /offers/answers`
|
|
180
279
|
Poll for answers to your offers
|
|
181
280
|
|
|
@@ -201,13 +300,62 @@ Environment variables:
|
|
|
201
300
|
| `PORT` | `3000` | Server port (Node.js/Docker) |
|
|
202
301
|
| `CORS_ORIGINS` | `*` | Comma-separated allowed origins |
|
|
203
302
|
| `STORAGE_PATH` | `./rondevu.db` | SQLite database path (use `:memory:` for in-memory) |
|
|
204
|
-
| `VERSION` | `0.
|
|
205
|
-
| `AUTH_SECRET` | Random 32-byte hex | Secret key for credential encryption |
|
|
303
|
+
| `VERSION` | `2.0.0` | Server version (semver) |
|
|
304
|
+
| `AUTH_SECRET` | Random 32-byte hex | Secret key for credential encryption (required for production) |
|
|
206
305
|
| `OFFER_DEFAULT_TTL` | `300000` | Default offer TTL in ms (5 minutes) |
|
|
207
306
|
| `OFFER_MIN_TTL` | `60000` | Minimum offer TTL in ms (1 minute) |
|
|
208
307
|
| `OFFER_MAX_TTL` | `3600000` | Maximum offer TTL in ms (1 hour) |
|
|
209
308
|
| `MAX_OFFERS_PER_REQUEST` | `10` | Maximum offers per create request |
|
|
210
|
-
|
|
309
|
+
|
|
310
|
+
## Database Schema
|
|
311
|
+
|
|
312
|
+
### usernames
|
|
313
|
+
- `username` (PK): Claimed username
|
|
314
|
+
- `public_key`: Ed25519 public key (base64)
|
|
315
|
+
- `claimed_at`: Claim timestamp
|
|
316
|
+
- `expires_at`: Expiry timestamp (365 days)
|
|
317
|
+
- `last_used`: Last activity timestamp
|
|
318
|
+
- `metadata`: Optional JSON metadata
|
|
319
|
+
|
|
320
|
+
### services
|
|
321
|
+
- `id` (PK): Service ID (UUID)
|
|
322
|
+
- `username` (FK): Owner username
|
|
323
|
+
- `service_fqn`: Fully qualified name (com.example.chat@1.0.0)
|
|
324
|
+
- `offer_id` (FK): WebRTC offer ID
|
|
325
|
+
- `is_public`: Public/private flag
|
|
326
|
+
- `metadata`: JSON metadata
|
|
327
|
+
- `created_at`, `expires_at`: Timestamps
|
|
328
|
+
|
|
329
|
+
### service_index (privacy layer)
|
|
330
|
+
- `uuid` (PK): Random UUID for discovery
|
|
331
|
+
- `service_id` (FK): Links to service
|
|
332
|
+
- `username`, `service_fqn`: Denormalized for performance
|
|
333
|
+
|
|
334
|
+
## Security
|
|
335
|
+
|
|
336
|
+
### Username Claiming
|
|
337
|
+
- **Algorithm**: Ed25519 signatures
|
|
338
|
+
- **Message Format**: `claim:{username}:{timestamp}`
|
|
339
|
+
- **Replay Protection**: Timestamp must be within 5 minutes
|
|
340
|
+
- **Key Management**: Private keys never leave the client
|
|
341
|
+
|
|
342
|
+
### Service Publishing
|
|
343
|
+
- **Ownership Verification**: Every publish requires username signature
|
|
344
|
+
- **Message Format**: `publish:{username}:{serviceFqn}:{timestamp}`
|
|
345
|
+
- **Auto-Renewal**: Publishing a service extends username expiry
|
|
346
|
+
|
|
347
|
+
### Privacy
|
|
348
|
+
- **Private Services**: Only UUID exposed, FQN hidden
|
|
349
|
+
- **Public Services**: FQN and metadata visible
|
|
350
|
+
- **No Enumeration**: Cannot list all services without knowing FQN
|
|
351
|
+
|
|
352
|
+
## Migration from V1
|
|
353
|
+
|
|
354
|
+
V2 is a **breaking change** that removes topic-based discovery. See [MIGRATION.md](../MIGRATION.md) for detailed migration guide.
|
|
355
|
+
|
|
356
|
+
**Key Changes:**
|
|
357
|
+
- ❌ Removed: Topic-based discovery, bloom filters, public peer listings
|
|
358
|
+
- ✅ Added: Username claiming, service publishing, UUID-based privacy
|
|
211
359
|
|
|
212
360
|
## License
|
|
213
361
|
|