@xtr-dev/rondevu-client 0.21.13 → 0.21.16

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.
@@ -1,16 +1,23 @@
1
1
  /**
2
2
  * RPC Request Batcher with throttling
3
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.
4
+ * Collects RPC requests over a short time window and sends them in a single
5
+ * HTTP request. Each request includes its own auth credentials in the JSON body,
6
+ * allowing true batching of authenticated requests.
9
7
  */
10
8
  export interface RpcRequest {
11
9
  method: string;
12
10
  params?: any;
13
11
  }
12
+ /**
13
+ * Per-request authentication credentials
14
+ */
15
+ export interface RequestAuth {
16
+ publicKey: string;
17
+ timestamp: number;
18
+ nonce: string;
19
+ signature: string;
20
+ }
14
21
  export interface RpcResponse {
15
22
  success: boolean;
16
23
  result?: any;
@@ -20,12 +27,16 @@ export interface RpcResponse {
20
27
  export interface BatcherOptions {
21
28
  /** Delay in ms before flushing queued requests (default: 10) */
22
29
  delay?: number;
23
- /** Maximum batch size for unauthenticated requests (default: 50) */
30
+ /** Maximum batch size (default: 50) */
24
31
  maxBatchSize?: number;
25
32
  }
26
33
  /**
27
34
  * RpcBatcher - Batches RPC requests with throttling
28
35
  *
36
+ * All requests (authenticated and unauthenticated) are batched together
37
+ * into a single HTTP request. Auth credentials are included per-request
38
+ * in the JSON body.
39
+ *
29
40
  * @example
30
41
  * ```typescript
31
42
  * const batcher = new RpcBatcher('https://api.example.com', {
@@ -33,10 +44,10 @@ export interface BatcherOptions {
33
44
  * maxBatchSize: 50
34
45
  * })
35
46
  *
36
- * // Requests made within the delay window are batched
47
+ * // Requests made within the delay window are batched into ONE HTTP call
37
48
  * const [result1, result2] = await Promise.all([
38
- * batcher.add({ method: 'getOffer', params: {...} }, null),
39
- * batcher.add({ method: 'getOffer', params: {...} }, null)
49
+ * batcher.add({ method: 'publish', params: {...} }, auth1),
50
+ * batcher.add({ method: 'discover', params: {...} }, auth2)
40
51
  * ])
41
52
  * ```
42
53
  */
@@ -50,10 +61,10 @@ export declare class RpcBatcher {
50
61
  /**
51
62
  * Add a request to the batch queue
52
63
  * @param request - The RPC request
53
- * @param authHeaders - Auth headers for authenticated requests, null for unauthenticated
64
+ * @param auth - Per-request auth credentials, null for unauthenticated
54
65
  * @returns Promise that resolves with the request result
55
66
  */
56
- add(request: RpcRequest, authHeaders: Record<string, string> | null): Promise<any>;
67
+ add(request: RpcRequest, auth: RequestAuth | null): Promise<any>;
57
68
  /**
58
69
  * Schedule a flush after the delay
59
70
  */
@@ -63,17 +74,8 @@ export declare class RpcBatcher {
63
74
  */
64
75
  private flush;
65
76
  /**
66
- * Process unauthenticated requests in batches
67
- */
68
- private processUnauthenticatedBatches;
69
- /**
70
- * Process authenticated requests individually
71
- * Each authenticated request needs its own HTTP call because
72
- * the signature covers the specific method+params
73
- */
74
- private processAuthenticatedRequests;
75
- /**
76
- * Send a batch of requests
77
+ * Send a batch of requests in a single HTTP call
78
+ * Each request includes its own auth credentials in the JSON body
77
79
  */
78
80
  private sendBatch;
79
81
  /**
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * RPC Request Batcher with throttling
3
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.
4
+ * Collects RPC requests over a short time window and sends them in a single
5
+ * HTTP request. Each request includes its own auth credentials in the JSON body,
6
+ * allowing true batching of authenticated requests.
9
7
  */
10
8
  /**
11
9
  * RpcBatcher - Batches RPC requests with throttling
12
10
  *
11
+ * All requests (authenticated and unauthenticated) are batched together
12
+ * into a single HTTP request. Auth credentials are included per-request
13
+ * in the JSON body.
14
+ *
13
15
  * @example
14
16
  * ```typescript
15
17
  * const batcher = new RpcBatcher('https://api.example.com', {
@@ -17,10 +19,10 @@
17
19
  * maxBatchSize: 50
18
20
  * })
19
21
  *
20
- * // Requests made within the delay window are batched
22
+ * // Requests made within the delay window are batched into ONE HTTP call
21
23
  * const [result1, result2] = await Promise.all([
22
- * batcher.add({ method: 'getOffer', params: {...} }, null),
23
- * batcher.add({ method: 'getOffer', params: {...} }, null)
24
+ * batcher.add({ method: 'publish', params: {...} }, auth1),
25
+ * batcher.add({ method: 'discover', params: {...} }, auth2)
24
26
  * ])
25
27
  * ```
26
28
  */
@@ -35,12 +37,12 @@ export class RpcBatcher {
35
37
  /**
36
38
  * Add a request to the batch queue
37
39
  * @param request - The RPC request
38
- * @param authHeaders - Auth headers for authenticated requests, null for unauthenticated
40
+ * @param auth - Per-request auth credentials, null for unauthenticated
39
41
  * @returns Promise that resolves with the request result
40
42
  */
41
- add(request, authHeaders) {
43
+ add(request, auth) {
42
44
  return new Promise((resolve, reject) => {
43
- this.queue.push({ request, authHeaders, resolve, reject });
45
+ this.queue.push({ request, auth, resolve, reject });
44
46
  this.scheduleFlush();
45
47
  });
46
48
  }
@@ -63,59 +65,35 @@ export class RpcBatcher {
63
65
  return;
64
66
  const items = this.queue;
65
67
  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);
72
- }
73
- else {
74
- unauthenticated.push(item);
75
- }
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);
81
- }
82
- /**
83
- * Process unauthenticated requests in batches
84
- */
85
- async processUnauthenticatedBatches(items) {
86
- if (items.length === 0)
87
- return;
88
- // Split into chunks of maxBatchSize
68
+ // Split into chunks of maxBatchSize and send each chunk
89
69
  for (let i = 0; i < items.length; i += this.maxBatchSize) {
90
70
  const chunk = items.slice(i, i + this.maxBatchSize);
91
- await this.sendBatch(chunk, null);
71
+ await this.sendBatch(chunk);
92
72
  }
93
73
  }
94
74
  /**
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)));
102
- }
103
- /**
104
- * Send a batch of requests
75
+ * Send a batch of requests in a single HTTP call
76
+ * Each request includes its own auth credentials in the JSON body
105
77
  */
106
- async sendBatch(items, authHeaders) {
78
+ async sendBatch(items) {
107
79
  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
- }
80
+ // Build wire requests with per-request auth
81
+ const wireRequests = items.map(item => {
82
+ const wireReq = {
83
+ method: item.request.method,
84
+ params: item.request.params,
85
+ };
86
+ if (item.auth) {
87
+ wireReq.auth = item.auth;
88
+ }
89
+ return wireReq;
90
+ });
115
91
  const response = await fetch(`${this.baseUrl}/rpc`, {
116
92
  method: 'POST',
117
- headers,
118
- body: JSON.stringify(requests), // Always send as array
93
+ headers: {
94
+ 'Content-Type': 'application/json',
95
+ },
96
+ body: JSON.stringify(wireRequests),
119
97
  });
120
98
  if (!response.ok) {
121
99
  const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
@@ -4,7 +4,7 @@
4
4
  import { CryptoAdapter, KeyPair } from '../crypto/adapter.js';
5
5
  import { BatcherOptions } from './batcher.js';
6
6
  export type { KeyPair } from '../crypto/adapter.js';
7
- export type { BatcherOptions } from './batcher.js';
7
+ export type { BatcherOptions, RequestAuth } from './batcher.js';
8
8
  export interface OfferRequest {
9
9
  sdp: string;
10
10
  }
@@ -96,7 +96,7 @@ export declare class RondevuAPI {
96
96
  */
97
97
  private generateNonce;
98
98
  /**
99
- * Generate authentication headers for RPC request
99
+ * Generate per-request authentication credentials
100
100
  * Uses Ed25519 signature with nonce for replay protection
101
101
  *
102
102
  * Security notes:
@@ -110,10 +110,11 @@ export declare class RondevuAPI {
110
110
  * - Each nonce can only be used once within the timestamp validity window
111
111
  * - Server maintains nonce cache with expiration matching timestamp window
112
112
  */
113
- private generateAuthHeaders;
113
+ private generateAuth;
114
114
  /**
115
115
  * Execute RPC call via batcher
116
116
  * Requests are batched with throttling for efficiency
117
+ * All requests within the batch delay window are sent in a single HTTP call
117
118
  */
118
119
  private rpc;
119
120
  /**
@@ -147,6 +148,15 @@ export declare class RondevuAPI {
147
148
  deleteOffer(offerId: string): Promise<{
148
149
  success: boolean;
149
150
  }>;
151
+ /**
152
+ * Update tags for all offers owned by this identity
153
+ * @param tags New tags to set on all offers
154
+ * @returns Number of offers updated
155
+ */
156
+ updateOfferTags(tags: string[]): Promise<{
157
+ success: boolean;
158
+ count: number;
159
+ }>;
150
160
  /**
151
161
  * Answer an offer
152
162
  * @param offerId The offer ID to answer
@@ -128,7 +128,7 @@ export class RondevuAPI {
128
128
  return this.crypto.bytesToHex(randomBytes);
129
129
  }
130
130
  /**
131
- * Generate authentication headers for RPC request
131
+ * Generate per-request authentication credentials
132
132
  * Uses Ed25519 signature with nonce for replay protection
133
133
  *
134
134
  * Security notes:
@@ -142,25 +142,26 @@ export class RondevuAPI {
142
142
  * - Each nonce can only be used once within the timestamp validity window
143
143
  * - Server maintains nonce cache with expiration matching timestamp window
144
144
  */
145
- async generateAuthHeaders(request) {
145
+ async generateAuth(request) {
146
146
  const timestamp = Date.now();
147
147
  const nonce = this.generateNonce();
148
148
  // Build message and generate Ed25519 signature
149
149
  const message = this.buildSignatureMessage(timestamp, nonce, request.method, request.params);
150
150
  const signature = await this.crypto.signMessage(this.keyPair.privateKey, message);
151
151
  return {
152
- 'X-PublicKey': this.keyPair.publicKey,
153
- 'X-Timestamp': timestamp.toString(),
154
- 'X-Nonce': nonce,
155
- 'X-Signature': signature,
152
+ publicKey: this.keyPair.publicKey,
153
+ timestamp,
154
+ nonce,
155
+ signature,
156
156
  };
157
157
  }
158
158
  /**
159
159
  * Execute RPC call via batcher
160
160
  * Requests are batched with throttling for efficiency
161
+ * All requests within the batch delay window are sent in a single HTTP call
161
162
  */
162
- async rpc(request, authHeaders) {
163
- return this.batcher.add(request, authHeaders);
163
+ async rpc(request, auth) {
164
+ return this.batcher.add(request, auth);
164
165
  }
165
166
  // ============================================
166
167
  // Identity Management (Ed25519 Public Key)
@@ -191,8 +192,8 @@ export class RondevuAPI {
191
192
  ttl: request.ttl,
192
193
  },
193
194
  };
194
- const authHeaders = await this.generateAuthHeaders(rpcRequest);
195
- return await this.rpc(rpcRequest, authHeaders);
195
+ const auth = await this.generateAuth(rpcRequest);
196
+ return await this.rpc(rpcRequest, auth);
196
197
  }
197
198
  /**
198
199
  * Discover offers by tags
@@ -208,8 +209,8 @@ export class RondevuAPI {
208
209
  offset: request.offset,
209
210
  },
210
211
  };
211
- const authHeaders = await this.generateAuthHeaders(rpcRequest);
212
- return await this.rpc(rpcRequest, authHeaders);
212
+ const auth = await this.generateAuth(rpcRequest);
213
+ return await this.rpc(rpcRequest, auth);
213
214
  }
214
215
  /**
215
216
  * Count available offers by tags
@@ -225,8 +226,8 @@ export class RondevuAPI {
225
226
  ...(request.unique !== undefined && { unique: request.unique }),
226
227
  },
227
228
  };
228
- const authHeaders = await this.generateAuthHeaders(rpcRequest);
229
- return await this.rpc(rpcRequest, authHeaders);
229
+ const auth = await this.generateAuth(rpcRequest);
230
+ return await this.rpc(rpcRequest, auth);
230
231
  }
231
232
  /**
232
233
  * Delete an offer by ID
@@ -236,8 +237,21 @@ export class RondevuAPI {
236
237
  method: 'deleteOffer',
237
238
  params: { offerId },
238
239
  };
239
- const authHeaders = await this.generateAuthHeaders(request);
240
- return await this.rpc(request, authHeaders);
240
+ const auth = await this.generateAuth(request);
241
+ return await this.rpc(request, auth);
242
+ }
243
+ /**
244
+ * Update tags for all offers owned by this identity
245
+ * @param tags New tags to set on all offers
246
+ * @returns Number of offers updated
247
+ */
248
+ async updateOfferTags(tags) {
249
+ const request = {
250
+ method: 'updateOfferTags',
251
+ params: { tags },
252
+ };
253
+ const auth = await this.generateAuth(request);
254
+ return await this.rpc(request, auth);
241
255
  }
242
256
  // ============================================
243
257
  // WebRTC Signaling
@@ -253,8 +267,8 @@ export class RondevuAPI {
253
267
  method: 'answerOffer',
254
268
  params: { offerId, sdp, matchedTags },
255
269
  };
256
- const authHeaders = await this.generateAuthHeaders(request);
257
- await this.rpc(request, authHeaders);
270
+ const auth = await this.generateAuth(request);
271
+ await this.rpc(request, auth);
258
272
  }
259
273
  /**
260
274
  * Get answer for a specific offer (offerer polls this)
@@ -265,8 +279,8 @@ export class RondevuAPI {
265
279
  method: 'getOfferAnswer',
266
280
  params: { offerId },
267
281
  };
268
- const authHeaders = await this.generateAuthHeaders(request);
269
- return await this.rpc(request, authHeaders);
282
+ const auth = await this.generateAuth(request);
283
+ return await this.rpc(request, auth);
270
284
  }
271
285
  catch (err) {
272
286
  if (err.message.includes('not yet answered')) {
@@ -283,8 +297,8 @@ export class RondevuAPI {
283
297
  method: 'poll',
284
298
  params: { since },
285
299
  };
286
- const authHeaders = await this.generateAuthHeaders(request);
287
- return await this.rpc(request, authHeaders);
300
+ const auth = await this.generateAuth(request);
301
+ return await this.rpc(request, auth);
288
302
  }
289
303
  /**
290
304
  * Add ICE candidates to a specific offer
@@ -294,8 +308,8 @@ export class RondevuAPI {
294
308
  method: 'addIceCandidates',
295
309
  params: { offerId, candidates },
296
310
  };
297
- const authHeaders = await this.generateAuthHeaders(request);
298
- return await this.rpc(request, authHeaders);
311
+ const auth = await this.generateAuth(request);
312
+ return await this.rpc(request, auth);
299
313
  }
300
314
  /**
301
315
  * Get ICE candidates for a specific offer
@@ -305,8 +319,8 @@ export class RondevuAPI {
305
319
  method: 'getIceCandidates',
306
320
  params: { offerId, since },
307
321
  };
308
- const authHeaders = await this.generateAuthHeaders(request);
309
- const result = await this.rpc(request, authHeaders);
322
+ const auth = await this.generateAuth(request);
323
+ const result = await this.rpc(request, auth);
310
324
  return {
311
325
  candidates: result.candidates || [],
312
326
  offerId: result.offerId,
@@ -15,6 +15,8 @@ export interface AnswererOptions {
15
15
  webrtcAdapter?: WebRTCAdapter;
16
16
  config?: Partial<ConnectionConfig>;
17
17
  matchedTags?: string[];
18
+ /** Callback invoked when RTCPeerConnection is created, before signaling starts */
19
+ onPeerConnectionCreated?: (pc: RTCPeerConnection) => void;
18
20
  }
19
21
  /**
20
22
  * Answerer connection - processes offers and creates answers
@@ -26,6 +28,7 @@ export declare class AnswererConnection extends RondevuConnection {
26
28
  private offerId;
27
29
  private offerSdp;
28
30
  private matchedTags?;
31
+ private onPeerConnectionCreated?;
29
32
  constructor(options: AnswererOptions);
30
33
  /**
31
34
  * Initialize the connection by processing offer and creating answer
@@ -15,6 +15,7 @@ export class AnswererConnection extends RondevuConnection {
15
15
  this.offerId = options.offerId;
16
16
  this.offerSdp = options.offerSdp;
17
17
  this.matchedTags = options.matchedTags;
18
+ this.onPeerConnectionCreated = options.onPeerConnectionCreated;
18
19
  }
19
20
  /**
20
21
  * Initialize the connection by processing offer and creating answer
@@ -25,6 +26,11 @@ export class AnswererConnection extends RondevuConnection {
25
26
  this.createPeerConnection();
26
27
  if (!this.pc)
27
28
  throw new Error('Peer connection not created');
29
+ // Call the callback to allow creating negotiated data channels
30
+ // This must happen BEFORE signaling starts so channels exist on both sides
31
+ if (this.onPeerConnectionCreated) {
32
+ this.onPeerConnectionCreated(this.pc);
33
+ }
28
34
  // Setup ondatachannel handler BEFORE setting remote description
29
35
  // This is critical to avoid race conditions
30
36
  this.pc.ondatachannel = event => {
@@ -72,9 +72,9 @@ export declare class OfferPool extends EventEmitter<OfferPoolEvents> {
72
72
  */
73
73
  getOfferCount(): number;
74
74
  /**
75
- * Update tags for new offers
76
- * Existing offers keep their old tags until they expire/rotate
77
- * New offers created during fill will use the updated tags
75
+ * Update tags for all offers (local and server-side)
76
+ * Updates existing offers on the server immediately for discoverability
77
+ * New offers created during fill will also use the updated tags
78
78
  */
79
79
  updateTags(newTags: string[]): void;
80
80
  /**
@@ -68,13 +68,23 @@ export class OfferPool extends EventEmitter {
68
68
  return this.activeConnections.size;
69
69
  }
70
70
  /**
71
- * Update tags for new offers
72
- * Existing offers keep their old tags until they expire/rotate
73
- * New offers created during fill will use the updated tags
71
+ * Update tags for all offers (local and server-side)
72
+ * Updates existing offers on the server immediately for discoverability
73
+ * New offers created during fill will also use the updated tags
74
74
  */
75
75
  updateTags(newTags) {
76
76
  this.debug(`Updating tags: ${newTags.join(', ')}`);
77
77
  this.tags = newTags;
78
+ // Update tags on existing offers via server API
79
+ // Fire and forget - errors are logged but don't block
80
+ this.api
81
+ .updateOfferTags(newTags)
82
+ .then(result => {
83
+ this.debug(`Server updated ${result.count} offers with new tags`);
84
+ })
85
+ .catch(err => {
86
+ this.debug('Failed to update offer tags on server:', err);
87
+ });
78
88
  }
79
89
  /**
80
90
  * Get current tags
@@ -43,6 +43,12 @@ export interface PeerOptions {
43
43
  rtcConfig?: RTCConfiguration;
44
44
  /** Optional: connection behavior configuration */
45
45
  config?: Partial<ConnectionConfig>;
46
+ /**
47
+ * Optional callback invoked when RTCPeerConnection is created.
48
+ * Use this to create negotiated data channels that must exist
49
+ * before the connection opens (e.g., control channels).
50
+ */
51
+ onPeerConnectionCreated?: (pc: RTCPeerConnection) => void;
46
52
  }
47
53
  /**
48
54
  * Internal options passed from Rondevu
@@ -92,6 +98,7 @@ export declare class Peer extends EventEmitter<PeerEventMap> {
92
98
  private webrtcAdapter?;
93
99
  private connectionConfig?;
94
100
  private debugEnabled;
101
+ private onPeerConnectionCreated?;
95
102
  private _state;
96
103
  private _peerPublicKey;
97
104
  private _offerId;
package/dist/core/peer.js CHANGED
@@ -50,6 +50,7 @@ export class Peer extends EventEmitter {
50
50
  this.webrtcAdapter = options.webrtcAdapter;
51
51
  this.connectionConfig = options.config;
52
52
  this.debugEnabled = options.debug || false;
53
+ this.onPeerConnectionCreated = options.onPeerConnectionCreated;
53
54
  }
54
55
  /**
55
56
  * Initialize the peer connection (called internally by Rondevu.peer())
@@ -78,6 +79,9 @@ export class Peer extends EventEmitter {
78
79
  this._peerPublicKey = offer.publicKey;
79
80
  this._offerId = offer.offerId;
80
81
  this.debug(`Selected offer ${offer.offerId} from ${offer.publicKey}`);
82
+ // Find which of our search tags actually exist on the offer (exact match)
83
+ const actualMatchedTags = this.tags.filter(searchTag => offer.tags.includes(searchTag));
84
+ this.debug(`Matched tags: ${actualMatchedTags.join(', ')} (from search: ${this.tags.join(', ')})`);
81
85
  // Create the underlying AnswererConnection
82
86
  this.connection = new AnswererConnection({
83
87
  api: this.api,
@@ -94,7 +98,8 @@ export class Peer extends EventEmitter {
94
98
  ...this.connectionConfig,
95
99
  debug: this.debugEnabled,
96
100
  },
97
- matchedTags: this.tags, // Pass the tags we used to discover this offer
101
+ matchedTags: actualMatchedTags.length > 0 ? actualMatchedTags : undefined,
102
+ onPeerConnectionCreated: this.onPeerConnectionCreated,
98
103
  });
99
104
  // Wire up events
100
105
  this.setupEventHandlers();
@@ -75,11 +75,6 @@ export class PollingManager extends EventEmitter {
75
75
  return;
76
76
  try {
77
77
  const result = await this.api.poll(this.lastPollTimestamp);
78
- // Log poll results for debugging (only when there are results)
79
- const iceCount = Object.values(result.iceCandidates).reduce((sum, candidates) => sum + candidates.length, 0);
80
- if (result.answers.length > 0 || iceCount > 0) {
81
- console.log(`[PollingManager] Poll: ${result.answers.length} answers, ${iceCount} ICE (since: ${this.lastPollTimestamp})`);
82
- }
83
78
  // Emit answer events
84
79
  for (const answer of result.answers) {
85
80
  this.debug(`Poll: answer for ${answer.offerId}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtr-dev/rondevu-client",
3
- "version": "0.21.13",
3
+ "version": "0.21.16",
4
4
  "description": "TypeScript client for Rondevu with durable WebRTC connections, automatic reconnection, and message queuing",
5
5
  "type": "module",
6
6
  "main": "dist/core/index.js",
@@ -13,7 +13,7 @@
13
13
  "lint:fix": "eslint src test --ext .ts,.tsx,.js --fix",
14
14
  "format": "prettier --write \"src/**/*.{ts,tsx,js}\" \"test/**/*.{ts,tsx,js}\"",
15
15
  "prepublishOnly": "npm run build",
16
- "prepare": "husky"
16
+ "prepare": "npm run build && husky || true"
17
17
  },
18
18
  "keywords": [
19
19
  "webrtc",