@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
@@ -1,111 +1,155 @@
1
1
  /**
2
- * RPC Batcher - Throttles and batches RPC requests to reduce HTTP overhead
2
+ * RPC Request Batcher with throttling
3
+ *
4
+ * Collects RPC requests over a short time window and sends them efficiently.
5
+ *
6
+ * Due to server authentication design (signature covers method+params),
7
+ * authenticated requests are sent individually while unauthenticated
8
+ * requests can be truly batched together.
3
9
  */
4
10
  /**
5
- * Batches and throttles RPC requests to optimize network usage
11
+ * RpcBatcher - Batches RPC requests with throttling
6
12
  *
7
13
  * @example
8
14
  * ```typescript
9
- * const batcher = new RpcBatcher(
10
- * (requests) => api.rpcBatch(requests),
11
- * { maxBatchSize: 10, maxWaitTime: 50 }
12
- * )
15
+ * const batcher = new RpcBatcher('https://api.example.com', {
16
+ * delay: 10,
17
+ * maxBatchSize: 50
18
+ * })
13
19
  *
14
- * // These will be batched together if called within maxWaitTime
15
- * const result1 = await batcher.add(request1)
16
- * const result2 = await batcher.add(request2)
17
- * const result3 = await batcher.add(request3)
20
+ * // Requests made within the delay window are batched
21
+ * const [result1, result2] = await Promise.all([
22
+ * batcher.add({ method: 'getOffer', params: {...} }, null),
23
+ * batcher.add({ method: 'getOffer', params: {...} }, null)
24
+ * ])
18
25
  * ```
19
26
  */
20
27
  export class RpcBatcher {
21
- constructor(sendBatch, options) {
28
+ constructor(baseUrl, options = {}) {
29
+ this.baseUrl = baseUrl;
22
30
  this.queue = [];
23
- this.batchTimeout = null;
24
- this.lastBatchTime = 0;
25
- this.sendBatch = sendBatch;
26
- this.options = {
27
- maxBatchSize: options?.maxBatchSize ?? 10,
28
- maxWaitTime: options?.maxWaitTime ?? 50,
29
- throttleInterval: options?.throttleInterval ?? 10,
30
- };
31
+ this.flushTimer = null;
32
+ this.delay = options.delay ?? 10;
33
+ this.maxBatchSize = options.maxBatchSize ?? 50;
31
34
  }
32
35
  /**
33
- * Add an RPC request to the batch queue
34
- * Returns a promise that resolves when the request completes
36
+ * Add a request to the batch queue
37
+ * @param request - The RPC request
38
+ * @param authHeaders - Auth headers for authenticated requests, null for unauthenticated
39
+ * @returns Promise that resolves with the request result
35
40
  */
36
- async add(request) {
41
+ add(request, authHeaders) {
37
42
  return new Promise((resolve, reject) => {
38
- this.queue.push({ request, resolve, reject });
39
- // Send immediately if batch is full
40
- if (this.queue.length >= this.options.maxBatchSize) {
41
- this.flush();
42
- return;
43
- }
44
- // Schedule batch if not already scheduled
45
- if (!this.batchTimeout) {
46
- this.batchTimeout = setTimeout(() => {
47
- this.flush();
48
- }, this.options.maxWaitTime);
49
- }
43
+ this.queue.push({ request, authHeaders, resolve, reject });
44
+ this.scheduleFlush();
50
45
  });
51
46
  }
52
47
  /**
53
- * Flush the queue immediately
48
+ * Schedule a flush after the delay
49
+ */
50
+ scheduleFlush() {
51
+ if (this.flushTimer)
52
+ return;
53
+ this.flushTimer = setTimeout(() => {
54
+ this.flushTimer = null;
55
+ this.flush();
56
+ }, this.delay);
57
+ }
58
+ /**
59
+ * Flush all queued requests
54
60
  */
55
61
  async flush() {
56
- // Clear timeout if set
57
- if (this.batchTimeout) {
58
- clearTimeout(this.batchTimeout);
59
- this.batchTimeout = null;
60
- }
61
- // Nothing to flush
62
- if (this.queue.length === 0) {
62
+ if (this.queue.length === 0)
63
63
  return;
64
- }
65
- // Throttle: wait if we sent a batch too recently
66
- const now = Date.now();
67
- const timeSinceLastBatch = now - this.lastBatchTime;
68
- if (timeSinceLastBatch < this.options.throttleInterval) {
69
- const waitTime = this.options.throttleInterval - timeSinceLastBatch;
70
- await new Promise(resolve => setTimeout(resolve, waitTime));
71
- }
72
- // Extract requests from queue
73
- const batch = this.queue.splice(0, this.options.maxBatchSize);
74
- const requests = batch.map(item => item.request);
75
- this.lastBatchTime = Date.now();
76
- try {
77
- // Send batch request
78
- const results = await this.sendBatch(requests);
79
- // Resolve individual promises
80
- for (let i = 0; i < batch.length; i++) {
81
- batch[i].resolve(results[i]);
64
+ const items = this.queue;
65
+ this.queue = [];
66
+ // Separate authenticated vs unauthenticated requests
67
+ const unauthenticated = [];
68
+ const authenticated = [];
69
+ for (const item of items) {
70
+ if (item.authHeaders) {
71
+ authenticated.push(item);
82
72
  }
83
- }
84
- catch (error) {
85
- // Reject all promises in batch
86
- for (const item of batch) {
87
- item.reject(error);
73
+ else {
74
+ unauthenticated.push(item);
88
75
  }
89
76
  }
77
+ // Process unauthenticated requests in batches
78
+ await this.processUnauthenticatedBatches(unauthenticated);
79
+ // Process authenticated requests individually (each needs unique signature)
80
+ await this.processAuthenticatedRequests(authenticated);
90
81
  }
91
82
  /**
92
- * Get current queue size
83
+ * Process unauthenticated requests in batches
93
84
  */
94
- getQueueSize() {
95
- return this.queue.length;
85
+ async processUnauthenticatedBatches(items) {
86
+ if (items.length === 0)
87
+ return;
88
+ // Split into chunks of maxBatchSize
89
+ for (let i = 0; i < items.length; i += this.maxBatchSize) {
90
+ const chunk = items.slice(i, i + this.maxBatchSize);
91
+ await this.sendBatch(chunk, null);
92
+ }
93
+ }
94
+ /**
95
+ * Process authenticated requests individually
96
+ * Each authenticated request needs its own HTTP call because
97
+ * the signature covers the specific method+params
98
+ */
99
+ async processAuthenticatedRequests(items) {
100
+ // Send all authenticated requests in parallel, each as its own batch of 1
101
+ await Promise.all(items.map(item => this.sendBatch([item], item.authHeaders)));
96
102
  }
97
103
  /**
98
- * Clear the queue without sending
104
+ * Send a batch of requests
99
105
  */
100
- clear() {
101
- if (this.batchTimeout) {
102
- clearTimeout(this.batchTimeout);
103
- this.batchTimeout = null;
106
+ async sendBatch(items, authHeaders) {
107
+ try {
108
+ const requests = items.map(item => item.request);
109
+ const headers = {
110
+ 'Content-Type': 'application/json',
111
+ };
112
+ if (authHeaders) {
113
+ Object.assign(headers, authHeaders);
114
+ }
115
+ const response = await fetch(`${this.baseUrl}/rpc`, {
116
+ method: 'POST',
117
+ headers,
118
+ body: JSON.stringify(requests), // Always send as array
119
+ });
120
+ if (!response.ok) {
121
+ const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
122
+ items.forEach(item => item.reject(error));
123
+ return;
124
+ }
125
+ const results = await response.json();
126
+ // Match responses to requests (server returns array in same order)
127
+ items.forEach((item, index) => {
128
+ const result = results[index];
129
+ if (!result) {
130
+ item.reject(new Error('Missing response from server'));
131
+ }
132
+ else if (!result.success) {
133
+ item.reject(new Error(result.error || 'RPC call failed'));
134
+ }
135
+ else {
136
+ item.resolve(result.result);
137
+ }
138
+ });
104
139
  }
105
- // Reject all pending requests
106
- for (const item of this.queue) {
107
- item.reject(new Error('Batch queue cleared'));
140
+ catch (error) {
141
+ // Network or parsing error - reject all
142
+ items.forEach(item => item.reject(error));
108
143
  }
109
- this.queue = [];
144
+ }
145
+ /**
146
+ * Flush immediately (useful for cleanup/testing)
147
+ */
148
+ async flushNow() {
149
+ if (this.flushTimer) {
150
+ clearTimeout(this.flushTimer);
151
+ this.flushTimer = null;
152
+ }
153
+ await this.flush();
110
154
  }
111
155
  }
@@ -1,29 +1,46 @@
1
1
  /**
2
2
  * Rondevu API Client - RPC interface
3
3
  */
4
- import { CryptoAdapter, Keypair } from '../crypto/adapter.js';
4
+ import { CryptoAdapter, Credential } from '../crypto/adapter.js';
5
5
  import { BatcherOptions } from './batcher.js';
6
- export type { Keypair } from '../crypto/adapter.js';
6
+ export type { Credential } from '../crypto/adapter.js';
7
7
  export type { BatcherOptions } from './batcher.js';
8
8
  export interface OfferRequest {
9
9
  sdp: string;
10
10
  }
11
- export interface ServiceRequest {
12
- serviceFqn: string;
11
+ export interface PublishRequest {
12
+ tags: string[];
13
13
  offers: OfferRequest[];
14
14
  ttl?: number;
15
15
  }
16
- export interface ServiceOffer {
16
+ export interface DiscoverRequest {
17
+ tags: string[];
18
+ limit?: number;
19
+ offset?: number;
20
+ }
21
+ export interface TaggedOffer {
17
22
  offerId: string;
23
+ username: string;
24
+ tags: string[];
18
25
  sdp: string;
19
26
  createdAt: number;
20
27
  expiresAt: number;
21
28
  }
22
- export interface Service {
23
- serviceId: string;
24
- offers: ServiceOffer[];
29
+ export interface DiscoverResponse {
30
+ offers: TaggedOffer[];
31
+ count: number;
32
+ limit: number;
33
+ offset: number;
34
+ }
35
+ export interface PublishResponse {
25
36
  username: string;
26
- serviceFqn: string;
37
+ tags: string[];
38
+ offers: Array<{
39
+ offerId: string;
40
+ sdp: string;
41
+ createdAt: number;
42
+ expiresAt: number;
43
+ }>;
27
44
  createdAt: number;
28
45
  expiresAt: number;
29
46
  }
@@ -37,84 +54,111 @@ export interface IceCandidate {
37
54
  */
38
55
  export declare class RondevuAPI {
39
56
  private baseUrl;
40
- private username;
41
- private keypair;
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;
42
64
  private crypto;
43
65
  private batcher;
44
- constructor(baseUrl: string, username: string, keypair: Keypair, cryptoAdapter?: CryptoAdapter, batcherOptions?: BatcherOptions | false);
66
+ constructor(baseUrl: string, credential: Credential, cryptoAdapter?: CryptoAdapter, batcherOptions?: BatcherOptions);
45
67
  /**
46
- * Create canonical JSON string with sorted keys for deterministic signing
68
+ * Canonical JSON serialization with sorted keys
69
+ * Ensures deterministic output regardless of property insertion order
47
70
  */
48
71
  private canonicalJSON;
49
72
  /**
50
- * Generate authentication headers for RPC request
51
- * Signs the payload (method + params + timestamp + username)
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.
52
81
  */
53
- private generateAuthHeaders;
82
+ private buildSignatureMessage;
54
83
  /**
55
- * Generate authentication fields embedded in request body (for batch requests)
56
- * Signs the payload (method + params + timestamp + username)
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.
57
89
  */
58
- private generateAuthForRequest;
59
- /**
60
- * Execute RPC call with optional batching
61
- */
62
- private rpc;
90
+ private generateNonce;
63
91
  /**
64
- * Execute single RPC call directly (bypasses batcher)
65
- */
66
- private rpcDirect;
67
- /**
68
- * Execute batch RPC calls directly (bypasses batcher)
69
- * Each request in the batch has its own embedded authentication (signature, timestamp, username, publicKey)
70
- */
71
- private rpcBatchDirect;
72
- /**
73
- * Generate an Ed25519 keypair for username claiming and service publishing
74
- * @param cryptoAdapter - Optional crypto adapter (defaults to WebCryptoAdapter)
75
- */
76
- static generateKeypair(cryptoAdapter?: CryptoAdapter): Promise<Keypair>;
77
- /**
78
- * Sign a message with an Ed25519 private key
79
- * @param cryptoAdapter - Optional crypto adapter (defaults to WebCryptoAdapter)
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
80
105
  */
81
- static signMessage(message: string, privateKeyBase64: string, cryptoAdapter?: CryptoAdapter): Promise<string>;
106
+ private generateAuthHeaders;
82
107
  /**
83
- * Verify an Ed25519 signature
84
- * @param cryptoAdapter - Optional crypto adapter (defaults to WebCryptoAdapter)
108
+ * Execute RPC call via batcher
109
+ * Requests are batched with throttling for efficiency
85
110
  */
86
- static verifySignature(message: string, signatureBase64: string, publicKeyBase64: string, cryptoAdapter?: CryptoAdapter): Promise<boolean>;
111
+ private rpc;
87
112
  /**
88
- * Check if a username is available
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
89
126
  */
90
- isUsernameAvailable(username: string): Promise<boolean>;
127
+ static generateCredentials(baseUrl: string, options?: {
128
+ name?: string;
129
+ expiresAt?: number;
130
+ maxRetries?: number;
131
+ timeout?: number;
132
+ }): Promise<Credential>;
91
133
  /**
92
- * Check if current username is claimed
134
+ * Generate a random secret locally (for advanced use cases)
135
+ * @param cryptoAdapter - Optional crypto adapter
93
136
  */
94
- isUsernameClaimed(): Promise<boolean>;
137
+ static generateSecret(cryptoAdapter?: CryptoAdapter): string;
95
138
  /**
96
- * Publish a service
139
+ * Publish offers with tags
97
140
  */
98
- publishService(service: ServiceRequest): Promise<Service>;
141
+ publish(request: PublishRequest): Promise<PublishResponse>;
99
142
  /**
100
- * Get service by FQN (direct lookup, random, or paginated)
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
101
146
  */
102
- getService(serviceFqn: string, options?: {
103
- limit?: number;
104
- offset?: number;
105
- }): Promise<any>;
147
+ discover(request: DiscoverRequest): Promise<DiscoverResponse | TaggedOffer>;
106
148
  /**
107
- * Delete a service
149
+ * Delete an offer by ID
108
150
  */
109
- deleteService(serviceFqn: string): Promise<void>;
151
+ deleteOffer(offerId: string): Promise<{
152
+ success: boolean;
153
+ }>;
110
154
  /**
111
155
  * Answer an offer
112
156
  */
113
- answerOffer(serviceFqn: string, offerId: string, sdp: string): Promise<void>;
157
+ answerOffer(offerId: string, sdp: string): Promise<void>;
114
158
  /**
115
159
  * Get answer for a specific offer (offerer polls this)
116
160
  */
117
- getOfferAnswer(serviceFqn: string, offerId: string): Promise<{
161
+ getOfferAnswer(offerId: string): Promise<{
118
162
  sdp: string;
119
163
  offerId: string;
120
164
  answererId: string;
@@ -126,7 +170,6 @@ export declare class RondevuAPI {
126
170
  poll(since?: number): Promise<{
127
171
  answers: Array<{
128
172
  offerId: string;
129
- serviceId?: string;
130
173
  answererId: string;
131
174
  sdp: string;
132
175
  answeredAt: number;
@@ -141,14 +184,14 @@ export declare class RondevuAPI {
141
184
  /**
142
185
  * Add ICE candidates to a specific offer
143
186
  */
144
- addOfferIceCandidates(serviceFqn: string, offerId: string, candidates: RTCIceCandidateInit[]): Promise<{
187
+ addOfferIceCandidates(offerId: string, candidates: RTCIceCandidateInit[]): Promise<{
145
188
  count: number;
146
189
  offerId: string;
147
190
  }>;
148
191
  /**
149
192
  * Get ICE candidates for a specific offer
150
193
  */
151
- getOfferIceCandidates(serviceFqn: string, offerId: string, since?: number): Promise<{
194
+ getOfferIceCandidates(offerId: string, since?: number): Promise<{
152
195
  candidates: IceCandidate[];
153
196
  offerId: string;
154
197
  }>;