@xtr-dev/rondevu-server 0.0.1 → 0.1.1

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/API.md CHANGED
@@ -19,6 +19,33 @@ This allows multiple peers from the same application (origin) to discover each o
19
19
 
20
20
  ## GET `/`
21
21
 
22
+ Returns server version information including the git commit hash used to build the server.
23
+
24
+ ### Response
25
+
26
+ **Content-Type:** `application/json`
27
+
28
+ **Success (200 OK):**
29
+ ```json
30
+ {
31
+ "version": "a1b2c3d"
32
+ }
33
+ ```
34
+
35
+ **Notes:**
36
+ - Returns the git commit hash from build time
37
+ - Returns "unknown" if git information is not available
38
+
39
+ ### Example
40
+
41
+ ```bash
42
+ curl -X GET http://localhost:3000/
43
+ ```
44
+
45
+ ---
46
+
47
+ ## GET `/topics`
48
+
22
49
  Lists all topics with the count of available peers for each (paginated). Returns only topics that have unanswered sessions.
23
50
 
24
51
  ### Request
@@ -70,13 +97,13 @@ Lists all topics with the count of available peers for each (paginated). Returns
70
97
 
71
98
  **Default pagination (page 1, limit 100):**
72
99
  ```bash
73
- curl -X GET http://localhost:3000/ \
100
+ curl -X GET http://localhost:3000/topics \
74
101
  -H "Origin: https://example.com"
75
102
  ```
76
103
 
77
104
  **Custom pagination:**
78
105
  ```bash
79
- curl -X GET "http://localhost:3000/?page=2&limit=50" \
106
+ curl -X GET "http://localhost:3000/topics?page=2&limit=50" \
80
107
  -H "Origin: https://example.com"
81
108
  ```
82
109
 
@@ -403,26 +430,29 @@ All endpoints may return the following error responses:
403
430
 
404
431
  ### Peer Discovery and Connection
405
432
 
406
- 1. **Discover active topics:**
407
- - GET `/` to see all topics and peer counts
433
+ 1. **Check server version (optional):**
434
+ - GET `/` to see server version information
435
+
436
+ 2. **Discover active topics:**
437
+ - GET `/topics` to see all topics and peer counts
408
438
  - Optional: paginate through results with `?page=2&limit=100`
409
439
 
410
- 2. **Peer A announces availability:**
440
+ 3. **Peer A announces availability:**
411
441
  - POST `/:topic/offer` with peer identifier and signaling data
412
442
  - Receives a unique session code
413
443
 
414
- 3. **Peer B discovers peers:**
444
+ 4. **Peer B discovers peers:**
415
445
  - GET `/:topic/sessions` to list available sessions in a topic
416
446
  - Filters out sessions with their own info to avoid self-connection
417
447
  - Selects a peer to connect to
418
448
 
419
- 4. **Peer B initiates connection:**
449
+ 5. **Peer B initiates connection:**
420
450
  - POST `/answer` with the session code and their signaling data
421
451
 
422
- 5. **Both peers exchange signaling information:**
452
+ 6. **Both peers exchange signaling information:**
423
453
  - POST `/answer` with additional signaling data as needed
424
454
  - POST `/poll` to retrieve signaling data from the other peer
425
455
 
426
- 6. **Peer connection established**
456
+ 7. **Peer connection established**
427
457
  - Peers use exchanged signaling data to establish direct connection
428
458
  - Session automatically expires after configured timeout
package/CLAUDE.md ADDED
@@ -0,0 +1,47 @@
1
+ # Rondevu Server Development Guidelines
2
+
3
+ ## WebRTC Signaling Best Practices
4
+
5
+ ### ICE Candidate Storage
6
+
7
+ **IMPORTANT: Store ICE candidates as raw JSON without enforcing structure.**
8
+
9
+ When handling ICE candidates in the signaling server:
10
+
11
+ - ✅ **DO** store candidates as `JSON.stringify(candidate)` in the database
12
+ - ✅ **DO** retrieve candidates as `JSON.parse(candidate)` from the database
13
+ - ✅ **DO** use generic types like `any` in TypeScript for candidate data
14
+ - ❌ **DON'T** define strict types for ICE candidate structure
15
+ - ❌ **DON'T** validate or modify candidate properties
16
+ - ❌ **DON'T** assume you know what properties clients will send
17
+
18
+ **Why?** The server is just a relay - it doesn't need to understand the candidate structure. Different browsers and future WebRTC versions may include different properties. By keeping the server agnostic, we maintain maximum compatibility.
19
+
20
+ ### Server Role Filtering
21
+
22
+ The server MUST filter ICE candidates by role:
23
+ - Offerers receive only answerer candidates (`WHERE role = 'answerer'`)
24
+ - Answerers receive only offerer candidates (`WHERE role = 'offerer'`)
25
+
26
+ This prevents peers from receiving their own candidates, which would cause connection failures.
27
+
28
+ ## Security
29
+
30
+ - Always validate authentication tokens before allowing operations
31
+ - Verify ownership before allowing modifications
32
+ - Rate limit API endpoints to prevent abuse
33
+ - Clean up expired offers regularly
34
+
35
+ ## Performance
36
+
37
+ - Use transactions for batch operations (SQLite)
38
+ - Index frequently queried columns (offer_id, role, created_at)
39
+ - Set appropriate TTLs for offers
40
+ - Implement pagination for large result sets
41
+
42
+ ## Code Quality
43
+
44
+ - Handle errors gracefully with informative HTTP status codes
45
+ - Log important events for debugging
46
+ - Use TypeScript types for API contracts, but keep data types generic
47
+ - Write tests for critical paths
package/README.md CHANGED
@@ -1,242 +1,199 @@
1
- # Rondevu
1
+ # Rondevu Server
2
2
 
3
- An open signaling and tracking server for peer discovery. Enables peers to find each other through a topic-based HTTP API with Origin isolation for organizing peer-to-peer applications.
3
+ [![npm version](https://img.shields.io/npm/v/@xtr-dev/rondevu-server)](https://www.npmjs.com/package/@xtr-dev/rondevu-server)
4
4
 
5
- ## Features
6
-
7
- - 🚀 **Fast & Lightweight** - Built with [Hono](https://hono.dev/) framework
8
- - 📂 **Topic-Based Organization** - Group sessions by topic for easy peer discovery
9
- - 🔒 **Origin Isolation** - Sessions are isolated by HTTP Origin header to group topics by domain
10
- - 🏷️ **Peer Identification** - Info field prevents duplicate connections to same peer
11
- - 🔌 **Pluggable Storage** - Storage interface supports SQLite and in-memory adapters
12
- - 🐳 **Docker Ready** - Minimal Alpine-based Docker image
13
- - ⏱️ **Session Timeout** - Configurable session expiration from initiation time
14
- - 🔐 **Type Safe** - Written in TypeScript with full type definitions
5
+ 🌐 **Topic-based peer discovery and WebRTC signaling**
15
6
 
16
- ## Quick Start
7
+ Scalable peer-to-peer connection establishment with topic-based discovery, stateless authentication, and complete WebRTC signaling.
17
8
 
18
- ### Using Node.js
9
+ **Related repositories:**
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))
11
+ - [@xtr-dev/rondevu-server](https://github.com/xtr-dev/rondevu-server) - HTTP signaling server ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-server), [live](https://api.ronde.vu))
12
+ - [@xtr-dev/rondevu-demo](https://github.com/xtr-dev/rondevu-demo) - Interactive demo ([live](https://ronde.vu))
19
13
 
20
- ```bash
21
- # Install dependencies
22
- npm install
14
+ ---
23
15
 
24
- # Run in development mode
25
- npm run dev
16
+ ## Features
26
17
 
27
- # Build and run in production
28
- npm run build
29
- npm start
30
- ```
18
+ - **Topic-Based Discovery**: Tag offers with topics (e.g., torrent infohashes) for efficient peer finding
19
+ - **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
+ - **Complete WebRTC Signaling**: Offer/answer exchange and ICE candidate relay
24
+ - **Dual Storage**: SQLite (Node.js/Docker) and Cloudflare D1 (Workers) backends
31
25
 
32
- ### Using Docker
26
+ ## Quick Start
33
27
 
28
+ **Node.js:**
34
29
  ```bash
35
- # Build the image
36
- docker build -t rondevu .
37
-
38
- # Run with default settings (SQLite database)
39
- docker run -p 3000:3000 rondevu
40
-
41
- # Run with in-memory storage
42
- docker run -p 3000:3000 -e STORAGE_TYPE=memory rondevu
43
-
44
- # Run with custom timeout (10 minutes)
45
- docker run -p 3000:3000 -e SESSION_TIMEOUT=600000 rondevu
30
+ npm install && npm start
46
31
  ```
47
32
 
48
- ### Using Cloudflare Workers
49
-
33
+ **Docker:**
50
34
  ```bash
51
- # Install Wrangler CLI
52
- npm install -g wrangler
53
-
54
- # Login to Cloudflare
55
- wrangler login
56
-
57
- # Create KV namespace
58
- wrangler kv:namespace create SESSIONS
59
-
60
- # Update wrangler.toml with the KV namespace ID
35
+ docker build -t rondevu . && docker run -p 3000:3000 -e STORAGE_PATH=:memory: rondevu
36
+ ```
61
37
 
62
- # Deploy to Cloudflare's edge network
38
+ **Cloudflare Workers:**
39
+ ```bash
63
40
  npx wrangler deploy
64
41
  ```
65
42
 
66
- See [DEPLOYMENT.md](./DEPLOYMENT.md#cloudflare-workers) for detailed instructions.
43
+ ## API Endpoints
67
44
 
68
- ## Configuration
45
+ ### Public Endpoints
69
46
 
70
- Configuration is done through environment variables:
47
+ #### `GET /`
48
+ Returns server version and info
71
49
 
72
- | Variable | Description | Default |
73
- |--------------------|--------------------------------------------------|-------------|
74
- | `PORT` | Server port | `3000` |
75
- | `STORAGE_TYPE` | Storage backend: `sqlite` or `memory` | `sqlite` |
76
- | `STORAGE_PATH` | Path to SQLite database file | `./data.db` |
77
- | `SESSION_TIMEOUT` | Session timeout in milliseconds | `300000` |
78
- | `CORS_ORIGINS` | Comma-separated list of allowed origins | `*` |
50
+ #### `GET /health`
51
+ Health check endpoint with version
79
52
 
80
- ### Example .env file
53
+ #### `POST /register`
54
+ Register a new peer and receive credentials (peerId + secret)
81
55
 
82
- ```env
83
- PORT=3000
84
- STORAGE_TYPE=sqlite
85
- STORAGE_PATH=./sessions.db
86
- SESSION_TIMEOUT=300000
87
- CORS_ORIGINS=https://example.com,https://app.example.com
56
+ **Response:**
57
+ ```json
58
+ {
59
+ "peerId": "f17c195f067255e357232e34cf0735d9",
60
+ "secret": "DdorTR8QgSn9yngn+4qqR8cs1aMijvX..."
61
+ }
88
62
  ```
89
63
 
90
- ## API Documentation
91
-
92
- See [API.md](./API.md) for complete API documentation.
93
-
94
- ### Quick Overview
95
-
96
- **List all active topics (with pagination):**
97
- ```bash
98
- curl -X GET http://localhost:3000/ \
99
- -H "Origin: https://example.com"
100
- # Returns: {"topics":[{"topic":"my-room","count":3}],"pagination":{...}}
64
+ #### `GET /topics?limit=50&offset=0`
65
+ List all topics with active peer counts (paginated)
66
+
67
+ **Query Parameters:**
68
+ - `limit` (optional): Maximum number of topics to return (default: 50, max: 200)
69
+ - `offset` (optional): Number of topics to skip (default: 0)
70
+
71
+ **Response:**
72
+ ```json
73
+ {
74
+ "topics": [
75
+ {"topic": "movie-xyz", "activePeers": 42},
76
+ {"topic": "torrent-abc", "activePeers": 15}
77
+ ],
78
+ "total": 123,
79
+ "limit": 50,
80
+ "offset": 0
81
+ }
101
82
  ```
102
83
 
103
- **Create an offer (announce yourself as available):**
104
- ```bash
105
- curl -X POST http://localhost:3000/my-room/offer \
106
- -H "Content-Type: application/json" \
107
- -H "Origin: https://example.com" \
108
- -d '{"info":"peer-123","offer":"<SIGNALING_DATA>"}'
109
- # Returns: {"code":"550e8400-e29b-41d4-a716-446655440000"}
84
+ #### `GET /offers/by-topic/:topic?limit=50&bloom=...`
85
+ Find offers by topic with optional bloom filter exclusion
86
+
87
+ **Query Parameters:**
88
+ - `limit` (optional): Maximum offers to return (default: 50, max: 200)
89
+ - `bloom` (optional): Base64-encoded bloom filter to exclude known peers
90
+
91
+ **Response:**
92
+ ```json
93
+ {
94
+ "topic": "movie-xyz",
95
+ "offers": [
96
+ {
97
+ "id": "offer-id",
98
+ "peerId": "peer-id",
99
+ "sdp": "v=0...",
100
+ "topics": ["movie-xyz", "hd-content"],
101
+ "expiresAt": 1234567890,
102
+ "lastSeen": 1234567890,
103
+ "hasSecret": true // Indicates if secret is required to answer
104
+ }
105
+ ],
106
+ "total": 42,
107
+ "returned": 10
108
+ }
110
109
  ```
111
110
 
112
- **List available peers in a topic:**
113
- ```bash
114
- curl -X GET http://localhost:3000/my-room/sessions \
115
- -H "Origin: https://example.com"
116
- # Returns: {"sessions":[...]}
117
- ```
111
+ **Notes:**
112
+ - `hasSecret`: Boolean flag indicating whether a secret is required to answer this offer. The actual secret is never exposed in public endpoints.
118
113
 
119
- **Connect to a peer:**
120
- ```bash
121
- curl -X POST http://localhost:3000/answer \
122
- -H "Content-Type: application/json" \
123
- -H "Origin: https://example.com" \
124
- -d '{"code":"550e8400-...","answer":"<SIGNALING_DATA>","side":"answerer"}'
125
- # Returns: {"success":true}
126
- ```
114
+ #### `GET /peers/:peerId/offers`
115
+ View all offers from a specific peer
127
116
 
128
- ## Architecture
117
+ ### Authenticated Endpoints
129
118
 
130
- ### Storage Interface
119
+ All authenticated endpoints require `Authorization: Bearer {peerId}:{secret}` header.
131
120
 
132
- The storage layer is abstracted through a simple interface, making it easy to implement custom storage backends:
121
+ #### `POST /offers`
122
+ Create one or more offers
133
123
 
134
- ```typescript
135
- interface Storage {
136
- createSession(origin: string, topic: string, info: string, offer: string, expiresAt: number): Promise<string>;
137
- listSessionsByTopic(origin: string, topic: string): Promise<Session[]>;
138
- getSession(code: string, origin: string): Promise<Session | null>;
139
- updateSession(code: string, origin: string, update: Partial<Session>): Promise<void>;
140
- deleteSession(code: string): Promise<void>;
141
- cleanup(): Promise<void>;
142
- close(): Promise<void>;
124
+ **Request:**
125
+ ```json
126
+ {
127
+ "offers": [
128
+ {
129
+ "sdp": "v=0...",
130
+ "topics": ["movie-xyz", "hd-content"],
131
+ "ttl": 300000,
132
+ "secret": "my-secret-password" // Optional: protect offer (max 128 chars)
133
+ }
134
+ ]
143
135
  }
144
136
  ```
145
137
 
146
- ### Built-in Storage Adapters
138
+ **Notes:**
139
+ - `secret` (optional): Protect the offer with a secret. Answerers must provide the correct secret to connect.
147
140
 
148
- **SQLite Storage** (`sqlite.ts`)
149
- - For Node.js/Docker deployments
150
- - Persistent file-based or in-memory
151
- - Automatic session cleanup
152
- - Simple and reliable
141
+ #### `GET /offers/mine`
142
+ List all offers owned by authenticated peer
153
143
 
154
- **Cloudflare KV Storage** (`kv.ts`)
155
- - For Cloudflare Workers deployments
156
- - Global edge storage
157
- - Automatic TTL-based expiration
158
- - Distributed and highly available
144
+ #### `PUT /offers/:offerId/heartbeat`
145
+ Update last_seen timestamp for an offer
159
146
 
160
- ### Custom Storage Adapters
147
+ #### `DELETE /offers/:offerId`
148
+ Delete a specific offer
161
149
 
162
- You can implement your own storage adapter by implementing the `Storage` interface:
150
+ #### `POST /offers/:offerId/answer`
151
+ Answer an offer (locks it to answerer)
163
152
 
164
- ```typescript
165
- import { Storage, Session } from './storage/types';
166
-
167
- export class CustomStorage implements Storage {
168
- async createSession(offer: string, expiresAt: number): Promise<string> {
169
- // Your implementation
170
- }
171
- // ... implement other methods
153
+ **Request:**
154
+ ```json
155
+ {
156
+ "sdp": "v=0...",
157
+ "secret": "my-secret-password" // Required if offer is protected
172
158
  }
173
159
  ```
174
160
 
175
- ## Development
176
-
177
- ### Project Structure
178
-
179
- ```
180
- rondevu/
181
- ├── src/
182
- │ ├── index.ts # Node.js server entry point
183
- │ ├── app.ts # Hono application
184
- │ ├── config.ts # Configuration
185
- │ └── storage/
186
- │ ├── types.ts # Storage interface
187
- │ ├── sqlite.ts # SQLite adapter
188
- │ └── codeGenerator.ts # Code generation utility
189
- ├── Dockerfile # Docker build configuration
190
- ├── build.js # Build script
191
- ├── API.md # API documentation
192
- └── README.md # This file
193
- ```
161
+ **Notes:**
162
+ - `secret` (optional): Required if the offer was created with a secret. Must match the offer's secret.
194
163
 
195
- ### Building
164
+ #### `GET /offers/answers`
165
+ Poll for answers to your offers
196
166
 
197
- ```bash
198
- # Build TypeScript
199
- npm run build
167
+ #### `POST /offers/:offerId/ice-candidates`
168
+ Post ICE candidates for an offer
200
169
 
201
- # Run built version
202
- npm start
203
- ```
204
-
205
- ### Docker Build
206
-
207
- ```bash
208
- # Build the image
209
- docker build -t rondevu .
210
-
211
- # Run with volume for persistent storage
212
- docker run -p 3000:3000 -v $(pwd)/data:/app/data rondevu
170
+ **Request:**
171
+ ```json
172
+ {
173
+ "candidates": ["candidate:1 1 UDP..."]
174
+ }
213
175
  ```
214
176
 
215
- ## How It Works
177
+ #### `GET /offers/:offerId/ice-candidates?since=1234567890`
178
+ Get ICE candidates from the other peer
216
179
 
217
- 1. **Discover topics** (optional): Call `GET /` to see all active topics and peer counts
218
- 2. **Peer A** announces availability by posting to `/:topic/offer` with peer identifier and signaling data
219
- 3. Server generates a unique UUID code and stores the session (bucketed by Origin and topic)
220
- 4. **Peer B** discovers available peers using `GET /:topic/sessions`
221
- 5. **Peer B** filters out their own session using the info field to avoid self-connection
222
- 6. **Peer B** selects a peer and posts their connection data to `POST /answer` with the session code
223
- 7. Both peers exchange signaling data through `POST /answer` endpoint
224
- 8. Both peers poll for updates using `POST /poll` to retrieve connection information
225
- 9. Sessions automatically expire after the configured timeout
226
-
227
- This allows peers in distributed systems to discover each other without requiring a centralized registry, while maintaining isolation between different applications through Origin headers.
228
-
229
- ### Origin Isolation
180
+ ## Configuration
230
181
 
231
- Sessions are isolated by the HTTP `Origin` header, ensuring that:
232
- - Peers can only see sessions from their own origin
233
- - Session codes cannot be accessed cross-origin
234
- - Topics are organized by application domain
182
+ Environment variables:
183
+
184
+ | Variable | Default | Description |
185
+ |----------|---------|-------------|
186
+ | `PORT` | `3000` | Server port (Node.js/Docker) |
187
+ | `CORS_ORIGINS` | `*` | Comma-separated allowed origins |
188
+ | `STORAGE_PATH` | `./rondevu.db` | SQLite database path (use `:memory:` for in-memory) |
189
+ | `VERSION` | `0.4.0` | Server version (semver) |
190
+ | `AUTH_SECRET` | Random 32-byte hex | Secret key for credential encryption |
191
+ | `OFFER_DEFAULT_TTL` | `300000` | Default offer TTL in ms (5 minutes) |
192
+ | `OFFER_MIN_TTL` | `60000` | Minimum offer TTL in ms (1 minute) |
193
+ | `OFFER_MAX_TTL` | `3600000` | Maximum offer TTL in ms (1 hour) |
194
+ | `MAX_OFFERS_PER_REQUEST` | `10` | Maximum offers per create request |
195
+ | `MAX_TOPICS_PER_OFFER` | `20` | Maximum topics per offer |
235
196
 
236
197
  ## License
237
198
 
238
199
  MIT
239
-
240
- ## Contributing
241
-
242
- Contributions are welcome! Please feel free to submit a Pull Request.
package/build.js CHANGED
@@ -1,5 +1,14 @@
1
1
  // Build script using esbuild
2
2
  const esbuild = require('esbuild');
3
+ const { execSync } = require('child_process');
4
+
5
+ // Get git commit hash
6
+ let version = 'unknown';
7
+ try {
8
+ version = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
9
+ } catch (err) {
10
+ console.warn('Could not get git commit hash, using "unknown"');
11
+ }
3
12
 
4
13
  esbuild.build({
5
14
  entryPoints: ['src/index.ts'],
@@ -14,4 +23,7 @@ esbuild.build({
14
23
  'hono'
15
24
  ],
16
25
  sourcemap: true,
26
+ define: {
27
+ 'process.env.RONDEVU_VERSION': JSON.stringify(version)
28
+ }
17
29
  }).catch(() => process.exit(1));