diff --git a/package-lock.json b/package-lock.json index f15e85c..33ee6f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@xtr-dev/rondevu-client", - "version": "0.12.0", + "version": "0.12.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@xtr-dev/rondevu-client", - "version": "0.12.0", + "version": "0.12.4", "license": "MIT", "dependencies": { "@noble/ed25519": "^3.0.0", diff --git a/package.json b/package.json index ed54f82..1d2f02c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/rondevu-client", - "version": "0.12.0", + "version": "0.12.4", "description": "TypeScript client for Rondevu with durable WebRTC connections, automatic reconnection, and message queuing", "type": "module", "main": "dist/index.js", diff --git a/src/api.ts b/src/api.ts index 6982605..cbefb72 100644 --- a/src/api.ts +++ b/src/api.ts @@ -42,12 +42,9 @@ export interface OfferRequest { } export interface ServiceRequest { - username: string - serviceFqn: string + serviceFqn: string // Must include username: service:version@username offers: OfferRequest[] ttl?: number - isPublic?: boolean - metadata?: Record signature: string message: string } @@ -61,12 +58,9 @@ export interface ServiceOffer { export interface Service { serviceId: string - uuid: string offers: ServiceOffer[] username: string serviceFqn: string - isPublic: boolean - metadata?: Record createdAt: number expiresAt: number } @@ -228,10 +222,10 @@ export class RondevuAPI { } /** - * Answer a service + * Answer a specific offer from a service */ - async answerService(serviceUuid: string, sdp: string): Promise<{ offerId: string }> { - const response = await fetch(`${this.baseUrl}/services/${serviceUuid}/answer`, { + async postOfferAnswer(serviceFqn: string, offerId: string, sdp: string): Promise<{ success: boolean; offerId: string }> { + const response = await fetch(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}/offers/${offerId}/answer`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -242,17 +236,17 @@ export class RondevuAPI { if (!response.ok) { const error = await response.json().catch(() => ({ error: 'Unknown error' })) - throw new Error(`Failed to answer service: ${error.error || response.statusText}`) + throw new Error(`Failed to answer offer: ${error.error || response.statusText}`) } return await response.json() } /** - * Get answer for a service (offerer polls this) + * Get answer for a specific offer (offerer polls this) */ - async getServiceAnswer(serviceUuid: string): Promise<{ sdp: string; offerId: string } | null> { - const response = await fetch(`${this.baseUrl}/services/${serviceUuid}/answer`, { + async getOfferAnswer(serviceFqn: string, offerId: string): Promise<{ sdp: string; offerId: string; answererId: string; answeredAt: number } | null> { + const response = await fetch(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}/offers/${offerId}/answer`, { headers: this.getAuthHeader(), }) @@ -265,8 +259,7 @@ export class RondevuAPI { throw new Error(`Failed to get answer: ${error.error || response.statusText}`) } - const data = await response.json() - return { sdp: data.sdp, offerId: data.offerId } + return await response.json() } /** @@ -290,16 +283,16 @@ export class RondevuAPI { // ============================================ /** - * Add ICE candidates to a service + * Add ICE candidates to a specific offer */ - async addServiceIceCandidates(serviceUuid: string, candidates: RTCIceCandidateInit[], offerId?: string): Promise<{ offerId: string }> { - const response = await fetch(`${this.baseUrl}/services/${serviceUuid}/ice-candidates`, { + async addOfferIceCandidates(serviceFqn: string, offerId: string, candidates: RTCIceCandidateInit[]): Promise<{ count: number; offerId: string }> { + const response = await fetch(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}/offers/${offerId}/ice-candidates`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.getAuthHeader(), }, - body: JSON.stringify({ candidates, offerId }), + body: JSON.stringify({ candidates }), }) if (!response.ok) { @@ -311,14 +304,11 @@ export class RondevuAPI { } /** - * Get ICE candidates for a service (with polling support) + * Get ICE candidates for a specific offer (with polling support) */ - async getServiceIceCandidates(serviceUuid: string, since: number = 0, offerId?: string): Promise<{ candidates: IceCandidate[]; offerId: string }> { - const url = new URL(`${this.baseUrl}/services/${serviceUuid}/ice-candidates`) + async getOfferIceCandidates(serviceFqn: string, offerId: string, since: number = 0): Promise<{ candidates: IceCandidate[]; offerId: string }> { + const url = new URL(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}/offers/${offerId}/ice-candidates`) url.searchParams.set('since', since.toString()) - if (offerId) { - url.searchParams.set('offerId', offerId) - } const response = await fetch(url.toString(), { headers: this.getAuthHeader() }) @@ -340,9 +330,10 @@ export class RondevuAPI { /** * Publish a service + * Service FQN must include username: service:version@username */ async publishService(service: ServiceRequest): Promise { - const response = await fetch(`${this.baseUrl}/users/${encodeURIComponent(service.username)}/services`, { + const response = await fetch(`${this.baseUrl}/services`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -360,10 +351,11 @@ export class RondevuAPI { } /** - * Get service by UUID + * Get service by FQN (with username) - Direct lookup + * Example: chat:1.0.0@alice */ - async getService(uuid: string): Promise { - const response = await fetch(`${this.baseUrl}/services/${uuid}`, { + async getService(serviceFqn: string): Promise<{ serviceId: string; username: string; serviceFqn: string; offerId: string; sdp: string; createdAt: number; expiresAt: number }> { + const response = await fetch(`${this.baseUrl}/services/${encodeURIComponent(serviceFqn)}`, { headers: this.getAuthHeader(), }) @@ -376,44 +368,44 @@ export class RondevuAPI { } /** - * Search services by username - lists all services for a username + * Discover a random available service without knowing the username + * Example: chat:1.0.0 (without @username) */ - async searchServicesByUsername(username: string): Promise { - const response = await fetch( - `${this.baseUrl}/users/${encodeURIComponent(username)}/services`, - { headers: this.getAuthHeader() } - ) + async discoverService(serviceVersion: string): Promise<{ serviceId: string; username: string; serviceFqn: string; offerId: string; sdp: string; createdAt: number; expiresAt: number }> { + const response = await fetch(`${this.baseUrl}/services/${encodeURIComponent(serviceVersion)}`, { + headers: this.getAuthHeader(), + }) if (!response.ok) { const error = await response.json().catch(() => ({ error: 'Unknown error' })) - throw new Error(`Failed to search services: ${error.error || response.statusText}`) + throw new Error(`Failed to discover service: ${error.error || response.statusText}`) } - const data = await response.json() - return data.services || [] + return await response.json() } /** - * Search services by username AND FQN - returns full service details + * Discover multiple available services with pagination + * Example: chat:1.0.0 (without @username) */ - async searchServices(username: string, serviceFqn: string): Promise { - const response = await fetch( - `${this.baseUrl}/users/${encodeURIComponent(username)}/services/${encodeURIComponent(serviceFqn)}`, - { headers: this.getAuthHeader() } - ) + async discoverServices(serviceVersion: string, limit: number = 10, offset: number = 0): Promise<{ services: Array<{ serviceId: string; username: string; serviceFqn: string; offerId: string; sdp: string; createdAt: number; expiresAt: number }>; count: number; limit: number; offset: number }> { + const url = new URL(`${this.baseUrl}/services/${encodeURIComponent(serviceVersion)}`) + url.searchParams.set('limit', limit.toString()) + url.searchParams.set('offset', offset.toString()) + + const response = await fetch(url.toString(), { + headers: this.getAuthHeader(), + }) if (!response.ok) { - if (response.status === 404) { - return [] - } const error = await response.json().catch(() => ({ error: 'Unknown error' })) - throw new Error(`Failed to search services: ${error.error || response.statusText}`) + throw new Error(`Failed to discover services: ${error.error || response.statusText}`) } - const service = await response.json() - return [service] + return await response.json() } + // ============================================ // Usernames // ============================================ @@ -421,7 +413,7 @@ export class RondevuAPI { /** * Check if username is available */ - async checkUsername(username: string): Promise<{ available: boolean; owner?: string }> { + async checkUsername(username: string): Promise<{ available: boolean; publicKey?: string; claimedAt?: number; expiresAt?: number }> { const response = await fetch( `${this.baseUrl}/users/${encodeURIComponent(username)}` ) diff --git a/src/bin.ts b/src/bin.ts deleted file mode 100644 index 9d02d65..0000000 --- a/src/bin.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Binnable - A cleanup function that can be synchronous or asynchronous - * - * Used to unsubscribe from events, close connections, or perform other cleanup operations. - */ -export type Binnable = () => void | Promise - -/** - * Create a cleanup function collector (garbage bin) - * - * Collects cleanup functions and provides a single `clean()` method to execute all of them. - * Useful for managing multiple cleanup operations in a single place. - * - * @returns A function that accepts cleanup functions and has a `clean()` method - * - * @example - * ```typescript - * const bin = createBin(); - * - * // Add cleanup functions - * bin( - * () => console.log('Cleanup 1'), - * () => connection.close(), - * () => clearInterval(timer) - * ); - * - * // Later, clean everything - * bin.clean(); // Executes all cleanup functions - * ``` - */ -export const createBin = () => { - const bin: Binnable[] = [] - return Object.assign((...rubbish: Binnable[]) => bin.push(...rubbish), { - /** - * Execute all cleanup functions and clear the bin - */ - clean: (): void => { - bin.forEach(binnable => binnable()) - bin.length = 0 - }, - }) -} diff --git a/src/durable-connection.ts b/src/durable-connection.ts deleted file mode 100644 index cfcb07b..0000000 --- a/src/durable-connection.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { - ConnectionEvents, - ConnectionInterface, - ConnectionStates, - isConnectionState, - Message, - QueueMessageOptions, - Signaler, -} from './types.js' -import { EventBus } from './event-bus.js' -import { createBin } from './bin.js' -import { WebRTCContext } from './webrtc-context' - -export type WebRTCRondevuConnectionOptions = { - offer?: RTCSessionDescriptionInit | null - context: WebRTCContext - signaler: Signaler -} - -/** - * WebRTCRondevuConnection - WebRTC peer connection wrapper with Rondevu signaling - * - * Manages a WebRTC peer connection lifecycle including: - * - Automatic offer/answer creation based on role - * - ICE candidate exchange via Rondevu signaling server - * - Connection state management with type-safe events - * - Data channel creation and message handling - * - * The connection automatically determines its role (offerer or answerer) based on whether - * an offer is provided in the constructor. The offerer creates the data channel, while - * the answerer receives it via the 'datachannel' event. - * - * @example - * ```typescript - * // Offerer side (creates offer) - * const connection = new WebRTCRondevuConnection( - * 'conn-123', - * 'peer-username', - * 'chat.service@1.0.0' - * ); - * - * await connection.ready; // Wait for local offer - * const sdp = connection.connection.localDescription!.sdp!; - * // Send sdp to signaling server... - * - * // Answerer side (receives offer) - * const connection = new WebRTCRondevuConnection( - * 'conn-123', - * 'peer-username', - * 'chat.service@1.0.0', - * { type: 'offer', sdp: remoteOfferSdp } - * ); - * - * await connection.ready; // Wait for local answer - * const answerSdp = connection.connection.localDescription!.sdp!; - * // Send answer to signaling server... - * - * // Both sides: Set up signaler and listen for state changes - * connection.setSignaler(signaler); - * connection.events.on('state-change', (state) => { - * console.log('Connection state:', state); - * }); - * ``` - */ -export class RTCDurableConnection implements ConnectionInterface { - private readonly side: 'offer' | 'answer' - public readonly expiresAt: number = 0 - public readonly lastActive: number = 0 - public readonly events: EventBus = new EventBus() - public readonly ready: Promise - private iceBin = createBin() - private context: WebRTCContext - private readonly signaler: Signaler - private _conn: RTCPeerConnection | null = null - private _state: ConnectionInterface['state'] = 'disconnected' - private _dataChannel: RTCDataChannel | null = null - private messageQueue: Array<{ - message: Message - options: QueueMessageOptions - timestamp: number - }> = [] - - constructor({ context, offer, signaler }: WebRTCRondevuConnectionOptions) { - this.context = context - this.signaler = signaler - this._conn = context.createPeerConnection() - this.side = offer ? 'answer' : 'offer' - - // setup data channel - if (offer) { - this._conn.addEventListener('datachannel', e => { - this._dataChannel = e.channel - this.setupDataChannelListeners(this._dataChannel) - }) - } else { - this._dataChannel = this._conn.createDataChannel('vu.ronde.protocol') - this.setupDataChannelListeners(this._dataChannel) - } - - // setup description exchange - this.ready = offer - ? this._conn - .setRemoteDescription(offer) - .then(() => this._conn?.createAnswer()) - .then(async answer => { - if (!answer || !this._conn) throw new Error('Connection disappeared') - await this._conn.setLocalDescription(answer) - return await signaler.setAnswer(answer) - }) - : this._conn.createOffer().then(async offer => { - if (!this._conn) throw new Error('Connection disappeared') - await this._conn.setLocalDescription(offer) - return await signaler.setOffer(offer) - }) - - // propagate connection state changes - this._conn.addEventListener('connectionstatechange', () => { - console.log(this.side, 'connection state changed: ', this._conn!.connectionState) - const state = isConnectionState(this._conn!.connectionState) - ? this._conn!.connectionState - : 'disconnected' - this.setState(state) - }) - - this._conn.addEventListener('iceconnectionstatechange', () => { - console.log(this.side, 'ice connection state changed: ', this._conn!.iceConnectionState) - }) - - // start ICE candidate exchange when gathering begins - this._conn.addEventListener('icegatheringstatechange', () => { - if (this._conn!.iceGatheringState === 'gathering') { - this.startIce() - } else if (this._conn!.iceGatheringState === 'complete') { - this.stopIce() - } - }) - } - - /** - * Getter method for retrieving the current connection. - * - * @return {RTCPeerConnection|null} The current connection instance. - */ - public get connection(): RTCPeerConnection | null { - return this._conn - } - - /** - * Update connection state and emit state-change event - */ - private setState(state: ConnectionInterface['state']) { - this._state = state - this.events.emit('state-change', state) - } - - /** - * Start ICE candidate exchange when gathering begins - */ - private startIce() { - const listener = ({ candidate }: { candidate: RTCIceCandidate | null }) => { - if (candidate) this.signaler.addIceCandidate(candidate) - } - if (!this._conn) throw new Error('Connection disappeared') - this._conn.addEventListener('icecandidate', listener) - this.iceBin( - this.signaler.addListener((candidate: RTCIceCandidate) => - this._conn?.addIceCandidate(candidate) - ), - () => this._conn?.removeEventListener('icecandidate', listener) - ) - } - - /** - * Stop ICE candidate exchange when gathering completes - */ - private stopIce() { - this.iceBin.clean() - } - - /** - * Disconnects the current connection and cleans up resources. - * Closes the active connection if it exists, resets the connection instance to null, - * stops the ICE process, and updates the state to 'disconnected'. - * - * @return {void} No return value. - */ - disconnect(): void { - this._conn?.close() - this._conn = null - this.stopIce() - this.setState('disconnected') - } - - /** - * Current connection state - */ - get state() { - return this._state - } - - /** - * Setup data channel event listeners - */ - private setupDataChannelListeners(channel: RTCDataChannel): void { - channel.addEventListener('message', e => { - this.events.emit('message', e.data) - }) - - channel.addEventListener('open', () => { - // Channel opened - flush queued messages - this.flushQueue().catch(err => { - console.error('Failed to flush message queue:', err) - }) - }) - - channel.addEventListener('error', err => { - console.error('Data channel error:', err) - }) - - channel.addEventListener('close', () => { - console.log('Data channel closed') - }) - } - - /** - * Flush the message queue - */ - private async flushQueue(): Promise { - while (this.messageQueue.length > 0 && this._state === 'connected') { - const item = this.messageQueue.shift()! - - // Check expiration - if (item.options.expiresAt && Date.now() > item.options.expiresAt) { - continue - } - - const success = await this.sendMessage(item.message) - if (!success) { - // Re-queue on failure - this.messageQueue.unshift(item) - break - } - } - } - - /** - * Queue a message for sending when connection is established - * - * @param message - Message to queue (string or ArrayBuffer) - * @param options - Queue options (e.g., expiration time) - */ - async queueMessage(message: Message, options: QueueMessageOptions = {}): Promise { - this.messageQueue.push({ - message, - options, - timestamp: Date.now() - }) - - // Try immediate send if connected - if (this._state === 'connected') { - await this.flushQueue() - } - } - - /** - * Send a message immediately - * - * @param message - Message to send (string or ArrayBuffer) - * @returns Promise resolving to true if sent successfully - */ - async sendMessage(message: Message): Promise { - if (this._state !== 'connected' || !this._dataChannel) { - return false - } - - if (this._dataChannel.readyState !== 'open') { - return false - } - - try { - // TypeScript has trouble with the union type, so we cast to any - // Both string and ArrayBuffer are valid for RTCDataChannel.send() - this._dataChannel.send(message as any) - return true - } catch (err) { - console.error('Send failed:', err) - return false - } - } -} diff --git a/src/event-bus.ts b/src/event-bus.ts deleted file mode 100644 index 410374d..0000000 --- a/src/event-bus.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Type-safe EventBus with event name to payload type mapping - */ - -type EventHandler = (data: T) => void - -/** - * EventBus - Type-safe event emitter with inferred event data types - * - * @example - * interface MyEvents { - * 'user:connected': { userId: string; timestamp: number }; - * 'user:disconnected': { userId: string }; - * 'message:received': string; - * } - * - * const bus = new EventBus(); - * - * // TypeScript knows data is { userId: string; timestamp: number } - * bus.on('user:connected', (data) => { - * console.log(data.userId, data.timestamp); - * }); - * - * // TypeScript knows data is string - * bus.on('message:received', (data) => { - * console.log(data.toUpperCase()); - * }); - */ -export class EventBus> { - private handlers: Map> - - constructor() { - this.handlers = new Map() - } - - /** - * Subscribe to an event - * Returns a cleanup function to unsubscribe - */ - on(event: K, handler: EventHandler): () => void { - if (!this.handlers.has(event)) { - this.handlers.set(event, new Set()) - } - this.handlers.get(event)!.add(handler) - - // Return cleanup function - return () => this.off(event, handler) - } - - /** - * Subscribe to an event once (auto-unsubscribe after first call) - */ - once(event: K, handler: EventHandler): void { - const wrappedHandler = (data: TEvents[K]) => { - handler(data) - this.off(event, wrappedHandler) - } - this.on(event, wrappedHandler) - } - - /** - * Unsubscribe from an event - */ - off(event: K, handler: EventHandler): void { - const eventHandlers = this.handlers.get(event) - if (eventHandlers) { - eventHandlers.delete(handler) - if (eventHandlers.size === 0) { - this.handlers.delete(event) - } - } - } - - /** - * Emit an event with data - */ - emit(event: K, data: TEvents[K]): void { - const eventHandlers = this.handlers.get(event) - if (eventHandlers) { - eventHandlers.forEach(handler => handler(data)) - } - } - - /** - * Remove all handlers for a specific event, or all handlers if no event specified - */ - clear(event?: K): void { - if (event !== undefined) { - this.handlers.delete(event) - } else { - this.handlers.clear() - } - } -} diff --git a/src/index.ts b/src/index.ts index d6b1240..b2d8005 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,23 +3,14 @@ * WebRTC peer signaling client */ -export { EventBus } from './event-bus.js' +export { Rondevu } from './rondevu.js' export { RondevuAPI } from './api.js' -export { RondevuService } from './rondevu-service.js' export { RondevuSignaler } from './rondevu-signaler.js' -export { WebRTCContext } from './webrtc-context.js' -export { RTCDurableConnection } from './durable-connection' -export { ServiceHost } from './service-host.js' -export { ServiceClient } from './service-client.js' -export { createBin } from './bin.js' // Export types export type { - ConnectionInterface, - QueueMessageOptions, - Message, - ConnectionEvents, Signaler, + Binnable, } from './types.js' export type { @@ -32,13 +23,7 @@ export type { IceCandidate, } from './api.js' -export type { Binnable } from './bin.js' - -export type { RondevuServiceOptions, PublishServiceOptions } from './rondevu-service.js' - -export type { ServiceHostOptions, ServiceHostEvents } from './service-host.js' - -export type { ServiceClientOptions, ServiceClientEvents } from './service-client.js' +export type { RondevuOptions, PublishServiceOptions } from './rondevu.js' export type { PollingConfig } from './rondevu-signaler.js' diff --git a/src/rondevu-service.ts b/src/rondevu-service.ts deleted file mode 100644 index b5ff264c..0000000 --- a/src/rondevu-service.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { RondevuAPI, Credentials, Keypair, Service, ServiceRequest } from './api.js' - -export interface RondevuServiceOptions { - apiUrl: string - username: string - keypair?: Keypair - credentials?: Credentials -} - -export interface PublishServiceOptions { - serviceFqn: string - offers: Array<{ sdp: string }> - ttl?: number - isPublic?: boolean - metadata?: Record -} - -/** - * RondevuService - High-level service management with automatic signature handling - * - * Provides a simplified API for: - * - Username claiming with Ed25519 signatures - * - Service publishing with automatic signature generation - * - Keypair management - * - * @example - * ```typescript - * // Initialize service (generates keypair automatically) - * const service = new RondevuService({ - * apiUrl: 'https://signal.example.com', - * username: 'myusername', - * }) - * - * await service.initialize() - * - * // Claim username (one time) - * await service.claimUsername() - * - * // Publish a service - * const publishedService = await service.publishService({ - * serviceFqn: 'chat.app@1.0.0', - * offers: [{ sdp: offerSdp }], - * ttl: 300000, - * isPublic: true, - * }) - * ``` - */ -export class RondevuService { - private readonly api: RondevuAPI - private readonly username: string - private keypair: Keypair | null = null - private usernameClaimed = false - - constructor(options: RondevuServiceOptions) { - this.username = options.username - this.keypair = options.keypair || null - this.api = new RondevuAPI(options.apiUrl, options.credentials) - } - - /** - * Initialize the service - generates keypair if not provided - * Call this before using other methods - */ - async initialize(): Promise { - if (!this.keypair) { - this.keypair = await RondevuAPI.generateKeypair() - } - - // Register with API if no credentials provided - if (!this.api['credentials']) { - const credentials = await this.api.register() - this.api.setCredentials(credentials) - } - } - - /** - * Claim the username with Ed25519 signature - * Should be called once before publishing services - */ - async claimUsername(): Promise { - if (!this.keypair) { - throw new Error('Service not initialized. Call initialize() first.') - } - - // Check if username is already claimed - const check = await this.api.checkUsername(this.username) - if (!check.available) { - // Verify it's claimed by us - if (check.owner === this.keypair.publicKey) { - this.usernameClaimed = true - return - } - throw new Error(`Username "${this.username}" is already claimed by another user`) - } - - // Generate signature for username claim - const message = `claim:${this.username}:${Date.now()}` - const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey) - - // Claim the username - await this.api.claimUsername(this.username, this.keypair.publicKey, signature, message) - this.usernameClaimed = true - } - - /** - * Publish a service with automatic signature generation - */ - async publishService(options: PublishServiceOptions): Promise { - if (!this.keypair) { - throw new Error('Service not initialized. Call initialize() first.') - } - - if (!this.usernameClaimed) { - throw new Error( - 'Username not claimed. Call claimUsername() first or the server will reject the service.' - ) - } - - const { serviceFqn, offers, ttl, isPublic, metadata } = options - - // Generate signature for service publication - const message = `publish:${this.username}:${serviceFqn}:${Date.now()}` - const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey) - - // Create service request - const serviceRequest: ServiceRequest = { - username: this.username, - serviceFqn, - offers, - signature, - message, - ttl, - isPublic, - metadata, - } - - // Publish to server - return await this.api.publishService(serviceRequest) - } - - /** - * Get the current keypair (for backup/storage) - */ - getKeypair(): Keypair | null { - return this.keypair - } - - /** - * Get the username - */ - getUsername(): string { - return this.username - } - - /** - * Get the public key - */ - getPublicKey(): string | null { - return this.keypair?.publicKey || null - } - - /** - * Check if username has been claimed - */ - isUsernameClaimed(): boolean { - return this.usernameClaimed - } - - /** - * Access to underlying API for advanced operations - */ - getAPI(): RondevuAPI { - return this.api - } -} diff --git a/src/rondevu-signaler.ts b/src/rondevu-signaler.ts index d5bdf0c..f6fdf80 100644 --- a/src/rondevu-signaler.ts +++ b/src/rondevu-signaler.ts @@ -1,6 +1,5 @@ -import { Signaler } from './types.js' -import { RondevuService } from './rondevu-service.js' -import { Binnable } from './bin.js' +import { Signaler, Binnable } from './types.js' +import { Rondevu } from './rondevu.js' export interface PollingConfig { initialInterval?: number // Default: 500ms @@ -43,7 +42,7 @@ export interface PollingConfig { */ export class RondevuSignaler implements Signaler { private offerId: string | null = null - private serviceUuid: string | null = null + private serviceFqn: string | null = null private offerListeners: Array<(offer: RTCSessionDescriptionInit) => void> = [] private answerListeners: Array<(answer: RTCSessionDescriptionInit) => void> = [] private iceListeners: Array<(candidate: RTCIceCandidate) => void> = [] @@ -54,7 +53,7 @@ export class RondevuSignaler implements Signaler { private pollingConfig: Required constructor( - private readonly rondevu: RondevuService, + private readonly rondevu: Rondevu, private readonly service: string, private readonly host?: string, pollingConfig?: PollingConfig @@ -82,7 +81,6 @@ export class RondevuSignaler implements Signaler { serviceFqn: this.service, offers: [{ sdp: offer.sdp }], ttl: 300000, // 5 minutes - isPublic: true, }) // Get the first offer from the published service @@ -91,7 +89,7 @@ export class RondevuSignaler implements Signaler { } this.offerId = publishedService.offers[0].offerId - this.serviceUuid = publishedService.uuid + this.serviceFqn = publishedService.serviceFqn // Start polling for answer this.startAnswerPolling() @@ -109,12 +107,12 @@ export class RondevuSignaler implements Signaler { throw new Error('Answer SDP is required') } - if (!this.serviceUuid) { - throw new Error('No service UUID available. Must receive offer first.') + if (!this.serviceFqn || !this.offerId) { + throw new Error('No service FQN or offer ID available. Must receive offer first.') } // Send answer to the service - const result = await this.rondevu.getAPI().answerService(this.serviceUuid, answer.sdp) + const result = await this.rondevu.getAPI().postOfferAnswer(this.serviceFqn, this.offerId, answer.sdp) this.offerId = result.offerId // Start polling for ICE candidates @@ -162,8 +160,8 @@ export class RondevuSignaler implements Signaler { * Send an ICE candidate to the remote peer */ async addIceCandidate(candidate: RTCIceCandidate): Promise { - if (!this.serviceUuid) { - console.warn('Cannot send ICE candidate: no service UUID') + if (!this.serviceFqn || !this.offerId) { + console.warn('Cannot send ICE candidate: no service FQN or offer ID') return } @@ -175,15 +173,11 @@ export class RondevuSignaler implements Signaler { } try { - const result = await this.rondevu.getAPI().addServiceIceCandidates( - this.serviceUuid, - [candidateData], - this.offerId || undefined + await this.rondevu.getAPI().addOfferIceCandidates( + this.serviceFqn, + this.offerId, + [candidateData] ) - // Store offerId if we didn't have it yet - if (!this.offerId) { - this.offerId = result.offerId - } } catch (err) { console.error('Failed to send ICE candidate:', err) } @@ -216,33 +210,24 @@ export class RondevuSignaler implements Signaler { this.isPolling = true try { - // Search for services by username and service FQN - const services = await this.rondevu.getAPI().searchServices(this.host, this.service) + // Get service by FQN (service should include @username) + const serviceFqn = `${this.service}@${this.host}` + const serviceData = await this.rondevu.getAPI().getService(serviceFqn) - if (services.length === 0) { - console.warn(`No services found for ${this.host}/${this.service}`) + if (!serviceData) { + console.warn(`No service found for ${serviceFqn}`) this.isPolling = false return } - // Get the first available service (already has full details from searchServices) - const service = services[0] as any - - // Get the first available offer from the service - if (!service.offers || service.offers.length === 0) { - console.warn(`No offers available for service ${this.host}/${this.service}`) - this.isPolling = false - return - } - - const firstOffer = service.offers[0] - this.offerId = firstOffer.offerId - this.serviceUuid = service.uuid + // Store service details + this.offerId = serviceData.offerId + this.serviceFqn = serviceData.serviceFqn // Notify offer listeners const offer: RTCSessionDescriptionInit = { type: 'offer', - sdp: firstOffer.sdp, + sdp: serviceData.sdp, } this.offerListeners.forEach(listener => { @@ -262,7 +247,7 @@ export class RondevuSignaler implements Signaler { * Start polling for answer (offerer side) with exponential backoff */ private startAnswerPolling(): void { - if (this.answerPollingTimeout || !this.serviceUuid) { + if (this.answerPollingTimeout || !this.serviceFqn || !this.offerId) { return } @@ -270,13 +255,13 @@ export class RondevuSignaler implements Signaler { let retries = 0 const poll = async () => { - if (!this.serviceUuid) { + if (!this.serviceFqn || !this.offerId) { this.stopAnswerPolling() return } try { - const answer = await this.rondevu.getAPI().getServiceAnswer(this.serviceUuid) + const answer = await this.rondevu.getAPI().getOfferAnswer(this.serviceFqn, this.offerId) if (answer && answer.sdp) { // Store offerId if we didn't have it yet @@ -354,14 +339,14 @@ export class RondevuSignaler implements Signaler { * Start polling for ICE candidates with adaptive backoff */ private startIcePolling(): void { - if (this.icePollingTimeout || !this.serviceUuid) { + if (this.icePollingTimeout || !this.serviceFqn || !this.offerId) { return } let interval = this.pollingConfig.initialInterval const poll = async () => { - if (!this.serviceUuid) { + if (!this.serviceFqn || !this.offerId) { this.stopIcePolling() return } @@ -369,12 +354,7 @@ export class RondevuSignaler implements Signaler { try { const result = await this.rondevu .getAPI() - .getServiceIceCandidates(this.serviceUuid, this.lastIceTimestamp, this.offerId || undefined) - - // Store offerId if we didn't have it yet - if (!this.offerId) { - this.offerId = result.offerId - } + .getOfferIceCandidates(this.serviceFqn, this.offerId, this.lastIceTimestamp) let foundCandidates = false diff --git a/src/rondevu.ts b/src/rondevu.ts new file mode 100644 index 0000000..b1d3ff6 --- /dev/null +++ b/src/rondevu.ts @@ -0,0 +1,335 @@ +import { RondevuAPI, Credentials, Keypair, Service, ServiceRequest, IceCandidate } from './api.js' + +export interface RondevuOptions { + apiUrl: string + username: string + keypair?: Keypair + credentials?: Credentials +} + +export interface PublishServiceOptions { + serviceFqn: string // Must include @username (e.g., "chat:1.0.0@alice") + offers: Array<{ sdp: string }> + ttl?: number +} + +/** + * Rondevu - Complete WebRTC signaling client + * + * Provides a unified API for: + * - Username claiming with Ed25519 signatures + * - Service publishing with automatic signature generation + * - Service discovery (direct, random, paginated) + * - WebRTC signaling (offer/answer exchange, ICE relay) + * - Keypair management + * + * @example + * ```typescript + * // Initialize (generates keypair automatically) + * const rondevu = new Rondevu({ + * apiUrl: 'https://signal.example.com', + * username: 'alice', + * }) + * + * await rondevu.initialize() + * + * // Claim username (one time) + * await rondevu.claimUsername() + * + * // Publish a service + * const publishedService = await rondevu.publishService({ + * serviceFqn: 'chat:1.0.0@alice', + * offers: [{ sdp: offerSdp }], + * ttl: 300000, + * }) + * + * // Discover a service + * const service = await rondevu.getService('chat:1.0.0@bob') + * + * // Post answer + * await rondevu.postOfferAnswer(service.serviceFqn, service.offerId, answerSdp) + * ``` + */ +export class Rondevu { + private readonly api: RondevuAPI + private readonly username: string + private keypair: Keypair | null = null + private usernameClaimed = false + + constructor(options: RondevuOptions) { + this.username = options.username + this.keypair = options.keypair || null + this.api = new RondevuAPI(options.apiUrl, options.credentials) + + console.log('[Rondevu] Constructor called:', { + username: this.username, + hasKeypair: !!this.keypair, + publicKey: this.keypair?.publicKey + }) + } + + // ============================================ + // Initialization + // ============================================ + + /** + * Initialize the service - generates keypair if not provided + * Call this before using other methods + */ + async initialize(): Promise { + console.log('[Rondevu] Initialize called, hasKeypair:', !!this.keypair) + + if (!this.keypair) { + console.log('[Rondevu] Generating new keypair...') + this.keypair = await RondevuAPI.generateKeypair() + console.log('[Rondevu] Generated keypair, publicKey:', this.keypair.publicKey) + } else { + console.log('[Rondevu] Using existing keypair, publicKey:', this.keypair.publicKey) + } + + // Register with API if no credentials provided + if (!this.api['credentials']) { + const credentials = await this.api.register() + this.api.setCredentials(credentials) + } + } + + // ============================================ + // Username Management + // ============================================ + + /** + * Claim the username with Ed25519 signature + * Should be called once before publishing services + */ + async claimUsername(): Promise { + if (!this.keypair) { + throw new Error('Not initialized. Call initialize() first.') + } + + // Check if username is already claimed + const check = await this.api.checkUsername(this.username) + if (!check.available) { + // Verify it's claimed by us + if (check.publicKey === this.keypair.publicKey) { + this.usernameClaimed = true + return + } + throw new Error(`Username "${this.username}" is already claimed by another user`) + } + + // Generate signature for username claim + const message = `claim:${this.username}:${Date.now()}` + const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey) + + // Claim the username + await this.api.claimUsername(this.username, this.keypair.publicKey, signature, message) + this.usernameClaimed = true + } + + /** + * Check if username has been claimed (checks with server) + */ + async isUsernameClaimed(): Promise { + if (!this.keypair) { + return false + } + + try { + const check = await this.api.checkUsername(this.username) + + // Debug logging + console.log('[Rondevu] Username check:', { + username: this.username, + available: check.available, + serverPublicKey: check.publicKey, + localPublicKey: this.keypair.publicKey, + match: check.publicKey === this.keypair.publicKey + }) + + // Username is claimed if it's not available and owned by our public key + const claimed = !check.available && check.publicKey === this.keypair.publicKey + + // Update internal flag to match server state + this.usernameClaimed = claimed + + return claimed + } catch (err) { + console.error('Failed to check username claim status:', err) + return false + } + } + + // ============================================ + // Service Publishing + // ============================================ + + /** + * Publish a service with automatic signature generation + */ + async publishService(options: PublishServiceOptions): Promise { + if (!this.keypair) { + throw new Error('Not initialized. Call initialize() first.') + } + + if (!this.usernameClaimed) { + throw new Error( + 'Username not claimed. Call claimUsername() first or the server will reject the service.' + ) + } + + const { serviceFqn, offers, ttl } = options + + // Generate signature for service publication + const message = `publish:${this.username}:${serviceFqn}:${Date.now()}` + const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey) + + // Create service request + const serviceRequest: ServiceRequest = { + serviceFqn, // Must include @username + offers, + signature, + message, + ttl, + } + + // Publish to server + return await this.api.publishService(serviceRequest) + } + + // ============================================ + // Service Discovery + // ============================================ + + /** + * Get service by FQN (with username) - Direct lookup + * Example: chat:1.0.0@alice + */ + async getService(serviceFqn: string): Promise<{ + serviceId: string + username: string + serviceFqn: string + offerId: string + sdp: string + createdAt: number + expiresAt: number + }> { + return await this.api.getService(serviceFqn) + } + + /** + * Discover a random available service without knowing the username + * Example: chat:1.0.0 (without @username) + */ + async discoverService(serviceVersion: string): Promise<{ + serviceId: string + username: string + serviceFqn: string + offerId: string + sdp: string + createdAt: number + expiresAt: number + }> { + return await this.api.discoverService(serviceVersion) + } + + /** + * Discover multiple available services with pagination + * Example: chat:1.0.0 (without @username) + */ + async discoverServices(serviceVersion: string, limit: number = 10, offset: number = 0): Promise<{ + services: Array<{ + serviceId: string + username: string + serviceFqn: string + offerId: string + sdp: string + createdAt: number + expiresAt: number + }> + count: number + limit: number + offset: number + }> { + return await this.api.discoverServices(serviceVersion, limit, offset) + } + + // ============================================ + // WebRTC Signaling + // ============================================ + + /** + * Post answer SDP to specific offer + */ + async postOfferAnswer(serviceFqn: string, offerId: string, sdp: string): Promise<{ + success: boolean + offerId: string + }> { + return await this.api.postOfferAnswer(serviceFqn, offerId, sdp) + } + + /** + * Get answer SDP (offerer polls this) + */ + async getOfferAnswer(serviceFqn: string, offerId: string): Promise<{ + sdp: string + offerId: string + answererId: string + answeredAt: number + } | null> { + return await this.api.getOfferAnswer(serviceFqn, offerId) + } + + /** + * Add ICE candidates to specific offer + */ + async addOfferIceCandidates(serviceFqn: string, offerId: string, candidates: RTCIceCandidateInit[]): Promise<{ + count: number + offerId: string + }> { + return await this.api.addOfferIceCandidates(serviceFqn, offerId, candidates) + } + + /** + * Get ICE candidates for specific offer (with polling support) + */ + async getOfferIceCandidates(serviceFqn: string, offerId: string, since: number = 0): Promise<{ + candidates: IceCandidate[] + offerId: string + }> { + return await this.api.getOfferIceCandidates(serviceFqn, offerId, since) + } + + // ============================================ + // Utility Methods + // ============================================ + + /** + * Get the current keypair (for backup/storage) + */ + getKeypair(): Keypair | null { + return this.keypair + } + + /** + * Get the username + */ + getUsername(): string { + return this.username + } + + /** + * Get the public key + */ + getPublicKey(): string | null { + return this.keypair?.publicKey || null + } + + /** + * Access to underlying API for advanced operations + * @deprecated Use direct methods on Rondevu instance instead + */ + getAPI(): RondevuAPI { + return this.api + } +} diff --git a/src/service-client.ts b/src/service-client.ts deleted file mode 100644 index 2c530b4..0000000 --- a/src/service-client.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { RondevuService } from './rondevu-service.js' -import { RondevuSignaler } from './rondevu-signaler.js' -import { WebRTCContext } from './webrtc-context.js' -import { RTCDurableConnection } from './durable-connection.js' -import { EventBus } from './event-bus.js' - -export interface ServiceClientOptions { - username: string // Host username - serviceFqn: string // e.g., 'chat.app@1.0.0' - rondevuService: RondevuService - autoReconnect?: boolean // Default: true - maxReconnectAttempts?: number // Default: 5 - rtcConfiguration?: RTCConfiguration -} - -export interface ServiceClientEvents { - connected: RTCDurableConnection - disconnected: void - reconnecting: { attempt: number; maxAttempts: number } - error: Error -} - -/** - * ServiceClient - High-level wrapper for connecting to a WebRTC service - * - * Simplifies client connection by handling: - * - Service discovery - * - Offer/answer exchange - * - ICE candidate polling - * - Automatic reconnection - * - * @example - * ```typescript - * const client = new ServiceClient({ - * username: 'host-user', - * serviceFqn: 'chat.app@1.0.0', - * rondevuService: myService - * }) - * - * client.events.on('connected', conn => { - * conn.events.on('message', msg => console.log('Received:', msg)) - * conn.sendMessage('Hello from client!') - * }) - * - * await client.connect() - * ``` - */ -export class ServiceClient { - events: EventBus - - private signaler: RondevuSignaler | null = null - private webrtcContext: WebRTCContext - private connection: RTCDurableConnection | null = null - private autoReconnect: boolean - private maxReconnectAttempts: number - private reconnectAttempts = 0 - private isConnecting = false - - constructor(private options: ServiceClientOptions) { - this.events = new EventBus() - this.webrtcContext = new WebRTCContext(options.rtcConfiguration) - this.autoReconnect = options.autoReconnect !== undefined ? options.autoReconnect : true - this.maxReconnectAttempts = options.maxReconnectAttempts || 5 - } - - /** - * Connect to the service - */ - async connect(): Promise { - if (this.isConnecting) { - throw new Error('Connection already in progress') - } - - if (this.connection) { - throw new Error('Already connected. Disconnect first.') - } - - this.isConnecting = true - - try { - // Create signaler - this.signaler = new RondevuSignaler( - this.options.rondevuService, - this.options.serviceFqn, - this.options.username - ) - - // Wait for remote offer from signaler - const remoteOffer = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Service discovery timeout')) - }, 30000) - - this.signaler!.addOfferListener((offer) => { - clearTimeout(timeout) - resolve(offer) - }) - }) - - // Create connection with remote offer (makes us the answerer) - const connection = new RTCDurableConnection({ - context: this.webrtcContext, - signaler: this.signaler, - offer: remoteOffer - }) - - // Wait for connection to be ready - await connection.ready - - // Set up connection event listeners - connection.events.on('state-change', (state) => { - if (state === 'connected') { - this.reconnectAttempts = 0 - this.events.emit('connected', connection) - } else if (state === 'disconnected') { - this.events.emit('disconnected', undefined) - if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) { - this.attemptReconnect() - } - } - }) - - this.connection = connection - this.isConnecting = false - - return connection - - } catch (err) { - this.isConnecting = false - const error = err instanceof Error ? err : new Error(String(err)) - this.events.emit('error', error) - throw error - } - } - - /** - * Disconnect from the service - */ - dispose(): void { - if (this.signaler) { - this.signaler.dispose() - this.signaler = null - } - - if (this.connection) { - this.connection.disconnect() - this.connection = null - } - - this.isConnecting = false - this.reconnectAttempts = 0 - } - - /** - * @deprecated Use dispose() instead - */ - disconnect(): void { - this.dispose() - } - - /** - * Attempt to reconnect - */ - private async attemptReconnect(): Promise { - this.reconnectAttempts++ - this.events.emit('reconnecting', { - attempt: this.reconnectAttempts, - maxAttempts: this.maxReconnectAttempts - }) - - // Cleanup old connection - if (this.signaler) { - this.signaler.dispose() - this.signaler = null - } - - if (this.connection) { - this.connection = null - } - - // Wait a bit before reconnecting - await new Promise(resolve => setTimeout(resolve, 1000 * this.reconnectAttempts)) - - try { - await this.connect() - } catch (err) { - console.error('Reconnection attempt failed:', err) - if (this.reconnectAttempts < this.maxReconnectAttempts) { - this.attemptReconnect() - } else { - const error = new Error('Max reconnection attempts reached') - this.events.emit('error', error) - } - } - } - - /** - * Get the current connection - */ - getConnection(): RTCDurableConnection | null { - return this.connection - } -} diff --git a/src/service-host.ts b/src/service-host.ts deleted file mode 100644 index f5c5f9b..0000000 --- a/src/service-host.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { RondevuService } from './rondevu-service.js' -import { RondevuSignaler } from './rondevu-signaler.js' -import { WebRTCContext } from './webrtc-context.js' -import { RTCDurableConnection } from './durable-connection.js' -import { EventBus } from './event-bus.js' - -export interface ServiceHostOptions { - service: string // e.g., 'chat.app@1.0.0' - rondevuService: RondevuService - maxPeers?: number // Default: 5 - ttl?: number // Default: 300000 (5 min) - isPublic?: boolean // Default: true - rtcConfiguration?: RTCConfiguration - metadata?: Record -} - -export interface ServiceHostEvents { - connection: RTCDurableConnection - error: Error -} - -/** - * ServiceHost - High-level wrapper for hosting a WebRTC service - * - * Simplifies hosting by handling: - * - Offer/answer exchange - * - ICE candidate polling - * - Connection pool management - * - Automatic reconnection - * - * @example - * ```typescript - * const host = new ServiceHost({ - * service: 'chat.app@1.0.0', - * rondevuService: myService, - * maxPeers: 5 - * }) - * - * host.events.on('connection', conn => { - * conn.events.on('message', msg => console.log('Received:', msg)) - * conn.sendMessage('Hello!') - * }) - * - * await host.start() - * ``` - */ -export class ServiceHost { - events: EventBus - - private signaler: RondevuSignaler | null = null - private webrtcContext: WebRTCContext - private connections: RTCDurableConnection[] = [] - private maxPeers: number - private running = false - - constructor(private options: ServiceHostOptions) { - this.events = new EventBus() - this.webrtcContext = new WebRTCContext(options.rtcConfiguration) - this.maxPeers = options.maxPeers || 5 - } - - /** - * Start hosting the service - */ - async start(): Promise { - if (this.running) { - throw new Error('ServiceHost already running') - } - - this.running = true - - // Create signaler - this.signaler = new RondevuSignaler( - this.options.rondevuService, - this.options.service - ) - - // Create first connection (offerer) - const connection = new RTCDurableConnection({ - context: this.webrtcContext, - signaler: this.signaler, - offer: null // null means we're the offerer - }) - - // Wait for connection to be ready - await connection.ready - - // Set up connection event listeners - connection.events.on('state-change', (state) => { - if (state === 'connected') { - this.connections.push(connection) - this.events.emit('connection', connection) - - // Create next connection if under maxPeers - if (this.connections.length < this.maxPeers) { - this.createNextConnection().catch(err => { - console.error('Failed to create next connection:', err) - this.events.emit('error', err) - }) - } - } else if (state === 'disconnected') { - // Remove from connections list - const index = this.connections.indexOf(connection) - if (index > -1) { - this.connections.splice(index, 1) - } - } - }) - - // Publish service with the offer - const offer = connection.connection?.localDescription - if (!offer?.sdp) { - throw new Error('Offer SDP is empty') - } - - await this.signaler.setOffer(offer) - } - - /** - * Create the next connection for incoming peers - */ - private async createNextConnection(): Promise { - if (!this.signaler || !this.running) { - return - } - - // For now, we'll use the same offer for all connections - // In a production scenario, you'd create multiple offers - // This is a limitation of the current service model - // which publishes one offer per service - } - - /** - * Stop hosting the service - */ - dispose(): void { - this.running = false - - // Cleanup signaler - if (this.signaler) { - this.signaler.dispose() - this.signaler = null - } - - // Disconnect all connections - for (const conn of this.connections) { - conn.disconnect() - } - this.connections = [] - } - - /** - * Get all active connections - */ - getConnections(): RTCDurableConnection[] { - return [...this.connections] - } -} diff --git a/src/types.ts b/src/types.ts index c9038f6..8abace1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,39 +1,15 @@ /** - * Core connection types + * Core signaling types */ -import { EventBus } from './event-bus.js' -import { Binnable } from './bin.js' -export type Message = string | ArrayBuffer - -export interface QueueMessageOptions { - expiresAt?: number -} - -export interface ConnectionEvents { - 'state-change': ConnectionInterface['state'] - message: Message -} - -export const ConnectionStates = [ - 'connected', - 'disconnected', - 'connecting' -] as const - -export const isConnectionState = (state: string): state is (typeof ConnectionStates)[number] => - ConnectionStates.includes(state as any) - -export interface ConnectionInterface { - state: (typeof ConnectionStates)[number] - lastActive: number - expiresAt?: number - events: EventBus - - queueMessage(message: Message, options?: QueueMessageOptions): Promise - sendMessage(message: Message): Promise -} +/** + * Cleanup function returned by listener methods + */ +export type Binnable = () => void +/** + * Signaler interface for WebRTC offer/answer/ICE exchange + */ export interface Signaler { addIceCandidate(candidate: RTCIceCandidate): Promise addListener(callback: (candidate: RTCIceCandidate) => void): Binnable diff --git a/src/webrtc-context.ts b/src/webrtc-context.ts deleted file mode 100644 index b24a6ba..0000000 --- a/src/webrtc-context.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Signaler } from './types' - -const DEFAULT_RTC_CONFIGURATION: RTCConfiguration = { - iceServers: [ - { - urls: 'stun:stun.relay.metered.ca:80', - }, - { - urls: 'turn:standard.relay.metered.ca:80', - username: 'c53a9c971da5e6f3bc959d8d', - credential: 'QaccPqtPPaxyokXp', - }, - { - urls: 'turn:standard.relay.metered.ca:80?transport=tcp', - username: 'c53a9c971da5e6f3bc959d8d', - credential: 'QaccPqtPPaxyokXp', - }, - { - urls: 'turn:standard.relay.metered.ca:443', - username: 'c53a9c971da5e6f3bc959d8d', - credential: 'QaccPqtPPaxyokXp', - }, - { - urls: 'turns:standard.relay.metered.ca:443?transport=tcp', - username: 'c53a9c971da5e6f3bc959d8d', - credential: 'QaccPqtPPaxyokXp', - }, - ], -} - -export class WebRTCContext { - constructor( - private readonly config?: RTCConfiguration - ) {} - - createPeerConnection(): RTCPeerConnection { - return new RTCPeerConnection(this.config || DEFAULT_RTC_CONFIGURATION) - } -}