@xtr-dev/rondevu-client 0.7.12 → 0.8.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 CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@xtr-dev/rondevu-client)](https://www.npmjs.com/package/@xtr-dev/rondevu-client)
4
4
 
5
- 🌐 **Topic-based peer discovery and WebRTC signaling client**
5
+ 🌐 **DNS-like WebRTC client with username claiming and service discovery**
6
6
 
7
- TypeScript/JavaScript client for Rondevu, providing topic-based peer discovery, stateless authentication, and complete WebRTC signaling with trickle ICE support.
7
+ TypeScript/JavaScript client for Rondevu, providing 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,12 @@ TypeScript/JavaScript client for Rondevu, providing topic-based peer discovery,
15
15
 
16
16
  ## Features
17
17
 
18
- - **Topic-Based Discovery**: Find peers by topics (e.g., torrent infohashes)
19
- - **Stateless Authentication**: No server-side sessions, portable credentials
20
- - **Protected Connections**: Optional secret-protected offers for access control
21
- - **Bloom Filters**: Efficient peer exclusion for repeated discoveries
22
- - **Multi-Offer Management**: Create and manage multiple offers per peer
18
+ - **Username Claiming**: Cryptographic ownership with Ed25519 signatures
19
+ - **Service Publishing**: Package-style naming (com.example.chat@1.0.0)
20
+ - **Privacy-Preserving Discovery**: UUID-based service index
21
+ - **Public/Private Services**: Control service visibility
23
22
  - **Complete WebRTC Signaling**: Full offer/answer and ICE candidate exchange
24
- - **Trickle ICE**: Send ICE candidates as they're discovered (faster connections)
25
- - **State Machine**: Clean state-based connection lifecycle
23
+ - **Trickle ICE**: Send ICE candidates as they're discovered
26
24
  - **TypeScript**: Full type safety and autocomplete
27
25
 
28
26
  ## Install
@@ -33,7 +31,7 @@ npm install @xtr-dev/rondevu-client
33
31
 
34
32
  ## Quick Start
35
33
 
36
- ### Creating an Offer (Peer A)
34
+ ### Publishing a Service (Alice)
37
35
 
38
36
  ```typescript
39
37
  import { Rondevu } from '@xtr-dev/rondevu-client';
@@ -42,43 +40,43 @@ import { Rondevu } from '@xtr-dev/rondevu-client';
42
40
  const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });
43
41
  await client.register();
44
42
 
45
- // Create peer connection
46
- const peer = client.createPeer();
43
+ // Step 1: Claim username (one-time)
44
+ const claim = await client.usernames.claimUsername('alice');
45
+ client.usernames.saveKeypairToStorage('alice', claim.publicKey, claim.privateKey);
47
46
 
48
- // Set up event listeners
49
- peer.on('state', (state) => {
50
- console.log('Peer state:', state);
51
- // States: idle → creating-offer → waiting-for-answer → exchanging-ice → connected
52
- });
47
+ console.log(`Username claimed: ${claim.username}`);
48
+ console.log(`Expires: ${new Date(claim.expiresAt)}`);
53
49
 
54
- peer.on('connected', () => {
55
- console.log('✅ Connected to peer!');
56
- });
50
+ // Step 2: Expose service with handler
51
+ const keypair = client.usernames.loadKeypairFromStorage('alice');
57
52
 
58
- peer.on('datachannel', (channel) => {
59
- console.log('📡 Data channel ready');
53
+ const handle = await client.services.exposeService({
54
+ username: 'alice',
55
+ privateKey: keypair.privateKey,
56
+ serviceFqn: 'com.example.chat@1.0.0',
57
+ isPublic: true,
58
+ handler: (channel, peer) => {
59
+ console.log('📡 New connection established');
60
60
 
61
- channel.addEventListener('message', (event) => {
62
- console.log('📥 Received:', event.data);
63
- });
61
+ channel.onmessage = (e) => {
62
+ console.log('📥 Received:', e.data);
63
+ channel.send(`Echo: ${e.data}`);
64
+ };
64
65
 
65
- channel.addEventListener('open', () => {
66
- channel.send('Hello from peer A!');
67
- });
66
+ channel.onopen = () => {
67
+ console.log(' Data channel open');
68
+ };
69
+ }
68
70
  });
69
71
 
70
- // Create offer and advertise on topics
71
- const offerId = await peer.createOffer({
72
- topics: ['my-app', 'room-123'],
73
- ttl: 300000, // 5 minutes
74
- secret: 'my-secret-password' // Optional: protect offer (max 128 chars)
75
- });
72
+ console.log(`Service published with UUID: ${handle.uuid}`);
73
+ console.log('Waiting for connections...');
76
74
 
77
- console.log('Offer created:', offerId);
78
- console.log('Share these topics with peers:', ['my-app', 'room-123']);
75
+ // Later: unpublish
76
+ await handle.unpublish();
79
77
  ```
80
78
 
81
- ### Answering an Offer (Peer B)
79
+ ### Connecting to a Service (Bob)
82
80
 
83
81
  ```typescript
84
82
  import { Rondevu } from '@xtr-dev/rondevu-client';
@@ -87,188 +85,296 @@ import { Rondevu } from '@xtr-dev/rondevu-client';
87
85
  const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });
88
86
  await client.register();
89
87
 
90
- // Discover offers by topic
91
- const offers = await client.offers.findByTopic('my-app', { limit: 10 });
88
+ // Option 1: Connect by username + FQN
89
+ const { peer, channel } = await client.discovery.connect(
90
+ 'alice',
91
+ 'com.example.chat@1.0.0'
92
+ );
92
93
 
93
- if (offers.length > 0) {
94
- const offer = offers[0];
95
-
96
- // Create peer connection
97
- const peer = client.createPeer();
98
-
99
- // Set up event listeners
100
- peer.on('state', (state) => {
101
- console.log('Peer state:', state);
102
- // States: idle → answering → exchanging-ice → connected
103
- });
104
-
105
- peer.on('connected', () => {
106
- console.log('✅ Connected!');
107
- });
94
+ channel.onmessage = (e) => {
95
+ console.log('📥 Received:', e.data);
96
+ };
108
97
 
109
- peer.on('datachannel', (channel) => {
110
- console.log('📡 Data channel ready');
98
+ channel.onopen = () => {
99
+ console.log(' Connected!');
100
+ channel.send('Hello Alice!');
101
+ };
111
102
 
112
- channel.addEventListener('message', (event) => {
113
- console.log('📥 Received:', event.data);
114
- });
103
+ peer.on('connected', () => {
104
+ console.log('🎉 WebRTC connection established');
105
+ });
115
106
 
116
- channel.addEventListener('open', () => {
117
- channel.send('Hello from peer B!');
118
- });
119
- });
107
+ peer.on('failed', (error) => {
108
+ console.error(' Connection failed:', error);
109
+ });
120
110
 
121
- peer.on('failed', (error) => {
122
- console.error('❌ Connection failed:', error);
123
- });
111
+ // Option 2: List services first, then connect
112
+ const services = await client.discovery.listServices('alice');
113
+ console.log(`Found ${services.services.length} services`);
124
114
 
125
- // Answer the offer
126
- await peer.answer(offer.id, offer.sdp, {
127
- topics: offer.topics,
128
- secret: 'my-secret-password' // Required if offer.hasSecret is true
129
- });
115
+ for (const service of services.services) {
116
+ console.log(`- UUID: ${service.uuid}`);
117
+ if (service.isPublic) {
118
+ console.log(` FQN: ${service.serviceFqn}`);
119
+ }
130
120
  }
131
- ```
132
121
 
133
- ## Protected Offers
122
+ // Connect by UUID
123
+ const { peer: peer2, channel: channel2 } = await client.discovery.connectByUuid(
124
+ services.services[0].uuid
125
+ );
126
+ ```
134
127
 
135
- You can protect offers with a secret to control who can answer them. This is useful for private rooms or invite-only connections.
128
+ ## API Reference
136
129
 
137
- ### Creating a Protected Offer
130
+ ### Main Client
138
131
 
139
132
  ```typescript
140
- const offerId = await peer.createOffer({
141
- topics: ['private-room'],
142
- secret: 'my-secret-password' // Max 128 characters
133
+ const client = new Rondevu({
134
+ baseUrl: 'https://api.ronde.vu', // optional, default shown
135
+ credentials?: { peerId, secret }, // optional, skip registration
136
+ fetch?: customFetch, // optional, for Node.js < 18
137
+ RTCPeerConnection?: RTCPeerConnection, // optional, for Node.js
138
+ RTCSessionDescription?: RTCSessionDescription,
139
+ RTCIceCandidate?: RTCIceCandidate
143
140
  });
144
141
 
145
- // Share the secret with authorized peers through a secure channel
142
+ // Register and get credentials
143
+ const creds = await client.register();
144
+ // { peerId: '...', secret: '...' }
145
+
146
+ // Check if authenticated
147
+ client.isAuthenticated(); // boolean
148
+
149
+ // Get current credentials
150
+ client.getCredentials(); // { peerId, secret } | undefined
146
151
  ```
147
152
 
148
- ### Answering a Protected Offer
153
+ ### Username API
149
154
 
150
155
  ```typescript
151
- const offers = await client.offers.findByTopic('private-room');
156
+ // Check username availability
157
+ const check = await client.usernames.checkUsername('alice');
158
+ // { available: true } or { available: false, expiresAt: number, publicKey: string }
152
159
 
153
- // Check if offer requires a secret
154
- if (offers[0].hasSecret) {
155
- console.log('This offer requires a secret');
156
- }
160
+ // Claim username with new keypair
161
+ const claim = await client.usernames.claimUsername('alice');
162
+ // { username, publicKey, privateKey, claimedAt, expiresAt }
157
163
 
158
- // Provide the secret when answering
159
- await peer.answer(offers[0].id, offers[0].sdp, {
160
- topics: offers[0].topics,
161
- secret: 'my-secret-password' // Must match the offer's secret
162
- });
163
- ```
164
+ // Claim with existing keypair
165
+ const keypair = await client.usernames.generateKeypair();
166
+ const claim2 = await client.usernames.claimUsername('bob', keypair);
164
167
 
165
- **Notes:**
166
- - The actual secret is never exposed in public API responses - only a `hasSecret` boolean flag
167
- - Answerers must provide the correct secret, or the answer will be rejected
168
- - Secrets are limited to 128 characters
169
- - Use this for access control, not for cryptographic security (use end-to-end encryption for that)
168
+ // Save keypair to localStorage
169
+ client.usernames.saveKeypairToStorage('alice', publicKey, privateKey);
170
170
 
171
- ## Connection Lifecycle
171
+ // Load keypair from localStorage
172
+ const stored = client.usernames.loadKeypairFromStorage('alice');
173
+ // { publicKey, privateKey } | null
172
174
 
173
- The `RondevuPeer` uses a state machine for connection management:
175
+ // Export keypair for backup
176
+ const exported = client.usernames.exportKeypair('alice');
177
+ // { username, publicKey, privateKey }
174
178
 
175
- ### Offerer States
176
- 1. **idle** - Initial state
177
- 2. **creating-offer** - Creating WebRTC offer
178
- 3. **waiting-for-answer** - Polling for answer from peer
179
- 4. **exchanging-ice** - Exchanging ICE candidates
180
- 5. **connected** - Successfully connected
181
- 6. **failed** - Connection failed
182
- 7. **closed** - Connection closed
179
+ // Import keypair from backup
180
+ client.usernames.importKeypair({ username: 'alice', publicKey, privateKey });
183
181
 
184
- ### Answerer States
185
- 1. **idle** - Initial state
186
- 2. **answering** - Creating WebRTC answer
187
- 3. **exchanging-ice** - Exchanging ICE candidates
188
- 4. **connected** - Successfully connected
189
- 5. **failed** - Connection failed
190
- 6. **closed** - Connection closed
182
+ // Low-level: Generate keypair
183
+ const { publicKey, privateKey } = await client.usernames.generateKeypair();
184
+
185
+ // Low-level: Sign message
186
+ const signature = await client.usernames.signMessage(
187
+ 'claim:alice:1234567890',
188
+ privateKey
189
+ );
190
+
191
+ // Low-level: Verify signature
192
+ const valid = await client.usernames.verifySignature(
193
+ 'claim:alice:1234567890',
194
+ signature,
195
+ publicKey
196
+ );
197
+ ```
198
+
199
+ **Username Rules:**
200
+ - Format: Lowercase alphanumeric + dash (`a-z`, `0-9`, `-`)
201
+ - Length: 3-32 characters
202
+ - Pattern: `^[a-z0-9][a-z0-9-]*[a-z0-9]$`
203
+ - Validity: 365 days from claim/last use
204
+ - Ownership: Secured by Ed25519 public key
191
205
 
192
- ### State Events
206
+ ### Services API
193
207
 
194
208
  ```typescript
195
- peer.on('state', (stateName) => {
196
- console.log('Current state:', stateName);
209
+ // Publish service (returns UUID)
210
+ const service = await client.services.publishService({
211
+ username: 'alice',
212
+ privateKey: keypair.privateKey,
213
+ serviceFqn: 'com.example.chat@1.0.0',
214
+ isPublic: false, // optional, default false
215
+ metadata: { description: '...' }, // optional
216
+ ttl: 5 * 60 * 1000, // optional, default 5 minutes
217
+ rtcConfig: { ... } // optional RTCConfiguration
197
218
  });
198
-
199
- peer.on('connected', () => {
200
- // Connection established successfully
219
+ // { serviceId, uuid, offerId, expiresAt }
220
+
221
+ console.log(`Service UUID: ${service.uuid}`);
222
+ console.log('Share this UUID to allow connections');
223
+
224
+ // Expose service with automatic connection handling
225
+ const handle = await client.services.exposeService({
226
+ username: 'alice',
227
+ privateKey: keypair.privateKey,
228
+ serviceFqn: 'com.example.echo@1.0.0',
229
+ isPublic: true,
230
+ handler: (channel, peer) => {
231
+ channel.onmessage = (e) => {
232
+ console.log('Received:', e.data);
233
+ channel.send(`Echo: ${e.data}`);
234
+ };
235
+ }
201
236
  });
202
237
 
203
- peer.on('disconnected', () => {
204
- // Connection lost or closed
205
- });
238
+ // Later: unpublish
239
+ await handle.unpublish();
206
240
 
207
- peer.on('failed', (error) => {
208
- // Connection failed
209
- console.error('Connection error:', error);
210
- });
241
+ // Unpublish service manually
242
+ await client.services.unpublishService(serviceId, username);
243
+ ```
211
244
 
212
- peer.on('datachannel', (channel) => {
213
- // Data channel is ready (use channel.addEventListener)
214
- });
245
+ #### Multi-Connection Service Hosting (Offer Pooling)
215
246
 
216
- peer.on('track', (event) => {
217
- // Media track received (for audio/video streaming)
218
- const stream = event.streams[0];
219
- videoElement.srcObject = stream;
247
+ By default, `exposeService()` creates a single offer and can only accept one connection. To handle multiple concurrent connections, use the `poolSize` option to enable **offer pooling**:
248
+
249
+ ```typescript
250
+ // Expose service with offer pooling for multiple concurrent connections
251
+ const handle = await client.services.exposeService({
252
+ username: 'alice',
253
+ privateKey: keypair.privateKey,
254
+ serviceFqn: 'com.example.chat@1.0.0',
255
+ isPublic: true,
256
+ poolSize: 5, // Maintain 5 simultaneous open offers
257
+ pollingInterval: 2000, // Optional: polling interval in ms (default: 2000)
258
+ handler: (channel, peer, connectionId) => {
259
+ console.log(`📡 New connection: ${connectionId}`);
260
+
261
+ channel.onmessage = (e) => {
262
+ console.log(`📥 [${connectionId}] Received:`, e.data);
263
+ channel.send(`Echo: ${e.data}`);
264
+ };
265
+
266
+ channel.onclose = () => {
267
+ console.log(`👋 [${connectionId}] Connection closed`);
268
+ };
269
+ },
270
+ onPoolStatus: (status) => {
271
+ console.log('Pool status:', {
272
+ activeOffers: status.activeOffers,
273
+ activeConnections: status.activeConnections,
274
+ totalHandled: status.totalConnectionsHandled
275
+ });
276
+ },
277
+ onError: (error, context) => {
278
+ console.error(`Pool error (${context}):`, error);
279
+ }
220
280
  });
221
- ```
222
281
 
223
- ## Trickle ICE
282
+ // Get current pool status
283
+ const status = handle.getStatus();
284
+ console.log(`Active offers: ${status.activeOffers}`);
285
+ console.log(`Active connections: ${status.activeConnections}`);
224
286
 
225
- This library implements **trickle ICE** for faster connection establishment:
287
+ // Manually add more offers if needed
288
+ await handle.addOffers(3);
289
+ ```
226
290
 
227
- - ICE candidates are sent to the server as they're discovered
228
- - No waiting for all candidates before sending offer/answer
229
- - Connections establish much faster (milliseconds vs seconds)
230
- - Proper event listener cleanup to prevent memory leaks
291
+ **How Offer Pooling Works:**
292
+ 1. The pool maintains `poolSize` simultaneous open offers at all times
293
+ 2. When an offer is answered (connection established), a new offer is automatically created
294
+ 3. Polling checks for answers every `pollingInterval` milliseconds (default: 2000ms)
295
+ 4. Each connection gets a unique `connectionId` passed to the handler
296
+ 5. No limit on total concurrent connections - only pool size (open offers) is controlled
231
297
 
232
- ## Adding Media Tracks
298
+ **Use Cases:**
299
+ - Chat servers handling multiple clients
300
+ - File sharing services with concurrent downloads
301
+ - Multiplayer game lobbies
302
+ - Collaborative editing sessions
303
+ - Any service that needs to accept multiple simultaneous connections
233
304
 
305
+ **Pool Status Interface:**
234
306
  ```typescript
235
- // Get user's camera/microphone
236
- const stream = await navigator.mediaDevices.getUserMedia({
237
- video: true,
238
- audio: true
239
- });
307
+ interface PoolStatus {
308
+ activeOffers: number; // Current number of open offers
309
+ activeConnections: number; // Current number of connected peers
310
+ totalConnectionsHandled: number; // Total connections since start
311
+ failedOfferCreations: number; // Failed offer creation attempts
312
+ }
313
+ ```
240
314
 
241
- // Add tracks to peer connection
242
- stream.getTracks().forEach(track => {
243
- peer.addTrack(track, stream);
244
- });
315
+ **Pooled Service Handle:**
316
+ ```typescript
317
+ interface PooledServiceHandle extends ServiceHandle {
318
+ getStatus: () => PoolStatus; // Get current pool status
319
+ addOffers: (count: number) => Promise<void>; // Manually add offers
320
+ }
245
321
  ```
246
322
 
247
- ## Peer Properties
323
+ **Service FQN Format:**
324
+ - Service name: Reverse domain notation (e.g., `com.example.chat`)
325
+ - Version: Semantic versioning (e.g., `1.0.0`, `2.1.3-beta`)
326
+ - Complete FQN: `service-name@version`
327
+ - Examples: `com.example.chat@1.0.0`, `io.github.alice.notes@0.1.0-beta`
328
+
329
+ **Validation Rules:**
330
+ - Service name pattern: `^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$`
331
+ - Length: 3-128 characters
332
+ - Minimum 2 components (at least one dot)
333
+ - Version pattern: `^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9.-]+)?$`
334
+
335
+ ### Discovery API
248
336
 
249
337
  ```typescript
250
- // Get current state name
251
- console.log(peer.stateName); // 'idle', 'creating-offer', 'connected', etc.
338
+ // List all services for a username
339
+ const services = await client.discovery.listServices('alice');
340
+ // {
341
+ // username: 'alice',
342
+ // services: [
343
+ // { uuid: 'abc123', isPublic: false },
344
+ // { uuid: 'def456', isPublic: true, serviceFqn: '...', metadata: {...} }
345
+ // ]
346
+ // }
252
347
 
253
- // Get connection state
254
- console.log(peer.connectionState); // RTCPeerConnectionState
348
+ // Query service by FQN
349
+ const query = await client.discovery.queryService('alice', 'com.example.chat@1.0.0');
350
+ // { uuid: 'abc123', allowed: true }
255
351
 
256
- // Get offer ID (after creating offer or answering)
257
- console.log(peer.offerId);
352
+ // Get service details by UUID
353
+ const details = await client.discovery.getServiceDetails('abc123');
354
+ // { serviceId, username, serviceFqn, offerId, sdp, isPublic, metadata, ... }
258
355
 
259
- // Get role
260
- console.log(peer.role); // 'offerer' or 'answerer'
261
- ```
356
+ // Connect to service by UUID
357
+ const peer = await client.discovery.connectToService('abc123', {
358
+ rtcConfig: { ... }, // optional
359
+ onConnected: () => { ... }, // optional
360
+ onData: (data) => { ... } // optional
361
+ });
262
362
 
263
- ## Closing a Connection
363
+ // Connect by username + FQN (convenience method)
364
+ const { peer, channel } = await client.discovery.connect(
365
+ 'alice',
366
+ 'com.example.chat@1.0.0',
367
+ { rtcConfig: { ... } } // optional
368
+ );
264
369
 
265
- ```typescript
266
- await peer.close();
370
+ // Connect by UUID with channel
371
+ const { peer, channel } = await client.discovery.connectByUuid('abc123');
267
372
  ```
268
373
 
269
- ## Custom RTCConfiguration
374
+ ### Low-Level Peer Connection
270
375
 
271
376
  ```typescript
377
+ // Create peer connection
272
378
  const peer = client.createPeer({
273
379
  iceServers: [
274
380
  { urls: 'stun:stun.l.google.com:19302' },
@@ -278,56 +384,114 @@ const peer = client.createPeer({
278
384
  credential: 'pass'
279
385
  }
280
386
  ],
281
- iceTransportPolicy: 'relay' // Force TURN relay (useful for testing)
387
+ iceTransportPolicy: 'relay' // optional: force TURN relay
282
388
  });
283
- ```
284
389
 
285
- ## Timeouts
390
+ // Event listeners
391
+ peer.on('state', (state) => {
392
+ console.log('Peer state:', state);
393
+ });
286
394
 
287
- Configure connection timeouts:
395
+ peer.on('connected', () => {
396
+ console.log('✅ Connected');
397
+ });
288
398
 
289
- ```typescript
290
- await peer.createOffer({
291
- topics: ['my-topic'],
292
- timeouts: {
293
- iceGathering: 10000, // ICE gathering timeout (10s)
294
- waitingForAnswer: 30000, // Waiting for answer timeout (30s)
295
- creatingAnswer: 10000, // Creating answer timeout (10s)
296
- iceConnection: 30000 // ICE connection timeout (30s)
399
+ peer.on('disconnected', () => {
400
+ console.log('🔌 Disconnected');
401
+ });
402
+
403
+ peer.on('failed', (error) => {
404
+ console.error('❌ Failed:', error);
405
+ });
406
+
407
+ peer.on('datachannel', (channel) => {
408
+ console.log('📡 Data channel ready');
409
+ });
410
+
411
+ peer.on('track', (event) => {
412
+ // Media track received
413
+ const stream = event.streams[0];
414
+ videoElement.srcObject = stream;
415
+ });
416
+
417
+ // Create offer
418
+ const offerId = await peer.createOffer({
419
+ ttl: 300000, // optional
420
+ timeouts: { // optional
421
+ iceGathering: 10000,
422
+ waitingForAnswer: 30000,
423
+ creatingAnswer: 10000,
424
+ iceConnection: 30000
297
425
  }
298
426
  });
427
+
428
+ // Answer offer
429
+ await peer.answer(offerId, sdp);
430
+
431
+ // Add media tracks
432
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
433
+ stream.getTracks().forEach(track => {
434
+ peer.addTrack(track, stream);
435
+ });
436
+
437
+ // Close connection
438
+ await peer.close();
439
+
440
+ // Properties
441
+ peer.stateName; // 'idle', 'creating-offer', 'connected', etc.
442
+ peer.connectionState; // RTCPeerConnectionState
443
+ peer.offerId; // string | undefined
444
+ peer.role; // 'offerer' | 'answerer' | undefined
299
445
  ```
300
446
 
301
- ## Platform-Specific Setup
447
+ ## Connection Lifecycle
302
448
 
303
- ### Node.js 18+ (with native fetch)
449
+ ### Service Publisher (Offerer)
450
+ 1. **idle** - Initial state
451
+ 2. **creating-offer** - Creating WebRTC offer
452
+ 3. **waiting-for-answer** - Polling for answer from peer
453
+ 4. **exchanging-ice** - Exchanging ICE candidates
454
+ 5. **connected** - Successfully connected
455
+ 6. **failed** - Connection failed
456
+ 7. **closed** - Connection closed
304
457
 
305
- Works out of the box - no additional setup needed.
458
+ ### Service Consumer (Answerer)
459
+ 1. **idle** - Initial state
460
+ 2. **answering** - Creating WebRTC answer
461
+ 3. **exchanging-ice** - Exchanging ICE candidates
462
+ 4. **connected** - Successfully connected
463
+ 5. **failed** - Connection failed
464
+ 6. **closed** - Connection closed
465
+
466
+ ## Platform-Specific Setup
306
467
 
307
- ### Node.js < 18 (without native fetch)
468
+ ### Modern Browsers
469
+ Works out of the box - no additional setup needed.
308
470
 
309
- Install node-fetch and provide it to the client:
471
+ ### Node.js 18+
472
+ Native fetch is available, but you need WebRTC polyfills:
310
473
 
311
474
  ```bash
312
- npm install node-fetch
475
+ npm install wrtc
313
476
  ```
314
477
 
315
478
  ```typescript
316
479
  import { Rondevu } from '@xtr-dev/rondevu-client';
317
- import fetch from 'node-fetch';
480
+ import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc';
318
481
 
319
482
  const client = new Rondevu({
320
483
  baseUrl: 'https://api.ronde.vu',
321
- fetch: fetch as any
484
+ RTCPeerConnection,
485
+ RTCSessionDescription,
486
+ RTCIceCandidate
322
487
  });
323
488
  ```
324
489
 
325
- ### Node.js with WebRTC (wrtc)
326
-
327
- For WebRTC functionality in Node.js, you need to provide WebRTC polyfills since Node.js doesn't have native WebRTC support:
490
+ ### Node.js < 18
491
+ Install both fetch and WebRTC polyfills:
328
492
 
329
493
  ```bash
330
- npm install wrtc node-fetch
494
+ npm install node-fetch wrtc
331
495
  ```
332
496
 
333
497
  ```typescript
@@ -342,25 +506,9 @@ const client = new Rondevu({
342
506
  RTCSessionDescription,
343
507
  RTCIceCandidate
344
508
  });
345
-
346
- // Now you can use WebRTC features
347
- await client.register();
348
- const peer = client.createPeer({
349
- iceServers: [
350
- { urls: 'stun:stun.l.google.com:19302' }
351
- ]
352
- });
353
-
354
- // Create offers, answer, etc.
355
- const offerId = await peer.createOffer({
356
- topics: ['my-topic']
357
- });
358
509
  ```
359
510
 
360
- **Note:** The `wrtc` package provides WebRTC bindings for Node.js. Alternative packages like `node-webrtc` can also be used - just pass their implementations to the Rondevu constructor.
361
-
362
511
  ### Deno
363
-
364
512
  ```typescript
365
513
  import { Rondevu } from 'npm:@xtr-dev/rondevu-client';
366
514
 
@@ -370,11 +518,9 @@ const client = new Rondevu({
370
518
  ```
371
519
 
372
520
  ### Bun
373
-
374
521
  Works out of the box - no additional setup needed.
375
522
 
376
523
  ### Cloudflare Workers
377
-
378
524
  ```typescript
379
525
  import { Rondevu } from '@xtr-dev/rondevu-client';
380
526
 
@@ -390,191 +536,114 @@ export default {
390
536
  };
391
537
  ```
392
538
 
393
- ## Low-Level API Usage
539
+ ## Examples
394
540
 
395
- For direct control over the signaling process without WebRTC:
541
+ ### Echo Service
396
542
 
397
543
  ```typescript
398
- import { Rondevu, BloomFilter } from '@xtr-dev/rondevu-client';
399
-
400
- const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });
401
-
402
- // Register and get credentials
403
- const creds = await client.register();
404
- console.log('Peer ID:', creds.peerId);
405
-
406
- // Save credentials for later use
407
- localStorage.setItem('rondevu-creds', JSON.stringify(creds));
408
-
409
- // Create offer with topics
410
- const offers = await client.offers.create([{
411
- sdp: 'v=0...', // Your WebRTC offer SDP
412
- topics: ['movie-xyz', 'hd-content'],
413
- ttl: 300000, // 5 minutes
414
- secret: 'my-secret-password', // Optional: protect offer (max 128 chars)
415
- info: 'Looking for peers in EU region' // Optional: public info (max 128 chars)
416
- }]);
417
-
418
- // Discover peers by topic
419
- const discovered = await client.offers.findByTopic('movie-xyz', {
420
- limit: 50
421
- });
422
-
423
- console.log(`Found ${discovered.length} peers`);
424
-
425
- // Use bloom filter to exclude known peers
426
- const knownPeers = new Set(['peer-id-1', 'peer-id-2']);
427
- const bloom = new BloomFilter(1024, 3);
428
- knownPeers.forEach(id => bloom.add(id));
429
-
430
- const newPeers = await client.offers.findByTopic('movie-xyz', {
431
- bloomFilter: bloom.toBytes(),
432
- limit: 50
544
+ // Publisher
545
+ const client1 = new Rondevu();
546
+ await client1.register();
547
+
548
+ const claim = await client1.usernames.claimUsername('alice');
549
+ client1.usernames.saveKeypairToStorage('alice', claim.publicKey, claim.privateKey);
550
+
551
+ const keypair = client1.usernames.loadKeypairFromStorage('alice');
552
+
553
+ await client1.services.exposeService({
554
+ username: 'alice',
555
+ privateKey: keypair.privateKey,
556
+ serviceFqn: 'com.example.echo@1.0.0',
557
+ isPublic: true,
558
+ handler: (channel, peer) => {
559
+ channel.onmessage = (e) => {
560
+ console.log('Received:', e.data);
561
+ channel.send(`Echo: ${e.data}`);
562
+ };
563
+ }
433
564
  });
434
- ```
435
-
436
- ## API Reference
437
-
438
- ### Authentication
439
-
440
- #### `client.register(customPeerId?)`
441
- Register a new peer and receive credentials.
442
-
443
- ```typescript
444
- // Auto-generate peer ID
445
- const creds = await client.register();
446
- // { peerId: 'f17c195f067255e357232e34cf0735d9', secret: '...' }
447
-
448
- // Or use a custom peer ID (1-128 characters)
449
- const customCreds = await client.register('my-custom-peer-id');
450
- // { peerId: 'my-custom-peer-id', secret: '...' }
451
- ```
452
-
453
- **Parameters:**
454
- - `customPeerId` (optional): Custom peer ID (1-128 characters). If not provided, a random ID will be generated.
455
-
456
- **Notes:**
457
- - Returns 409 Conflict if the custom peer ID is already in use
458
- - Custom peer IDs must be non-empty and between 1-128 characters
459
-
460
- ### Topics
461
565
 
462
- #### `client.offers.getTopics(options?)`
463
- List all topics with active peer counts (paginated).
566
+ // Consumer
567
+ const client2 = new Rondevu();
568
+ await client2.register();
464
569
 
465
- ```typescript
466
- const result = await client.offers.getTopics({
467
- limit: 50,
468
- offset: 0
469
- });
570
+ const { peer, channel } = await client2.discovery.connect(
571
+ 'alice',
572
+ 'com.example.echo@1.0.0'
573
+ );
470
574
 
471
- // {
472
- // topics: [
473
- // { topic: 'movie-xyz', activePeers: 42 },
474
- // { topic: 'torrent-abc', activePeers: 15 }
475
- // ],
476
- // total: 123,
477
- // limit: 50,
478
- // offset: 0
479
- // }
575
+ channel.onmessage = (e) => console.log('Received:', e.data);
576
+ channel.send('Hello!');
480
577
  ```
481
578
 
482
- ### Offers
483
-
484
- #### `client.offers.create(offers)`
485
- Create one or more offers with topics.
579
+ ### File Transfer Service
486
580
 
487
581
  ```typescript
488
- const offers = await client.offers.create([
489
- {
490
- sdp: 'v=0...',
491
- topics: ['topic-1', 'topic-2'],
492
- ttl: 300000, // optional, default 5 minutes
493
- secret: 'my-secret-password', // optional, max 128 chars
494
- info: 'Looking for peers in EU region' // optional, public info, max 128 chars
582
+ // Publisher
583
+ await client.services.exposeService({
584
+ username: 'alice',
585
+ privateKey: keypair.privateKey,
586
+ serviceFqn: 'com.example.files@1.0.0',
587
+ isPublic: false,
588
+ handler: (channel, peer) => {
589
+ channel.binaryType = 'arraybuffer';
590
+
591
+ channel.onmessage = (e) => {
592
+ if (typeof e.data === 'string') {
593
+ console.log('Request:', JSON.parse(e.data));
594
+ } else {
595
+ console.log('Received file chunk:', e.data.byteLength, 'bytes');
596
+ }
597
+ };
495
598
  }
496
- ]);
497
- ```
498
-
499
- #### `client.offers.findByTopic(topic, options?)`
500
- Find offers by topic with optional bloom filter.
501
-
502
- ```typescript
503
- const offers = await client.offers.findByTopic('movie-xyz', {
504
- limit: 50,
505
- bloomFilter: bloomBytes // optional
506
599
  });
507
- ```
508
-
509
- #### `client.offers.getMine()`
510
- Get all offers owned by the authenticated peer.
511
-
512
- ```typescript
513
- const myOffers = await client.offers.getMine();
514
- ```
515
-
516
- #### `client.offers.delete(offerId)`
517
- Delete a specific offer.
518
600
 
519
- ```typescript
520
- await client.offers.delete(offerId);
521
- ```
522
-
523
- #### `client.offers.answer(offerId, sdp, secret?)`
524
- Answer an offer (locks it to answerer).
525
-
526
- ```typescript
527
- await client.offers.answer(offerId, answerSdp, 'my-secret-password');
528
- ```
601
+ // Consumer
602
+ const { peer, channel } = await client.discovery.connect(
603
+ 'alice',
604
+ 'com.example.files@1.0.0'
605
+ );
529
606
 
530
- **Parameters:**
531
- - `offerId`: The offer ID to answer
532
- - `sdp`: The WebRTC answer SDP
533
- - `secret` (optional): Required if the offer has `hasSecret: true`
607
+ channel.binaryType = 'arraybuffer';
534
608
 
535
- #### `client.offers.getAnswers()`
536
- Poll for answers to your offers.
609
+ // Request file
610
+ channel.send(JSON.stringify({ action: 'get', path: '/readme.txt' }));
537
611
 
538
- ```typescript
539
- const answers = await client.offers.getAnswers();
540
- ```
541
-
542
- ### ICE Candidates
543
-
544
- #### `client.offers.addIceCandidates(offerId, candidates)`
545
- Post ICE candidates for an offer.
546
-
547
- ```typescript
548
- await client.offers.addIceCandidates(offerId, [
549
- { candidate: 'candidate:1 1 UDP...', sdpMid: '0', sdpMLineIndex: 0 }
550
- ]);
612
+ channel.onmessage = (e) => {
613
+ if (e.data instanceof ArrayBuffer) {
614
+ console.log('Received file:', e.data.byteLength, 'bytes');
615
+ }
616
+ };
551
617
  ```
552
618
 
553
- #### `client.offers.getIceCandidates(offerId, since?)`
554
- Get ICE candidates from the other peer.
619
+ ### Video Chat Service
555
620
 
556
621
  ```typescript
557
- const candidates = await client.offers.getIceCandidates(offerId, since);
558
- ```
622
+ // Publisher
623
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
559
624
 
560
- ### Bloom Filter
561
-
562
- ```typescript
563
- import { BloomFilter } from '@xtr-dev/rondevu-client';
625
+ const peer = client.createPeer();
626
+ stream.getTracks().forEach(track => peer.addTrack(track, stream));
564
627
 
565
- // Create filter: size=1024 bits, hash=3 functions
566
- const bloom = new BloomFilter(1024, 3);
628
+ const offerId = await peer.createOffer({ ttl: 300000 });
567
629
 
568
- // Add items
569
- bloom.add('peer-id-1');
570
- bloom.add('peer-id-2');
630
+ await client.services.publishService({
631
+ username: 'alice',
632
+ privateKey: keypair.privateKey,
633
+ serviceFqn: 'com.example.videochat@1.0.0',
634
+ isPublic: true
635
+ });
571
636
 
572
- // Test membership
573
- bloom.test('peer-id-1'); // true (probably)
574
- bloom.test('unknown'); // false (definitely)
637
+ // Consumer
638
+ const { peer, channel } = await client.discovery.connect(
639
+ 'alice',
640
+ 'com.example.videochat@1.0.0'
641
+ );
575
642
 
576
- // Export for API
577
- const bytes = bloom.toBytes();
643
+ peer.on('track', (event) => {
644
+ const remoteStream = event.streams[0];
645
+ videoElement.srcObject = remoteStream;
646
+ });
578
647
  ```
579
648
 
580
649
  ## TypeScript
@@ -584,54 +653,40 @@ All types are exported:
584
653
  ```typescript
585
654
  import type {
586
655
  Credentials,
587
- Offer,
588
- CreateOfferRequest,
589
- TopicInfo,
590
- IceCandidate,
591
- FetchFunction,
592
656
  RondevuOptions,
657
+
658
+ // Username types
659
+ UsernameCheckResult,
660
+ UsernameClaimResult,
661
+ Keypair,
662
+
663
+ // Service types
664
+ ServicePublishResult,
665
+ PublishServiceOptions,
666
+ ServiceHandle,
667
+
668
+ // Discovery types
669
+ ServiceInfo,
670
+ ServiceListResult,
671
+ ServiceQueryResult,
672
+ ServiceDetails,
673
+ ConnectResult,
674
+
675
+ // Peer types
593
676
  PeerOptions,
594
677
  PeerEvents,
595
678
  PeerTimeouts
596
679
  } from '@xtr-dev/rondevu-client';
597
680
  ```
598
681
 
599
- ## Environment Compatibility
600
-
601
- The client library is designed to work across different JavaScript runtimes:
602
-
603
- | Environment | Native Fetch | Native WebRTC | Polyfills Needed |
604
- |-------------|--------------|---------------|------------------|
605
- | Modern Browsers | ✅ Yes | ✅ Yes | ❌ None |
606
- | Node.js 18+ | ✅ Yes | ❌ No | ✅ WebRTC (wrtc) |
607
- | Node.js < 18 | ❌ No | ❌ No | ✅ Fetch + WebRTC |
608
- | Deno | ✅ Yes | ⚠️ Partial | ❌ None (signaling only) |
609
- | Bun | ✅ Yes | ❌ No | ✅ WebRTC (wrtc) |
610
- | Cloudflare Workers | ✅ Yes | ❌ No | ❌ None (signaling only) |
611
-
612
- **For signaling-only (no WebRTC peer connections):**
613
-
614
- Use the low-level API with `client.offers` - no WebRTC polyfills needed.
615
-
616
- **For full WebRTC support in Node.js:**
682
+ ## Migration from V1
617
683
 
618
- ```bash
619
- npm install wrtc node-fetch
620
- ```
621
-
622
- ```typescript
623
- import { Rondevu } from '@xtr-dev/rondevu-client';
624
- import fetch from 'node-fetch';
625
- import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc';
684
+ V2 is a **breaking change** that replaces topic-based discovery with username claiming and service publishing. See the main [MIGRATION.md](../MIGRATION.md) for detailed migration guide.
626
685
 
627
- const client = new Rondevu({
628
- baseUrl: 'https://api.ronde.vu',
629
- fetch: fetch as any,
630
- RTCPeerConnection,
631
- RTCSessionDescription,
632
- RTCIceCandidate
633
- });
634
- ```
686
+ **Key Changes:**
687
+ - ❌ Removed: `offers.findByTopic()`, `offers.getTopics()`, bloom filters
688
+ - ✅ Added: `usernames.*`, `services.*`, `discovery.*` APIs
689
+ - ✅ Changed: Focus on service-based discovery instead of topics
635
690
 
636
691
  ## License
637
692