@xtr-dev/rondevu-client 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/offers.js ADDED
@@ -0,0 +1,214 @@
1
+ import { RondevuAuth } from './auth.js';
2
+ export class RondevuOffers {
3
+ constructor(baseUrl, credentials, fetchFn) {
4
+ this.baseUrl = baseUrl;
5
+ this.credentials = credentials;
6
+ // Use provided fetch or fall back to global fetch
7
+ this.fetchFn = fetchFn || ((...args) => {
8
+ if (typeof globalThis.fetch === 'function') {
9
+ return globalThis.fetch(...args);
10
+ }
11
+ throw new Error('fetch is not available. Please provide a fetch implementation in the constructor options.');
12
+ });
13
+ }
14
+ /**
15
+ * Create one or more offers
16
+ */
17
+ async create(offers) {
18
+ const response = await this.fetchFn(`${this.baseUrl}/offers`, {
19
+ method: 'POST',
20
+ headers: {
21
+ 'Content-Type': 'application/json',
22
+ Authorization: RondevuAuth.createAuthHeader(this.credentials),
23
+ },
24
+ body: JSON.stringify({ offers }),
25
+ });
26
+ if (!response.ok) {
27
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
28
+ throw new Error(`Failed to create offers: ${error.error || response.statusText}`);
29
+ }
30
+ const data = await response.json();
31
+ return data.offers;
32
+ }
33
+ /**
34
+ * Find offers by topic with optional bloom filter
35
+ */
36
+ async findByTopic(topic, options) {
37
+ const params = new URLSearchParams();
38
+ if (options?.bloomFilter) {
39
+ // Convert to base64
40
+ const binaryString = String.fromCharCode(...Array.from(options.bloomFilter));
41
+ const base64 = typeof btoa !== 'undefined'
42
+ ? btoa(binaryString)
43
+ : (typeof Buffer !== 'undefined' ? Buffer.from(options.bloomFilter).toString('base64') : '');
44
+ params.set('bloom', base64);
45
+ }
46
+ if (options?.limit) {
47
+ params.set('limit', options.limit.toString());
48
+ }
49
+ const url = `${this.baseUrl}/offers/by-topic/${encodeURIComponent(topic)}${params.toString() ? '?' + params.toString() : ''}`;
50
+ const response = await this.fetchFn(url, {
51
+ method: 'GET',
52
+ });
53
+ if (!response.ok) {
54
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
55
+ throw new Error(`Failed to find offers: ${error.error || response.statusText}`);
56
+ }
57
+ const data = await response.json();
58
+ return data.offers;
59
+ }
60
+ /**
61
+ * Get all offers from a specific peer
62
+ */
63
+ async getByPeerId(peerId) {
64
+ const response = await this.fetchFn(`${this.baseUrl}/peers/${encodeURIComponent(peerId)}/offers`, {
65
+ method: 'GET',
66
+ });
67
+ if (!response.ok) {
68
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
69
+ throw new Error(`Failed to get peer offers: ${error.error || response.statusText}`);
70
+ }
71
+ return await response.json();
72
+ }
73
+ /**
74
+ * Get topics with active peer counts (paginated)
75
+ */
76
+ async getTopics(options) {
77
+ const params = new URLSearchParams();
78
+ if (options?.limit) {
79
+ params.set('limit', options.limit.toString());
80
+ }
81
+ if (options?.offset) {
82
+ params.set('offset', options.offset.toString());
83
+ }
84
+ const url = `${this.baseUrl}/topics${params.toString() ? '?' + params.toString() : ''}`;
85
+ const response = await this.fetchFn(url, {
86
+ method: 'GET',
87
+ });
88
+ if (!response.ok) {
89
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
90
+ throw new Error(`Failed to get topics: ${error.error || response.statusText}`);
91
+ }
92
+ return await response.json();
93
+ }
94
+ /**
95
+ * Get own offers
96
+ */
97
+ async getMine() {
98
+ const response = await this.fetchFn(`${this.baseUrl}/offers/mine`, {
99
+ method: 'GET',
100
+ headers: {
101
+ Authorization: RondevuAuth.createAuthHeader(this.credentials),
102
+ },
103
+ });
104
+ if (!response.ok) {
105
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
106
+ throw new Error(`Failed to get own offers: ${error.error || response.statusText}`);
107
+ }
108
+ const data = await response.json();
109
+ return data.offers;
110
+ }
111
+ /**
112
+ * Update offer heartbeat
113
+ */
114
+ async heartbeat(offerId) {
115
+ const response = await this.fetchFn(`${this.baseUrl}/offers/${encodeURIComponent(offerId)}/heartbeat`, {
116
+ method: 'PUT',
117
+ headers: {
118
+ Authorization: RondevuAuth.createAuthHeader(this.credentials),
119
+ },
120
+ });
121
+ if (!response.ok) {
122
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
123
+ throw new Error(`Failed to update heartbeat: ${error.error || response.statusText}`);
124
+ }
125
+ }
126
+ /**
127
+ * Delete an offer
128
+ */
129
+ async delete(offerId) {
130
+ const response = await this.fetchFn(`${this.baseUrl}/offers/${encodeURIComponent(offerId)}`, {
131
+ method: 'DELETE',
132
+ headers: {
133
+ Authorization: RondevuAuth.createAuthHeader(this.credentials),
134
+ },
135
+ });
136
+ if (!response.ok) {
137
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
138
+ throw new Error(`Failed to delete offer: ${error.error || response.statusText}`);
139
+ }
140
+ }
141
+ /**
142
+ * Answer an offer
143
+ */
144
+ async answer(offerId, sdp) {
145
+ const response = await this.fetchFn(`${this.baseUrl}/offers/${encodeURIComponent(offerId)}/answer`, {
146
+ method: 'POST',
147
+ headers: {
148
+ 'Content-Type': 'application/json',
149
+ Authorization: RondevuAuth.createAuthHeader(this.credentials),
150
+ },
151
+ body: JSON.stringify({ sdp }),
152
+ });
153
+ if (!response.ok) {
154
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
155
+ throw new Error(`Failed to answer offer: ${error.error || response.statusText}`);
156
+ }
157
+ }
158
+ /**
159
+ * Get answers to your offers
160
+ */
161
+ async getAnswers() {
162
+ const response = await this.fetchFn(`${this.baseUrl}/offers/answers`, {
163
+ method: 'GET',
164
+ headers: {
165
+ Authorization: RondevuAuth.createAuthHeader(this.credentials),
166
+ },
167
+ });
168
+ if (!response.ok) {
169
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
170
+ throw new Error(`Failed to get answers: ${error.error || response.statusText}`);
171
+ }
172
+ const data = await response.json();
173
+ return data.answers;
174
+ }
175
+ /**
176
+ * Post ICE candidates for an offer
177
+ */
178
+ async addIceCandidates(offerId, candidates) {
179
+ const response = await this.fetchFn(`${this.baseUrl}/offers/${encodeURIComponent(offerId)}/ice-candidates`, {
180
+ method: 'POST',
181
+ headers: {
182
+ 'Content-Type': 'application/json',
183
+ Authorization: RondevuAuth.createAuthHeader(this.credentials),
184
+ },
185
+ body: JSON.stringify({ candidates }),
186
+ });
187
+ if (!response.ok) {
188
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
189
+ throw new Error(`Failed to add ICE candidates: ${error.error || response.statusText}`);
190
+ }
191
+ }
192
+ /**
193
+ * Get ICE candidates for an offer
194
+ */
195
+ async getIceCandidates(offerId, since) {
196
+ const params = new URLSearchParams();
197
+ if (since !== undefined) {
198
+ params.set('since', since.toString());
199
+ }
200
+ const url = `${this.baseUrl}/offers/${encodeURIComponent(offerId)}/ice-candidates${params.toString() ? '?' + params.toString() : ''}`;
201
+ const response = await this.fetchFn(url, {
202
+ method: 'GET',
203
+ headers: {
204
+ Authorization: RondevuAuth.createAuthHeader(this.credentials),
205
+ },
206
+ });
207
+ if (!response.ok) {
208
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
209
+ throw new Error(`Failed to get ICE candidates: ${error.error || response.statusText}`);
210
+ }
211
+ const data = await response.json();
212
+ return data.candidates;
213
+ }
214
+ }
package/dist/rondevu.d.ts CHANGED
@@ -1,60 +1,57 @@
1
- import { RondevuAPI } from './client.js';
1
+ import { RondevuAuth, Credentials, FetchFunction } from './auth.js';
2
+ import { RondevuOffers } from './offers.js';
2
3
  import { RondevuConnection } from './connection.js';
3
- import { RondevuOptions } from './types.js';
4
- /**
5
- * Main Rondevu WebRTC client with automatic connection management
6
- */
7
- export declare class Rondevu {
8
- readonly peerId: string;
9
- readonly api: RondevuAPI;
10
- private baseUrl;
11
- private fetchImpl?;
12
- private rtcConfig?;
13
- private pollingInterval;
14
- private connectionTimeout;
15
- private wrtc?;
16
- private RTCPeerConnection;
17
- private RTCIceCandidate;
18
- /**
19
- * Creates a new Rondevu client instance
20
- * @param options - Client configuration options
21
- */
22
- constructor(options?: RondevuOptions);
4
+ export interface RondevuOptions {
23
5
  /**
24
- * Check server version compatibility
6
+ * Base URL of the Rondevu server
7
+ * @default 'https://api.ronde.vu'
25
8
  */
26
- private checkServerVersion;
9
+ baseUrl?: string;
27
10
  /**
28
- * Check if client and server versions are compatible
29
- * For now, just check major version compatibility
11
+ * Existing credentials (peerId + secret) to skip registration
30
12
  */
31
- private isVersionCompatible;
13
+ credentials?: Credentials;
32
14
  /**
33
- * Generate a unique peer ID
15
+ * Custom fetch implementation for environments without native fetch
16
+ * (Node.js < 18, some Workers environments, etc.)
17
+ *
18
+ * @example Node.js
19
+ * ```typescript
20
+ * import fetch from 'node-fetch';
21
+ * const client = new Rondevu({ fetch });
22
+ * ```
34
23
  */
35
- private generatePeerId;
24
+ fetch?: FetchFunction;
25
+ }
26
+ export declare class Rondevu {
27
+ readonly auth: RondevuAuth;
28
+ private _offers?;
29
+ private credentials?;
30
+ private baseUrl;
31
+ private fetchFn?;
32
+ constructor(options?: RondevuOptions);
36
33
  /**
37
- * Update the peer ID (useful when user identity changes)
34
+ * Get offers API (requires authentication)
38
35
  */
39
- updatePeerId(newPeerId: string): void;
36
+ get offers(): RondevuOffers;
40
37
  /**
41
- * Create an offer (offerer role)
42
- * @param id - Offer identifier (custom code)
43
- * @returns Promise that resolves to RondevuConnection
38
+ * Register and initialize authenticated client
44
39
  */
45
- offer(id: string): Promise<RondevuConnection>;
40
+ register(): Promise<Credentials>;
46
41
  /**
47
- * Answer an existing offer by ID (answerer role)
48
- * @param id - Offer code
49
- * @returns Promise that resolves to RondevuConnection
42
+ * Check if client is authenticated
50
43
  */
51
- answer(id: string): Promise<RondevuConnection>;
44
+ isAuthenticated(): boolean;
52
45
  /**
53
- * Wait for ICE gathering to complete
46
+ * Get current credentials
54
47
  */
55
- private waitForIceGathering;
48
+ getCredentials(): Credentials | undefined;
56
49
  /**
57
- * Find an offer by code
50
+ * Create a new WebRTC connection (requires authentication)
51
+ * This is a high-level helper that creates and manages WebRTC connections
52
+ *
53
+ * @param rtcConfig Optional RTCConfiguration for the peer connection
54
+ * @returns RondevuConnection instance
58
55
  */
59
- private findOfferById;
56
+ createConnection(rtcConfig?: RTCConfiguration): RondevuConnection;
60
57
  }
package/dist/rondevu.js CHANGED
@@ -1,199 +1,57 @@
1
- import { RondevuAPI } from './client.js';
1
+ import { RondevuAuth } from './auth.js';
2
+ import { RondevuOffers } from './offers.js';
2
3
  import { RondevuConnection } from './connection.js';
3
- /**
4
- * Main Rondevu WebRTC client with automatic connection management
5
- */
6
4
  export class Rondevu {
7
- /**
8
- * Creates a new Rondevu client instance
9
- * @param options - Client configuration options
10
- */
11
5
  constructor(options = {}) {
12
6
  this.baseUrl = options.baseUrl || 'https://api.ronde.vu';
13
- this.fetchImpl = options.fetch;
14
- this.wrtc = options.wrtc;
15
- this.api = new RondevuAPI({
16
- baseUrl: this.baseUrl,
17
- fetch: options.fetch,
18
- });
19
- // Auto-generate peer ID if not provided
20
- this.peerId = options.peerId || this.generatePeerId();
21
- this.rtcConfig = options.rtcConfig;
22
- this.pollingInterval = options.pollingInterval || 1000;
23
- this.connectionTimeout = options.connectionTimeout || 30000;
24
- // Use injected WebRTC polyfill or fall back to global
25
- this.RTCPeerConnection = options.wrtc?.RTCPeerConnection || globalThis.RTCPeerConnection;
26
- this.RTCIceCandidate = options.wrtc?.RTCIceCandidate || globalThis.RTCIceCandidate;
27
- if (!this.RTCPeerConnection) {
28
- throw new Error('RTCPeerConnection not available. ' +
29
- 'In Node.js, provide a WebRTC polyfill via the wrtc option. ' +
30
- 'Install: npm install @roamhq/wrtc or npm install wrtc');
7
+ this.fetchFn = options.fetch;
8
+ this.auth = new RondevuAuth(this.baseUrl, this.fetchFn);
9
+ if (options.credentials) {
10
+ this.credentials = options.credentials;
11
+ this._offers = new RondevuOffers(this.baseUrl, this.credentials, this.fetchFn);
31
12
  }
32
- // Check server version compatibility (async, don't block constructor)
33
- this.checkServerVersion().catch(() => {
34
- // Silently fail version check - connection will work even if version check fails
35
- });
36
13
  }
37
14
  /**
38
- * Check server version compatibility
15
+ * Get offers API (requires authentication)
39
16
  */
40
- async checkServerVersion() {
41
- try {
42
- const { version: serverVersion } = await this.api.health();
43
- const clientVersion = '0.3.5'; // Should match package.json
44
- if (!this.isVersionCompatible(clientVersion, serverVersion)) {
45
- console.warn(`[Rondevu] Version mismatch: client v${clientVersion}, server v${serverVersion}. ` +
46
- 'This may cause compatibility issues.');
47
- }
17
+ get offers() {
18
+ if (!this._offers) {
19
+ throw new Error('Not authenticated. Call register() first or provide credentials.');
48
20
  }
49
- catch (error) {
50
- // Version check failed - server might not support /health endpoint
51
- console.debug('[Rondevu] Could not check server version');
52
- }
53
- }
54
- /**
55
- * Check if client and server versions are compatible
56
- * For now, just check major version compatibility
57
- */
58
- isVersionCompatible(clientVersion, serverVersion) {
59
- const clientMajor = parseInt(clientVersion.split('.')[0]);
60
- const serverMajor = parseInt(serverVersion.split('.')[0]);
61
- // Major versions must match
62
- return clientMajor === serverMajor;
21
+ return this._offers;
63
22
  }
64
23
  /**
65
- * Generate a unique peer ID
24
+ * Register and initialize authenticated client
66
25
  */
67
- generatePeerId() {
68
- return `rdv_${Math.random().toString(36).substring(2, 14)}`;
26
+ async register() {
27
+ this.credentials = await this.auth.register();
28
+ // Create offers API instance
29
+ this._offers = new RondevuOffers(this.baseUrl, this.credentials, this.fetchFn);
30
+ return this.credentials;
69
31
  }
70
32
  /**
71
- * Update the peer ID (useful when user identity changes)
33
+ * Check if client is authenticated
72
34
  */
73
- updatePeerId(newPeerId) {
74
- this.peerId = newPeerId;
35
+ isAuthenticated() {
36
+ return !!this.credentials;
75
37
  }
76
38
  /**
77
- * Create an offer (offerer role)
78
- * @param id - Offer identifier (custom code)
79
- * @returns Promise that resolves to RondevuConnection
39
+ * Get current credentials
80
40
  */
81
- async offer(id) {
82
- // Create peer connection
83
- const pc = new this.RTCPeerConnection(this.rtcConfig);
84
- // Create initial data channel for negotiation (required for offer creation)
85
- pc.createDataChannel('_negotiation');
86
- // Generate offer
87
- const offer = await pc.createOffer();
88
- await pc.setLocalDescription(offer);
89
- // Wait for ICE gathering to complete
90
- await this.waitForIceGathering(pc);
91
- // Create offer on server with custom code
92
- await this.api.createOffer({
93
- peerId: this.peerId,
94
- offer: pc.localDescription.sdp,
95
- code: id,
96
- });
97
- // Create connection object
98
- const connectionParams = {
99
- id,
100
- role: 'offerer',
101
- pc,
102
- localPeerId: this.peerId,
103
- remotePeerId: '', // Will be populated when answer is received
104
- pollingInterval: this.pollingInterval,
105
- connectionTimeout: this.connectionTimeout,
106
- wrtc: this.wrtc,
107
- };
108
- const connection = new RondevuConnection(connectionParams, this.api);
109
- // Start polling for answer
110
- connection.startPolling();
111
- return connection;
41
+ getCredentials() {
42
+ return this.credentials;
112
43
  }
113
44
  /**
114
- * Answer an existing offer by ID (answerer role)
115
- * @param id - Offer code
116
- * @returns Promise that resolves to RondevuConnection
45
+ * Create a new WebRTC connection (requires authentication)
46
+ * This is a high-level helper that creates and manages WebRTC connections
47
+ *
48
+ * @param rtcConfig Optional RTCConfiguration for the peer connection
49
+ * @returns RondevuConnection instance
117
50
  */
118
- async answer(id) {
119
- // Poll server to get offer by ID
120
- const offerData = await this.findOfferById(id);
121
- if (!offerData) {
122
- throw new Error(`Offer ${id} not found or expired`);
123
- }
124
- // Create peer connection
125
- const pc = new this.RTCPeerConnection(this.rtcConfig);
126
- // Set remote offer
127
- await pc.setRemoteDescription({
128
- type: 'offer',
129
- sdp: offerData.offer,
130
- });
131
- // Generate answer
132
- const answer = await pc.createAnswer();
133
- await pc.setLocalDescription(answer);
134
- // Wait for ICE gathering
135
- await this.waitForIceGathering(pc);
136
- // Send answer to server
137
- await this.api.sendAnswer({
138
- code: id,
139
- answer: pc.localDescription.sdp,
140
- side: 'answerer',
141
- });
142
- // Create connection object
143
- const connectionParams = {
144
- id,
145
- role: 'answerer',
146
- pc,
147
- localPeerId: this.peerId,
148
- remotePeerId: '', // Will be determined from peerId in offer
149
- pollingInterval: this.pollingInterval,
150
- connectionTimeout: this.connectionTimeout,
151
- wrtc: this.wrtc,
152
- };
153
- const connection = new RondevuConnection(connectionParams, this.api);
154
- // Start polling for ICE candidates
155
- connection.startPolling();
156
- return connection;
157
- }
158
- /**
159
- * Wait for ICE gathering to complete
160
- */
161
- async waitForIceGathering(pc) {
162
- if (pc.iceGatheringState === 'complete') {
163
- return;
164
- }
165
- return new Promise((resolve) => {
166
- const checkState = () => {
167
- if (pc.iceGatheringState === 'complete') {
168
- pc.removeEventListener('icegatheringstatechange', checkState);
169
- resolve();
170
- }
171
- };
172
- pc.addEventListener('icegatheringstatechange', checkState);
173
- // Also set a timeout in case gathering takes too long
174
- setTimeout(() => {
175
- pc.removeEventListener('icegatheringstatechange', checkState);
176
- resolve();
177
- }, 5000);
178
- });
179
- }
180
- /**
181
- * Find an offer by code
182
- */
183
- async findOfferById(id) {
184
- try {
185
- // Poll for the offer directly
186
- const response = await this.api.poll(id, 'answerer');
187
- const answererResponse = response;
188
- if (answererResponse.offer) {
189
- return {
190
- offer: answererResponse.offer,
191
- };
192
- }
193
- return null;
194
- }
195
- catch (err) {
196
- throw new Error(`Failed to find offer ${id}: ${err.message}`);
51
+ createConnection(rtcConfig) {
52
+ if (!this._offers) {
53
+ throw new Error('Not authenticated. Call register() first or provide credentials.');
197
54
  }
55
+ return new RondevuConnection(this._offers, rtcConfig);
198
56
  }
199
57
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-client",
3
- "version": "0.3.5",
4
- "description": "TypeScript client for Rondevu peer signaling and discovery server",
3
+ "version": "0.4.0",
4
+ "description": "TypeScript client for Rondevu topic-based peer discovery and signaling server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",