@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.
Files changed (70) hide show
  1. package/README.md +92 -117
  2. package/dist/api/batcher.d.ts +83 -0
  3. package/dist/api/batcher.js +155 -0
  4. package/dist/api/client.d.ts +198 -0
  5. package/dist/api/client.js +400 -0
  6. package/dist/{answerer-connection.d.ts → connections/answerer.d.ts} +25 -8
  7. package/dist/{answerer-connection.js → connections/answerer.js} +70 -48
  8. package/dist/{connection.d.ts → connections/base.d.ts} +30 -7
  9. package/dist/{connection.js → connections/base.js} +65 -14
  10. package/dist/connections/config.d.ts +51 -0
  11. package/dist/{connection-config.js → connections/config.js} +20 -0
  12. package/dist/{connection-events.d.ts → connections/events.d.ts} +6 -6
  13. package/dist/connections/offerer.d.ts +108 -0
  14. package/dist/connections/offerer.js +306 -0
  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 +22 -0
  18. package/dist/core/index.js +22 -0
  19. package/dist/core/offer-pool.d.ts +113 -0
  20. package/dist/core/offer-pool.js +281 -0
  21. package/dist/core/peer.d.ts +155 -0
  22. package/dist/core/peer.js +252 -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 +296 -0
  30. package/dist/core/rondevu.js +472 -0
  31. package/dist/crypto/adapter.d.ts +53 -0
  32. package/dist/crypto/node.d.ts +57 -0
  33. package/dist/crypto/node.js +149 -0
  34. package/dist/crypto/web.d.ts +38 -0
  35. package/dist/crypto/web.js +129 -0
  36. package/dist/utils/async-lock.d.ts +42 -0
  37. package/dist/utils/async-lock.js +75 -0
  38. package/dist/{message-buffer.d.ts → utils/message-buffer.d.ts} +1 -1
  39. package/dist/{message-buffer.js → utils/message-buffer.js} +4 -4
  40. package/dist/webrtc/adapter.d.ts +22 -0
  41. package/dist/webrtc/adapter.js +5 -0
  42. package/dist/webrtc/browser.d.ts +12 -0
  43. package/dist/webrtc/browser.js +15 -0
  44. package/dist/webrtc/node.d.ts +32 -0
  45. package/dist/webrtc/node.js +32 -0
  46. package/package.json +20 -9
  47. package/dist/api.d.ts +0 -146
  48. package/dist/api.js +0 -279
  49. package/dist/connection-config.d.ts +0 -21
  50. package/dist/crypto-adapter.d.ts +0 -37
  51. package/dist/index.d.ts +0 -13
  52. package/dist/index.js +0 -10
  53. package/dist/node-crypto-adapter.d.ts +0 -35
  54. package/dist/node-crypto-adapter.js +0 -78
  55. package/dist/offerer-connection.d.ts +0 -54
  56. package/dist/offerer-connection.js +0 -177
  57. package/dist/rondevu-signaler.d.ts +0 -112
  58. package/dist/rondevu-signaler.js +0 -401
  59. package/dist/rondevu.d.ts +0 -407
  60. package/dist/rondevu.js +0 -847
  61. package/dist/rpc-batcher.d.ts +0 -61
  62. package/dist/rpc-batcher.js +0 -111
  63. package/dist/web-crypto-adapter.d.ts +0 -16
  64. package/dist/web-crypto-adapter.js +0 -52
  65. /package/dist/{connection-events.js → connections/events.js} +0 -0
  66. /package/dist/{types.d.ts → core/types.d.ts} +0 -0
  67. /package/dist/{types.js → core/types.js} +0 -0
  68. /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
  69. /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
  70. /package/dist/{exponential-backoff.js → utils/exponential-backoff.js} +0 -0
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Rondevu API Client - RPC interface
3
+ */
4
+ import { CryptoAdapter, Credential } from '../crypto/adapter.js';
5
+ import { BatcherOptions } from './batcher.js';
6
+ export type { Credential } from '../crypto/adapter.js';
7
+ export type { BatcherOptions } from './batcher.js';
8
+ export interface OfferRequest {
9
+ sdp: string;
10
+ }
11
+ export interface PublishRequest {
12
+ tags: string[];
13
+ offers: OfferRequest[];
14
+ ttl?: number;
15
+ }
16
+ export interface DiscoverRequest {
17
+ tags: string[];
18
+ limit?: number;
19
+ offset?: number;
20
+ }
21
+ export interface TaggedOffer {
22
+ offerId: string;
23
+ username: string;
24
+ tags: string[];
25
+ sdp: string;
26
+ createdAt: number;
27
+ expiresAt: number;
28
+ }
29
+ export interface DiscoverResponse {
30
+ offers: TaggedOffer[];
31
+ count: number;
32
+ limit: number;
33
+ offset: number;
34
+ }
35
+ export interface PublishResponse {
36
+ username: string;
37
+ tags: string[];
38
+ offers: Array<{
39
+ offerId: string;
40
+ sdp: string;
41
+ createdAt: number;
42
+ expiresAt: number;
43
+ }>;
44
+ createdAt: number;
45
+ expiresAt: number;
46
+ }
47
+ export interface IceCandidate {
48
+ candidate: RTCIceCandidateInit | null;
49
+ role: 'offerer' | 'answerer';
50
+ createdAt: number;
51
+ }
52
+ /**
53
+ * RondevuAPI - RPC-based API client for Rondevu signaling server
54
+ */
55
+ export declare class RondevuAPI {
56
+ private baseUrl;
57
+ private credential;
58
+ private static readonly DEFAULT_MAX_RETRIES;
59
+ private static readonly DEFAULT_TIMEOUT_MS;
60
+ private static readonly DEFAULT_CREDENTIAL_NAME_MAX_LENGTH;
61
+ private static readonly DEFAULT_SECRET_MIN_LENGTH;
62
+ private static readonly MAX_BACKOFF_MS;
63
+ private static readonly MAX_CANONICALIZE_DEPTH;
64
+ private crypto;
65
+ private batcher;
66
+ constructor(baseUrl: string, credential: Credential, cryptoAdapter?: CryptoAdapter, batcherOptions?: BatcherOptions);
67
+ /**
68
+ * Canonical JSON serialization with sorted keys
69
+ * Ensures deterministic output regardless of property insertion order
70
+ */
71
+ private canonicalJSON;
72
+ /**
73
+ * Build signature message following server format
74
+ * Format: timestamp:nonce:method:canonicalJSON(params || {})
75
+ *
76
+ * Uses canonical JSON (sorted keys) to ensure deterministic serialization
77
+ * across different JavaScript engines and platforms.
78
+ *
79
+ * Note: When params is undefined, it's serialized as "{}" (empty object).
80
+ * This matches the server's expectation for parameterless RPC calls.
81
+ */
82
+ private buildSignatureMessage;
83
+ /**
84
+ * Generate cryptographically secure nonce
85
+ * Uses crypto.randomUUID() if available, falls back to secure random bytes
86
+ *
87
+ * Note: this.crypto is always initialized in constructor (WebCryptoAdapter or NodeCryptoAdapter)
88
+ * and TypeScript enforces that both implement randomBytes(), so the fallback is always safe.
89
+ */
90
+ private generateNonce;
91
+ /**
92
+ * Generate authentication headers for RPC request
93
+ * Uses HMAC-SHA256 signature with nonce for replay protection
94
+ *
95
+ * Security notes:
96
+ * - Nonce: Cryptographically secure random value (UUID or 128-bit hex)
97
+ * - Timestamp: Prevents replay attacks outside the server's time window
98
+ * - Server validates timestamp is within acceptable range (typically ±5 minutes)
99
+ * - Tolerates reasonable clock skew between client and server
100
+ * - Requests with stale timestamps are rejected
101
+ * - Signature: HMAC-SHA256 ensures message integrity and authenticity
102
+ * - Server validates nonce uniqueness to prevent replay within time window
103
+ * - Each nonce can only be used once within the timestamp validity window
104
+ * - Server maintains nonce cache with expiration matching timestamp window
105
+ */
106
+ private generateAuthHeaders;
107
+ /**
108
+ * Execute RPC call via batcher
109
+ * Requests are batched with throttling for efficiency
110
+ */
111
+ private rpc;
112
+ /**
113
+ * Generate new credentials (name + secret pair)
114
+ * This is the entry point for new users - no authentication required
115
+ * Credentials are generated server-side to ensure security and uniqueness
116
+ *
117
+ * ⚠️ SECURITY NOTE:
118
+ * - Store the returned credential securely
119
+ * - The secret provides full access to this identity
120
+ * - Credentials should be persisted encrypted and never logged
121
+ *
122
+ * @param baseUrl - Rondevu server URL
123
+ * @param expiresAt - Optional custom expiry timestamp (defaults to 1 year)
124
+ * @param options - Optional: { maxRetries: number, timeout: number }
125
+ * @returns Generated credential with name and secret
126
+ */
127
+ static generateCredentials(baseUrl: string, options?: {
128
+ name?: string;
129
+ expiresAt?: number;
130
+ maxRetries?: number;
131
+ timeout?: number;
132
+ }): Promise<Credential>;
133
+ /**
134
+ * Generate a random secret locally (for advanced use cases)
135
+ * @param cryptoAdapter - Optional crypto adapter
136
+ */
137
+ static generateSecret(cryptoAdapter?: CryptoAdapter): string;
138
+ /**
139
+ * Publish offers with tags
140
+ */
141
+ publish(request: PublishRequest): Promise<PublishResponse>;
142
+ /**
143
+ * Discover offers by tags
144
+ * @param request - Discovery request with tags and optional pagination
145
+ * @returns Paginated response if limit provided, single offer if not
146
+ */
147
+ discover(request: DiscoverRequest): Promise<DiscoverResponse | TaggedOffer>;
148
+ /**
149
+ * Delete an offer by ID
150
+ */
151
+ deleteOffer(offerId: string): Promise<{
152
+ success: boolean;
153
+ }>;
154
+ /**
155
+ * Answer an offer
156
+ */
157
+ answerOffer(offerId: string, sdp: string): Promise<void>;
158
+ /**
159
+ * Get answer for a specific offer (offerer polls this)
160
+ */
161
+ getOfferAnswer(offerId: string): Promise<{
162
+ sdp: string;
163
+ offerId: string;
164
+ answererId: string;
165
+ answeredAt: number;
166
+ } | null>;
167
+ /**
168
+ * Combined polling for answers and ICE candidates
169
+ */
170
+ poll(since?: number): Promise<{
171
+ answers: Array<{
172
+ offerId: string;
173
+ answererId: string;
174
+ sdp: string;
175
+ answeredAt: number;
176
+ }>;
177
+ iceCandidates: Record<string, Array<{
178
+ candidate: RTCIceCandidateInit | null;
179
+ role: 'offerer' | 'answerer';
180
+ peerId: string;
181
+ createdAt: number;
182
+ }>>;
183
+ }>;
184
+ /**
185
+ * Add ICE candidates to a specific offer
186
+ */
187
+ addOfferIceCandidates(offerId: string, candidates: RTCIceCandidateInit[]): Promise<{
188
+ count: number;
189
+ offerId: string;
190
+ }>;
191
+ /**
192
+ * Get ICE candidates for a specific offer
193
+ */
194
+ getOfferIceCandidates(offerId: string, since?: number): Promise<{
195
+ candidates: IceCandidate[];
196
+ offerId: string;
197
+ }>;
198
+ }
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Rondevu API Client - RPC interface
3
+ */
4
+ import { WebCryptoAdapter } from '../crypto/web.js';
5
+ import { RpcBatcher } from './batcher.js';
6
+ /**
7
+ * RondevuAPI - RPC-based API client for Rondevu signaling server
8
+ */
9
+ export class RondevuAPI {
10
+ constructor(baseUrl, credential, cryptoAdapter, batcherOptions) {
11
+ this.baseUrl = baseUrl;
12
+ this.credential = credential;
13
+ // Use WebCryptoAdapter by default (browser environment)
14
+ this.crypto = cryptoAdapter || new WebCryptoAdapter();
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');
43
+ }
44
+ }
45
+ /**
46
+ * Canonical JSON serialization with sorted keys
47
+ * Ensures deterministic output regardless of property insertion order
48
+ */
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);
61
+ }
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') {
77
+ return JSON.stringify(obj);
78
+ }
79
+ // Handle arrays recursively
80
+ if (Array.isArray(obj)) {
81
+ return '[' + obj.map(item => this.canonicalJSON(item, depth + 1)).join(',') + ']';
82
+ }
83
+ // Handle objects - sort keys alphabetically for deterministic output
84
+ const sortedKeys = Object.keys(obj).sort();
85
+ const pairs = sortedKeys.map(key => {
86
+ return JSON.stringify(key) + ':' + this.canonicalJSON(obj[key], depth + 1);
87
+ });
88
+ return '{' + pairs.join(',') + '}';
89
+ }
90
+ /**
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.
99
+ */
100
+ buildSignatureMessage(timestamp, nonce, method, params) {
101
+ if (!method || typeof method !== 'string') {
102
+ throw new Error('Invalid method: must be a non-empty string');
103
+ }
104
+ const paramsJson = this.canonicalJSON(params || {});
105
+ return `${timestamp}:${nonce}:${method}:${paramsJson}`;
106
+ }
107
+ /**
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.
113
+ */
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();
127
+ }
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);
133
+ }
134
+ /**
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
148
+ */
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
+ };
161
+ }
162
+ /**
163
+ * Execute RPC call via batcher
164
+ * Requests are batched with throttling for efficiency
165
+ */
166
+ async rpc(request, authHeaders) {
167
+ return this.batcher.add(request, authHeaders);
168
+ }
169
+ // ============================================
170
+ // Credential Management
171
+ // ============================================
172
+ /**
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
186
+ */
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;
197
+ const request = {
198
+ method: 'generateCredentials',
199
+ params: Object.keys(params).length > 0 ? params : undefined,
200
+ };
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'}`);
268
+ }
269
+ /**
270
+ * Generate a random secret locally (for advanced use cases)
271
+ * @param cryptoAdapter - Optional crypto adapter
272
+ */
273
+ static generateSecret(cryptoAdapter) {
274
+ const adapter = cryptoAdapter || new WebCryptoAdapter();
275
+ return adapter.generateSecret();
276
+ }
277
+ // ============================================
278
+ // Tags-based Offer Management (v2)
279
+ // ============================================
280
+ /**
281
+ * Publish offers with tags
282
+ */
283
+ async publish(request) {
284
+ const rpcRequest = {
285
+ method: 'publishOffer',
286
+ params: {
287
+ tags: request.tags,
288
+ offers: request.offers,
289
+ ttl: request.ttl,
290
+ },
291
+ };
292
+ const authHeaders = await this.generateAuthHeaders(rpcRequest);
293
+ return await this.rpc(rpcRequest, authHeaders);
294
+ }
295
+ /**
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
299
+ */
300
+ async discover(request) {
301
+ const rpcRequest = {
302
+ method: 'discover',
303
+ params: {
304
+ tags: request.tags,
305
+ limit: request.limit,
306
+ offset: request.offset,
307
+ },
308
+ };
309
+ const authHeaders = await this.generateAuthHeaders(rpcRequest);
310
+ return await this.rpc(rpcRequest, authHeaders);
311
+ }
312
+ /**
313
+ * Delete an offer by ID
314
+ */
315
+ async deleteOffer(offerId) {
316
+ const request = {
317
+ method: 'deleteOffer',
318
+ params: { offerId },
319
+ };
320
+ const authHeaders = await this.generateAuthHeaders(request);
321
+ return await this.rpc(request, authHeaders);
322
+ }
323
+ // ============================================
324
+ // WebRTC Signaling
325
+ // ============================================
326
+ /**
327
+ * Answer an offer
328
+ */
329
+ async answerOffer(offerId, sdp) {
330
+ const request = {
331
+ method: 'answerOffer',
332
+ params: { offerId, sdp },
333
+ };
334
+ const authHeaders = await this.generateAuthHeaders(request);
335
+ await this.rpc(request, authHeaders);
336
+ }
337
+ /**
338
+ * Get answer for a specific offer (offerer polls this)
339
+ */
340
+ async getOfferAnswer(offerId) {
341
+ try {
342
+ const request = {
343
+ method: 'getOfferAnswer',
344
+ params: { offerId },
345
+ };
346
+ const authHeaders = await this.generateAuthHeaders(request);
347
+ return await this.rpc(request, authHeaders);
348
+ }
349
+ catch (err) {
350
+ if (err.message.includes('not yet answered')) {
351
+ return null;
352
+ }
353
+ throw err;
354
+ }
355
+ }
356
+ /**
357
+ * Combined polling for answers and ICE candidates
358
+ */
359
+ async poll(since) {
360
+ const request = {
361
+ method: 'poll',
362
+ params: { since },
363
+ };
364
+ const authHeaders = await this.generateAuthHeaders(request);
365
+ return await this.rpc(request, authHeaders);
366
+ }
367
+ /**
368
+ * Add ICE candidates to a specific offer
369
+ */
370
+ async addOfferIceCandidates(offerId, candidates) {
371
+ const request = {
372
+ method: 'addIceCandidates',
373
+ params: { offerId, candidates },
374
+ };
375
+ const authHeaders = await this.generateAuthHeaders(request);
376
+ return await this.rpc(request, authHeaders);
377
+ }
378
+ /**
379
+ * Get ICE candidates for a specific offer
380
+ */
381
+ async getOfferIceCandidates(offerId, since = 0) {
382
+ const request = {
383
+ method: 'getIceCandidates',
384
+ params: { offerId, since },
385
+ };
386
+ const authHeaders = await this.generateAuthHeaders(request);
387
+ const result = await this.rpc(request, authHeaders);
388
+ return {
389
+ candidates: result.candidates || [],
390
+ offerId: result.offerId,
391
+ };
392
+ }
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
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * Answerer-side WebRTC connection with answer creation and offer processing
3
3
  */
4
- import { RondevuConnection } from './connection.js';
5
- import { RondevuAPI } from './api.js';
6
- import { ConnectionConfig } from './connection-config.js';
4
+ import { RondevuConnection } from './base.js';
5
+ import { RondevuAPI, IceCandidate } from '../api/client.js';
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);
@@ -30,15 +34,28 @@ export declare class AnswererConnection extends RondevuConnection {
30
34
  */
31
35
  protected onLocalIceCandidate(candidate: RTCIceCandidate): void;
32
36
  /**
33
- * Poll for remote ICE candidates (from offerer)
37
+ * Get the API instance
34
38
  */
35
- protected pollIceCandidates(): void;
39
+ protected getApi(): any;
36
40
  /**
37
- * Attempt to reconnect
41
+ * Get the owner username
42
+ */
43
+ protected getOwnerUsername(): string;
44
+ /**
45
+ * Answerers accept ICE candidates from offerers only
46
+ */
47
+ protected getIceCandidateRole(): 'offerer' | null;
48
+ /**
49
+ * Attempt to reconnect to the same user
38
50
  */
39
51
  protected attemptReconnect(): void;
40
52
  /**
41
53
  * Get the offer ID we're answering
42
54
  */
43
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;
44
61
  }