# Rondevu Client [![npm version](https://img.shields.io/npm/v/@xtr-dev/rondevu-client)](https://www.npmjs.com/package/@xtr-dev/rondevu-client) 🌐 **Topic-based peer discovery and WebRTC signaling client** TypeScript/JavaScript client for Rondevu, providing topic-based peer discovery, stateless authentication, and complete WebRTC signaling with trickle ICE support. **Related repositories:** - [@xtr-dev/rondevu-client](https://github.com/xtr-dev/rondevu-client) - TypeScript client library ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-client)) - [@xtr-dev/rondevu-server](https://github.com/xtr-dev/rondevu-server) - HTTP signaling server ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-server), [live](https://api.ronde.vu)) - [@xtr-dev/rondevu-demo](https://github.com/xtr-dev/rondevu-demo) - Interactive demo ([live](https://ronde.vu)) --- ## Features - **Topic-Based Discovery**: Find peers by topics (e.g., torrent infohashes) - **Stateless Authentication**: No server-side sessions, portable credentials - **Protected Connections**: Optional secret-protected offers for access control - **Bloom Filters**: Efficient peer exclusion for repeated discoveries - **Multi-Offer Management**: Create and manage multiple offers per peer - **Complete WebRTC Signaling**: Full offer/answer and ICE candidate exchange - **Trickle ICE**: Send ICE candidates as they're discovered (faster connections) - **State Machine**: Clean state-based connection lifecycle - **TypeScript**: Full type safety and autocomplete ## Install ```bash npm install @xtr-dev/rondevu-client ``` ## Quick Start ### Creating an Offer (Peer A) ```typescript import { Rondevu } from '@xtr-dev/rondevu-client'; // Initialize client and register const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' }); await client.register(); // Create peer connection const peer = client.createPeer(); // Set up event listeners peer.on('state', (state) => { console.log('Peer state:', state); // States: idle → creating-offer → waiting-for-answer → exchanging-ice → connected }); peer.on('connected', () => { console.log('✅ Connected to peer!'); }); peer.on('datachannel', (channel) => { console.log('📡 Data channel ready'); channel.addEventListener('message', (event) => { console.log('📥 Received:', event.data); }); channel.addEventListener('open', () => { channel.send('Hello from peer A!'); }); }); // Create offer and advertise on topics const offerId = await peer.createOffer({ topics: ['my-app', 'room-123'], ttl: 300000, // 5 minutes secret: 'my-secret-password' // Optional: protect offer (max 128 chars) }); console.log('Offer created:', offerId); console.log('Share these topics with peers:', ['my-app', 'room-123']); ``` ### Answering an Offer (Peer B) ```typescript import { Rondevu } from '@xtr-dev/rondevu-client'; // Initialize client and register const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' }); await client.register(); // Discover offers by topic const offers = await client.offers.findByTopic('my-app', { limit: 10 }); if (offers.length > 0) { const offer = offers[0]; // Create peer connection const peer = client.createPeer(); // Set up event listeners peer.on('state', (state) => { console.log('Peer state:', state); // States: idle → answering → exchanging-ice → connected }); peer.on('connected', () => { console.log('✅ Connected!'); }); peer.on('datachannel', (channel) => { console.log('📡 Data channel ready'); channel.addEventListener('message', (event) => { console.log('📥 Received:', event.data); }); channel.addEventListener('open', () => { channel.send('Hello from peer B!'); }); }); peer.on('failed', (error) => { console.error('❌ Connection failed:', error); }); // Answer the offer await peer.answer(offer.id, offer.sdp, { topics: offer.topics, secret: 'my-secret-password' // Required if offer.hasSecret is true }); } ``` ## Protected Offers You can protect offers with a secret to control who can answer them. This is useful for private rooms or invite-only connections. ### Creating a Protected Offer ```typescript const offerId = await peer.createOffer({ topics: ['private-room'], secret: 'my-secret-password' // Max 128 characters }); // Share the secret with authorized peers through a secure channel ``` ### Answering a Protected Offer ```typescript const offers = await client.offers.findByTopic('private-room'); // Check if offer requires a secret if (offers[0].hasSecret) { console.log('This offer requires a secret'); } // Provide the secret when answering await peer.answer(offers[0].id, offers[0].sdp, { topics: offers[0].topics, secret: 'my-secret-password' // Must match the offer's secret }); ``` **Notes:** - The actual secret is never exposed in public API responses - only a `hasSecret` boolean flag - Answerers must provide the correct secret, or the answer will be rejected - Secrets are limited to 128 characters - Use this for access control, not for cryptographic security (use end-to-end encryption for that) ## Connection Lifecycle The `RondevuPeer` uses a state machine for connection management: ### Offerer States 1. **idle** - Initial state 2. **creating-offer** - Creating WebRTC offer 3. **waiting-for-answer** - Polling for answer from peer 4. **exchanging-ice** - Exchanging ICE candidates 5. **connected** - Successfully connected 6. **failed** - Connection failed 7. **closed** - Connection closed ### Answerer States 1. **idle** - Initial state 2. **answering** - Creating WebRTC answer 3. **exchanging-ice** - Exchanging ICE candidates 4. **connected** - Successfully connected 5. **failed** - Connection failed 6. **closed** - Connection closed ### State Events ```typescript peer.on('state', (stateName) => { console.log('Current state:', stateName); }); peer.on('connected', () => { // Connection established successfully }); peer.on('disconnected', () => { // Connection lost or closed }); peer.on('failed', (error) => { // Connection failed console.error('Connection error:', error); }); peer.on('datachannel', (channel) => { // Data channel is ready (use channel.addEventListener) }); peer.on('track', (event) => { // Media track received (for audio/video streaming) const stream = event.streams[0]; videoElement.srcObject = stream; }); ``` ## Trickle ICE This library implements **trickle ICE** for faster connection establishment: - ICE candidates are sent to the server as they're discovered - No waiting for all candidates before sending offer/answer - Connections establish much faster (milliseconds vs seconds) - Proper event listener cleanup to prevent memory leaks ## Adding Media Tracks ```typescript // Get user's camera/microphone const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); // Add tracks to peer connection stream.getTracks().forEach(track => { peer.addTrack(track, stream); }); ``` ## Peer Properties ```typescript // Get current state name console.log(peer.stateName); // 'idle', 'creating-offer', 'connected', etc. // Get connection state console.log(peer.connectionState); // RTCPeerConnectionState // Get offer ID (after creating offer or answering) console.log(peer.offerId); // Get role console.log(peer.role); // 'offerer' or 'answerer' ``` ## Closing a Connection ```typescript await peer.close(); ``` ## Custom RTCConfiguration ```typescript const peer = client.createPeer({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'turn:turn.example.com:3478', username: 'user', credential: 'pass' } ], iceTransportPolicy: 'relay' // Force TURN relay (useful for testing) }); ``` ## Timeouts Configure connection timeouts: ```typescript await peer.createOffer({ topics: ['my-topic'], timeouts: { iceGathering: 10000, // ICE gathering timeout (10s) waitingForAnswer: 30000, // Waiting for answer timeout (30s) creatingAnswer: 10000, // Creating answer timeout (10s) iceConnection: 30000 // ICE connection timeout (30s) } }); ``` ## Platform-Specific Setup ### Node.js 18+ (with native fetch) Works out of the box - no additional setup needed. ### Node.js < 18 (without native fetch) Install node-fetch and provide it to the client: ```bash npm install node-fetch ``` ```typescript import { Rondevu } from '@xtr-dev/rondevu-client'; import fetch from 'node-fetch'; const client = new Rondevu({ baseUrl: 'https://api.ronde.vu', fetch: fetch as any }); ``` ### Node.js with WebRTC (wrtc) For WebRTC functionality in Node.js, you need to provide WebRTC polyfills since Node.js doesn't have native WebRTC support: ```bash npm install wrtc node-fetch ``` ```typescript import { Rondevu } from '@xtr-dev/rondevu-client'; import fetch from 'node-fetch'; import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc'; const client = new Rondevu({ baseUrl: 'https://api.ronde.vu', fetch: fetch as any, RTCPeerConnection, RTCSessionDescription, RTCIceCandidate }); // Now you can use WebRTC features await client.register(); const peer = client.createPeer({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }); // Create offers, answer, etc. const offerId = await peer.createOffer({ topics: ['my-topic'] }); ``` **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. ### Deno ```typescript import { Rondevu } from 'npm:@xtr-dev/rondevu-client'; const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' }); ``` ### Bun Works out of the box - no additional setup needed. ### Cloudflare Workers ```typescript import { Rondevu } from '@xtr-dev/rondevu-client'; export default { async fetch(request: Request, env: Env) { const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' }); const creds = await client.register(); return new Response(JSON.stringify(creds)); } }; ``` ## Low-Level API Usage For direct control over the signaling process without WebRTC: ```typescript import { Rondevu, BloomFilter } from '@xtr-dev/rondevu-client'; const client = new Rondevu({ baseUrl: 'https://api.ronde.vu' }); // Register and get credentials const creds = await client.register(); console.log('Peer ID:', creds.peerId); // Save credentials for later use localStorage.setItem('rondevu-creds', JSON.stringify(creds)); // Create offer with topics const offers = await client.offers.create([{ sdp: 'v=0...', // Your WebRTC offer SDP topics: ['movie-xyz', 'hd-content'], ttl: 300000, // 5 minutes secret: 'my-secret-password' // Optional: protect offer (max 128 chars) }]); // Discover peers by topic const discovered = await client.offers.findByTopic('movie-xyz', { limit: 50 }); console.log(`Found ${discovered.length} peers`); // Use bloom filter to exclude known peers const knownPeers = new Set(['peer-id-1', 'peer-id-2']); const bloom = new BloomFilter(1024, 3); knownPeers.forEach(id => bloom.add(id)); const newPeers = await client.offers.findByTopic('movie-xyz', { bloomFilter: bloom.toBytes(), limit: 50 }); ``` ## API Reference ### Authentication #### `client.register()` Register a new peer and receive credentials. ```typescript const creds = await client.register(); // { peerId: '...', secret: '...' } ``` ### Topics #### `client.offers.getTopics(options?)` List all topics with active peer counts (paginated). ```typescript const result = await client.offers.getTopics({ limit: 50, offset: 0 }); // { // topics: [ // { topic: 'movie-xyz', activePeers: 42 }, // { topic: 'torrent-abc', activePeers: 15 } // ], // total: 123, // limit: 50, // offset: 0 // } ``` ### Offers #### `client.offers.create(offers)` Create one or more offers with topics. ```typescript const offers = await client.offers.create([ { sdp: 'v=0...', topics: ['topic-1', 'topic-2'], ttl: 300000, // optional, default 5 minutes secret: 'my-secret-password' // optional, max 128 chars } ]); ``` #### `client.offers.findByTopic(topic, options?)` Find offers by topic with optional bloom filter. ```typescript const offers = await client.offers.findByTopic('movie-xyz', { limit: 50, bloomFilter: bloomBytes // optional }); ``` #### `client.offers.getMine()` Get all offers owned by the authenticated peer. ```typescript const myOffers = await client.offers.getMine(); ``` #### `client.offers.delete(offerId)` Delete a specific offer. ```typescript await client.offers.delete(offerId); ``` #### `client.offers.answer(offerId, sdp, secret?)` Answer an offer (locks it to answerer). ```typescript await client.offers.answer(offerId, answerSdp, 'my-secret-password'); ``` **Parameters:** - `offerId`: The offer ID to answer - `sdp`: The WebRTC answer SDP - `secret` (optional): Required if the offer has `hasSecret: true` #### `client.offers.getAnswers()` Poll for answers to your offers. ```typescript const answers = await client.offers.getAnswers(); ``` ### ICE Candidates #### `client.offers.addIceCandidates(offerId, candidates)` Post ICE candidates for an offer. ```typescript await client.offers.addIceCandidates(offerId, [ { candidate: 'candidate:1 1 UDP...', sdpMid: '0', sdpMLineIndex: 0 } ]); ``` #### `client.offers.getIceCandidates(offerId, since?)` Get ICE candidates from the other peer. ```typescript const candidates = await client.offers.getIceCandidates(offerId, since); ``` ### Bloom Filter ```typescript import { BloomFilter } from '@xtr-dev/rondevu-client'; // Create filter: size=1024 bits, hash=3 functions const bloom = new BloomFilter(1024, 3); // Add items bloom.add('peer-id-1'); bloom.add('peer-id-2'); // Test membership bloom.test('peer-id-1'); // true (probably) bloom.test('unknown'); // false (definitely) // Export for API const bytes = bloom.toBytes(); ``` ## TypeScript All types are exported: ```typescript import type { Credentials, Offer, CreateOfferRequest, TopicInfo, IceCandidate, FetchFunction, RondevuOptions, PeerOptions, PeerEvents, PeerTimeouts } from '@xtr-dev/rondevu-client'; ``` ## Environment Compatibility The client library is designed to work across different JavaScript runtimes: | Environment | Native Fetch | Native WebRTC | Polyfills Needed | |-------------|--------------|---------------|------------------| | Modern Browsers | ✅ Yes | ✅ Yes | ❌ None | | Node.js 18+ | ✅ Yes | ❌ No | ✅ WebRTC (wrtc) | | Node.js < 18 | ❌ No | ❌ No | ✅ Fetch + WebRTC | | Deno | ✅ Yes | ⚠️ Partial | ❌ None (signaling only) | | Bun | ✅ Yes | ❌ No | ✅ WebRTC (wrtc) | | Cloudflare Workers | ✅ Yes | ❌ No | ❌ None (signaling only) | **For signaling-only (no WebRTC peer connections):** Use the low-level API with `client.offers` - no WebRTC polyfills needed. **For full WebRTC support in Node.js:** ```bash npm install wrtc node-fetch ``` ```typescript import { Rondevu } from '@xtr-dev/rondevu-client'; import fetch from 'node-fetch'; import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc'; const client = new Rondevu({ baseUrl: 'https://api.ronde.vu', fetch: fetch as any, RTCPeerConnection, RTCSessionDescription, RTCIceCandidate }); ``` ## License MIT