Replace origin override with global option

- Remove origin parameter from connect() method
- Add ConnectOptions interface with global flag
- When global: true, sends X-Rondevu-Global header instead of trying to override Origin
- Update client methods to accept customHeaders parameter
- Pass custom headers through connection polling and ICE candidate exchange
- Bump version to 0.1.0

This change works around browser restriction on Origin header modification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-07 23:45:52 +01:00
parent de6244cf24
commit 9df9966381
5 changed files with 38 additions and 26 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@xtr-dev/rondevu-client", "name": "@xtr-dev/rondevu-client",
"version": "0.0.6", "version": "0.1.0",
"description": "TypeScript client for Rondevu peer signaling and discovery server", "description": "TypeScript client for Rondevu peer signaling and discovery server",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -37,13 +37,15 @@ export class RondevuClient {
*/ */
private async request<T>( private async request<T>(
endpoint: string, endpoint: string,
options: RequestInit = {} options: RequestInit = {},
customHeaders?: Record<string, string>
): Promise<T> { ): Promise<T> {
const url = `${this.baseUrl}${endpoint}`; const url = `${this.baseUrl}${endpoint}`;
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Origin': this.origin, 'Origin': this.origin,
...(options.headers as Record<string, string>), ...(options.headers as Record<string, string>),
...(customHeaders || {}),
}; };
if (options.body) { if (options.body) {
@@ -142,6 +144,7 @@ export class RondevuClient {
* Sends an answer or candidate to an existing session * Sends an answer or candidate to an existing session
* *
* @param request - Answer details including session code and signaling data * @param request - Answer details including session code and signaling data
* @param customHeaders - Optional custom headers to send with the request
* @returns Success confirmation * @returns Success confirmation
* *
* @example * @example
@@ -163,11 +166,11 @@ export class RondevuClient {
* }); * });
* ``` * ```
*/ */
async sendAnswer(request: AnswerRequest): Promise<AnswerResponse> { async sendAnswer(request: AnswerRequest, customHeaders?: Record<string, string>): Promise<AnswerResponse> {
return this.request<AnswerResponse>('/answer', { return this.request<AnswerResponse>('/answer', {
method: 'POST', method: 'POST',
body: JSON.stringify(request), body: JSON.stringify(request),
}); }, customHeaders);
} }
/** /**
@@ -175,6 +178,7 @@ export class RondevuClient {
* *
* @param code - Session UUID * @param code - Session UUID
* @param side - Which side is polling ('offerer' or 'answerer') * @param side - Which side is polling ('offerer' or 'answerer')
* @param customHeaders - Optional custom headers to send with the request
* @returns Session data including offers, answers, and candidates * @returns Session data including offers, answers, and candidates
* *
* @example * @example
@@ -194,13 +198,14 @@ export class RondevuClient {
*/ */
async poll( async poll(
code: string, code: string,
side: Side side: Side,
customHeaders?: Record<string, string>
): Promise<PollOffererResponse | PollAnswererResponse> { ): Promise<PollOffererResponse | PollAnswererResponse> {
const request: PollRequest = { code, side }; const request: PollRequest = { code, side };
return this.request<PollOffererResponse | PollAnswererResponse>('/poll', { return this.request<PollOffererResponse | PollAnswererResponse>('/poll', {
method: 'POST', method: 'POST',
body: JSON.stringify(request), body: JSON.stringify(request),
}); }, customHeaders);
} }
/** /**

View File

@@ -21,8 +21,9 @@ export class RondevuConnection extends EventEmitter {
private connectionTimer?: ReturnType<typeof setTimeout>; private connectionTimer?: ReturnType<typeof setTimeout>;
private isPolling: boolean = false; private isPolling: boolean = false;
private isClosed: boolean = false; private isClosed: boolean = false;
private customHeaders?: Record<string, string>;
constructor(params: RondevuConnectionParams, client: RondevuClient) { constructor(params: RondevuConnectionParams, client: RondevuClient, customHeaders?: Record<string, string>) {
super(); super();
this.id = params.id; this.id = params.id;
this.topic = params.topic; this.topic = params.topic;
@@ -34,6 +35,7 @@ export class RondevuConnection extends EventEmitter {
this.dataChannels = new Map(); this.dataChannels = new Map();
this.pollingIntervalMs = params.pollingInterval; this.pollingIntervalMs = params.pollingInterval;
this.connectionTimeoutMs = params.connectionTimeout; this.connectionTimeoutMs = params.connectionTimeout;
this.customHeaders = customHeaders;
this.setupEventHandlers(); this.setupEventHandlers();
this.startConnectionTimeout(); this.startConnectionTimeout();
@@ -119,7 +121,7 @@ export class RondevuConnection extends EventEmitter {
code: this.id, code: this.id,
candidate: JSON.stringify(candidate.toJSON()), candidate: JSON.stringify(candidate.toJSON()),
side: this.role, side: this.role,
}); }, this.customHeaders);
} catch (err: any) { } catch (err: any) {
throw new Error(`Failed to send ICE candidate: ${err.message}`); throw new Error(`Failed to send ICE candidate: ${err.message}`);
} }
@@ -169,7 +171,7 @@ export class RondevuConnection extends EventEmitter {
} }
try { try {
const response = await this.client.poll(this.id, this.role); const response = await this.client.poll(this.id, this.role, this.customHeaders);
if (this.role === 'offerer') { if (this.role === 'offerer') {
const offererResponse = response as { answer: string | null; answerCandidates: string[] }; const offererResponse = response as { answer: string | null; answerCandidates: string[] };

View File

@@ -1,6 +1,6 @@
import { RondevuClient } from './client'; import { RondevuClient } from './client';
import { RondevuConnection } from './connection'; import { RondevuConnection } from './connection';
import { RondevuOptions, JoinOptions, RondevuConnectionParams } from './types'; import { RondevuOptions, JoinOptions, ConnectOptions, RondevuConnectionParams } from './types';
/** /**
* Main Rondevu WebRTC client with automatic connection management * Main Rondevu WebRTC client with automatic connection management
@@ -100,21 +100,17 @@ export class Rondevu {
/** /**
* Connect to an existing connection by ID (answerer role) * Connect to an existing connection by ID (answerer role)
* @param id - Connection identifier * @param id - Connection identifier
* @param origin - Optional origin header override for this connection * @param options - Optional connection options (e.g., { global: true } for global origin)
* @returns Promise that resolves to RondevuConnection * @returns Promise that resolves to RondevuConnection
*/ */
async connect(id: string, origin?: string): Promise<RondevuConnection> { async connect(id: string, options?: ConnectOptions): Promise<RondevuConnection> {
// Create a client with overridden origin if specified // Build custom headers if global option is set
const client = origin const customHeaders = options?.global
? new RondevuClient({ ? { 'X-Rondevu-Global': 'true' }
baseUrl: this.baseUrl, : undefined;
origin,
fetch: this.fetchImpl,
})
: this.client;
// Poll server to get session by ID // Poll server to get session by ID
const sessionData = await this.findSessionByIdWithClient(id, client); const sessionData = await this.findSessionByIdWithClient(id, this.client, customHeaders);
if (!sessionData) { if (!sessionData) {
throw new Error(`Connection ${id} not found or expired`); throw new Error(`Connection ${id} not found or expired`);
@@ -137,11 +133,11 @@ export class Rondevu {
await this.waitForIceGathering(pc); await this.waitForIceGathering(pc);
// Send answer to server // Send answer to server
await client.sendAnswer({ await this.client.sendAnswer({
code: id, code: id,
answer: pc.localDescription!.sdp, answer: pc.localDescription!.sdp,
side: 'answerer', side: 'answerer',
}); }, customHeaders);
// Create connection object // Create connection object
const connectionParams: RondevuConnectionParams = { const connectionParams: RondevuConnectionParams = {
@@ -155,7 +151,7 @@ export class Rondevu {
connectionTimeout: this.connectionTimeout, connectionTimeout: this.connectionTimeout,
}; };
const connection = new RondevuConnection(connectionParams, client); const connection = new RondevuConnection(connectionParams, this.client, customHeaders);
// Start polling for ICE candidates // Start polling for ICE candidates
connection.startPolling(); connection.startPolling();
@@ -254,7 +250,8 @@ export class Rondevu {
*/ */
private async findSessionByIdWithClient( private async findSessionByIdWithClient(
id: string, id: string,
client: RondevuClient client: RondevuClient,
customHeaders?: Record<string, string>
): Promise<{ ): Promise<{
code: string; code: string;
peerId: string; peerId: string;
@@ -264,7 +261,7 @@ export class Rondevu {
try { try {
// Try to poll for the session directly // Try to poll for the session directly
// The poll endpoint should return the session data // The poll endpoint should return the session data
const response = await client.poll(id, 'answerer'); const response = await client.poll(id, 'answerer', customHeaders);
const answererResponse = response as { offer: string; offerCandidates: string[] }; const answererResponse = response as { offer: string; offerCandidates: string[] };
if (answererResponse.offer) { if (answererResponse.offer) {

View File

@@ -191,6 +191,14 @@ export interface RondevuOptions {
connectionTimeout?: number; connectionTimeout?: number;
} }
/**
* Options for connecting to a session
*/
export interface ConnectOptions {
/** Use global origin (https://ronde.vu) instead of request origin for session isolation */
global?: boolean;
}
/** /**
* Options for joining a topic * Options for joining a topic
*/ */