@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 +459 -404
- package/dist/discovery.d.ts +93 -0
- package/dist/discovery.js +164 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -2
- package/dist/offer-pool.d.ts +74 -0
- package/dist/offer-pool.js +119 -0
- package/dist/rondevu.d.ts +16 -1
- package/dist/rondevu.js +29 -2
- package/dist/service-pool.d.ts +115 -0
- package/dist/service-pool.js +339 -0
- package/dist/services.d.ts +79 -0
- package/dist/services.js +206 -0
- package/dist/usernames.d.ts +79 -0
- package/dist/usernames.js +147 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@xtr-dev/rondevu-client)
|
|
4
4
|
|
|
5
|
-
🌐 **
|
|
5
|
+
🌐 **DNS-like WebRTC client with username claiming and service discovery**
|
|
6
6
|
|
|
7
|
-
TypeScript/JavaScript client for Rondevu, providing
|
|
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
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
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
|
|
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
|
-
###
|
|
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
|
-
//
|
|
46
|
-
const
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
50
|
+
// Step 2: Expose service with handler
|
|
51
|
+
const keypair = client.usernames.loadKeypairFromStorage('alice');
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
channel.onmessage = (e) => {
|
|
62
|
+
console.log('📥 Received:', e.data);
|
|
63
|
+
channel.send(`Echo: ${e.data}`);
|
|
64
|
+
};
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
channel.onopen = () => {
|
|
67
|
+
console.log('✅ Data channel open');
|
|
68
|
+
};
|
|
69
|
+
}
|
|
68
70
|
});
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
78
|
-
|
|
75
|
+
// Later: unpublish
|
|
76
|
+
await handle.unpublish();
|
|
79
77
|
```
|
|
80
78
|
|
|
81
|
-
###
|
|
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
|
-
//
|
|
91
|
-
const
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
110
|
-
|
|
98
|
+
channel.onopen = () => {
|
|
99
|
+
console.log('✅ Connected!');
|
|
100
|
+
channel.send('Hello Alice!');
|
|
101
|
+
};
|
|
111
102
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
peer.on('connected', () => {
|
|
104
|
+
console.log('🎉 WebRTC connection established');
|
|
105
|
+
});
|
|
115
106
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
});
|
|
107
|
+
peer.on('failed', (error) => {
|
|
108
|
+
console.error('❌ Connection failed:', error);
|
|
109
|
+
});
|
|
120
110
|
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
122
|
+
// Connect by UUID
|
|
123
|
+
const { peer: peer2, channel: channel2 } = await client.discovery.connectByUuid(
|
|
124
|
+
services.services[0].uuid
|
|
125
|
+
);
|
|
126
|
+
```
|
|
134
127
|
|
|
135
|
-
|
|
128
|
+
## API Reference
|
|
136
129
|
|
|
137
|
-
###
|
|
130
|
+
### Main Client
|
|
138
131
|
|
|
139
132
|
```typescript
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
secret
|
|
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
|
-
//
|
|
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
|
-
###
|
|
153
|
+
### Username API
|
|
149
154
|
|
|
150
155
|
```typescript
|
|
151
|
-
|
|
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
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
160
|
+
// Claim username with new keypair
|
|
161
|
+
const claim = await client.usernames.claimUsername('alice');
|
|
162
|
+
// { username, publicKey, privateKey, claimedAt, expiresAt }
|
|
157
163
|
|
|
158
|
-
//
|
|
159
|
-
await
|
|
160
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
171
|
+
// Load keypair from localStorage
|
|
172
|
+
const stored = client.usernames.loadKeypairFromStorage('alice');
|
|
173
|
+
// { publicKey, privateKey } | null
|
|
172
174
|
|
|
173
|
-
|
|
175
|
+
// Export keypair for backup
|
|
176
|
+
const exported = client.usernames.exportKeypair('alice');
|
|
177
|
+
// { username, publicKey, privateKey }
|
|
174
178
|
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
###
|
|
206
|
+
### Services API
|
|
193
207
|
|
|
194
208
|
```typescript
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
238
|
+
// Later: unpublish
|
|
239
|
+
await handle.unpublish();
|
|
206
240
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
});
|
|
241
|
+
// Unpublish service manually
|
|
242
|
+
await client.services.unpublishService(serviceId, username);
|
|
243
|
+
```
|
|
211
244
|
|
|
212
|
-
|
|
213
|
-
// Data channel is ready (use channel.addEventListener)
|
|
214
|
-
});
|
|
245
|
+
#### Multi-Connection Service Hosting (Offer Pooling)
|
|
215
246
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
+
// Manually add more offers if needed
|
|
288
|
+
await handle.addOffers(3);
|
|
289
|
+
```
|
|
226
290
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
251
|
-
|
|
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
|
-
//
|
|
254
|
-
|
|
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
|
|
257
|
-
|
|
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
|
-
//
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
-
await
|
|
370
|
+
// Connect by UUID with channel
|
|
371
|
+
const { peer, channel } = await client.discovery.connectByUuid('abc123');
|
|
267
372
|
```
|
|
268
373
|
|
|
269
|
-
|
|
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'
|
|
387
|
+
iceTransportPolicy: 'relay' // optional: force TURN relay
|
|
282
388
|
});
|
|
283
|
-
```
|
|
284
389
|
|
|
285
|
-
|
|
390
|
+
// Event listeners
|
|
391
|
+
peer.on('state', (state) => {
|
|
392
|
+
console.log('Peer state:', state);
|
|
393
|
+
});
|
|
286
394
|
|
|
287
|
-
|
|
395
|
+
peer.on('connected', () => {
|
|
396
|
+
console.log('✅ Connected');
|
|
397
|
+
});
|
|
288
398
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
##
|
|
447
|
+
## Connection Lifecycle
|
|
302
448
|
|
|
303
|
-
###
|
|
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
|
-
|
|
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
|
-
###
|
|
468
|
+
### Modern Browsers
|
|
469
|
+
Works out of the box - no additional setup needed.
|
|
308
470
|
|
|
309
|
-
|
|
471
|
+
### Node.js 18+
|
|
472
|
+
Native fetch is available, but you need WebRTC polyfills:
|
|
310
473
|
|
|
311
474
|
```bash
|
|
312
|
-
npm install
|
|
475
|
+
npm install wrtc
|
|
313
476
|
```
|
|
314
477
|
|
|
315
478
|
```typescript
|
|
316
479
|
import { Rondevu } from '@xtr-dev/rondevu-client';
|
|
317
|
-
import
|
|
480
|
+
import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc';
|
|
318
481
|
|
|
319
482
|
const client = new Rondevu({
|
|
320
483
|
baseUrl: 'https://api.ronde.vu',
|
|
321
|
-
|
|
484
|
+
RTCPeerConnection,
|
|
485
|
+
RTCSessionDescription,
|
|
486
|
+
RTCIceCandidate
|
|
322
487
|
});
|
|
323
488
|
```
|
|
324
489
|
|
|
325
|
-
### Node.js
|
|
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
|
|
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
|
-
##
|
|
539
|
+
## Examples
|
|
394
540
|
|
|
395
|
-
|
|
541
|
+
### Echo Service
|
|
396
542
|
|
|
397
543
|
```typescript
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
463
|
-
|
|
566
|
+
// Consumer
|
|
567
|
+
const client2 = new Rondevu();
|
|
568
|
+
await client2.register();
|
|
464
569
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
});
|
|
570
|
+
const { peer, channel } = await client2.discovery.connect(
|
|
571
|
+
'alice',
|
|
572
|
+
'com.example.echo@1.0.0'
|
|
573
|
+
);
|
|
470
574
|
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
###
|
|
483
|
-
|
|
484
|
-
#### `client.offers.create(offers)`
|
|
485
|
-
Create one or more offers with topics.
|
|
579
|
+
### File Transfer Service
|
|
486
580
|
|
|
487
581
|
```typescript
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
520
|
-
await client.
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
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
|
-
|
|
536
|
-
|
|
609
|
+
// Request file
|
|
610
|
+
channel.send(JSON.stringify({ action: 'get', path: '/readme.txt' }));
|
|
537
611
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
554
|
-
Get ICE candidates from the other peer.
|
|
619
|
+
### Video Chat Service
|
|
555
620
|
|
|
556
621
|
```typescript
|
|
557
|
-
|
|
558
|
-
|
|
622
|
+
// Publisher
|
|
623
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
|
|
559
624
|
|
|
560
|
-
|
|
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
|
-
|
|
566
|
-
const bloom = new BloomFilter(1024, 3);
|
|
628
|
+
const offerId = await peer.createOffer({ ttl: 300000 });
|
|
567
629
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
//
|
|
573
|
-
|
|
574
|
-
|
|
637
|
+
// Consumer
|
|
638
|
+
const { peer, channel } = await client.discovery.connect(
|
|
639
|
+
'alice',
|
|
640
|
+
'com.example.videochat@1.0.0'
|
|
641
|
+
);
|
|
575
642
|
|
|
576
|
-
|
|
577
|
-
const
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
|