@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.
- package/README.md +83 -385
- package/dist/api/batcher.d.ts +60 -38
- package/dist/api/batcher.js +121 -77
- package/dist/api/client.d.ts +104 -61
- package/dist/api/client.js +273 -185
- package/dist/connections/answerer.d.ts +15 -6
- package/dist/connections/answerer.js +56 -19
- package/dist/connections/base.d.ts +6 -4
- package/dist/connections/base.js +26 -16
- package/dist/connections/config.d.ts +30 -0
- package/dist/connections/config.js +20 -0
- package/dist/connections/events.d.ts +6 -6
- package/dist/connections/offerer.d.ts +37 -8
- package/dist/connections/offerer.js +92 -24
- package/dist/core/ice-config.d.ts +35 -0
- package/dist/core/ice-config.js +111 -0
- package/dist/core/index.d.ts +18 -18
- package/dist/core/index.js +18 -13
- package/dist/core/offer-pool.d.ts +30 -11
- package/dist/core/offer-pool.js +90 -76
- package/dist/core/peer.d.ts +158 -0
- package/dist/core/peer.js +254 -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 +106 -209
- package/dist/core/rondevu.js +222 -349
- package/dist/crypto/adapter.d.ts +25 -9
- package/dist/crypto/node.d.ts +27 -5
- package/dist/crypto/node.js +96 -25
- package/dist/crypto/web.d.ts +26 -4
- package/dist/crypto/web.js +102 -25
- package/dist/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 +17 -6
package/dist/api/client.js
CHANGED
|
@@ -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,
|
|
10
|
+
constructor(baseUrl, credential, cryptoAdapter, batcherOptions) {
|
|
11
11
|
this.baseUrl = baseUrl;
|
|
12
|
-
this.
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
*
|
|
46
|
+
* Canonical JSON serialization with sorted keys
|
|
47
|
+
* Ensures deterministic output regardless of property insertion order
|
|
24
48
|
*/
|
|
25
|
-
canonicalJSON(obj) {
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
113
|
-
|
|
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
|
-
*
|
|
120
|
-
*
|
|
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
|
-
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
150
|
-
*
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
*
|
|
158
|
-
*
|
|
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
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
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
|
-
*
|
|
166
|
-
*
|
|
163
|
+
* Execute RPC call via batcher
|
|
164
|
+
* Requests are batched with throttling for efficiency
|
|
167
165
|
*/
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
//
|
|
170
|
+
// Credential Management
|
|
174
171
|
// ============================================
|
|
175
172
|
/**
|
|
176
|
-
*
|
|
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
|
|
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: '
|
|
181
|
-
params:
|
|
198
|
+
method: 'generateCredentials',
|
|
199
|
+
params: Object.keys(params).length > 0 ? params : undefined,
|
|
182
200
|
};
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
*
|
|
270
|
+
* Generate a random secret locally (for advanced use cases)
|
|
271
|
+
* @param cryptoAdapter - Optional crypto adapter
|
|
189
272
|
*/
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
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
|
-
//
|
|
278
|
+
// Tags-based Offer Management (v2)
|
|
201
279
|
// ============================================
|
|
202
280
|
/**
|
|
203
|
-
* Publish
|
|
281
|
+
* Publish offers with tags
|
|
204
282
|
*/
|
|
205
|
-
async
|
|
206
|
-
const
|
|
207
|
-
method: '
|
|
283
|
+
async publish(request) {
|
|
284
|
+
const rpcRequest = {
|
|
285
|
+
method: 'publishOffer',
|
|
208
286
|
params: {
|
|
209
|
-
|
|
210
|
-
offers:
|
|
211
|
-
ttl:
|
|
287
|
+
tags: request.tags,
|
|
288
|
+
offers: request.offers,
|
|
289
|
+
ttl: request.ttl,
|
|
212
290
|
},
|
|
213
291
|
};
|
|
214
|
-
const authHeaders = await this.generateAuthHeaders(
|
|
215
|
-
return await this.rpc(
|
|
292
|
+
const authHeaders = await this.generateAuthHeaders(rpcRequest);
|
|
293
|
+
return await this.rpc(rpcRequest, authHeaders);
|
|
216
294
|
}
|
|
217
295
|
/**
|
|
218
|
-
*
|
|
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
|
|
221
|
-
const
|
|
222
|
-
method: '
|
|
300
|
+
async discover(request) {
|
|
301
|
+
const rpcRequest = {
|
|
302
|
+
method: 'discover',
|
|
223
303
|
params: {
|
|
224
|
-
|
|
225
|
-
|
|
304
|
+
tags: request.tags,
|
|
305
|
+
limit: request.limit,
|
|
306
|
+
offset: request.offset,
|
|
226
307
|
},
|
|
227
308
|
};
|
|
228
|
-
const authHeaders = await this.generateAuthHeaders(
|
|
229
|
-
return await this.rpc(
|
|
309
|
+
const authHeaders = await this.generateAuthHeaders(rpcRequest);
|
|
310
|
+
return await this.rpc(rpcRequest, authHeaders);
|
|
230
311
|
}
|
|
231
312
|
/**
|
|
232
|
-
* Delete
|
|
313
|
+
* Delete an offer by ID
|
|
233
314
|
*/
|
|
234
|
-
async
|
|
315
|
+
async deleteOffer(offerId) {
|
|
235
316
|
const request = {
|
|
236
|
-
method: '
|
|
237
|
-
params: {
|
|
317
|
+
method: 'deleteOffer',
|
|
318
|
+
params: { offerId },
|
|
238
319
|
};
|
|
239
|
-
const authHeaders = await this.generateAuthHeaders(request
|
|
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(
|
|
329
|
+
async answerOffer(offerId, sdp) {
|
|
249
330
|
const request = {
|
|
250
331
|
method: 'answerOffer',
|
|
251
|
-
params: {
|
|
332
|
+
params: { offerId, sdp },
|
|
252
333
|
};
|
|
253
|
-
const authHeaders = await this.generateAuthHeaders(request
|
|
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(
|
|
340
|
+
async getOfferAnswer(offerId) {
|
|
260
341
|
try {
|
|
261
342
|
const request = {
|
|
262
343
|
method: 'getOfferAnswer',
|
|
263
|
-
params: {
|
|
344
|
+
params: { offerId },
|
|
264
345
|
};
|
|
265
|
-
const authHeaders = await this.generateAuthHeaders(request
|
|
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
|
|
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(
|
|
370
|
+
async addOfferIceCandidates(offerId, candidates) {
|
|
290
371
|
const request = {
|
|
291
372
|
method: 'addIceCandidates',
|
|
292
|
-
params: {
|
|
373
|
+
params: { offerId, candidates },
|
|
293
374
|
};
|
|
294
|
-
const authHeaders = await this.generateAuthHeaders(request
|
|
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(
|
|
381
|
+
async getOfferIceCandidates(offerId, since = 0) {
|
|
301
382
|
const request = {
|
|
302
383
|
method: 'getIceCandidates',
|
|
303
|
-
params: {
|
|
384
|
+
params: { offerId, since },
|
|
304
385
|
};
|
|
305
|
-
const authHeaders = await this.generateAuthHeaders(request
|
|
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
|
-
|
|
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
|
|
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
|
|
41
|
+
* Get the owner username
|
|
38
42
|
*/
|
|
39
|
-
protected
|
|
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
|
}
|