@xtr-dev/rondevu-client 0.3.4 → 0.4.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 +401 -60
- package/dist/auth.d.ts +18 -0
- package/dist/auth.js +39 -0
- package/dist/bloom.d.ts +30 -0
- package/dist/bloom.js +73 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.js +18 -0
- package/dist/connection.d.ts +96 -45
- package/dist/connection.js +217 -196
- package/dist/index.d.ts +8 -3
- package/dist/index.js +9 -5
- package/dist/offers.d.ts +108 -0
- package/dist/offers.js +214 -0
- package/dist/rondevu.d.ts +39 -42
- package/dist/rondevu.js +33 -175
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,112 +1,453 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @xtr-dev/rondevu-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@xtr-dev/rondevu-client)
|
|
4
|
+
|
|
5
|
+
🌐 **Topic-based peer discovery and WebRTC signaling client**
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
TypeScript/JavaScript client for Rondevu, providing topic-based peer discovery, stateless authentication, and complete WebRTC signaling.
|
|
6
8
|
|
|
7
9
|
**Related repositories:**
|
|
8
|
-
- [rondevu-server](https://github.com/xtr-dev/rondevu
|
|
9
|
-
- [rondevu-demo](https://
|
|
10
|
+
- [rondevu-server](https://github.com/xtr-dev/rondevu) - HTTP signaling server
|
|
11
|
+
- [rondevu-demo](https://rondevu-demo.pages.dev) - Interactive demo
|
|
10
12
|
|
|
11
13
|
---
|
|
12
14
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
[](https://www.npmjs.com/package/@xtr-dev/rondevu-client)
|
|
15
|
+
## Features
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
- **Topic-Based Discovery**: Find peers by topics (e.g., torrent infohashes)
|
|
18
|
+
- **Stateless Authentication**: No server-side sessions, portable credentials
|
|
19
|
+
- **Bloom Filters**: Efficient peer exclusion for repeated discoveries
|
|
20
|
+
- **Multi-Offer Management**: Create and manage multiple offers per peer
|
|
21
|
+
- **Complete WebRTC Signaling**: Full offer/answer and ICE candidate exchange
|
|
22
|
+
- **TypeScript**: Full type safety and autocomplete
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
## Install
|
|
20
25
|
|
|
21
26
|
```bash
|
|
22
27
|
npm install @xtr-dev/rondevu-client
|
|
23
28
|
```
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
The easiest way to use Rondevu is with the high-level `RondevuConnection` class, which handles all WebRTC connection complexity including offer/answer exchange, ICE candidates, and connection lifecycle.
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
### Creating an Offer (Peer A)
|
|
28
35
|
|
|
29
36
|
```typescript
|
|
30
37
|
import { Rondevu } from '@xtr-dev/rondevu-client';
|
|
31
38
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });
|
|
40
|
+
await client.register();
|
|
41
|
+
|
|
42
|
+
// Create a connection
|
|
43
|
+
const conn = client.createConnection();
|
|
44
|
+
|
|
45
|
+
// Set up event listeners
|
|
46
|
+
conn.on('connected', () => {
|
|
47
|
+
console.log('Connected to peer!');
|
|
40
48
|
});
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
conn.on('datachannel', (channel) => {
|
|
51
|
+
console.log('Data channel ready');
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
channel.onmessage = (event) => {
|
|
54
|
+
console.log('Received:', event.data);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
channel.send('Hello from peer A!');
|
|
58
|
+
});
|
|
47
59
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
// Create offer and advertise on topics
|
|
61
|
+
const offerId = await conn.createOffer({
|
|
62
|
+
topics: ['my-app', 'room-123'],
|
|
63
|
+
ttl: 300000 // 5 minutes
|
|
52
64
|
});
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
console.log('Offer created:', offerId);
|
|
67
|
+
console.log('Share these topics with peers:', ['my-app', 'room-123']);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Answering an Offer (Peer B)
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { Rondevu } from '@xtr-dev/rondevu-client';
|
|
74
|
+
|
|
75
|
+
const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });
|
|
76
|
+
await client.register();
|
|
77
|
+
|
|
78
|
+
// Discover offers by topic
|
|
79
|
+
const offers = await client.offers.findByTopic('my-app', { limit: 10 });
|
|
80
|
+
|
|
81
|
+
if (offers.length > 0) {
|
|
82
|
+
const offer = offers[0];
|
|
83
|
+
|
|
84
|
+
// Create connection
|
|
85
|
+
const conn = client.createConnection();
|
|
86
|
+
|
|
87
|
+
// Set up event listeners
|
|
88
|
+
conn.on('connecting', () => {
|
|
89
|
+
console.log('Connecting...');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
conn.on('connected', () => {
|
|
93
|
+
console.log('Connected!');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
conn.on('datachannel', (channel) => {
|
|
97
|
+
console.log('Data channel ready');
|
|
98
|
+
|
|
56
99
|
channel.onmessage = (event) => {
|
|
57
100
|
console.log('Received:', event.data);
|
|
58
101
|
};
|
|
59
|
-
|
|
102
|
+
|
|
103
|
+
channel.send('Hello from peer B!');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Answer the offer
|
|
107
|
+
await conn.answer(offer.id, offer.sdp);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Connection Events
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
conn.on('connecting', () => {
|
|
115
|
+
// Connection is being established
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
conn.on('connected', () => {
|
|
119
|
+
// Connection established successfully
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
conn.on('disconnected', () => {
|
|
123
|
+
// Connection lost or closed
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
conn.on('error', (error) => {
|
|
127
|
+
// An error occurred
|
|
128
|
+
console.error('Connection error:', error);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
conn.on('datachannel', (channel) => {
|
|
132
|
+
// Data channel is ready to use
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
conn.on('track', (event) => {
|
|
136
|
+
// Media track received (for audio/video streaming)
|
|
137
|
+
const stream = event.streams[0];
|
|
138
|
+
videoElement.srcObject = stream;
|
|
60
139
|
});
|
|
61
140
|
```
|
|
62
141
|
|
|
63
|
-
|
|
142
|
+
### Adding Media Tracks
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// Get user's camera/microphone
|
|
146
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
147
|
+
video: true,
|
|
148
|
+
audio: true
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Add tracks to connection
|
|
152
|
+
stream.getTracks().forEach(track => {
|
|
153
|
+
conn.addTrack(track, stream);
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Connection Properties
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// Get connection state
|
|
161
|
+
console.log(conn.connectionState); // 'connecting', 'connected', 'disconnected', etc.
|
|
162
|
+
|
|
163
|
+
// Get offer ID
|
|
164
|
+
console.log(conn.id);
|
|
165
|
+
|
|
166
|
+
// Get data channel
|
|
167
|
+
console.log(conn.channel);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Closing a Connection
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
conn.close();
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Platform-Specific Setup
|
|
177
|
+
|
|
178
|
+
### Node.js 18+ (with native fetch)
|
|
179
|
+
|
|
180
|
+
Works out of the box - no additional setup needed.
|
|
181
|
+
|
|
182
|
+
### Node.js < 18 (without native fetch)
|
|
183
|
+
|
|
184
|
+
Install node-fetch and provide it to the client:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npm install node-fetch
|
|
188
|
+
```
|
|
64
189
|
|
|
65
190
|
```typescript
|
|
66
191
|
import { Rondevu } from '@xtr-dev/rondevu-client';
|
|
67
|
-
import wrtc from '@roamhq/wrtc';
|
|
68
192
|
import fetch from 'node-fetch';
|
|
69
193
|
|
|
70
|
-
const
|
|
194
|
+
const client = new Rondevu({
|
|
71
195
|
baseUrl: 'https://api.ronde.vu',
|
|
72
|
-
fetch: fetch as any
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
196
|
+
fetch: fetch as any
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Deno
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { Rondevu } from 'npm:@xtr-dev/rondevu-client';
|
|
204
|
+
|
|
205
|
+
const client = new Rondevu({
|
|
206
|
+
baseUrl: 'https://api.ronde.vu'
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Bun
|
|
211
|
+
|
|
212
|
+
Works out of the box - no additional setup needed.
|
|
213
|
+
|
|
214
|
+
### Cloudflare Workers
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import { Rondevu } from '@xtr-dev/rondevu-client';
|
|
218
|
+
|
|
219
|
+
export default {
|
|
220
|
+
async fetch(request: Request, env: Env) {
|
|
221
|
+
const client = new Rondevu({
|
|
222
|
+
baseUrl: 'https://api.ronde.vu'
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const creds = await client.register();
|
|
226
|
+
return new Response(JSON.stringify(creds));
|
|
77
227
|
}
|
|
228
|
+
};
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Low-Level API Usage
|
|
232
|
+
|
|
233
|
+
For advanced use cases where you need direct control over the signaling process, you can use the low-level API:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { Rondevu, BloomFilter } from '@xtr-dev/rondevu-client';
|
|
237
|
+
|
|
238
|
+
const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' });
|
|
239
|
+
|
|
240
|
+
// Register and get credentials
|
|
241
|
+
const creds = await client.register();
|
|
242
|
+
console.log('Peer ID:', creds.peerId);
|
|
243
|
+
|
|
244
|
+
// Save credentials for later use
|
|
245
|
+
localStorage.setItem('rondevu-creds', JSON.stringify(creds));
|
|
246
|
+
|
|
247
|
+
// Create offer with topics
|
|
248
|
+
const offers = await client.offers.create([{
|
|
249
|
+
sdp: 'v=0...', // Your WebRTC offer SDP
|
|
250
|
+
topics: ['movie-xyz', 'hd-content'],
|
|
251
|
+
ttl: 300000 // 5 minutes
|
|
252
|
+
}]);
|
|
253
|
+
|
|
254
|
+
// Discover peers by topic
|
|
255
|
+
const discovered = await client.offers.findByTopic('movie-xyz', {
|
|
256
|
+
limit: 50
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
console.log(`Found ${discovered.length} peers`);
|
|
260
|
+
|
|
261
|
+
// Use bloom filter to exclude known peers
|
|
262
|
+
const knownPeers = new Set(['peer-id-1', 'peer-id-2']);
|
|
263
|
+
const bloom = new BloomFilter(1024, 3);
|
|
264
|
+
knownPeers.forEach(id => bloom.add(id));
|
|
265
|
+
|
|
266
|
+
const newPeers = await client.offers.findByTopic('movie-xyz', {
|
|
267
|
+
bloomFilter: bloom.toBytes(),
|
|
268
|
+
limit: 50
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## API Reference
|
|
273
|
+
|
|
274
|
+
### Authentication
|
|
275
|
+
|
|
276
|
+
#### `client.register()`
|
|
277
|
+
Register a new peer and receive credentials.
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
const creds = await client.register();
|
|
281
|
+
// { peerId: '...', secret: '...' }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Topics
|
|
285
|
+
|
|
286
|
+
#### `client.offers.getTopics(options?)`
|
|
287
|
+
List all topics with active peer counts (paginated).
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
const result = await client.offers.getTopics({
|
|
291
|
+
limit: 50,
|
|
292
|
+
offset: 0
|
|
78
293
|
});
|
|
79
294
|
|
|
80
|
-
|
|
295
|
+
// {
|
|
296
|
+
// topics: [
|
|
297
|
+
// { topic: 'movie-xyz', activePeers: 42 },
|
|
298
|
+
// { topic: 'torrent-abc', activePeers: 15 }
|
|
299
|
+
// ],
|
|
300
|
+
// total: 123,
|
|
301
|
+
// limit: 50,
|
|
302
|
+
// offset: 0
|
|
303
|
+
// }
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Offers
|
|
307
|
+
|
|
308
|
+
#### `client.offers.create(offers)`
|
|
309
|
+
Create one or more offers with topics.
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
const offers = await client.offers.create([
|
|
313
|
+
{
|
|
314
|
+
sdp: 'v=0...',
|
|
315
|
+
topics: ['topic-1', 'topic-2'],
|
|
316
|
+
ttl: 300000 // optional, default 5 minutes
|
|
317
|
+
}
|
|
318
|
+
]);
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### `client.offers.findByTopic(topic, options?)`
|
|
322
|
+
Find offers by topic with optional bloom filter.
|
|
81
323
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
324
|
+
```typescript
|
|
325
|
+
const offers = await client.offers.findByTopic('movie-xyz', {
|
|
326
|
+
limit: 50,
|
|
327
|
+
bloomFilter: bloomBytes // optional
|
|
85
328
|
});
|
|
86
329
|
```
|
|
87
330
|
|
|
88
|
-
|
|
331
|
+
#### `client.offers.getMine()`
|
|
332
|
+
Get all offers owned by the authenticated peer.
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
const myOffers = await client.offers.getMine();
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### `client.offers.heartbeat(offerId)`
|
|
339
|
+
Update last_seen timestamp for an offer.
|
|
89
340
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
341
|
+
```typescript
|
|
342
|
+
await client.offers.heartbeat(offerId);
|
|
343
|
+
```
|
|
93
344
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
- `disconnect` - Connection closed
|
|
97
|
-
- `error` - Connection error
|
|
98
|
-
- `datachannel` - New data channel received
|
|
99
|
-
- `stream` - Media stream received
|
|
345
|
+
#### `client.offers.delete(offerId)`
|
|
346
|
+
Delete a specific offer.
|
|
100
347
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
- `connection.close()` - Close connection
|
|
348
|
+
```typescript
|
|
349
|
+
await client.offers.delete(offerId);
|
|
350
|
+
```
|
|
105
351
|
|
|
106
|
-
|
|
352
|
+
#### `client.offers.answer(offerId, sdp)`
|
|
353
|
+
Answer an offer (locks it to answerer).
|
|
107
354
|
|
|
108
|
-
|
|
355
|
+
```typescript
|
|
356
|
+
await client.offers.answer(offerId, answerSdp);
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
#### `client.offers.getAnswers()`
|
|
360
|
+
Poll for answers to your offers.
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
const answers = await client.offers.getAnswers();
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### ICE Candidates
|
|
367
|
+
|
|
368
|
+
#### `client.offers.addIceCandidates(offerId, candidates)`
|
|
369
|
+
Post ICE candidates for an offer.
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
await client.offers.addIceCandidates(offerId, [
|
|
373
|
+
'candidate:1 1 UDP...'
|
|
374
|
+
]);
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### `client.offers.getIceCandidates(offerId, since?)`
|
|
378
|
+
Get ICE candidates from the other peer.
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
const candidates = await client.offers.getIceCandidates(offerId);
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Bloom Filter
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
import { BloomFilter } from '@xtr-dev/rondevu-client';
|
|
388
|
+
|
|
389
|
+
// Create filter: size=1024 bits, hash=3 functions
|
|
390
|
+
const bloom = new BloomFilter(1024, 3);
|
|
391
|
+
|
|
392
|
+
// Add items
|
|
393
|
+
bloom.add('peer-id-1');
|
|
394
|
+
bloom.add('peer-id-2');
|
|
395
|
+
|
|
396
|
+
// Test membership
|
|
397
|
+
bloom.test('peer-id-1'); // true (probably)
|
|
398
|
+
bloom.test('unknown'); // false (definitely)
|
|
399
|
+
|
|
400
|
+
// Export for API
|
|
401
|
+
const bytes = bloom.toBytes();
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## TypeScript
|
|
405
|
+
|
|
406
|
+
All types are exported:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import type {
|
|
410
|
+
Credentials,
|
|
411
|
+
Offer,
|
|
412
|
+
CreateOfferRequest,
|
|
413
|
+
TopicInfo,
|
|
414
|
+
IceCandidate,
|
|
415
|
+
FetchFunction,
|
|
416
|
+
RondevuOptions,
|
|
417
|
+
ConnectionOptions,
|
|
418
|
+
RondevuConnectionEvents
|
|
419
|
+
} from '@xtr-dev/rondevu-client';
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Environment Compatibility
|
|
423
|
+
|
|
424
|
+
The client library is designed to work across different JavaScript runtimes:
|
|
425
|
+
|
|
426
|
+
| Environment | Native Fetch | Custom Fetch Needed |
|
|
427
|
+
|-------------|--------------|---------------------|
|
|
428
|
+
| Modern Browsers | ✅ Yes | ❌ No |
|
|
429
|
+
| Node.js 18+ | ✅ Yes | ❌ No |
|
|
430
|
+
| Node.js < 18 | ❌ No | ✅ Yes (node-fetch) |
|
|
431
|
+
| Deno | ✅ Yes | ❌ No |
|
|
432
|
+
| Bun | ✅ Yes | ❌ No |
|
|
433
|
+
| Cloudflare Workers | ✅ Yes | ❌ No |
|
|
434
|
+
|
|
435
|
+
**If your environment doesn't have native fetch:**
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
npm install node-fetch
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
import { Rondevu } from '@xtr-dev/rondevu-client';
|
|
443
|
+
import fetch from 'node-fetch';
|
|
444
|
+
|
|
445
|
+
const client = new Rondevu({
|
|
446
|
+
baseUrl: 'https://rondevu.xtrdev.workers.dev',
|
|
447
|
+
fetch: fetch as any
|
|
448
|
+
});
|
|
449
|
+
```
|
|
109
450
|
|
|
110
|
-
|
|
451
|
+
## License
|
|
111
452
|
|
|
112
453
|
MIT
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface Credentials {
|
|
2
|
+
peerId: string;
|
|
3
|
+
secret: string;
|
|
4
|
+
}
|
|
5
|
+
export type FetchFunction = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
6
|
+
export declare class RondevuAuth {
|
|
7
|
+
private baseUrl;
|
|
8
|
+
private fetchFn;
|
|
9
|
+
constructor(baseUrl: string, fetchFn?: FetchFunction);
|
|
10
|
+
/**
|
|
11
|
+
* Register a new peer and receive credentials
|
|
12
|
+
*/
|
|
13
|
+
register(): Promise<Credentials>;
|
|
14
|
+
/**
|
|
15
|
+
* Create Authorization header value
|
|
16
|
+
*/
|
|
17
|
+
static createAuthHeader(credentials: Credentials): string;
|
|
18
|
+
}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export class RondevuAuth {
|
|
2
|
+
constructor(baseUrl, fetchFn) {
|
|
3
|
+
this.baseUrl = baseUrl;
|
|
4
|
+
// Use provided fetch or fall back to global fetch
|
|
5
|
+
this.fetchFn = fetchFn || ((...args) => {
|
|
6
|
+
if (typeof globalThis.fetch === 'function') {
|
|
7
|
+
return globalThis.fetch(...args);
|
|
8
|
+
}
|
|
9
|
+
throw new Error('fetch is not available. Please provide a fetch implementation in the constructor options.');
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Register a new peer and receive credentials
|
|
14
|
+
*/
|
|
15
|
+
async register() {
|
|
16
|
+
const response = await this.fetchFn(`${this.baseUrl}/register`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: {
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({}),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
25
|
+
throw new Error(`Registration failed: ${error.error || response.statusText}`);
|
|
26
|
+
}
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
return {
|
|
29
|
+
peerId: data.peerId,
|
|
30
|
+
secret: data.secret,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create Authorization header value
|
|
35
|
+
*/
|
|
36
|
+
static createAuthHeader(credentials) {
|
|
37
|
+
return `Bearer ${credentials.peerId}:${credentials.secret}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/bloom.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple bloom filter implementation for peer ID exclusion
|
|
3
|
+
* Uses multiple hash functions for better distribution
|
|
4
|
+
*/
|
|
5
|
+
export declare class BloomFilter {
|
|
6
|
+
private bits;
|
|
7
|
+
private size;
|
|
8
|
+
private numHashes;
|
|
9
|
+
constructor(size?: number, numHashes?: number);
|
|
10
|
+
/**
|
|
11
|
+
* Add a peer ID to the filter
|
|
12
|
+
*/
|
|
13
|
+
add(peerId: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* Test if peer ID might be in the filter
|
|
16
|
+
*/
|
|
17
|
+
test(peerId: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Get raw bits for transmission
|
|
20
|
+
*/
|
|
21
|
+
toBytes(): Uint8Array;
|
|
22
|
+
/**
|
|
23
|
+
* Convert to base64 for URL parameters
|
|
24
|
+
*/
|
|
25
|
+
toBase64(): string;
|
|
26
|
+
/**
|
|
27
|
+
* Simple hash function (FNV-1a variant)
|
|
28
|
+
*/
|
|
29
|
+
private hash;
|
|
30
|
+
}
|
package/dist/bloom.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple bloom filter implementation for peer ID exclusion
|
|
3
|
+
* Uses multiple hash functions for better distribution
|
|
4
|
+
*/
|
|
5
|
+
export class BloomFilter {
|
|
6
|
+
constructor(size = 1024, numHashes = 3) {
|
|
7
|
+
this.size = size;
|
|
8
|
+
this.numHashes = numHashes;
|
|
9
|
+
this.bits = new Uint8Array(Math.ceil(size / 8));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Add a peer ID to the filter
|
|
13
|
+
*/
|
|
14
|
+
add(peerId) {
|
|
15
|
+
for (let i = 0; i < this.numHashes; i++) {
|
|
16
|
+
const hash = this.hash(peerId, i);
|
|
17
|
+
const index = hash % this.size;
|
|
18
|
+
const byteIndex = Math.floor(index / 8);
|
|
19
|
+
const bitIndex = index % 8;
|
|
20
|
+
this.bits[byteIndex] |= 1 << bitIndex;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Test if peer ID might be in the filter
|
|
25
|
+
*/
|
|
26
|
+
test(peerId) {
|
|
27
|
+
for (let i = 0; i < this.numHashes; i++) {
|
|
28
|
+
const hash = this.hash(peerId, i);
|
|
29
|
+
const index = hash % this.size;
|
|
30
|
+
const byteIndex = Math.floor(index / 8);
|
|
31
|
+
const bitIndex = index % 8;
|
|
32
|
+
if (!(this.bits[byteIndex] & (1 << bitIndex))) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get raw bits for transmission
|
|
40
|
+
*/
|
|
41
|
+
toBytes() {
|
|
42
|
+
return this.bits;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Convert to base64 for URL parameters
|
|
46
|
+
*/
|
|
47
|
+
toBase64() {
|
|
48
|
+
// Convert Uint8Array to regular array then to string
|
|
49
|
+
const binaryString = String.fromCharCode(...Array.from(this.bits));
|
|
50
|
+
// Use btoa for browser, or Buffer for Node.js
|
|
51
|
+
if (typeof btoa !== 'undefined') {
|
|
52
|
+
return btoa(binaryString);
|
|
53
|
+
}
|
|
54
|
+
else if (typeof Buffer !== 'undefined') {
|
|
55
|
+
return Buffer.from(this.bits).toString('base64');
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Fallback: manual base64 encoding
|
|
59
|
+
throw new Error('No base64 encoding available');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Simple hash function (FNV-1a variant)
|
|
64
|
+
*/
|
|
65
|
+
hash(str, seed) {
|
|
66
|
+
let hash = 2166136261 ^ seed;
|
|
67
|
+
for (let i = 0; i < str.length; i++) {
|
|
68
|
+
hash ^= str.charCodeAt(i);
|
|
69
|
+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
70
|
+
}
|
|
71
|
+
return hash >>> 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -108,4 +108,19 @@ export declare class RondevuAPI {
|
|
|
108
108
|
* ```
|
|
109
109
|
*/
|
|
110
110
|
health(): Promise<HealthResponse>;
|
|
111
|
+
/**
|
|
112
|
+
* Ends a session by deleting the offer from the server
|
|
113
|
+
*
|
|
114
|
+
* @param code - The offer code
|
|
115
|
+
* @returns Success confirmation
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* const api = new RondevuAPI({ baseUrl: 'https://example.com' });
|
|
120
|
+
* await api.leave('my-offer-code');
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
leave(code: string): Promise<{
|
|
124
|
+
success: boolean;
|
|
125
|
+
}>;
|
|
111
126
|
}
|