Replace `candidate: any` with `candidate: RTCIceCandidateInit | null`: Changes: - api.ts poll() return type (line 366) - rondevu.ts pollOffers() return type (line 827) - IceCandidate interface (line 41) Benefits: - Better type safety and IntelliSense support - Matches WebRTC spec (candidates can be null) - Catches potential type errors at compile time - Clearer API contracts for consumers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Rondevu Client
🌐 Simple WebRTC signaling client with username-based discovery
TypeScript/JavaScript client for Rondevu, providing WebRTC signaling with username claiming, service publishing/discovery, and efficient batch polling.
Related repositories:
- @xtr-dev/rondevu-client - TypeScript client library (npm)
- @xtr-dev/rondevu-server - HTTP signaling server (npm, live)
- @xtr-dev/rondevu-demo - Interactive demo (live)
Features
- Username Claiming: Secure ownership with Ed25519 signatures
- Anonymous Users: Auto-generated anonymous usernames for quick testing
- Service Publishing: Publish services with multiple offers for connection pooling
- Service Discovery: Direct lookup, random discovery, or paginated search
- Efficient Batch Polling: Single endpoint for answers and ICE candidates (50% fewer requests)
- Semantic Version Matching: Compatible version resolution (chat:1.0.0 matches any 1.x.x)
- TypeScript: Full type safety and autocomplete
- Keypair Management: Generate or reuse Ed25519 keypairs
- Automatic Signatures: All authenticated requests signed automatically
Installation
npm install @xtr-dev/rondevu-client
Quick Start
Publishing a Service (Offerer)
import { Rondevu } from '@xtr-dev/rondevu-client'
// 1. Connect to Rondevu (generates keypair, username auto-claimed on first request)
const rondevu = await Rondevu.connect({
apiUrl: 'https://api.ronde.vu',
username: 'alice', // Or omit for anonymous username
iceServers: 'ipv4-turn' // Use preset: 'ipv4-turn', 'hostname-turns', 'google-stun', or 'relay-only'
})
// 2. Publish service
await rondevu.publishService({
service: 'chat:1.0.0',
maxOffers: 5, // Maintain up to 5 concurrent offers
offerFactory: async (rtcConfig) => {
const pc = new RTCPeerConnection(rtcConfig)
const dc = pc.createDataChannel('chat')
// Set up event listeners during creation
dc.addEventListener('open', () => {
console.log('Connection opened!')
dc.send('Hello from Alice!')
})
dc.addEventListener('message', (e) => {
console.log('Received message:', e.data)
})
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
return { pc, dc, offer }
},
ttl: 300000
})
// 3. Start accepting connections
await rondevu.startFilling()
// 4. Stop when done
// rondevu.stopFilling()
Connecting to a Service (Answerer)
import { Rondevu } from '@xtr-dev/rondevu-client'
// 1. Connect to Rondevu with ICE server preset
const rondevu = await Rondevu.connect({
apiUrl: 'https://api.ronde.vu',
username: 'bob',
iceServers: 'ipv4-turn'
})
// 2. Connect to service
const connection = await rondevu.connectToService({
serviceFqn: 'chat:1.0.0@alice',
onConnection: ({ dc, peerUsername }) => {
console.log('Connected to', peerUsername)
dc.addEventListener('message', (e) => {
console.log('Received:', e.data)
})
dc.addEventListener('open', () => {
dc.send('Hello from Bob!')
})
}
})
// Access connection
connection.dc.send('Another message')
connection.pc.close() // Close when done
API Reference
Rondevu Class
Main class for all Rondevu operations.
import { Rondevu } from '@xtr-dev/rondevu-client'
// Create and connect to Rondevu
const rondevu = await Rondevu.connect({
apiUrl: string, // Signaling server URL
username?: string, // Optional: your username (auto-generates anonymous if omitted)
keypair?: Keypair, // Optional: reuse existing keypair
cryptoAdapter?: CryptoAdapter // Optional: platform-specific crypto (defaults to WebCryptoAdapter)
batching?: BatcherOptions | false // Optional: RPC batching configuration
})
Platform Support (Browser & Node.js)
The client supports both browser and Node.js environments using crypto adapters:
Browser (default):
import { Rondevu } from '@xtr-dev/rondevu-client'
// WebCryptoAdapter is used by default - no configuration needed
const rondevu = await Rondevu.connect({
apiUrl: 'https://api.ronde.vu',
username: 'alice'
})
Node.js (19+ or 18 with --experimental-global-webcrypto):
import { Rondevu, NodeCryptoAdapter } from '@xtr-dev/rondevu-client'
const rondevu = await Rondevu.connect({
apiUrl: 'https://api.ronde.vu',
username: 'alice',
cryptoAdapter: new NodeCryptoAdapter()
})
Note: Node.js support requires:
- Node.js 19+ (crypto.subtle available globally), OR
- Node.js 18 with
--experimental-global-webcryptoflag - WebRTC implementation like
wrtcornode-webrtcfor RTCPeerConnection
Custom Crypto Adapter:
import { CryptoAdapter, Keypair } from '@xtr-dev/rondevu-client'
class CustomCryptoAdapter implements CryptoAdapter {
async generateKeypair(): Promise<Keypair> { /* ... */ }
async signMessage(message: string, privateKey: string): Promise<string> { /* ... */ }
async verifySignature(message: string, signature: string, publicKey: string): Promise<boolean> { /* ... */ }
bytesToBase64(bytes: Uint8Array): string { /* ... */ }
base64ToBytes(base64: string): Uint8Array { /* ... */ }
randomBytes(length: number): Uint8Array { /* ... */ }
}
const rondevu = await Rondevu.connect({
apiUrl: 'https://api.ronde.vu',
cryptoAdapter: new CustomCryptoAdapter()
})
Username Management
Usernames are automatically claimed on the first authenticated request (like publishService()).
// Check if username is claimed (checks server)
await rondevu.isUsernameClaimed(): Promise<boolean>
// Get username
rondevu.getUsername(): string
// Get public key
rondevu.getPublicKey(): string
// Get keypair (for backup/storage)
rondevu.getKeypair(): Keypair
Service Publishing
// Publish service with offers
await rondevu.publishService({
service: string, // e.g., 'chat:1.0.0' (username auto-appended)
offers: Array<{ sdp: string }>,
ttl?: number // Optional: milliseconds (default: 300000)
}): Promise<Service>
Service Discovery
// Direct lookup by FQN (with username)
await rondevu.getService('chat:1.0.0@alice'): Promise<ServiceOffer>
// Random discovery (without username)
await rondevu.discoverService('chat:1.0.0'): Promise<ServiceOffer>
// Paginated discovery (returns multiple offers)
await rondevu.discoverServices(
'chat:1.0.0', // serviceVersion
10, // limit
0 // offset
): Promise<{ services: ServiceOffer[], count: number, limit: number, offset: number }>
WebRTC Signaling
// Post answer SDP
await rondevu.postOfferAnswer(
serviceFqn: string,
offerId: string,
sdp: string
): Promise<{ success: boolean, offerId: string }>
// Get answer SDP (offerer polls this - deprecated, use pollOffers instead)
await rondevu.getOfferAnswer(
serviceFqn: string,
offerId: string
): Promise<{ sdp: string, offerId: string, answererId: string, answeredAt: number } | null>
// Combined polling for answers and ICE candidates (RECOMMENDED for offerers)
await rondevu.pollOffers(since?: number): Promise<{
answers: Array<{
offerId: string
serviceId?: string
answererId: string
sdp: string
answeredAt: number
}>
iceCandidates: Record<string, Array<{
candidate: any
role: 'offerer' | 'answerer'
peerId: string
createdAt: number
}>>
}>
// Add ICE candidates
await rondevu.addOfferIceCandidates(
serviceFqn: string,
offerId: string,
candidates: RTCIceCandidateInit[]
): Promise<{ count: number, offerId: string }>
// Get ICE candidates (with polling support)
await rondevu.getOfferIceCandidates(
serviceFqn: string,
offerId: string,
since: number = 0
): Promise<{ candidates: IceCandidate[], offerId: string }>
RondevuAPI Class
Low-level HTTP API client (used internally by Rondevu class).
import { RondevuAPI } from '@xtr-dev/rondevu-client'
const api = new RondevuAPI(
baseUrl: string,
username: string,
keypair: Keypair
)
// Check username
await api.checkUsername(username: string): Promise<{
available: boolean
publicKey?: string
claimedAt?: number
expiresAt?: number
}>
// Note: Username claiming is now implicit - usernames are auto-claimed
// on first authenticated request to the server
// ... (all other HTTP endpoints)
Cryptographic Helpers
// Generate Ed25519 keypair
const keypair = await RondevuAPI.generateKeypair(): Promise<Keypair>
// Sign message
const signature = await RondevuAPI.signMessage(
message: string,
privateKey: string
): Promise<string>
// Verify signature
const valid = await RondevuAPI.verifySignature(
message: string,
signature: string,
publicKey: string
): Promise<boolean>
Types
interface Keypair {
publicKey: string // Base64-encoded Ed25519 public key
privateKey: string // Base64-encoded Ed25519 private key
}
interface Service {
serviceId: string
offers: ServiceOffer[]
username: string
serviceFqn: string
createdAt: number
expiresAt: number
}
interface ServiceOffer {
offerId: string
sdp: string
createdAt: number
expiresAt: number
}
interface IceCandidate {
candidate: RTCIceCandidateInit
createdAt: number
}
Advanced Usage
Anonymous Username
// Auto-generate anonymous username (format: anon-{timestamp}-{random})
const rondevu = await Rondevu.connect({
apiUrl: 'https://api.ronde.vu'
// No username provided - will generate anonymous username
})
console.log(rondevu.getUsername()) // e.g., "anon-lx2w34-a3f501"
// Anonymous users behave exactly like regular users
await rondevu.publishService({
service: 'chat:1.0.0',
maxOffers: 5
})
await rondevu.startFilling()
Persistent Keypair
// Save keypair and username to localStorage
const rondevu = await Rondevu.connect({
apiUrl: 'https://api.ronde.vu',
username: 'alice'
})
// Save for later (username will be auto-claimed on first authenticated request)
localStorage.setItem('rondevu-username', rondevu.getUsername())
localStorage.setItem('rondevu-keypair', JSON.stringify(rondevu.getKeypair()))
// Load on next session
const savedUsername = localStorage.getItem('rondevu-username')
const savedKeypair = JSON.parse(localStorage.getItem('rondevu-keypair'))
const rondevu2 = await Rondevu.connect({
apiUrl: 'https://api.ronde.vu',
username: savedUsername,
keypair: savedKeypair
})
Service Discovery
// Get a random available service
const service = await rondevu.discoverService('chat:1.0.0')
console.log('Discovered:', service.username)
// Get multiple services (paginated)
const result = await rondevu.discoverServices('chat:1.0.0', 10, 0)
console.log(`Found ${result.count} services:`)
result.services.forEach(s => console.log(` - ${s.username}`))
Multiple Concurrent Offers
// Publish service with multiple offers for connection pooling
const offers = []
const connections = []
for (let i = 0; i < 5; i++) {
const pc = new RTCPeerConnection(rtcConfig)
const dc = pc.createDataChannel('chat')
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
offers.push({ sdp: offer.sdp })
connections.push({ pc, dc })
}
const service = await rondevu.publishService({
service: 'chat:1.0.0',
offers,
ttl: 300000
})
// Each offer can be answered independently
console.log(`Published ${service.offers.length} offers`)
Platform Support
Modern Browsers
Works out of the box - no additional setup needed.
Node.js 18+
Native fetch is available, but WebRTC requires polyfills:
npm install wrtc
import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate } from 'wrtc'
// Use wrtc implementations
const pc = new RTCPeerConnection()
Username Rules
- Format: Lowercase alphanumeric + dash (
a-z,0-9,-) - Length: 3-32 characters
- Pattern:
^[a-z0-9][a-z0-9-]*[a-z0-9]$ - Validity: 365 days from claim/last use
- Ownership: Secured by Ed25519 public key signature
Service FQN Format
- Format:
service:version@username - Service: Lowercase alphanumeric + dash (e.g.,
chat,video-call) - Version: Semantic versioning (e.g.,
1.0.0,2.1.3) - Username: Claimed username
- Example:
chat:1.0.0@alice
Examples
Node.js Service Host Example
You can host WebRTC services in Node.js that browser clients can connect to. See the Node.js Host Guide for a complete guide.
Quick example:
import { Rondevu, NodeCryptoAdapter } from '@xtr-dev/rondevu-client'
import wrtc from 'wrtc'
const { RTCPeerConnection } = wrtc
// Initialize with Node crypto adapter
const rondevu = await Rondevu.connect({
apiUrl: 'https://api.ronde.vu',
username: 'mybot',
cryptoAdapter: new NodeCryptoAdapter()
})
// Create peer connection (offerer creates data channel)
const pc = new RTCPeerConnection(rtcConfig)
const dc = pc.createDataChannel('chat')
// Publish service (username auto-claimed on first publish)
await rondevu.publishService({
service: 'chat:1.0.0',
maxOffers: 5
})
await rondevu.startFilling()
// Browser clients can now discover and connect to chat:1.0.0@mybot
See complete examples:
- Node.js Host Guide - Full guide with complete examples
- test-connect.js - Working Node.js client example
- React Demo - Complete browser UI (live)
Migration from v0.3.x
v0.4.0 removes high-level abstractions and uses manual WebRTC setup:
Removed:
ServiceHostclass (use manual WebRTC +publishService())ServiceClientclass (use manual WebRTC +getService())RTCDurableConnectionclass (use native WebRTC APIs)RondevuServiceclass (merged intoRondevu)
Added:
pollOffers()- Combined polling for answers and ICE candidatespublishService()- Automatic offer pool managementconnectToService()- Automatic answering side setup
Migration Example:
// Before (v0.3.x) - ServiceHost
const host = new ServiceHost({
service: 'chat@1.0.0',
rondevuService: service
})
await host.start()
// After (v0.4.0+) - Automatic setup
await rondevu.publishService({
service: 'chat:1.0.0',
maxOffers: 5
})
await rondevu.startFilling()
License
MIT