@xtr-dev/rondevu-client 0.20.1 → 0.21.3

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.
Files changed (43) hide show
  1. package/README.md +83 -385
  2. package/dist/api/batcher.d.ts +60 -38
  3. package/dist/api/batcher.js +121 -77
  4. package/dist/api/client.d.ts +104 -61
  5. package/dist/api/client.js +273 -185
  6. package/dist/connections/answerer.d.ts +15 -6
  7. package/dist/connections/answerer.js +56 -19
  8. package/dist/connections/base.d.ts +6 -4
  9. package/dist/connections/base.js +26 -16
  10. package/dist/connections/config.d.ts +30 -0
  11. package/dist/connections/config.js +20 -0
  12. package/dist/connections/events.d.ts +6 -6
  13. package/dist/connections/offerer.d.ts +37 -8
  14. package/dist/connections/offerer.js +92 -24
  15. package/dist/core/ice-config.d.ts +35 -0
  16. package/dist/core/ice-config.js +111 -0
  17. package/dist/core/index.d.ts +18 -18
  18. package/dist/core/index.js +18 -13
  19. package/dist/core/offer-pool.d.ts +30 -11
  20. package/dist/core/offer-pool.js +90 -76
  21. package/dist/core/peer.d.ts +158 -0
  22. package/dist/core/peer.js +254 -0
  23. package/dist/core/polling-manager.d.ts +71 -0
  24. package/dist/core/polling-manager.js +122 -0
  25. package/dist/core/rondevu-errors.d.ts +59 -0
  26. package/dist/core/rondevu-errors.js +75 -0
  27. package/dist/core/rondevu-types.d.ts +125 -0
  28. package/dist/core/rondevu-types.js +6 -0
  29. package/dist/core/rondevu.d.ts +106 -209
  30. package/dist/core/rondevu.js +222 -349
  31. package/dist/crypto/adapter.d.ts +25 -9
  32. package/dist/crypto/node.d.ts +27 -5
  33. package/dist/crypto/node.js +96 -25
  34. package/dist/crypto/web.d.ts +26 -4
  35. package/dist/crypto/web.js +102 -25
  36. package/dist/utils/message-buffer.js +4 -4
  37. package/dist/webrtc/adapter.d.ts +22 -0
  38. package/dist/webrtc/adapter.js +5 -0
  39. package/dist/webrtc/browser.d.ts +12 -0
  40. package/dist/webrtc/browser.js +15 -0
  41. package/dist/webrtc/node.d.ts +32 -0
  42. package/dist/webrtc/node.js +32 -0
  43. package/package.json +17 -6
@@ -7,237 +7,318 @@ import { RpcBatcher } from './batcher.js';
7
7
  * RondevuAPI - RPC-based API client for Rondevu signaling server
8
8
  */
9
9
  export class RondevuAPI {
10
- constructor(baseUrl, username, keypair, cryptoAdapter, batcherOptions) {
10
+ constructor(baseUrl, credential, cryptoAdapter, batcherOptions) {
11
11
  this.baseUrl = baseUrl;
12
- this.username = username;
13
- this.keypair = keypair;
14
- this.batcher = null;
12
+ this.credential = credential;
15
13
  // Use WebCryptoAdapter by default (browser environment)
16
14
  this.crypto = cryptoAdapter || new WebCryptoAdapter();
17
- // Create batcher if not explicitly disabled
18
- if (batcherOptions !== false) {
19
- this.batcher = new RpcBatcher((requests) => this.rpcBatchDirect(requests), batcherOptions);
15
+ // Create batcher for request batching with throttling
16
+ this.batcher = new RpcBatcher(baseUrl, batcherOptions);
17
+ // Validate credential format early to provide clear error messages
18
+ if (!credential.name || typeof credential.name !== 'string') {
19
+ throw new Error('Invalid credential: name must be a non-empty string');
20
+ }
21
+ // Validate name format (alphanumeric, dots, underscores, hyphens only)
22
+ // Limit to prevent HTTP header size issues
23
+ if (credential.name.length > RondevuAPI.DEFAULT_CREDENTIAL_NAME_MAX_LENGTH) {
24
+ throw new Error(`Invalid credential: name must not exceed ${RondevuAPI.DEFAULT_CREDENTIAL_NAME_MAX_LENGTH} characters`);
25
+ }
26
+ if (!/^[a-zA-Z0-9._-]+$/.test(credential.name)) {
27
+ throw new Error('Invalid credential: name must contain only alphanumeric characters, dots, underscores, and hyphens');
28
+ }
29
+ // Validate secret
30
+ if (!credential.secret || typeof credential.secret !== 'string') {
31
+ throw new Error('Invalid credential: secret must be a non-empty string');
32
+ }
33
+ // Minimum 256 bits (64 hex characters) for security
34
+ if (credential.secret.length < RondevuAPI.DEFAULT_SECRET_MIN_LENGTH) {
35
+ throw new Error(`Invalid credential: secret must be at least 256 bits (${RondevuAPI.DEFAULT_SECRET_MIN_LENGTH} hex characters)`);
36
+ }
37
+ // Validate secret is valid hex (even length, only hex characters)
38
+ if (credential.secret.length % 2 !== 0) {
39
+ throw new Error('Invalid credential: secret must be a valid hex string (even length)');
40
+ }
41
+ if (!/^[0-9a-fA-F]+$/.test(credential.secret)) {
42
+ throw new Error('Invalid credential: secret must contain only hexadecimal characters');
20
43
  }
21
44
  }
22
45
  /**
23
- * Create canonical JSON string with sorted keys for deterministic signing
46
+ * Canonical JSON serialization with sorted keys
47
+ * Ensures deterministic output regardless of property insertion order
24
48
  */
25
- canonicalJSON(obj) {
26
- if (obj === null || obj === undefined) {
27
- return JSON.stringify(obj);
49
+ canonicalJSON(obj, depth = 0) {
50
+ // Prevent stack overflow from deeply nested objects
51
+ if (depth > RondevuAPI.MAX_CANONICALIZE_DEPTH) {
52
+ throw new Error('Object nesting too deep for canonicalization');
53
+ }
54
+ // Handle null
55
+ if (obj === null) {
56
+ return 'null';
57
+ }
58
+ // Handle undefined
59
+ if (obj === undefined) {
60
+ return JSON.stringify(undefined);
28
61
  }
29
- if (typeof obj !== 'object') {
62
+ // Validate primitive types
63
+ const type = typeof obj;
64
+ // Reject unsupported types
65
+ if (type === 'function') {
66
+ throw new Error('Functions are not supported in RPC parameters');
67
+ }
68
+ if (type === 'symbol' || type === 'bigint') {
69
+ throw new Error(`${type} is not supported in RPC parameters`);
70
+ }
71
+ // Validate numbers (reject NaN and Infinity)
72
+ if (type === 'number' && !Number.isFinite(obj)) {
73
+ throw new Error('NaN and Infinity are not supported in RPC parameters');
74
+ }
75
+ // Handle primitives (string, number, boolean)
76
+ if (type !== 'object') {
30
77
  return JSON.stringify(obj);
31
78
  }
79
+ // Handle arrays recursively
32
80
  if (Array.isArray(obj)) {
33
- return '[' + obj.map(item => this.canonicalJSON(item)).join(',') + ']';
81
+ return '[' + obj.map(item => this.canonicalJSON(item, depth + 1)).join(',') + ']';
34
82
  }
83
+ // Handle objects - sort keys alphabetically for deterministic output
35
84
  const sortedKeys = Object.keys(obj).sort();
36
85
  const pairs = sortedKeys.map(key => {
37
- return JSON.stringify(key) + ':' + this.canonicalJSON(obj[key]);
86
+ return JSON.stringify(key) + ':' + this.canonicalJSON(obj[key], depth + 1);
38
87
  });
39
88
  return '{' + pairs.join(',') + '}';
40
89
  }
41
90
  /**
42
- * Generate authentication headers for RPC request
43
- * Signs the payload (method + params + timestamp + username)
44
- */
45
- async generateAuthHeaders(request, includePublicKey = false) {
46
- const timestamp = Date.now();
47
- // Create payload with timestamp and username for signing: { method, params, timestamp, username }
48
- const payload = { ...request, timestamp, username: this.username };
49
- // Create canonical JSON representation for signing
50
- const canonical = this.canonicalJSON(payload);
51
- // Sign the canonical representation
52
- const signature = await this.crypto.signMessage(canonical, this.keypair.privateKey);
53
- const headers = {
54
- 'X-Signature': signature,
55
- 'X-Timestamp': timestamp.toString(),
56
- 'X-Username': this.username,
57
- };
58
- if (includePublicKey) {
59
- headers['X-Public-Key'] = this.keypair.publicKey;
60
- }
61
- return headers;
62
- }
63
- /**
64
- * Generate authentication fields embedded in request body (for batch requests)
65
- * Signs the payload (method + params + timestamp + username)
91
+ * Build signature message following server format
92
+ * Format: timestamp:nonce:method:canonicalJSON(params || {})
93
+ *
94
+ * Uses canonical JSON (sorted keys) to ensure deterministic serialization
95
+ * across different JavaScript engines and platforms.
96
+ *
97
+ * Note: When params is undefined, it's serialized as "{}" (empty object).
98
+ * This matches the server's expectation for parameterless RPC calls.
66
99
  */
67
- async generateAuthForRequest(request, includePublicKey = false) {
68
- const timestamp = Date.now();
69
- // Create payload with timestamp and username for signing: { method, params, timestamp, username }
70
- const payload = { ...request, timestamp, username: this.username };
71
- // Create canonical JSON representation for signing
72
- const canonical = this.canonicalJSON(payload);
73
- // Sign the canonical representation
74
- const signature = await this.crypto.signMessage(canonical, this.keypair.privateKey);
75
- const authRequest = {
76
- ...request,
77
- signature,
78
- timestamp,
79
- username: this.username,
80
- };
81
- if (includePublicKey) {
82
- authRequest.publicKey = this.keypair.publicKey;
83
- }
84
- return authRequest;
85
- }
86
- /**
87
- * Execute RPC call with optional batching
88
- */
89
- async rpc(request, authHeaders) {
90
- // Use batcher if enabled
91
- if (this.batcher) {
92
- return await this.batcher.add(request);
93
- }
94
- // Direct call without batching
95
- return await this.rpcDirect(request, authHeaders);
96
- }
97
- /**
98
- * Execute single RPC call directly (bypasses batcher)
99
- */
100
- async rpcDirect(request, authHeaders) {
101
- const response = await fetch(`${this.baseUrl}/rpc`, {
102
- method: 'POST',
103
- headers: {
104
- 'Content-Type': 'application/json',
105
- ...authHeaders,
106
- },
107
- body: JSON.stringify(request),
108
- });
109
- if (!response.ok) {
110
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
100
+ buildSignatureMessage(timestamp, nonce, method, params) {
101
+ if (!method || typeof method !== 'string') {
102
+ throw new Error('Invalid method: must be a non-empty string');
111
103
  }
112
- const result = await response.json();
113
- if (!result.success) {
114
- throw new Error(result.error || 'RPC call failed');
115
- }
116
- return result.result;
104
+ const paramsJson = this.canonicalJSON(params || {});
105
+ return `${timestamp}:${nonce}:${method}:${paramsJson}`;
117
106
  }
118
107
  /**
119
- * Execute batch RPC calls directly (bypasses batcher)
120
- * Each request in the batch has its own embedded authentication (signature, timestamp, username, publicKey)
108
+ * Generate cryptographically secure nonce
109
+ * Uses crypto.randomUUID() if available, falls back to secure random bytes
110
+ *
111
+ * Note: this.crypto is always initialized in constructor (WebCryptoAdapter or NodeCryptoAdapter)
112
+ * and TypeScript enforces that both implement randomBytes(), so the fallback is always safe.
121
113
  */
122
- async rpcBatchDirect(requests) {
123
- // Add auth to each request in the batch
124
- const requestsWithAuth = await Promise.all(requests.map(req => this.generateAuthForRequest(req, true)));
125
- const response = await fetch(`${this.baseUrl}/rpc`, {
126
- method: 'POST',
127
- headers: { 'Content-Type': 'application/json' },
128
- body: JSON.stringify(requestsWithAuth),
129
- });
130
- if (!response.ok) {
131
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
132
- }
133
- const results = await response.json();
134
- if (!Array.isArray(results)) {
135
- throw new Error('Server returned invalid batch response (not an array)');
114
+ generateNonce() {
115
+ // Get crypto object from global scope (supports various contexts)
116
+ // In browsers: window.crypto or self.crypto
117
+ // In modern environments: global crypto
118
+ const globalCrypto = typeof crypto !== 'undefined'
119
+ ? crypto
120
+ : (typeof window !== 'undefined' && window.crypto) ||
121
+ (typeof self !== 'undefined' && self.crypto) ||
122
+ undefined;
123
+ // Prefer crypto.randomUUID() for widespread support and standard format
124
+ // UUIDv4 provides 122 bits of entropy (6 fixed version/variant bits)
125
+ if (globalCrypto && typeof globalCrypto.randomUUID === 'function') {
126
+ return globalCrypto.randomUUID();
136
127
  }
137
- // Map results, throwing error for any failed request
138
- return results.map((result, i) => {
139
- if (!result.success) {
140
- throw new Error(result.error || `RPC call ${i} failed`);
141
- }
142
- return result.result;
143
- });
128
+ // Fallback: 16 random bytes (128 bits entropy) as hex string
129
+ // Slightly more entropy than UUID, but both are cryptographically secure
130
+ // Safe because this.crypto is guaranteed to implement CryptoAdapter interface
131
+ const randomBytes = this.crypto.randomBytes(16);
132
+ return this.crypto.bytesToHex(randomBytes);
144
133
  }
145
- // ============================================
146
- // Ed25519 Cryptography Helpers
147
- // ============================================
148
134
  /**
149
- * Generate an Ed25519 keypair for username claiming and service publishing
150
- * @param cryptoAdapter - Optional crypto adapter (defaults to WebCryptoAdapter)
151
- */
152
- static async generateKeypair(cryptoAdapter) {
153
- const adapter = cryptoAdapter || new WebCryptoAdapter();
154
- return await adapter.generateKeypair();
155
- }
156
- /**
157
- * Sign a message with an Ed25519 private key
158
- * @param cryptoAdapter - Optional crypto adapter (defaults to WebCryptoAdapter)
135
+ * Generate authentication headers for RPC request
136
+ * Uses HMAC-SHA256 signature with nonce for replay protection
137
+ *
138
+ * Security notes:
139
+ * - Nonce: Cryptographically secure random value (UUID or 128-bit hex)
140
+ * - Timestamp: Prevents replay attacks outside the server's time window
141
+ * - Server validates timestamp is within acceptable range (typically ±5 minutes)
142
+ * - Tolerates reasonable clock skew between client and server
143
+ * - Requests with stale timestamps are rejected
144
+ * - Signature: HMAC-SHA256 ensures message integrity and authenticity
145
+ * - Server validates nonce uniqueness to prevent replay within time window
146
+ * - Each nonce can only be used once within the timestamp validity window
147
+ * - Server maintains nonce cache with expiration matching timestamp window
159
148
  */
160
- static async signMessage(message, privateKeyBase64, cryptoAdapter) {
161
- const adapter = cryptoAdapter || new WebCryptoAdapter();
162
- return await adapter.signMessage(message, privateKeyBase64);
149
+ async generateAuthHeaders(request) {
150
+ const timestamp = Date.now();
151
+ const nonce = this.generateNonce();
152
+ // Build message and generate signature
153
+ const message = this.buildSignatureMessage(timestamp, nonce, request.method, request.params);
154
+ const signature = await this.crypto.generateSignature(this.credential.secret, message);
155
+ return {
156
+ 'X-Name': this.credential.name,
157
+ 'X-Timestamp': timestamp.toString(),
158
+ 'X-Nonce': nonce,
159
+ 'X-Signature': signature,
160
+ };
163
161
  }
164
162
  /**
165
- * Verify an Ed25519 signature
166
- * @param cryptoAdapter - Optional crypto adapter (defaults to WebCryptoAdapter)
163
+ * Execute RPC call via batcher
164
+ * Requests are batched with throttling for efficiency
167
165
  */
168
- static async verifySignature(message, signatureBase64, publicKeyBase64, cryptoAdapter) {
169
- const adapter = cryptoAdapter || new WebCryptoAdapter();
170
- return await adapter.verifySignature(message, signatureBase64, publicKeyBase64);
166
+ async rpc(request, authHeaders) {
167
+ return this.batcher.add(request, authHeaders);
171
168
  }
172
169
  // ============================================
173
- // Username Management
170
+ // Credential Management
174
171
  // ============================================
175
172
  /**
176
- * Check if a username is available
173
+ * Generate new credentials (name + secret pair)
174
+ * This is the entry point for new users - no authentication required
175
+ * Credentials are generated server-side to ensure security and uniqueness
176
+ *
177
+ * ⚠️ SECURITY NOTE:
178
+ * - Store the returned credential securely
179
+ * - The secret provides full access to this identity
180
+ * - Credentials should be persisted encrypted and never logged
181
+ *
182
+ * @param baseUrl - Rondevu server URL
183
+ * @param expiresAt - Optional custom expiry timestamp (defaults to 1 year)
184
+ * @param options - Optional: { maxRetries: number, timeout: number }
185
+ * @returns Generated credential with name and secret
177
186
  */
178
- async isUsernameAvailable(username) {
187
+ static async generateCredentials(baseUrl, options) {
188
+ const maxRetries = options?.maxRetries ?? RondevuAPI.DEFAULT_MAX_RETRIES;
189
+ const timeout = options?.timeout ?? RondevuAPI.DEFAULT_TIMEOUT_MS;
190
+ let lastError = null;
191
+ // Build params object with optional name and expiresAt
192
+ const params = {};
193
+ if (options?.name)
194
+ params.name = options.name;
195
+ if (options?.expiresAt)
196
+ params.expiresAt = options.expiresAt;
179
197
  const request = {
180
- method: 'getUser',
181
- params: { username },
198
+ method: 'generateCredentials',
199
+ params: Object.keys(params).length > 0 ? params : undefined,
182
200
  };
183
- const authHeaders = await this.generateAuthHeaders(request, false);
184
- const result = await this.rpc(request, authHeaders);
185
- return result.available;
201
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
202
+ // httpStatus is scoped to each iteration intentionally - resets on each retry
203
+ let httpStatus = null;
204
+ try {
205
+ // Create abort controller for timeout
206
+ if (typeof AbortController === 'undefined') {
207
+ throw new Error('AbortController not supported in this environment');
208
+ }
209
+ const controller = new AbortController();
210
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
211
+ try {
212
+ const response = await fetch(`${baseUrl}/rpc`, {
213
+ method: 'POST',
214
+ headers: {
215
+ 'Content-Type': 'application/json',
216
+ },
217
+ body: JSON.stringify([request]), // Server expects array (batch format)
218
+ signal: controller.signal,
219
+ });
220
+ httpStatus = response.status;
221
+ if (!response.ok) {
222
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
223
+ }
224
+ // Server returns array of responses
225
+ const results = await response.json();
226
+ const result = results[0];
227
+ if (!result || !result.success) {
228
+ throw new Error(result?.error || 'Failed to generate credentials');
229
+ }
230
+ // Validate credential structure
231
+ const credential = result.result;
232
+ if (!credential || typeof credential !== 'object') {
233
+ throw new Error('Invalid credential response: result is not an object');
234
+ }
235
+ if (typeof credential.name !== 'string' || !credential.name) {
236
+ throw new Error('Invalid credential response: missing or invalid name');
237
+ }
238
+ if (typeof credential.secret !== 'string' || !credential.secret) {
239
+ throw new Error('Invalid credential response: missing or invalid secret');
240
+ }
241
+ return credential;
242
+ }
243
+ finally {
244
+ // Always clear timeout to prevent memory leaks
245
+ clearTimeout(timeoutId);
246
+ }
247
+ }
248
+ catch (error) {
249
+ lastError = error;
250
+ // Don't retry on abort (timeout)
251
+ if (error instanceof Error && error.name === 'AbortError') {
252
+ throw new Error(`Credential generation timed out after ${timeout}ms`);
253
+ }
254
+ // Don't retry on 4xx errors (client errors) - check actual status
255
+ if (httpStatus !== null && httpStatus >= 400 && httpStatus < 500) {
256
+ throw error;
257
+ }
258
+ // Retry with exponential backoff + jitter for network/server errors (5xx or network failures)
259
+ // Jitter prevents thundering herd when many clients retry simultaneously
260
+ // Cap backoff to prevent excessive waits
261
+ if (attempt < maxRetries - 1) {
262
+ const backoffMs = Math.min(1000 * Math.pow(2, attempt) + Math.random() * 1000, RondevuAPI.MAX_BACKOFF_MS);
263
+ await new Promise(resolve => setTimeout(resolve, backoffMs));
264
+ }
265
+ }
266
+ }
267
+ throw new Error(`Failed to generate credentials after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
186
268
  }
187
269
  /**
188
- * Check if current username is claimed
270
+ * Generate a random secret locally (for advanced use cases)
271
+ * @param cryptoAdapter - Optional crypto adapter
189
272
  */
190
- async isUsernameClaimed() {
191
- const request = {
192
- method: 'getUser',
193
- params: { username: this.username },
194
- };
195
- const authHeaders = await this.generateAuthHeaders(request, false);
196
- const result = await this.rpc(request, authHeaders);
197
- return !result.available;
273
+ static generateSecret(cryptoAdapter) {
274
+ const adapter = cryptoAdapter || new WebCryptoAdapter();
275
+ return adapter.generateSecret();
198
276
  }
199
277
  // ============================================
200
- // Service Management
278
+ // Tags-based Offer Management (v2)
201
279
  // ============================================
202
280
  /**
203
- * Publish a service
281
+ * Publish offers with tags
204
282
  */
205
- async publishService(service) {
206
- const request = {
207
- method: 'publishService',
283
+ async publish(request) {
284
+ const rpcRequest = {
285
+ method: 'publishOffer',
208
286
  params: {
209
- serviceFqn: service.serviceFqn,
210
- offers: service.offers,
211
- ttl: service.ttl,
287
+ tags: request.tags,
288
+ offers: request.offers,
289
+ ttl: request.ttl,
212
290
  },
213
291
  };
214
- const authHeaders = await this.generateAuthHeaders(request, true);
215
- return await this.rpc(request, authHeaders);
292
+ const authHeaders = await this.generateAuthHeaders(rpcRequest);
293
+ return await this.rpc(rpcRequest, authHeaders);
216
294
  }
217
295
  /**
218
- * Get service by FQN (direct lookup, random, or paginated)
296
+ * Discover offers by tags
297
+ * @param request - Discovery request with tags and optional pagination
298
+ * @returns Paginated response if limit provided, single offer if not
219
299
  */
220
- async getService(serviceFqn, options) {
221
- const request = {
222
- method: 'getService',
300
+ async discover(request) {
301
+ const rpcRequest = {
302
+ method: 'discover',
223
303
  params: {
224
- serviceFqn,
225
- ...options,
304
+ tags: request.tags,
305
+ limit: request.limit,
306
+ offset: request.offset,
226
307
  },
227
308
  };
228
- const authHeaders = await this.generateAuthHeaders(request, true);
229
- return await this.rpc(request, authHeaders);
309
+ const authHeaders = await this.generateAuthHeaders(rpcRequest);
310
+ return await this.rpc(rpcRequest, authHeaders);
230
311
  }
231
312
  /**
232
- * Delete a service
313
+ * Delete an offer by ID
233
314
  */
234
- async deleteService(serviceFqn) {
315
+ async deleteOffer(offerId) {
235
316
  const request = {
236
- method: 'deleteService',
237
- params: { serviceFqn },
317
+ method: 'deleteOffer',
318
+ params: { offerId },
238
319
  };
239
- const authHeaders = await this.generateAuthHeaders(request, true);
240
- await this.rpc(request, authHeaders);
320
+ const authHeaders = await this.generateAuthHeaders(request);
321
+ return await this.rpc(request, authHeaders);
241
322
  }
242
323
  // ============================================
243
324
  // WebRTC Signaling
@@ -245,24 +326,24 @@ export class RondevuAPI {
245
326
  /**
246
327
  * Answer an offer
247
328
  */
248
- async answerOffer(serviceFqn, offerId, sdp) {
329
+ async answerOffer(offerId, sdp) {
249
330
  const request = {
250
331
  method: 'answerOffer',
251
- params: { serviceFqn, offerId, sdp },
332
+ params: { offerId, sdp },
252
333
  };
253
- const authHeaders = await this.generateAuthHeaders(request, true);
334
+ const authHeaders = await this.generateAuthHeaders(request);
254
335
  await this.rpc(request, authHeaders);
255
336
  }
256
337
  /**
257
338
  * Get answer for a specific offer (offerer polls this)
258
339
  */
259
- async getOfferAnswer(serviceFqn, offerId) {
340
+ async getOfferAnswer(offerId) {
260
341
  try {
261
342
  const request = {
262
343
  method: 'getOfferAnswer',
263
- params: { serviceFqn, offerId },
344
+ params: { offerId },
264
345
  };
265
- const authHeaders = await this.generateAuthHeaders(request, true);
346
+ const authHeaders = await this.generateAuthHeaders(request);
266
347
  return await this.rpc(request, authHeaders);
267
348
  }
268
349
  catch (err) {
@@ -280,29 +361,29 @@ export class RondevuAPI {
280
361
  method: 'poll',
281
362
  params: { since },
282
363
  };
283
- const authHeaders = await this.generateAuthHeaders(request, true);
364
+ const authHeaders = await this.generateAuthHeaders(request);
284
365
  return await this.rpc(request, authHeaders);
285
366
  }
286
367
  /**
287
368
  * Add ICE candidates to a specific offer
288
369
  */
289
- async addOfferIceCandidates(serviceFqn, offerId, candidates) {
370
+ async addOfferIceCandidates(offerId, candidates) {
290
371
  const request = {
291
372
  method: 'addIceCandidates',
292
- params: { serviceFqn, offerId, candidates },
373
+ params: { offerId, candidates },
293
374
  };
294
- const authHeaders = await this.generateAuthHeaders(request, true);
375
+ const authHeaders = await this.generateAuthHeaders(request);
295
376
  return await this.rpc(request, authHeaders);
296
377
  }
297
378
  /**
298
379
  * Get ICE candidates for a specific offer
299
380
  */
300
- async getOfferIceCandidates(serviceFqn, offerId, since = 0) {
381
+ async getOfferIceCandidates(offerId, since = 0) {
301
382
  const request = {
302
383
  method: 'getIceCandidates',
303
- params: { serviceFqn, offerId, since },
384
+ params: { offerId, since },
304
385
  };
305
- const authHeaders = await this.generateAuthHeaders(request, true);
386
+ const authHeaders = await this.generateAuthHeaders(request);
306
387
  const result = await this.rpc(request, authHeaders);
307
388
  return {
308
389
  candidates: result.candidates || [],
@@ -310,3 +391,10 @@ export class RondevuAPI {
310
391
  };
311
392
  }
312
393
  }
394
+ // Default values for credential generation
395
+ RondevuAPI.DEFAULT_MAX_RETRIES = 3;
396
+ RondevuAPI.DEFAULT_TIMEOUT_MS = 30000; // 30 seconds
397
+ RondevuAPI.DEFAULT_CREDENTIAL_NAME_MAX_LENGTH = 128;
398
+ RondevuAPI.DEFAULT_SECRET_MIN_LENGTH = 64; // 256 bits
399
+ RondevuAPI.MAX_BACKOFF_MS = 60000; // 60 seconds max backoff
400
+ RondevuAPI.MAX_CANONICALIZE_DEPTH = 100; // Prevent stack overflow
@@ -2,14 +2,17 @@
2
2
  * Answerer-side WebRTC connection with answer creation and offer processing
3
3
  */
4
4
  import { RondevuConnection } from './base.js';
5
- import { RondevuAPI } from '../api/client.js';
5
+ import { RondevuAPI, IceCandidate } from '../api/client.js';
6
6
  import { ConnectionConfig } from './config.js';
7
+ import { WebRTCAdapter } from '../webrtc/adapter.js';
7
8
  export interface AnswererOptions {
8
9
  api: RondevuAPI;
9
- serviceFqn: string;
10
+ ownerUsername: string;
11
+ tags: string[];
10
12
  offerId: string;
11
13
  offerSdp: string;
12
14
  rtcConfig?: RTCConfiguration;
15
+ webrtcAdapter?: WebRTCAdapter;
13
16
  config?: Partial<ConnectionConfig>;
14
17
  }
15
18
  /**
@@ -17,7 +20,8 @@ export interface AnswererOptions {
17
20
  */
18
21
  export declare class AnswererConnection extends RondevuConnection {
19
22
  private api;
20
- private serviceFqn;
23
+ private ownerUsername;
24
+ private tags;
21
25
  private offerId;
22
26
  private offerSdp;
23
27
  constructor(options: AnswererOptions);
@@ -34,19 +38,24 @@ export declare class AnswererConnection extends RondevuConnection {
34
38
  */
35
39
  protected getApi(): any;
36
40
  /**
37
- * Get the service FQN
41
+ * Get the owner username
38
42
  */
39
- protected getServiceFqn(): string;
43
+ protected getOwnerUsername(): string;
40
44
  /**
41
45
  * Answerers accept ICE candidates from offerers only
42
46
  */
43
47
  protected getIceCandidateRole(): 'offerer' | null;
44
48
  /**
45
- * Attempt to reconnect
49
+ * Attempt to reconnect to the same user
46
50
  */
47
51
  protected attemptReconnect(): void;
48
52
  /**
49
53
  * Get the offer ID we're answering
50
54
  */
51
55
  getOfferId(): string;
56
+ /**
57
+ * Handle remote ICE candidates received from polling
58
+ * Called by Rondevu when poll:ice event is received
59
+ */
60
+ handleRemoteIceCandidates(candidates: IceCandidate[]): void;
52
61
  }