node-rtc-connection 1.0.19 → 2.0.4

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 (64) hide show
  1. package/README.md +94 -85
  2. package/dist/index.cjs +20 -5606
  3. package/dist/index.mjs +25 -5598
  4. package/dist/types/crypto/der.d.ts +107 -0
  5. package/dist/types/crypto/x509.d.ts +56 -0
  6. package/dist/types/datachannel/RTCDataChannel.d.ts +179 -0
  7. package/dist/types/dtls/RTCCertificate.d.ts +163 -0
  8. package/dist/types/dtls/cipher.d.ts +81 -0
  9. package/dist/types/dtls/connection.d.ts +81 -0
  10. package/dist/types/dtls/prf.d.ts +29 -0
  11. package/dist/types/dtls/protocol.d.ts +127 -0
  12. package/dist/types/foundation/ByteBufferQueue.d.ts +71 -0
  13. package/dist/types/foundation/RTCError.d.ts +152 -0
  14. package/dist/types/ice/RTCIceCandidate.d.ts +161 -0
  15. package/dist/types/ice/ice-agent.d.ts +154 -0
  16. package/dist/types/ice/stun-message.d.ts +92 -0
  17. package/dist/types/index.d.ts +29 -0
  18. package/dist/types/peerconnection/RTCPeerConnection.d.ts +74 -0
  19. package/dist/types/sctp/association.d.ts +77 -0
  20. package/dist/types/sctp/chunks.d.ts +200 -0
  21. package/dist/types/sctp/crc32c.d.ts +24 -0
  22. package/dist/types/sctp/datachannel-manager.d.ts +51 -0
  23. package/dist/types/sctp/dcep.d.ts +56 -0
  24. package/dist/types/sdp/RTCSessionDescription.d.ts +73 -0
  25. package/dist/types/sdp/sdp-utils.d.ts +103 -0
  26. package/dist/types/stun/stun-client.d.ts +119 -0
  27. package/dist/types/transport-stack.d.ts +68 -0
  28. package/package.json +26 -21
  29. package/src/crypto/der.ts +205 -0
  30. package/src/crypto/x509.ts +146 -0
  31. package/src/datachannel/RTCDataChannel.ts +388 -0
  32. package/src/dtls/RTCCertificate.ts +396 -0
  33. package/src/dtls/cipher.ts +198 -0
  34. package/src/dtls/connection.ts +974 -0
  35. package/src/dtls/prf.ts +62 -0
  36. package/src/dtls/protocol.ts +204 -0
  37. package/src/foundation/{ByteBufferQueue.js → ByteBufferQueue.ts} +74 -72
  38. package/src/foundation/{RTCError.js → RTCError.ts} +110 -60
  39. package/src/ice/{RTCIceCandidate.js → RTCIceCandidate.ts} +140 -92
  40. package/src/ice/ice-agent.ts +609 -0
  41. package/src/ice/stun-message.ts +260 -0
  42. package/src/index.ts +72 -0
  43. package/src/peerconnection/RTCPeerConnection.ts +430 -0
  44. package/src/sctp/association.ts +523 -0
  45. package/src/sctp/chunks.ts +350 -0
  46. package/src/sctp/crc32c.ts +57 -0
  47. package/src/sctp/datachannel-manager.ts +187 -0
  48. package/src/sctp/dcep.ts +94 -0
  49. package/src/sdp/{RTCSessionDescription.js → RTCSessionDescription.ts} +42 -29
  50. package/src/sdp/sdp-utils.ts +229 -0
  51. package/src/stun/{stun-client.js → stun-client.ts} +346 -187
  52. package/src/transport-stack.ts +165 -0
  53. package/dist/index.cjs.map +0 -1
  54. package/dist/index.mjs.map +0 -1
  55. package/src/datachannel/RTCDataChannel.js +0 -354
  56. package/src/dtls/RTCCertificate.js +0 -310
  57. package/src/dtls/RTCDtlsTransport.js +0 -247
  58. package/src/ice/RTCIceTransport.js +0 -1018
  59. package/src/index.d.ts +0 -400
  60. package/src/index.js +0 -92
  61. package/src/network/network-transport.js +0 -478
  62. package/src/peerconnection/RTCPeerConnection.js +0 -875
  63. package/src/sctp/RTCSctpTransport.js +0 -253
  64. package/src/sdp/sdp-utils.js +0 -224
@@ -1,18 +1,18 @@
1
1
  /**
2
- * @file stun-client.js
2
+ * @file stun-client.ts
3
3
  * @description STUN (Session Traversal Utilities for NAT) client implementation
4
4
  * @module stun/stun-client
5
- *
5
+ *
6
6
  * STUN Protocol: RFC 5389
7
7
  * TURN Protocol: RFC 5766
8
8
  */
9
9
 
10
10
  'use strict';
11
11
 
12
- const dgram = require('dgram');
13
- const crypto = require('crypto');
12
+ import * as dgram from 'dgram';
13
+ import * as crypto from 'crypto';
14
14
 
15
- const EventEmitter = require('events');
15
+ import { EventEmitter } from 'events';
16
16
 
17
17
  /**
18
18
  * STUN message types
@@ -21,24 +21,24 @@ const STUN_MESSAGE_TYPES = {
21
21
  BINDING_REQUEST: 0x0001,
22
22
  BINDING_RESPONSE: 0x0101,
23
23
  BINDING_ERROR_RESPONSE: 0x0111,
24
-
24
+
25
25
  // TURN
26
26
  ALLOCATE_REQUEST: 0x0003,
27
27
  ALLOCATE_RESPONSE: 0x0103,
28
28
  ALLOCATE_ERROR_RESPONSE: 0x0113,
29
-
29
+
30
30
  REFRESH_REQUEST: 0x0004,
31
31
  REFRESH_RESPONSE: 0x0104,
32
-
32
+
33
33
  SEND_INDICATION: 0x0016,
34
34
  DATA_INDICATION: 0x0017,
35
-
35
+
36
36
  CREATE_PERMISSION_REQUEST: 0x0008,
37
37
  CREATE_PERMISSION_RESPONSE: 0x0108,
38
-
38
+
39
39
  CHANNEL_BIND_REQUEST: 0x0009,
40
- CHANNEL_BIND_RESPONSE: 0x0109
41
- };
40
+ CHANNEL_BIND_RESPONSE: 0x0109,
41
+ } as const;
42
42
 
43
43
  /**
44
44
  * STUN attribute types
@@ -48,30 +48,145 @@ const STUN_ATTRIBUTES = {
48
48
  USERNAME: 0x0006,
49
49
  MESSAGE_INTEGRITY: 0x0008,
50
50
  ERROR_CODE: 0x0009,
51
- UNKNOWN_ATTRIBUTES: 0x000A,
51
+ UNKNOWN_ATTRIBUTES: 0x000a,
52
52
  REALM: 0x0014,
53
53
  NONCE: 0x0015,
54
54
  XOR_MAPPED_ADDRESS: 0x0020,
55
-
55
+
56
56
  // TURN
57
- CHANNEL_NUMBER: 0x000C,
58
- LIFETIME: 0x000D,
57
+ CHANNEL_NUMBER: 0x000c,
58
+ LIFETIME: 0x000d,
59
59
  XOR_PEER_ADDRESS: 0x0012,
60
60
  DATA: 0x0013,
61
61
  XOR_RELAYED_ADDRESS: 0x0016,
62
62
  REQUESTED_TRANSPORT: 0x0019,
63
-
63
+
64
64
  SOFTWARE: 0x8022,
65
- FINGERPRINT: 0x8028
66
- };
65
+ FINGERPRINT: 0x8028,
66
+ } as const;
67
+
68
+ const MAGIC_COOKIE = 0x2112a442;
69
+
70
+ /**
71
+ * Constructor options for {@link STUNClient}.
72
+ */
73
+ interface STUNClientOptions {
74
+ /** STUN/TURN server address */
75
+ server: string;
76
+ /** Server port */
77
+ port: number;
78
+ /** TURN username */
79
+ username?: string;
80
+ /** TURN password */
81
+ credential?: string;
82
+ /** Transport protocol (udp/tcp) */
83
+ transport?: string;
84
+ /** Additional query parameters from URL */
85
+ params?: Record<string, unknown>;
86
+ }
67
87
 
68
- const MAGIC_COOKIE = 0x2112A442;
88
+ /**
89
+ * Parsed IPv4 address info.
90
+ */
91
+ interface AddressInfo {
92
+ family: string;
93
+ port: number;
94
+ address: string;
95
+ }
96
+
97
+ /**
98
+ * Reflexive address info resolved from a STUN Binding response.
99
+ */
100
+ interface ReflexiveAddress {
101
+ address: string;
102
+ port: number;
103
+ family: string;
104
+ }
105
+
106
+ /**
107
+ * Relay address info resolved from a TURN Allocate response.
108
+ */
109
+ interface RelayAddress {
110
+ relayedAddress: string;
111
+ relayedPort: number;
112
+ lifetime: number;
113
+ type: 'relay';
114
+ }
115
+
116
+ /**
117
+ * Result of a TURN Refresh response.
118
+ */
119
+ interface RefreshResult {
120
+ lifetime: number;
121
+ }
122
+
123
+ /**
124
+ * Result of a generic success response (CreatePermission / ChannelBind).
125
+ */
126
+ interface OkResult {
127
+ ok: true;
128
+ }
129
+
130
+ /**
131
+ * Union of every result shape a transaction may resolve with.
132
+ */
133
+ type TransactionResult = ReflexiveAddress | RelayAddress | RefreshResult | OkResult;
134
+
135
+ /**
136
+ * A pending request transaction.
137
+ */
138
+ interface Transaction {
139
+ type?: string;
140
+ resolve: (result: TransactionResult) => void;
141
+ reject: (error: Error) => void;
142
+ }
143
+
144
+ /**
145
+ * Parsed STUN attributes object.
146
+ */
147
+ interface ParsedAttributes {
148
+ xorMappedAddress?: AddressInfo | null;
149
+ xorRelayedAddress?: AddressInfo | null;
150
+ xorPeerAddress?: AddressInfo | null;
151
+ mappedAddress?: AddressInfo | null;
152
+ data?: Buffer;
153
+ lifetime?: number;
154
+ errorCode?: string;
155
+ realm?: string;
156
+ nonce?: string;
157
+ }
158
+
159
+ /**
160
+ * Payload emitted with the 'data' event for relayed peer data.
161
+ */
162
+ interface DataEventInfo {
163
+ address: string;
164
+ port: number;
165
+ family: string;
166
+ }
167
+
168
+ /**
169
+ * Build product passed back from a request builder used with auth retry.
170
+ */
171
+ interface RequestBuild {
172
+ transactionId: Buffer;
173
+ request: Buffer;
174
+ }
69
175
 
70
176
  /**
71
177
  * @class STUNClient
72
178
  * @description STUN/TURN client for NAT traversal
73
179
  */
74
180
  class STUNClient extends EventEmitter {
181
+ #server: string;
182
+ #port: number;
183
+ #username: string | undefined;
184
+ #credential: string | undefined;
185
+ #socket: dgram.Socket | null;
186
+ #transactions: Map<string, Transaction>;
187
+ #realm: string | null;
188
+ #nonce: string | null;
189
+
75
190
  /**
76
191
  * Create a STUN client
77
192
  * @param {Object} options - Client options
@@ -82,43 +197,42 @@ class STUNClient extends EventEmitter {
82
197
  * @param {string} [options.transport='udp'] - Transport protocol (udp/tcp)
83
198
  * @param {Object} [options.params={}] - Additional query parameters from URL
84
199
  */
85
- constructor(options) {
200
+ constructor(options: STUNClientOptions) {
86
201
  super();
87
- this.server = options.server;
88
- this.port = options.port;
89
- this.username = options.username;
90
- this.credential = options.credential;
91
- this.transport = options.transport || 'udp';
92
- this.params = options.params || {};
93
-
94
- this.socket = null;
95
- this.transactions = new Map();
96
- this.realm = null;
97
- this.nonce = null;
202
+ this.#server = options.server;
203
+ this.#port = options.port;
204
+ this.#username = options.username;
205
+ this.#credential = options.credential;
206
+
207
+ this.#socket = null;
208
+ this.#transactions = new Map();
209
+ this.#realm = null;
210
+ this.#nonce = null;
98
211
  }
99
212
 
100
213
  /**
101
214
  * Connect to the STUN/TURN server
102
215
  * @returns {Promise<void>}
103
216
  */
104
- async connect() {
105
- if (this.socket) {
217
+ async connect(): Promise<void> {
218
+ if (this.#socket) {
106
219
  return;
107
220
  }
108
221
 
109
- return new Promise((resolve, reject) => {
110
- this.socket = dgram.createSocket('udp4');
111
-
112
- this.socket.on('message', (msg, rinfo) => {
113
- this._handleMessage(msg, rinfo);
222
+ return new Promise<void>((resolve, reject) => {
223
+ const socket = dgram.createSocket('udp4');
224
+ this.#socket = socket;
225
+
226
+ socket.on('message', (msg: Buffer, rinfo: dgram.RemoteInfo) => {
227
+ this.#handleMessage(msg, rinfo);
114
228
  });
115
229
 
116
- this.socket.on('error', (err) => {
230
+ socket.on('error', (err: Error) => {
117
231
  console.error('STUN socket error:', err);
118
232
  reject(err);
119
233
  });
120
234
 
121
- this.socket.bind(() => {
235
+ socket.bind(() => {
122
236
  resolve();
123
237
  });
124
238
  });
@@ -128,33 +242,33 @@ class STUNClient extends EventEmitter {
128
242
  * Send a STUN Binding Request to get reflexive address
129
243
  * @returns {Promise<Object>} Reflexive address info
130
244
  */
131
- async getReflexiveAddress() {
245
+ async getReflexiveAddress(): Promise<TransactionResult> {
132
246
  await this.connect();
133
247
 
134
248
  const transactionId = crypto.randomBytes(12);
135
- const request = this._createBindingRequest(transactionId);
249
+ const request = this.#createBindingRequest(transactionId);
136
250
 
137
- return new Promise((resolve, reject) => {
251
+ return new Promise<TransactionResult>((resolve, reject) => {
138
252
  const timeout = setTimeout(() => {
139
- this.transactions.delete(transactionId.toString('hex'));
253
+ this.#transactions.delete(transactionId.toString('hex'));
140
254
  reject(new Error('STUN request timeout'));
141
255
  }, 5000);
142
256
 
143
- this.transactions.set(transactionId.toString('hex'), {
144
- resolve: (result) => {
257
+ this.#transactions.set(transactionId.toString('hex'), {
258
+ resolve: (result: TransactionResult) => {
145
259
  clearTimeout(timeout);
146
260
  resolve(result);
147
261
  },
148
- reject: (error) => {
262
+ reject: (error: Error) => {
149
263
  clearTimeout(timeout);
150
264
  reject(error);
151
- }
265
+ },
152
266
  });
153
267
 
154
- this.socket.send(request, this.port, this.server, (err) => {
268
+ this.#socket!.send(request, this.#port, this.#server, (err) => {
155
269
  if (err) {
156
270
  clearTimeout(timeout);
157
- this.transactions.delete(transactionId.toString('hex'));
271
+ this.#transactions.delete(transactionId.toString('hex'));
158
272
  reject(err);
159
273
  }
160
274
  });
@@ -166,26 +280,26 @@ class STUNClient extends EventEmitter {
166
280
  * @param {number} [lifetime=600] - Allocation lifetime in seconds
167
281
  * @returns {Promise<Object>} Relay address info
168
282
  */
169
- async allocateRelay(lifetime = 600) {
170
- if (!this.username || !this.credential) {
283
+ async allocateRelay(lifetime: number = 600): Promise<TransactionResult> {
284
+ if (!this.#username || !this.#credential) {
171
285
  throw new Error('TURN requires username and credential');
172
286
  }
173
287
 
174
288
  await this.connect();
175
289
 
176
290
  let transactionId = crypto.randomBytes(12);
177
- let request = this._createAllocateRequest(transactionId, lifetime);
291
+ let request = this.#createAllocateRequest(transactionId, lifetime);
178
292
 
179
293
  // First attempt without credentials to get realm and nonce
180
294
  try {
181
- return await this._sendRequest(request, transactionId, 'allocate');
295
+ return await this.#sendRequest(request, transactionId, 'allocate');
182
296
  } catch (error) {
183
297
  // If we get 401 Unauthorized, retry with credentials
184
- if (error.message.includes('401') && this.realm && this.nonce) {
298
+ if (error instanceof Error && error.message.includes('401') && this.#realm && this.#nonce) {
185
299
  // Create new transaction ID for retry
186
300
  transactionId = crypto.randomBytes(12);
187
- request = this._createAllocateRequest(transactionId, lifetime, true);
188
- return await this._sendRequest(request, transactionId, 'allocate');
301
+ request = this.#createAllocateRequest(transactionId, lifetime, true);
302
+ return await this.#sendRequest(request, transactionId, 'allocate');
189
303
  }
190
304
  throw error;
191
305
  }
@@ -196,15 +310,36 @@ class STUNClient extends EventEmitter {
196
310
  * @param {number} [lifetime=600] - Allocation lifetime in seconds
197
311
  * @returns {Promise<Object>} Updated allocation info
198
312
  */
199
- async refreshAllocation(lifetime = 600) {
200
- if (!this.username || !this.credential) {
313
+ async refreshAllocation(lifetime: number = 600): Promise<TransactionResult> {
314
+ if (!this.#username || !this.#credential) {
201
315
  throw new Error('TURN requires username and credential');
202
316
  }
203
317
 
204
- const transactionId = crypto.randomBytes(12);
205
- const request = this._createRefreshRequest(transactionId, lifetime);
318
+ return this.#withAuthRetry('refresh', () => {
319
+ const transactionId = crypto.randomBytes(12);
320
+ return { transactionId, request: this.#createRefreshRequest(transactionId, lifetime) };
321
+ });
322
+ }
206
323
 
207
- return this._sendRequest(request, transactionId, 'refresh');
324
+ /**
325
+ * Send an authenticated TURN request, retrying once on a 401 (stale-nonce or
326
+ * first-time challenge) after refreshing realm/nonce from the error.
327
+ * @param {string} type - request label for diagnostics
328
+ * @param {() => {transactionId: Buffer, request: Buffer}} build
329
+ * @returns {Promise<Object>}
330
+ * @private
331
+ */
332
+ async #withAuthRetry(type: string, build: () => RequestBuild): Promise<TransactionResult> {
333
+ const first = build();
334
+ try {
335
+ return await this.#sendRequest(first.request, first.transactionId, type);
336
+ } catch (error) {
337
+ if (error instanceof Error && error.message.includes('401') && this.#realm && this.#nonce) {
338
+ const retry = build(); // rebuilt with the refreshed realm/nonce
339
+ return this.#sendRequest(retry.request, retry.transactionId, type);
340
+ }
341
+ throw error;
342
+ }
208
343
  }
209
344
 
210
345
  /**
@@ -212,15 +347,15 @@ class STUNClient extends EventEmitter {
212
347
  * @param {string} peerAddress - Peer IP address
213
348
  * @returns {Promise<void>}
214
349
  */
215
- async createPermission(peerAddress) {
216
- if (!this.username || !this.credential) {
350
+ async createPermission(peerAddress: string): Promise<void> {
351
+ if (!this.#username || !this.#credential) {
217
352
  throw new Error('TURN requires username and credential');
218
353
  }
219
354
 
220
- const transactionId = crypto.randomBytes(12);
221
- const request = this._createCreatePermissionRequest(transactionId, peerAddress);
222
-
223
- await this._sendRequest(request, transactionId, 'createPermission');
355
+ await this.#withAuthRetry('createPermission', () => {
356
+ const transactionId = crypto.randomBytes(12);
357
+ return { transactionId, request: this.#createCreatePermissionRequest(transactionId, peerAddress) };
358
+ });
224
359
  }
225
360
 
226
361
  /**
@@ -230,17 +365,17 @@ class STUNClient extends EventEmitter {
230
365
  * @param {Buffer} data - Data to send
231
366
  * @returns {Promise<void>}
232
367
  */
233
- async sendIndication(peerAddress, peerPort, data) {
234
- if (!this.username || !this.credential) {
368
+ async sendIndication(peerAddress: string, peerPort: number, data: Buffer): Promise<void> {
369
+ if (!this.#username || !this.#credential) {
235
370
  throw new Error('TURN requires username and credential');
236
371
  }
237
372
 
238
373
  const transactionId = crypto.randomBytes(12);
239
- const indication = this._createSendIndication(transactionId, peerAddress, peerPort, data);
374
+ const indication = this.#createSendIndication(transactionId, peerAddress, peerPort, data);
240
375
 
241
376
  // Indications are fire-and-forget, no response expected
242
- return new Promise((resolve, reject) => {
243
- this.socket.send(indication, this.port, this.server, (err) => {
377
+ return new Promise<void>((resolve, reject) => {
378
+ this.#socket!.send(indication, this.#port, this.#server, (err) => {
244
379
  if (err) reject(err);
245
380
  else resolve();
246
381
  });
@@ -255,29 +390,29 @@ class STUNClient extends EventEmitter {
255
390
  * @returns {Promise<Object>}
256
391
  * @private
257
392
  */
258
- _sendRequest(request, transactionId, requestType) {
259
- return new Promise((resolve, reject) => {
393
+ #sendRequest(request: Buffer, transactionId: Buffer, requestType: string): Promise<TransactionResult> {
394
+ return new Promise<TransactionResult>((resolve, reject) => {
260
395
  const timeout = setTimeout(() => {
261
- this.transactions.delete(transactionId.toString('hex'));
396
+ this.#transactions.delete(transactionId.toString('hex'));
262
397
  reject(new Error(`${requestType} request timeout`));
263
398
  }, 5000);
264
399
 
265
- this.transactions.set(transactionId.toString('hex'), {
400
+ this.#transactions.set(transactionId.toString('hex'), {
266
401
  type: requestType,
267
- resolve: (result) => {
402
+ resolve: (result: TransactionResult) => {
268
403
  clearTimeout(timeout);
269
404
  resolve(result);
270
405
  },
271
- reject: (error) => {
406
+ reject: (error: Error) => {
272
407
  clearTimeout(timeout);
273
408
  reject(error);
274
- }
409
+ },
275
410
  });
276
411
 
277
- this.socket.send(request, this.port, this.server, (err) => {
412
+ this.#socket!.send(request, this.#port, this.#server, (err) => {
278
413
  if (err) {
279
414
  clearTimeout(timeout);
280
- this.transactions.delete(transactionId.toString('hex'));
415
+ this.#transactions.delete(transactionId.toString('hex'));
281
416
  reject(err);
282
417
  }
283
418
  });
@@ -290,21 +425,21 @@ class STUNClient extends EventEmitter {
290
425
  * @returns {Buffer} STUN message
291
426
  * @private
292
427
  */
293
- _createBindingRequest(transactionId) {
428
+ #createBindingRequest(transactionId: Buffer): Buffer {
294
429
  const header = Buffer.alloc(20);
295
-
430
+
296
431
  // Message Type (2 bytes)
297
432
  header.writeUInt16BE(STUN_MESSAGE_TYPES.BINDING_REQUEST, 0);
298
-
433
+
299
434
  // Message Length (2 bytes) - 0 for now, no attributes
300
435
  header.writeUInt16BE(0, 2);
301
-
436
+
302
437
  // Magic Cookie (4 bytes)
303
438
  header.writeUInt32BE(MAGIC_COOKIE, 4);
304
-
439
+
305
440
  // Transaction ID (12 bytes)
306
441
  transactionId.copy(header, 8);
307
-
442
+
308
443
  return header;
309
444
  }
310
445
 
@@ -316,8 +451,8 @@ class STUNClient extends EventEmitter {
316
451
  * @returns {Buffer} STUN message
317
452
  * @private
318
453
  */
319
- _createAllocateRequest(transactionId, lifetime, withAuth = false) {
320
- const attributes = [];
454
+ #createAllocateRequest(transactionId: Buffer, lifetime: number, withAuth: boolean = false): Buffer {
455
+ const attributes: Buffer[] = [];
321
456
 
322
457
  // REQUESTED-TRANSPORT (UDP = 17)
323
458
  const transport = Buffer.alloc(8);
@@ -333,21 +468,21 @@ class STUNClient extends EventEmitter {
333
468
  lifetimeAttr.writeUInt32BE(lifetime, 4);
334
469
  attributes.push(lifetimeAttr);
335
470
 
336
- if (withAuth && this.realm && this.nonce) {
471
+ if (withAuth && this.#realm && this.#nonce) {
337
472
  // USERNAME
338
- const usernameAttr = this._createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.username);
473
+ const usernameAttr = this.#createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.#username!);
339
474
  attributes.push(usernameAttr);
340
475
 
341
476
  // REALM
342
- const realmAttr = this._createStringAttribute(STUN_ATTRIBUTES.REALM, this.realm);
477
+ const realmAttr = this.#createStringAttribute(STUN_ATTRIBUTES.REALM, this.#realm);
343
478
  attributes.push(realmAttr);
344
479
 
345
480
  // NONCE
346
- const nonceAttr = this._createStringAttribute(STUN_ATTRIBUTES.NONCE, this.nonce);
481
+ const nonceAttr = this.#createStringAttribute(STUN_ATTRIBUTES.NONCE, this.#nonce);
347
482
  attributes.push(nonceAttr);
348
483
  }
349
484
 
350
- return this._createMessage(STUN_MESSAGE_TYPES.ALLOCATE_REQUEST, transactionId, attributes, withAuth);
485
+ return this.#createMessage(STUN_MESSAGE_TYPES.ALLOCATE_REQUEST, transactionId, attributes, withAuth);
351
486
  }
352
487
 
353
488
  /**
@@ -357,21 +492,21 @@ class STUNClient extends EventEmitter {
357
492
  * @returns {Buffer} STUN message
358
493
  * @private
359
494
  */
360
- _createCreatePermissionRequest(transactionId, peerAddress) {
361
- const attributes = [];
495
+ #createCreatePermissionRequest(transactionId: Buffer, peerAddress: string): Buffer {
496
+ const attributes: Buffer[] = [];
362
497
 
363
498
  // XOR-PEER-ADDRESS
364
- const peerAttr = this._createXorPeerAddressAttribute(peerAddress, 0, transactionId);
499
+ const peerAttr = this.#createXorPeerAddressAttribute(peerAddress, 0, transactionId);
365
500
  attributes.push(peerAttr);
366
501
 
367
502
  // Auth attributes
368
- if (this.realm && this.nonce) {
369
- attributes.push(this._createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.username));
370
- attributes.push(this._createStringAttribute(STUN_ATTRIBUTES.REALM, this.realm));
371
- attributes.push(this._createStringAttribute(STUN_ATTRIBUTES.NONCE, this.nonce));
503
+ if (this.#realm && this.#nonce) {
504
+ attributes.push(this.#createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.#username!));
505
+ attributes.push(this.#createStringAttribute(STUN_ATTRIBUTES.REALM, this.#realm));
506
+ attributes.push(this.#createStringAttribute(STUN_ATTRIBUTES.NONCE, this.#nonce));
372
507
  }
373
508
 
374
- return this._createMessage(STUN_MESSAGE_TYPES.CREATE_PERMISSION_REQUEST, transactionId, attributes, true);
509
+ return this.#createMessage(STUN_MESSAGE_TYPES.CREATE_PERMISSION_REQUEST, transactionId, attributes, true);
375
510
  }
376
511
 
377
512
  /**
@@ -383,21 +518,21 @@ class STUNClient extends EventEmitter {
383
518
  * @returns {Buffer} STUN message
384
519
  * @private
385
520
  */
386
- _createSendIndication(transactionId, peerAddress, peerPort, data) {
387
- const attributes = [];
521
+ #createSendIndication(transactionId: Buffer, peerAddress: string, peerPort: number, data: Buffer): Buffer {
522
+ const attributes: Buffer[] = [];
388
523
 
389
524
  // XOR-PEER-ADDRESS
390
- const peerAttr = this._createXorPeerAddressAttribute(peerAddress, peerPort, transactionId);
525
+ const peerAttr = this.#createXorPeerAddressAttribute(peerAddress, peerPort, transactionId);
391
526
  attributes.push(peerAttr);
392
527
 
393
528
  // DATA
394
- const dataAttr = Buffer.alloc(4 + data.length + (4 - (data.length % 4)) % 4);
529
+ const dataAttr = Buffer.alloc(4 + data.length + ((4 - (data.length % 4)) % 4));
395
530
  dataAttr.writeUInt16BE(STUN_ATTRIBUTES.DATA, 0);
396
531
  dataAttr.writeUInt16BE(data.length, 2);
397
532
  data.copy(dataAttr, 4);
398
533
  attributes.push(dataAttr);
399
534
 
400
- return this._createMessage(STUN_MESSAGE_TYPES.SEND_INDICATION, transactionId, attributes, false);
535
+ return this.#createMessage(STUN_MESSAGE_TYPES.SEND_INDICATION, transactionId, attributes, false);
401
536
  }
402
537
 
403
538
  /**
@@ -408,10 +543,10 @@ class STUNClient extends EventEmitter {
408
543
  * @returns {Buffer} Attribute buffer
409
544
  * @private
410
545
  */
411
- _createXorPeerAddressAttribute(address, port, transactionId) {
546
+ #createXorPeerAddressAttribute(address: string, port: number, _transactionId: Buffer): Buffer {
412
547
  const family = 0x01; // IPv4
413
548
  const buffer = Buffer.alloc(4 + 8); // Type(2) + Length(2) + Reserved(1) + Family(1) + Port(2) + Address(4)
414
-
549
+
415
550
  buffer.writeUInt16BE(STUN_ATTRIBUTES.XOR_PEER_ADDRESS, 0);
416
551
  buffer.writeUInt16BE(8, 2);
417
552
  buffer.writeUInt8(0, 4);
@@ -423,9 +558,9 @@ class STUNClient extends EventEmitter {
423
558
 
424
559
  // XOR Address
425
560
  const parts = address.split('.').map(Number);
426
- const addrInt = (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
561
+ const addrInt = (parts[0]! << 24) | (parts[1]! << 16) | (parts[2]! << 8) | parts[3]!;
427
562
  const xorAddr = addrInt ^ MAGIC_COOKIE;
428
-
563
+
429
564
  buffer.writeUInt32BE(xorAddr >>> 0, 8); // Ensure unsigned
430
565
 
431
566
  return buffer;
@@ -438,8 +573,8 @@ class STUNClient extends EventEmitter {
438
573
  * @returns {Buffer} STUN message
439
574
  * @private
440
575
  */
441
- _createRefreshRequest(transactionId, lifetime) {
442
- const attributes = [];
576
+ #createRefreshRequest(transactionId: Buffer, lifetime: number): Buffer {
577
+ const attributes: Buffer[] = [];
443
578
 
444
579
  // LIFETIME
445
580
  const lifetimeAttr = Buffer.alloc(8);
@@ -449,22 +584,22 @@ class STUNClient extends EventEmitter {
449
584
  attributes.push(lifetimeAttr);
450
585
 
451
586
  // USERNAME
452
- const usernameAttr = this._createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.username);
587
+ const usernameAttr = this.#createStringAttribute(STUN_ATTRIBUTES.USERNAME, this.#username!);
453
588
  attributes.push(usernameAttr);
454
589
 
455
590
  // REALM
456
- if (this.realm) {
457
- const realmAttr = this._createStringAttribute(STUN_ATTRIBUTES.REALM, this.realm);
591
+ if (this.#realm) {
592
+ const realmAttr = this.#createStringAttribute(STUN_ATTRIBUTES.REALM, this.#realm);
458
593
  attributes.push(realmAttr);
459
594
  }
460
595
 
461
596
  // NONCE
462
- if (this.nonce) {
463
- const nonceAttr = this._createStringAttribute(STUN_ATTRIBUTES.NONCE, this.nonce);
597
+ if (this.#nonce) {
598
+ const nonceAttr = this.#createStringAttribute(STUN_ATTRIBUTES.NONCE, this.#nonce);
464
599
  attributes.push(nonceAttr);
465
600
  }
466
601
 
467
- return this._createMessage(STUN_MESSAGE_TYPES.REFRESH_REQUEST, transactionId, attributes, true);
602
+ return this.#createMessage(STUN_MESSAGE_TYPES.REFRESH_REQUEST, transactionId, attributes, true);
468
603
  }
469
604
 
470
605
  /**
@@ -476,11 +611,16 @@ class STUNClient extends EventEmitter {
476
611
  * @returns {Buffer} Complete STUN message
477
612
  * @private
478
613
  */
479
- _createMessage(messageType, transactionId, attributes, withIntegrity = false) {
614
+ #createMessage(
615
+ messageType: number,
616
+ transactionId: Buffer,
617
+ attributes: Buffer[],
618
+ withIntegrity: boolean = false
619
+ ): Buffer {
480
620
  let attributesBuffer = Buffer.concat(attributes);
481
621
 
482
622
  // Add MESSAGE-INTEGRITY if needed
483
- if (withIntegrity && this.credential) {
623
+ if (withIntegrity && this.#credential) {
484
624
  const tempHeader = Buffer.alloc(20);
485
625
  tempHeader.writeUInt16BE(messageType, 0);
486
626
  tempHeader.writeUInt16BE(attributesBuffer.length + 24, 2); // +24 for MESSAGE-INTEGRITY
@@ -488,14 +628,14 @@ class STUNClient extends EventEmitter {
488
628
  transactionId.copy(tempHeader, 8);
489
629
 
490
630
  const tempMessage = Buffer.concat([tempHeader, attributesBuffer]);
491
-
631
+
492
632
  // For TURN, compute key as MD5(username:realm:password) per RFC 5766
493
- let key = this.credential;
494
- if (this.username && this.realm) {
495
- const keyString = `${this.username}:${this.realm}:${this.credential}`;
633
+ let key: string | Buffer = this.#credential;
634
+ if (this.#username && this.#realm) {
635
+ const keyString = `${this.#username}:${this.#realm}:${this.#credential}`;
496
636
  key = crypto.createHash('md5').update(keyString).digest();
497
637
  }
498
-
638
+
499
639
  const hmac = crypto.createHmac('sha1', key);
500
640
  hmac.update(tempMessage);
501
641
  const integrity = hmac.digest();
@@ -525,16 +665,16 @@ class STUNClient extends EventEmitter {
525
665
  * @returns {Buffer} Attribute buffer
526
666
  * @private
527
667
  */
528
- _createStringAttribute(type, value) {
668
+ #createStringAttribute(type: number, value: string): Buffer {
529
669
  const valueBuffer = Buffer.from(value, 'utf8');
530
670
  const length = valueBuffer.length;
531
671
  const padding = (4 - (length % 4)) % 4;
532
672
  const buffer = Buffer.alloc(4 + length + padding);
533
-
673
+
534
674
  buffer.writeUInt16BE(type, 0);
535
675
  buffer.writeUInt16BE(length, 2);
536
676
  valueBuffer.copy(buffer, 4);
537
-
677
+
538
678
  return buffer;
539
679
  }
540
680
 
@@ -544,7 +684,7 @@ class STUNClient extends EventEmitter {
544
684
  * @param {Object} rinfo - Remote info
545
685
  * @private
546
686
  */
547
- _handleMessage(msg, rinfo) {
687
+ #handleMessage(msg: Buffer, _rinfo: dgram.RemoteInfo): void {
548
688
  if (msg.length < 20) {
549
689
  return; // Invalid STUN message
550
690
  }
@@ -558,14 +698,30 @@ class STUNClient extends EventEmitter {
558
698
  return; // Not a STUN message
559
699
  }
560
700
 
701
+ // DATA indications are server-initiated (relayed peer data) and carry a
702
+ // fresh transaction id that matches no pending request — handle them before
703
+ // the transaction lookup.
704
+ if (messageType === STUN_MESSAGE_TYPES.DATA_INDICATION) {
705
+ const attrs = this.#parseAttributes(msg.slice(20, 20 + messageLength), transactionId);
706
+ if (attrs.xorPeerAddress && attrs.data) {
707
+ const info: DataEventInfo = {
708
+ address: attrs.xorPeerAddress.address,
709
+ port: attrs.xorPeerAddress.port,
710
+ family: attrs.xorPeerAddress.family || 'IPv4',
711
+ };
712
+ this.emit('data', attrs.data, info);
713
+ }
714
+ return;
715
+ }
716
+
561
717
  const transactionKey = transactionId.toString('hex');
562
- const transaction = this.transactions.get(transactionKey);
718
+ const transaction = this.#transactions.get(transactionKey);
563
719
 
564
720
  if (!transaction) {
565
721
  return; // Unknown transaction
566
722
  }
567
723
 
568
- const attributes = this._parseAttributes(msg.slice(20, 20 + messageLength), transactionId);
724
+ const attributes = this.#parseAttributes(msg.slice(20, 20 + messageLength), transactionId);
569
725
 
570
726
  // Handle STUN Binding responses
571
727
  if (messageType === STUN_MESSAGE_TYPES.BINDING_RESPONSE) {
@@ -573,18 +729,18 @@ class STUNClient extends EventEmitter {
573
729
  transaction.resolve({
574
730
  address: attributes.xorMappedAddress.address,
575
731
  port: attributes.xorMappedAddress.port,
576
- family: attributes.xorMappedAddress.family
732
+ family: attributes.xorMappedAddress.family,
577
733
  });
578
734
  } else if (attributes.mappedAddress) {
579
735
  transaction.resolve({
580
736
  address: attributes.mappedAddress.address,
581
737
  port: attributes.mappedAddress.port,
582
- family: attributes.mappedAddress.family
738
+ family: attributes.mappedAddress.family,
583
739
  });
584
740
  } else {
585
741
  transaction.reject(new Error('No mapped address in STUN response'));
586
742
  }
587
- this.transactions.delete(transactionKey);
743
+ this.#transactions.delete(transactionKey);
588
744
  }
589
745
  // Handle TURN Allocate responses
590
746
  else if (messageType === STUN_MESSAGE_TYPES.ALLOCATE_RESPONSE) {
@@ -593,45 +749,44 @@ class STUNClient extends EventEmitter {
593
749
  relayedAddress: attributes.xorRelayedAddress.address,
594
750
  relayedPort: attributes.xorRelayedAddress.port,
595
751
  lifetime: attributes.lifetime || 600,
596
- type: 'relay'
752
+ type: 'relay',
597
753
  });
598
754
  } else {
599
755
  transaction.reject(new Error('No relayed address in ALLOCATE response'));
600
756
  }
601
- this.transactions.delete(transactionKey);
757
+ this.#transactions.delete(transactionKey);
602
758
  }
603
759
  // Handle TURN Refresh responses
604
760
  else if (messageType === STUN_MESSAGE_TYPES.REFRESH_RESPONSE) {
605
761
  transaction.resolve({
606
- lifetime: attributes.lifetime || 600
762
+ lifetime: attributes.lifetime || 600,
607
763
  });
608
- this.transactions.delete(transactionKey);
764
+ this.#transactions.delete(transactionKey);
609
765
  }
610
- // Handle Data Indication
611
- else if (messageType === STUN_MESSAGE_TYPES.DATA_INDICATION) {
612
- if (attributes.xorPeerAddress && attributes.data) {
613
- this.emit('data', attributes.data, {
614
- address: attributes.xorPeerAddress.address,
615
- port: attributes.xorPeerAddress.port,
616
- family: attributes.xorPeerAddress.family || 'IPv4'
617
- });
618
- }
766
+ // Handle TURN CreatePermission / ChannelBind success responses
767
+ else if (
768
+ messageType === STUN_MESSAGE_TYPES.CREATE_PERMISSION_RESPONSE ||
769
+ messageType === STUN_MESSAGE_TYPES.CHANNEL_BIND_RESPONSE
770
+ ) {
771
+ transaction.resolve({ ok: true });
772
+ this.#transactions.delete(transactionKey);
619
773
  }
620
- // Handle error responses
621
- else if (messageType === STUN_MESSAGE_TYPES.BINDING_ERROR_RESPONSE ||
622
- messageType === STUN_MESSAGE_TYPES.ALLOCATE_ERROR_RESPONSE) {
623
-
624
- // Store realm and nonce for subsequent requests
774
+ // (DATA indications are handled earlier, before the transaction lookup.)
775
+ // Handle error responses generically: any class-of-error message
776
+ // (the 0x0110 bits set). This covers ALLOCATE (0x0113), CreatePermission
777
+ // (0x0118), Refresh (0x0114), Binding (0x0111), etc.
778
+ else if ((messageType & 0x0110) === 0x0110) {
779
+ // Store realm and nonce so the caller can retry with fresh credentials.
625
780
  if (attributes.realm) {
626
- this.realm = attributes.realm;
781
+ this.#realm = attributes.realm;
627
782
  }
628
783
  if (attributes.nonce) {
629
- this.nonce = attributes.nonce;
784
+ this.#nonce = attributes.nonce;
630
785
  }
631
786
 
632
787
  const errorMsg = attributes.errorCode || 'Unknown error';
633
788
  transaction.reject(new Error(`STUN error: ${errorMsg}`));
634
- this.transactions.delete(transactionKey);
789
+ this.#transactions.delete(transactionKey);
635
790
  }
636
791
  }
637
792
 
@@ -642,8 +797,8 @@ class STUNClient extends EventEmitter {
642
797
  * @returns {Object} Parsed attributes
643
798
  * @private
644
799
  */
645
- _parseAttributes(data, transactionId) {
646
- const attributes = {};
800
+ #parseAttributes(data: Buffer, transactionId: Buffer): ParsedAttributes {
801
+ const attributes: ParsedAttributes = {};
647
802
  let offset = 0;
648
803
 
649
804
  while (offset < data.length) {
@@ -659,27 +814,33 @@ class STUNClient extends EventEmitter {
659
814
 
660
815
  switch (type) {
661
816
  case STUN_ATTRIBUTES.XOR_MAPPED_ADDRESS:
662
- attributes.xorMappedAddress = this._parseXorAddress(value, transactionId);
817
+ attributes.xorMappedAddress = this.#parseXorAddress(value, transactionId);
663
818
  break;
664
819
  case STUN_ATTRIBUTES.XOR_RELAYED_ADDRESS:
665
- attributes.xorRelayedAddress = this._parseXorAddress(value, transactionId);
820
+ attributes.xorRelayedAddress = this.#parseXorAddress(value, transactionId);
821
+ break;
822
+ case STUN_ATTRIBUTES.XOR_PEER_ADDRESS:
823
+ attributes.xorPeerAddress = this.#parseXorAddress(value, transactionId);
824
+ break;
825
+ case STUN_ATTRIBUTES.DATA:
826
+ attributes.data = value;
666
827
  break;
667
828
  case STUN_ATTRIBUTES.MAPPED_ADDRESS:
668
- attributes.mappedAddress = this._parseAddress(value);
829
+ attributes.mappedAddress = this.#parseAddress(value);
669
830
  break;
670
831
  case STUN_ATTRIBUTES.LIFETIME:
671
832
  attributes.lifetime = value.readUInt32BE(0);
672
833
  break;
673
834
  case STUN_ATTRIBUTES.ERROR_CODE:
674
- attributes.errorCode = this._parseErrorCode(value);
835
+ attributes.errorCode = this.#parseErrorCode(value);
675
836
  break;
676
837
  case STUN_ATTRIBUTES.REALM:
677
838
  attributes.realm = value.toString('utf8');
678
- this.realm = attributes.realm;
839
+ this.#realm = attributes.realm;
679
840
  break;
680
841
  case STUN_ATTRIBUTES.NONCE:
681
842
  attributes.nonce = value.toString('utf8');
682
- this.nonce = attributes.nonce;
843
+ this.#nonce = attributes.nonce;
683
844
  break;
684
845
  }
685
846
 
@@ -699,26 +860,22 @@ class STUNClient extends EventEmitter {
699
860
  * @returns {Object} Address info
700
861
  * @private
701
862
  */
702
- _parseXorAddress(data, transactionId) {
863
+ #parseXorAddress(data: Buffer, _transactionId: Buffer): AddressInfo | null {
703
864
  const family = data.readUInt8(1);
704
865
  const xorPort = data.readUInt16BE(2);
705
-
866
+
706
867
  // XOR port with magic cookie high 16 bits
707
868
  const port = xorPort ^ (MAGIC_COOKIE >> 16);
708
869
 
709
- if (family === 0x01) { // IPv4
870
+ if (family === 0x01) {
871
+ // IPv4
710
872
  const xorAddress = data.readUInt32BE(4);
711
873
  const address = xorAddress ^ MAGIC_COOKIE;
712
-
874
+
713
875
  return {
714
876
  family: 'IPv4',
715
877
  port,
716
- address: [
717
- (address >> 24) & 0xFF,
718
- (address >> 16) & 0xFF,
719
- (address >> 8) & 0xFF,
720
- address & 0xFF
721
- ].join('.')
878
+ address: [(address >> 24) & 0xff, (address >> 16) & 0xff, (address >> 8) & 0xff, address & 0xff].join('.'),
722
879
  };
723
880
  }
724
881
 
@@ -731,16 +888,17 @@ class STUNClient extends EventEmitter {
731
888
  * @returns {Object} Address info
732
889
  * @private
733
890
  */
734
- _parseAddress(data) {
891
+ #parseAddress(data: Buffer): AddressInfo | null {
735
892
  const family = data.readUInt8(1);
736
893
  const port = data.readUInt16BE(2);
737
894
 
738
- if (family === 0x01) { // IPv4
895
+ if (family === 0x01) {
896
+ // IPv4
739
897
  const address = data.slice(4, 8);
740
898
  return {
741
899
  family: 'IPv4',
742
900
  port,
743
- address: Array.from(address).join('.')
901
+ address: Array.from(address).join('.'),
744
902
  };
745
903
  }
746
904
 
@@ -753,25 +911,26 @@ class STUNClient extends EventEmitter {
753
911
  * @returns {string} Error message
754
912
  * @private
755
913
  */
756
- _parseErrorCode(data) {
914
+ #parseErrorCode(data: Buffer): string {
757
915
  const errorClass = data.readUInt8(2) & 0x07;
758
916
  const errorNumber = data.readUInt8(3);
759
917
  const errorCode = errorClass * 100 + errorNumber;
760
918
  const reason = data.slice(4).toString('utf8');
761
-
919
+
762
920
  return `${errorCode} ${reason}`;
763
921
  }
764
922
 
765
923
  /**
766
924
  * Close the client
767
925
  */
768
- close() {
769
- if (this.socket) {
770
- this.socket.close();
771
- this.socket = null;
926
+ close(): void {
927
+ if (this.#socket) {
928
+ this.#socket.close();
929
+ this.#socket = null;
772
930
  }
773
- this.transactions.clear();
931
+ this.#transactions.clear();
774
932
  }
775
933
  }
776
934
 
777
- module.exports = STUNClient;
935
+ export default STUNClient;
936
+ export { STUNClient };