@xtr-dev/rondevu-client 0.18.10 → 0.21.1
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/README.md +92 -117
- package/dist/api/batcher.d.ts +83 -0
- package/dist/api/batcher.js +155 -0
- package/dist/api/client.d.ts +198 -0
- package/dist/api/client.js +400 -0
- package/dist/{answerer-connection.d.ts → connections/answerer.d.ts} +25 -8
- package/dist/{answerer-connection.js → connections/answerer.js} +70 -48
- package/dist/{connection.d.ts → connections/base.d.ts} +30 -7
- package/dist/{connection.js → connections/base.js} +65 -14
- package/dist/connections/config.d.ts +51 -0
- package/dist/{connection-config.js → connections/config.js} +20 -0
- package/dist/{connection-events.d.ts → connections/events.d.ts} +6 -6
- package/dist/connections/offerer.d.ts +108 -0
- package/dist/connections/offerer.js +306 -0
- package/dist/core/ice-config.d.ts +35 -0
- package/dist/core/ice-config.js +111 -0
- package/dist/core/index.d.ts +22 -0
- package/dist/core/index.js +22 -0
- package/dist/core/offer-pool.d.ts +113 -0
- package/dist/core/offer-pool.js +281 -0
- package/dist/core/peer.d.ts +155 -0
- package/dist/core/peer.js +252 -0
- package/dist/core/polling-manager.d.ts +71 -0
- package/dist/core/polling-manager.js +122 -0
- package/dist/core/rondevu-errors.d.ts +59 -0
- package/dist/core/rondevu-errors.js +75 -0
- package/dist/core/rondevu-types.d.ts +125 -0
- package/dist/core/rondevu-types.js +6 -0
- package/dist/core/rondevu.d.ts +296 -0
- package/dist/core/rondevu.js +472 -0
- package/dist/crypto/adapter.d.ts +53 -0
- package/dist/crypto/node.d.ts +57 -0
- package/dist/crypto/node.js +149 -0
- package/dist/crypto/web.d.ts +38 -0
- package/dist/crypto/web.js +129 -0
- package/dist/utils/async-lock.d.ts +42 -0
- package/dist/utils/async-lock.js +75 -0
- package/dist/{message-buffer.d.ts → utils/message-buffer.d.ts} +1 -1
- package/dist/{message-buffer.js → utils/message-buffer.js} +4 -4
- package/dist/webrtc/adapter.d.ts +22 -0
- package/dist/webrtc/adapter.js +5 -0
- package/dist/webrtc/browser.d.ts +12 -0
- package/dist/webrtc/browser.js +15 -0
- package/dist/webrtc/node.d.ts +32 -0
- package/dist/webrtc/node.js +32 -0
- package/package.json +20 -9
- package/dist/api.d.ts +0 -146
- package/dist/api.js +0 -279
- package/dist/connection-config.d.ts +0 -21
- package/dist/crypto-adapter.d.ts +0 -37
- package/dist/index.d.ts +0 -13
- package/dist/index.js +0 -10
- package/dist/node-crypto-adapter.d.ts +0 -35
- package/dist/node-crypto-adapter.js +0 -78
- package/dist/offerer-connection.d.ts +0 -54
- package/dist/offerer-connection.js +0 -177
- package/dist/rondevu-signaler.d.ts +0 -112
- package/dist/rondevu-signaler.js +0 -401
- package/dist/rondevu.d.ts +0 -407
- package/dist/rondevu.js +0 -847
- package/dist/rpc-batcher.d.ts +0 -61
- package/dist/rpc-batcher.js +0 -111
- package/dist/web-crypto-adapter.d.ts +0 -16
- package/dist/web-crypto-adapter.js +0 -52
- /package/dist/{connection-events.js → connections/events.js} +0 -0
- /package/dist/{types.d.ts → core/types.d.ts} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
- /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
- /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
- /package/dist/{exponential-backoff.js → utils/exponential-backoff.js} +0 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { RondevuAPI } from '../api/client.js';
|
|
2
|
+
import { BrowserWebRTCAdapter } from '../webrtc/browser.js';
|
|
3
|
+
import { EventEmitter } from 'eventemitter3';
|
|
4
|
+
import { OfferPool } from './offer-pool.js';
|
|
5
|
+
import { Peer } from './peer.js';
|
|
6
|
+
import { getIceConfiguration } from './ice-config.js';
|
|
7
|
+
import { PollingManager } from './polling-manager.js';
|
|
8
|
+
// Re-export ICE config for backward compatibility
|
|
9
|
+
export { ICE_SERVER_PRESETS } from './ice-config.js';
|
|
10
|
+
/**
|
|
11
|
+
* Rondevu - Complete WebRTC signaling client with durable connections
|
|
12
|
+
*
|
|
13
|
+
* Uses a tags-based discovery system where offers have 1+ tags for matching.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // Create and initialize Rondevu instance with preset ICE servers
|
|
18
|
+
* const rondevu = await Rondevu.connect({
|
|
19
|
+
* apiUrl: 'https://signal.example.com',
|
|
20
|
+
* iceServers: 'ipv4-turn' // Use preset: 'ipv4-turn', 'hostname-turns', 'google-stun', or 'relay-only'
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* // Create offers with tags for discovery
|
|
24
|
+
* await rondevu.offer({
|
|
25
|
+
* tags: ['chat', 'video'],
|
|
26
|
+
* maxOffers: 5 // Maintain up to 5 concurrent offers
|
|
27
|
+
* })
|
|
28
|
+
*
|
|
29
|
+
* // Start accepting connections (auto-fills offers and polls)
|
|
30
|
+
* await rondevu.startFilling()
|
|
31
|
+
*
|
|
32
|
+
* // Listen for connections
|
|
33
|
+
* rondevu.on('connection:opened', (offerId, connection) => {
|
|
34
|
+
* connection.on('connected', () => console.log('Connected!'))
|
|
35
|
+
* connection.on('message', (data) => console.log('Received:', data))
|
|
36
|
+
* connection.send('Hello!')
|
|
37
|
+
* })
|
|
38
|
+
*
|
|
39
|
+
* // Connect by discovering offers with matching tags
|
|
40
|
+
* const connection = await rondevu.connect({
|
|
41
|
+
* tags: ['chat']
|
|
42
|
+
* })
|
|
43
|
+
*
|
|
44
|
+
* connection.on('connected', () => {
|
|
45
|
+
* console.log('Connected!')
|
|
46
|
+
* connection.send('Hello!')
|
|
47
|
+
* })
|
|
48
|
+
*
|
|
49
|
+
* connection.on('message', (data) => {
|
|
50
|
+
* console.log('Received:', data)
|
|
51
|
+
* })
|
|
52
|
+
*
|
|
53
|
+
* connection.on('reconnecting', (attempt) => {
|
|
54
|
+
* console.log(`Reconnecting, attempt ${attempt}`)
|
|
55
|
+
* })
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export class Rondevu extends EventEmitter {
|
|
59
|
+
constructor(apiUrl, credential, api, iceServers, iceTransportPolicy, webrtcAdapter, cryptoAdapter, debugEnabled = false) {
|
|
60
|
+
super();
|
|
61
|
+
// Publishing state
|
|
62
|
+
this.currentTags = null;
|
|
63
|
+
this.offerPool = null;
|
|
64
|
+
this.apiUrl = apiUrl;
|
|
65
|
+
this.credential = credential;
|
|
66
|
+
this.api = api;
|
|
67
|
+
this.iceServers = iceServers;
|
|
68
|
+
this.iceTransportPolicy = iceTransportPolicy;
|
|
69
|
+
this.webrtcAdapter = webrtcAdapter;
|
|
70
|
+
this.cryptoAdapter = cryptoAdapter;
|
|
71
|
+
this.debugEnabled = debugEnabled;
|
|
72
|
+
// Initialize centralized polling manager
|
|
73
|
+
this.pollingManager = new PollingManager({
|
|
74
|
+
api: this.api,
|
|
75
|
+
debugEnabled: this.debugEnabled,
|
|
76
|
+
});
|
|
77
|
+
// Forward polling events to Rondevu instance
|
|
78
|
+
this.pollingManager.on('poll:answer', data => {
|
|
79
|
+
this.emit('poll:answer', data);
|
|
80
|
+
});
|
|
81
|
+
this.pollingManager.on('poll:ice', data => {
|
|
82
|
+
this.emit('poll:ice', data);
|
|
83
|
+
});
|
|
84
|
+
this.debug('Instance created:', {
|
|
85
|
+
name: this.credential.name,
|
|
86
|
+
hasIceServers: iceServers.length > 0,
|
|
87
|
+
iceTransportPolicy: iceTransportPolicy || 'all',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Internal debug logging - only logs if debug mode is enabled
|
|
92
|
+
*/
|
|
93
|
+
debug(message, ...args) {
|
|
94
|
+
if (this.debugEnabled) {
|
|
95
|
+
console.log(`[Rondevu] ${message}`, ...args);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Create and initialize a Rondevu client
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* const rondevu = await Rondevu.connect({}) // Uses default API URL
|
|
104
|
+
* // or
|
|
105
|
+
* const rondevu = await Rondevu.connect({
|
|
106
|
+
* apiUrl: 'https://custom.api.com'
|
|
107
|
+
* })
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
static async connect(options = {}) {
|
|
111
|
+
const apiUrl = options.apiUrl || Rondevu.DEFAULT_API_URL;
|
|
112
|
+
// Use provided WebRTC adapter or default to browser adapter
|
|
113
|
+
const webrtcAdapter = options.webrtcAdapter || new BrowserWebRTCAdapter();
|
|
114
|
+
// Handle preset string or custom array, extracting iceTransportPolicy if present
|
|
115
|
+
const iceConfig = getIceConfiguration(options.iceServers);
|
|
116
|
+
if (options.debug) {
|
|
117
|
+
console.log('[Rondevu] Connecting:', {
|
|
118
|
+
apiUrl,
|
|
119
|
+
hasCredential: !!options.credential,
|
|
120
|
+
iceServers: iceConfig.iceServers?.length ?? 0,
|
|
121
|
+
iceTransportPolicy: iceConfig.iceTransportPolicy || 'all',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// Generate credential if not provided
|
|
125
|
+
let credential = options.credential;
|
|
126
|
+
if (!credential) {
|
|
127
|
+
if (options.debug)
|
|
128
|
+
console.log('[Rondevu] Generating new credentials...');
|
|
129
|
+
credential = await RondevuAPI.generateCredentials(apiUrl, {
|
|
130
|
+
name: options.username, // Will claim this username if provided
|
|
131
|
+
});
|
|
132
|
+
if (options.debug)
|
|
133
|
+
console.log('[Rondevu] Generated credentials, name:', credential.name);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
if (options.debug)
|
|
137
|
+
console.log('[Rondevu] Using existing credential, name:', credential.name);
|
|
138
|
+
}
|
|
139
|
+
// Create API instance
|
|
140
|
+
const api = new RondevuAPI(apiUrl, credential, options.cryptoAdapter);
|
|
141
|
+
if (options.debug)
|
|
142
|
+
console.log('[Rondevu] Created API instance');
|
|
143
|
+
return new Rondevu(apiUrl, credential, api, iceConfig.iceServers || [], iceConfig.iceTransportPolicy, webrtcAdapter, options.cryptoAdapter, options.debug || false);
|
|
144
|
+
}
|
|
145
|
+
// ============================================
|
|
146
|
+
// Credential Access
|
|
147
|
+
// ============================================
|
|
148
|
+
/**
|
|
149
|
+
* Get the current credential name
|
|
150
|
+
*/
|
|
151
|
+
getName() {
|
|
152
|
+
return this.credential.name;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get the full credential (name + secret)
|
|
156
|
+
* Use this to persist credentials for future sessions
|
|
157
|
+
*
|
|
158
|
+
* ⚠️ SECURITY WARNING:
|
|
159
|
+
* - The secret grants full access to this identity
|
|
160
|
+
* - Store credentials securely (encrypted storage, never in logs)
|
|
161
|
+
* - Never expose credentials in URLs, console output, or error messages
|
|
162
|
+
* - Treat the secret like a password or API key
|
|
163
|
+
*/
|
|
164
|
+
getCredential() {
|
|
165
|
+
return { ...this.credential };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get the WebRTC adapter for creating peer connections
|
|
169
|
+
* Used internally by offer pool and connections
|
|
170
|
+
*/
|
|
171
|
+
getWebRTCAdapter() {
|
|
172
|
+
return this.webrtcAdapter;
|
|
173
|
+
}
|
|
174
|
+
// ============================================
|
|
175
|
+
// Service Publishing
|
|
176
|
+
// ============================================
|
|
177
|
+
/**
|
|
178
|
+
* Default offer factory - creates a simple data channel connection
|
|
179
|
+
* The RTCPeerConnection is created by Rondevu and passed in
|
|
180
|
+
*/
|
|
181
|
+
async defaultOfferFactory(pc) {
|
|
182
|
+
const dc = pc.createDataChannel('default');
|
|
183
|
+
const offer = await pc.createOffer();
|
|
184
|
+
await pc.setLocalDescription(offer);
|
|
185
|
+
return { dc, offer };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Create offers with tags for discovery (offerer/host side)
|
|
189
|
+
* Auto-starts filling by default. Use the returned object to cancel.
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* // Auto-start (default)
|
|
194
|
+
* const offer = await rondevu.offer({
|
|
195
|
+
* tags: ['chat', 'video'],
|
|
196
|
+
* maxOffers: 5
|
|
197
|
+
* })
|
|
198
|
+
* // Later: offer.cancel() to stop
|
|
199
|
+
*
|
|
200
|
+
* // Manual start
|
|
201
|
+
* await rondevu.offer({ tags: ['chat'], maxOffers: 5, autoStart: false })
|
|
202
|
+
* await rondevu.startFilling()
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
async offer(options) {
|
|
206
|
+
const { tags, maxOffers, offerFactory, ttl, connectionConfig, autoStart = true } = options;
|
|
207
|
+
this.currentTags = tags;
|
|
208
|
+
this.connectionConfig = connectionConfig;
|
|
209
|
+
this.debug(`Creating offers with tags: ${tags.join(', ')} with maxOffers: ${maxOffers}`);
|
|
210
|
+
// Create OfferPool
|
|
211
|
+
this.offerPool = new OfferPool({
|
|
212
|
+
api: this.api,
|
|
213
|
+
tags,
|
|
214
|
+
ownerUsername: this.credential.name,
|
|
215
|
+
maxOffers,
|
|
216
|
+
offerFactory: offerFactory || this.defaultOfferFactory.bind(this),
|
|
217
|
+
ttl: ttl || Rondevu.DEFAULT_TTL_MS,
|
|
218
|
+
iceServers: this.iceServers,
|
|
219
|
+
iceTransportPolicy: this.iceTransportPolicy,
|
|
220
|
+
webrtcAdapter: this.webrtcAdapter,
|
|
221
|
+
connectionConfig,
|
|
222
|
+
debugEnabled: this.debugEnabled,
|
|
223
|
+
});
|
|
224
|
+
// Forward events from OfferPool
|
|
225
|
+
this.offerPool.on('connection:opened', (offerId, connection) => {
|
|
226
|
+
this.emit('connection:opened', offerId, connection);
|
|
227
|
+
});
|
|
228
|
+
this.offerPool.on('offer:created', (offerId, tags) => {
|
|
229
|
+
this.emit('offer:created', offerId, tags);
|
|
230
|
+
});
|
|
231
|
+
this.offerPool.on('connection:rotated', (oldOfferId, newOfferId, connection) => {
|
|
232
|
+
this.emit('connection:rotated', oldOfferId, newOfferId, connection);
|
|
233
|
+
});
|
|
234
|
+
// Subscribe to polling events and forward to OfferPool
|
|
235
|
+
this.on('poll:answer', data => {
|
|
236
|
+
this.offerPool?.handlePollAnswer(data);
|
|
237
|
+
});
|
|
238
|
+
this.on('poll:ice', data => {
|
|
239
|
+
this.offerPool?.handlePollIce(data);
|
|
240
|
+
});
|
|
241
|
+
// Auto-start if enabled (default)
|
|
242
|
+
if (autoStart) {
|
|
243
|
+
await this.startFilling();
|
|
244
|
+
}
|
|
245
|
+
// Return handle for cancellation
|
|
246
|
+
return {
|
|
247
|
+
cancel: () => this.stopFilling(),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Start filling offers and polling for answers/ICE
|
|
252
|
+
* Call this after offer() to begin accepting connections
|
|
253
|
+
*/
|
|
254
|
+
async startFilling() {
|
|
255
|
+
if (!this.offerPool) {
|
|
256
|
+
throw new Error('No offers created. Call offer() first.');
|
|
257
|
+
}
|
|
258
|
+
this.debug('Starting offer filling and polling');
|
|
259
|
+
// Start the centralized polling manager
|
|
260
|
+
this.pollingManager.start();
|
|
261
|
+
await this.offerPool.start();
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Stop filling offers and polling
|
|
265
|
+
* Closes all active peer connections
|
|
266
|
+
*/
|
|
267
|
+
stopFilling() {
|
|
268
|
+
this.debug('Stopping offer filling and polling');
|
|
269
|
+
// Stop the centralized polling manager
|
|
270
|
+
this.pollingManager.stop();
|
|
271
|
+
this.offerPool?.stop();
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Start the centralized polling manager
|
|
275
|
+
* Use this when you need polling without offers (e.g., answerer connections)
|
|
276
|
+
*/
|
|
277
|
+
startPolling() {
|
|
278
|
+
this.debug('Starting polling manager');
|
|
279
|
+
this.pollingManager.start();
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Stop the centralized polling manager
|
|
283
|
+
*/
|
|
284
|
+
stopPolling() {
|
|
285
|
+
this.debug('Stopping polling manager');
|
|
286
|
+
this.pollingManager.stop();
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Check if polling is active
|
|
290
|
+
*/
|
|
291
|
+
isPolling() {
|
|
292
|
+
return this.pollingManager.isRunning();
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Get the count of active offers
|
|
296
|
+
* @returns Number of active offers
|
|
297
|
+
*/
|
|
298
|
+
getOfferCount() {
|
|
299
|
+
return this.offerPool?.getOfferCount() ?? 0;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Check if an offer is currently connected
|
|
303
|
+
* @param offerId - The offer ID to check
|
|
304
|
+
* @returns True if the offer exists and is connected
|
|
305
|
+
*/
|
|
306
|
+
isConnected(offerId) {
|
|
307
|
+
return this.offerPool?.isConnected(offerId) ?? false;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Disconnect all active offers
|
|
311
|
+
* Similar to stopFilling() but doesn't stop the polling/filling process
|
|
312
|
+
*/
|
|
313
|
+
disconnectAll() {
|
|
314
|
+
this.debug('Disconnecting all offers');
|
|
315
|
+
this.offerPool?.disconnectAll();
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Get the current publishing status
|
|
319
|
+
* @returns Object with publishing state information
|
|
320
|
+
*/
|
|
321
|
+
getPublishStatus() {
|
|
322
|
+
return {
|
|
323
|
+
active: this.currentTags !== null,
|
|
324
|
+
offerCount: this.offerPool?.getOfferCount() ?? 0,
|
|
325
|
+
tags: this.currentTags,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Create a peer connection with simplified DX
|
|
330
|
+
* Returns a Peer object with clean state management and events
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```typescript
|
|
334
|
+
* // Connect to any peer matching tags
|
|
335
|
+
* const peer = await rondevu.peer({ tags: ['chat'] })
|
|
336
|
+
*
|
|
337
|
+
* // Connect to specific user
|
|
338
|
+
* const peer = await rondevu.peer({
|
|
339
|
+
* username: 'alice',
|
|
340
|
+
* tags: ['chat']
|
|
341
|
+
* })
|
|
342
|
+
*
|
|
343
|
+
* peer.on('open', () => {
|
|
344
|
+
* console.log('Connected to', peer.peerUsername)
|
|
345
|
+
* peer.send('Hello!')
|
|
346
|
+
* })
|
|
347
|
+
*
|
|
348
|
+
* peer.on('message', (data) => {
|
|
349
|
+
* console.log('Received:', data)
|
|
350
|
+
* })
|
|
351
|
+
*
|
|
352
|
+
* peer.on('state', (state, prevState) => {
|
|
353
|
+
* console.log(`State: ${prevState} → ${state}`)
|
|
354
|
+
* })
|
|
355
|
+
*
|
|
356
|
+
* // Access underlying RTCPeerConnection
|
|
357
|
+
* if (peer.peerConnection) {
|
|
358
|
+
* console.log('ICE state:', peer.peerConnection.iceConnectionState)
|
|
359
|
+
* }
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
362
|
+
async peer(options) {
|
|
363
|
+
const peer = new Peer({
|
|
364
|
+
...options,
|
|
365
|
+
api: this.api,
|
|
366
|
+
iceServers: this.iceServers,
|
|
367
|
+
iceTransportPolicy: this.iceTransportPolicy,
|
|
368
|
+
debug: this.debugEnabled,
|
|
369
|
+
});
|
|
370
|
+
await peer.initialize();
|
|
371
|
+
// Subscribe to poll:ice events for this peer's connection
|
|
372
|
+
const peerOfferId = peer.offerId;
|
|
373
|
+
const peerConnection = peer.getConnection();
|
|
374
|
+
if (peerConnection) {
|
|
375
|
+
const pollIceHandler = (data) => {
|
|
376
|
+
if (data.offerId === peerOfferId) {
|
|
377
|
+
peerConnection.handleRemoteIceCandidates(data.candidates);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
this.on('poll:ice', pollIceHandler);
|
|
381
|
+
// Clean up handler when connection closes
|
|
382
|
+
peerConnection.on('closed', () => {
|
|
383
|
+
this.off('poll:ice', pollIceHandler);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
// Start polling if not already running
|
|
387
|
+
if (!this.pollingManager.isRunning()) {
|
|
388
|
+
this.debug('Starting polling for peer connection');
|
|
389
|
+
this.pollingManager.start();
|
|
390
|
+
}
|
|
391
|
+
return peer;
|
|
392
|
+
}
|
|
393
|
+
// ============================================
|
|
394
|
+
// Discovery
|
|
395
|
+
// ============================================
|
|
396
|
+
/**
|
|
397
|
+
* Discover offers by tags
|
|
398
|
+
*
|
|
399
|
+
* @param tags - Tags to search for (OR logic - matches any tag)
|
|
400
|
+
* @param options - Discovery options (pagination)
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* ```typescript
|
|
404
|
+
* // Discover offers matching any of the tags
|
|
405
|
+
* const result = await rondevu.discover(['chat', 'video'])
|
|
406
|
+
*
|
|
407
|
+
* // Paginated discovery
|
|
408
|
+
* const result = await rondevu.discover(['chat'], {
|
|
409
|
+
* limit: 20,
|
|
410
|
+
* offset: 0
|
|
411
|
+
* })
|
|
412
|
+
*
|
|
413
|
+
* // Access offers
|
|
414
|
+
* for (const offer of result.offers) {
|
|
415
|
+
* console.log(offer.username, offer.tags)
|
|
416
|
+
* }
|
|
417
|
+
* ```
|
|
418
|
+
*/
|
|
419
|
+
async discover(tags, options) {
|
|
420
|
+
const { limit = 10, offset = 0 } = options || {};
|
|
421
|
+
// Always pass limit to ensure we get DiscoverResponse (paginated mode)
|
|
422
|
+
return (await this.api.discover({ tags, limit, offset }));
|
|
423
|
+
}
|
|
424
|
+
// ============================================
|
|
425
|
+
// WebRTC Signaling
|
|
426
|
+
// ============================================
|
|
427
|
+
/**
|
|
428
|
+
* Post answer SDP to specific offer
|
|
429
|
+
*/
|
|
430
|
+
async postOfferAnswer(offerId, sdp) {
|
|
431
|
+
await this.api.answerOffer(offerId, sdp);
|
|
432
|
+
return { success: true, offerId };
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Get answer SDP (offerer polls this)
|
|
436
|
+
*/
|
|
437
|
+
async getOfferAnswer(offerId) {
|
|
438
|
+
return await this.api.getOfferAnswer(offerId);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Combined polling for answers and ICE candidates
|
|
442
|
+
* Returns all answered offers and ICE candidates for all peer's offers since timestamp
|
|
443
|
+
*/
|
|
444
|
+
async poll(since) {
|
|
445
|
+
return await this.api.poll(since);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Add ICE candidates to specific offer
|
|
449
|
+
*/
|
|
450
|
+
async addOfferIceCandidates(offerId, candidates) {
|
|
451
|
+
return await this.api.addOfferIceCandidates(offerId, candidates);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Get ICE candidates for specific offer (with polling support)
|
|
455
|
+
*/
|
|
456
|
+
async getOfferIceCandidates(offerId, since = 0) {
|
|
457
|
+
return await this.api.getOfferIceCandidates(offerId, since);
|
|
458
|
+
}
|
|
459
|
+
// ============================================
|
|
460
|
+
// Utility Methods
|
|
461
|
+
// ============================================
|
|
462
|
+
/**
|
|
463
|
+
* Get active connections (for offerer side)
|
|
464
|
+
*/
|
|
465
|
+
getActiveConnections() {
|
|
466
|
+
return this.offerPool?.getActiveConnections() ?? new Map();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Constants
|
|
470
|
+
Rondevu.DEFAULT_API_URL = 'https://api.ronde.vu';
|
|
471
|
+
Rondevu.DEFAULT_TTL_MS = 300000; // 5 minutes
|
|
472
|
+
Rondevu.POLLING_INTERVAL_MS = 1000; // 1 second
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto adapter interface for platform-independent cryptographic operations
|
|
3
|
+
*/
|
|
4
|
+
export interface Credential {
|
|
5
|
+
name: string;
|
|
6
|
+
secret: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Platform-independent crypto adapter interface
|
|
10
|
+
* Implementations provide platform-specific crypto operations
|
|
11
|
+
*/
|
|
12
|
+
export interface CryptoAdapter {
|
|
13
|
+
/**
|
|
14
|
+
* Generate HMAC-SHA256 signature for message authentication
|
|
15
|
+
* @param secret - The credential secret (hex string)
|
|
16
|
+
* @param message - The message to sign
|
|
17
|
+
* @returns Base64-encoded signature
|
|
18
|
+
*/
|
|
19
|
+
generateSignature(secret: string, message: string): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Verify HMAC-SHA256 signature
|
|
22
|
+
* @param secret - The credential secret (hex string)
|
|
23
|
+
* @param message - The message that was signed
|
|
24
|
+
* @param signature - The signature to verify (base64)
|
|
25
|
+
* @returns True if signature is valid
|
|
26
|
+
*/
|
|
27
|
+
verifySignature(secret: string, message: string, signature: string): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Generate a random secret (256-bit hex string)
|
|
30
|
+
* @returns 64-character hex string
|
|
31
|
+
*/
|
|
32
|
+
generateSecret(): string;
|
|
33
|
+
/**
|
|
34
|
+
* Convert hex string to bytes
|
|
35
|
+
*/
|
|
36
|
+
hexToBytes(hex: string): Uint8Array;
|
|
37
|
+
/**
|
|
38
|
+
* Convert bytes to hex string
|
|
39
|
+
*/
|
|
40
|
+
bytesToHex(bytes: Uint8Array): string;
|
|
41
|
+
/**
|
|
42
|
+
* Convert Uint8Array to base64 string
|
|
43
|
+
*/
|
|
44
|
+
bytesToBase64(bytes: Uint8Array): string;
|
|
45
|
+
/**
|
|
46
|
+
* Convert base64 string to Uint8Array
|
|
47
|
+
*/
|
|
48
|
+
base64ToBytes(base64: string): Uint8Array;
|
|
49
|
+
/**
|
|
50
|
+
* Generate random bytes
|
|
51
|
+
*/
|
|
52
|
+
randomBytes(length: number): Uint8Array;
|
|
53
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js Crypto adapter for Node.js environments
|
|
3
|
+
* Requires Node.js 19+ or Node.js 18 with --experimental-global-webcrypto flag
|
|
4
|
+
*/
|
|
5
|
+
import { CryptoAdapter } from './adapter.js';
|
|
6
|
+
/**
|
|
7
|
+
* Node.js Crypto implementation using Node.js built-in APIs
|
|
8
|
+
* Uses Buffer for base64 encoding and crypto.randomBytes for random generation
|
|
9
|
+
*
|
|
10
|
+
* Requirements:
|
|
11
|
+
* - Node.js 19+ (crypto.subtle available globally)
|
|
12
|
+
* - OR Node.js 18 with --experimental-global-webcrypto flag
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { RondevuAPI } from '@xtr-dev/rondevu-client'
|
|
17
|
+
* import { NodeCryptoAdapter } from '@xtr-dev/rondevu-client/node'
|
|
18
|
+
*
|
|
19
|
+
* const api = new RondevuAPI(
|
|
20
|
+
* 'https://signal.example.com',
|
|
21
|
+
* 'alice',
|
|
22
|
+
* { name: 'alice', secret: '...' },
|
|
23
|
+
* new NodeCryptoAdapter()
|
|
24
|
+
* )
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare class NodeCryptoAdapter implements CryptoAdapter {
|
|
28
|
+
constructor();
|
|
29
|
+
/**
|
|
30
|
+
* Generate HMAC-SHA256 signature
|
|
31
|
+
*/
|
|
32
|
+
generateSignature(secret: string, message: string): Promise<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Verify HMAC-SHA256 signature
|
|
35
|
+
* Uses constant-time comparison via Web Crypto API to prevent timing attacks
|
|
36
|
+
*
|
|
37
|
+
* @returns false for invalid signatures, throws for malformed input
|
|
38
|
+
* @throws Error if secret/signature format is invalid (not a verification failure)
|
|
39
|
+
*/
|
|
40
|
+
verifySignature(secret: string, message: string, signature: string): Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Generate a random secret (256-bit hex string)
|
|
43
|
+
*/
|
|
44
|
+
generateSecret(): string;
|
|
45
|
+
/**
|
|
46
|
+
* Convert hex string to bytes
|
|
47
|
+
* @throws Error if hex string is invalid
|
|
48
|
+
*/
|
|
49
|
+
hexToBytes(hex: string): Uint8Array;
|
|
50
|
+
/**
|
|
51
|
+
* Convert bytes to hex string
|
|
52
|
+
*/
|
|
53
|
+
bytesToHex(bytes: Uint8Array): string;
|
|
54
|
+
bytesToBase64(bytes: Uint8Array): string;
|
|
55
|
+
base64ToBytes(base64: string): Uint8Array;
|
|
56
|
+
randomBytes(length: number): Uint8Array;
|
|
57
|
+
}
|