@dainprotocol/tunnel 1.1.33 → 2.0.0

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.
@@ -9,9 +9,11 @@ declare class DainTunnel extends EventEmitter {
9
9
  private tunnelId;
10
10
  private secret;
11
11
  private webSocketClients;
12
- private sseClients;
13
- private httpAgent;
12
+ private pendingWebSocketMessages;
13
+ private sseAbortControllers;
14
14
  private heartbeatInterval;
15
+ private reconnectTimer;
16
+ private isStopping;
15
17
  constructor(serverUrl: string, apiKey: string);
16
18
  private signChallenge;
17
19
  private safeSend;
@@ -25,14 +27,12 @@ declare class DainTunnel extends EventEmitter {
25
27
  private handleRequest;
26
28
  private handleWebSocketConnection;
27
29
  private handleWebSocketMessage;
30
+ private flushPendingWebSocketMessages;
31
+ private cleanupWebSocketConnection;
28
32
  private handleSSEConnection;
29
33
  private handleSSEClose;
30
34
  private forwardRequest;
31
35
  private attemptReconnect;
32
- /**
33
- * Reset reconnection attempts counter.
34
- * Call this to allow reconnection after max_reconnect_attempts was reached.
35
- */
36
36
  resetReconnection(): void;
37
37
  stop(): Promise<void>;
38
38
  }
@@ -1,14 +1,7 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DainTunnel = void 0;
7
- const ws_1 = __importDefault(require("ws"));
8
- const http_1 = __importDefault(require("http"));
9
- const events_1 = require("events");
10
- const crypto_1 = require("crypto");
11
- const auth_1 = require("@dainprotocol/service-sdk/service/auth");
1
+ import WebSocket from "ws";
2
+ import { EventEmitter } from "events";
3
+ import { createHmac } from "crypto";
4
+ import { parseAPIKey } from "@dainprotocol/service-sdk/service/auth";
12
5
  const TIMEOUTS = {
13
6
  HEARTBEAT: 25000,
14
7
  REQUEST: 25000,
@@ -17,12 +10,33 @@ const TIMEOUTS = {
17
10
  SHUTDOWN_GRACE: 500,
18
11
  };
19
12
  const RECONNECT = {
20
- MAX_ATTEMPTS: 10, // Increase from 5
21
- BASE_DELAY: 1000, // Start at 1s
22
- MAX_DELAY: 30000, // Cap at 30s
23
- JITTER: 0.3, // 30% randomization
13
+ MAX_ATTEMPTS: 10,
14
+ BASE_DELAY: 1000,
15
+ MAX_DELAY: 30000,
16
+ JITTER: 0.3,
17
+ };
18
+ const LIMITS = {
19
+ MAX_PENDING_WS_MESSAGES_PER_CONNECTION: 256,
24
20
  };
25
- class DainTunnel extends events_1.EventEmitter {
21
+ function rawDataToString(message) {
22
+ if (typeof message === "string")
23
+ return message;
24
+ if (Buffer.isBuffer(message))
25
+ return message.toString("utf8");
26
+ if (Array.isArray(message))
27
+ return Buffer.concat(message).toString("utf8");
28
+ return Buffer.from(message).toString("utf8");
29
+ }
30
+ function rawDataToBuffer(data) {
31
+ if (Buffer.isBuffer(data))
32
+ return data;
33
+ if (Array.isArray(data))
34
+ return Buffer.concat(data);
35
+ if (typeof data === "string")
36
+ return Buffer.from(data, "utf8");
37
+ return Buffer.from(data);
38
+ }
39
+ class DainTunnel extends EventEmitter {
26
40
  constructor(serverUrl, apiKey) {
27
41
  super();
28
42
  this.serverUrl = serverUrl;
@@ -31,34 +45,30 @@ class DainTunnel extends events_1.EventEmitter {
31
45
  this.port = null;
32
46
  this.reconnectAttempts = 0;
33
47
  this.webSocketClients = new Map();
34
- this.sseClients = new Map();
48
+ this.pendingWebSocketMessages = new Map();
49
+ this.sseAbortControllers = new Map();
35
50
  this.heartbeatInterval = null;
36
- const parsed = (0, auth_1.parseAPIKey)(apiKey);
51
+ this.reconnectTimer = null;
52
+ this.isStopping = false;
53
+ const parsed = parseAPIKey(apiKey);
37
54
  if (!parsed) {
38
55
  throw new Error('Invalid API key format. Expected: sk_agent_{agentId}_{orgId}_{secret}');
39
56
  }
40
57
  this.apiKey = apiKey;
41
58
  this.tunnelId = `${parsed.orgId}_${parsed.agentId}`;
42
59
  this.secret = parsed.secret;
43
- this.httpAgent = new http_1.default.Agent({
44
- keepAlive: true,
45
- keepAliveMsecs: 30000,
46
- maxSockets: 100,
47
- maxFreeSockets: 20,
48
- });
49
60
  }
50
61
  signChallenge(challenge) {
51
- return (0, crypto_1.createHmac)('sha256', this.secret).update(challenge).digest('hex');
62
+ return createHmac('sha256', this.secret).update(challenge).digest('hex');
52
63
  }
53
64
  safeSend(data) {
54
- var _a;
55
65
  try {
56
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
66
+ if (this.ws?.readyState === WebSocket.OPEN) {
57
67
  this.ws.send(JSON.stringify(data));
58
68
  return true;
59
69
  }
60
70
  }
61
- catch (_b) {
71
+ catch {
62
72
  // Connection lost during send
63
73
  }
64
74
  return false;
@@ -66,12 +76,11 @@ class DainTunnel extends events_1.EventEmitter {
66
76
  startHeartbeat() {
67
77
  this.stopHeartbeat();
68
78
  this.heartbeatInterval = setInterval(() => {
69
- var _a;
70
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
79
+ if (this.ws?.readyState === WebSocket.OPEN) {
71
80
  try {
72
81
  this.ws.ping();
73
82
  }
74
- catch (_b) {
83
+ catch {
75
84
  this.emit("error", new Error("Heartbeat ping failed"));
76
85
  }
77
86
  }
@@ -84,6 +93,7 @@ class DainTunnel extends events_1.EventEmitter {
84
93
  }
85
94
  }
86
95
  async start(port) {
96
+ this.isStopping = false;
87
97
  this.port = port;
88
98
  return this.connect();
89
99
  }
@@ -107,7 +117,7 @@ class DainTunnel extends events_1.EventEmitter {
107
117
  }
108
118
  };
109
119
  try {
110
- this.ws = new ws_1.default(this.serverUrl);
120
+ this.ws = new WebSocket(this.serverUrl);
111
121
  this.ws.on("open", async () => {
112
122
  this.reconnectAttempts = 0;
113
123
  try {
@@ -130,7 +140,7 @@ class DainTunnel extends events_1.EventEmitter {
130
140
  });
131
141
  this.ws.on("message", (data) => {
132
142
  try {
133
- this.handleMessage(JSON.parse(data), (url) => finish(url));
143
+ this.handleMessage(JSON.parse(rawDataToString(data)), (url) => finish(url));
134
144
  }
135
145
  catch (err) {
136
146
  finish(undefined, err);
@@ -139,6 +149,9 @@ class DainTunnel extends events_1.EventEmitter {
139
149
  this.ws.on("close", () => {
140
150
  this.stopHeartbeat();
141
151
  this.cleanupAllClients();
152
+ this.ws = null;
153
+ if (this.isStopping)
154
+ return;
142
155
  if (this.tunnelUrl) {
143
156
  this.emit("disconnected");
144
157
  this.attemptReconnect();
@@ -149,13 +162,12 @@ class DainTunnel extends events_1.EventEmitter {
149
162
  });
150
163
  this.ws.on("error", (error) => this.emit("error", error));
151
164
  connectionTimeoutId = setTimeout(() => {
152
- var _a;
153
- if (!resolved && (!this.ws || this.ws.readyState !== ws_1.default.OPEN)) {
165
+ if (!resolved && (!this.ws || this.ws.readyState !== WebSocket.OPEN)) {
154
166
  finish(undefined, new Error("Connection timeout"));
155
167
  try {
156
- (_a = this.ws) === null || _a === void 0 ? void 0 : _a.terminate();
168
+ this.ws?.terminate();
157
169
  }
158
- catch (_b) { }
170
+ catch { }
159
171
  }
160
172
  }, TIMEOUTS.CONNECTION);
161
173
  }
@@ -165,21 +177,22 @@ class DainTunnel extends events_1.EventEmitter {
165
177
  });
166
178
  }
167
179
  cleanupAllClients() {
168
- for (const client of this.sseClients.values()) {
180
+ for (const controller of this.sseAbortControllers.values()) {
169
181
  try {
170
- client.destroy();
182
+ controller.abort();
171
183
  }
172
- catch (_a) { }
184
+ catch { }
173
185
  }
174
- this.sseClients.clear();
186
+ this.sseAbortControllers.clear();
175
187
  for (const client of this.webSocketClients.values()) {
176
188
  try {
177
- if (client.readyState === ws_1.default.OPEN)
189
+ if (client.readyState === WebSocket.OPEN)
178
190
  client.close(1001);
179
191
  }
180
- catch (_b) { }
192
+ catch { }
181
193
  }
182
194
  this.webSocketClients.clear();
195
+ this.pendingWebSocketMessages.clear();
183
196
  }
184
197
  async requestChallenge() {
185
198
  return new Promise((resolve, reject) => {
@@ -190,11 +203,10 @@ class DainTunnel extends events_1.EventEmitter {
190
203
  let resolved = false;
191
204
  let timeoutId = null;
192
205
  const finish = (challenge, error) => {
193
- var _a;
194
206
  if (resolved)
195
207
  return;
196
208
  resolved = true;
197
- (_a = this.ws) === null || _a === void 0 ? void 0 : _a.removeListener("message", challengeHandler);
209
+ this.ws?.removeListener("message", challengeHandler);
198
210
  if (timeoutId) {
199
211
  clearTimeout(timeoutId);
200
212
  timeoutId = null;
@@ -208,12 +220,12 @@ class DainTunnel extends events_1.EventEmitter {
208
220
  };
209
221
  const challengeHandler = (message) => {
210
222
  try {
211
- const data = JSON.parse(message);
223
+ const data = JSON.parse(rawDataToString(message));
212
224
  if (data.type === "challenge") {
213
225
  finish(data.challenge);
214
226
  }
215
227
  }
216
- catch (_a) { }
228
+ catch { }
217
229
  };
218
230
  this.ws.on("message", challengeHandler);
219
231
  this.ws.send(JSON.stringify({ type: "challenge_request" }));
@@ -272,28 +284,31 @@ class DainTunnel extends events_1.EventEmitter {
272
284
  this.safeSend({ type: 'websocket', id: message.id, event, data });
273
285
  };
274
286
  try {
275
- // Preserve original host for JWT audience validation
276
287
  const headers = { ...message.headers };
277
288
  const originalHost = message.headers.host;
278
289
  if (originalHost && !headers['x-forwarded-host']) {
279
290
  headers['x-forwarded-host'] = originalHost;
280
291
  headers['x-forwarded-proto'] = 'https';
281
292
  }
282
- delete headers.host; // Remove so WebSocket library doesn't send tunnel host to localhost
283
- const client = new ws_1.default(`ws://localhost:${this.port}${message.path}`, {
293
+ delete headers.host;
294
+ const client = new WebSocket(`ws://localhost:${this.port}${message.path}`, {
284
295
  headers
285
296
  });
297
+ this.pendingWebSocketMessages.set(message.id, []);
286
298
  this.webSocketClients.set(message.id, client);
299
+ client.on('open', () => {
300
+ this.flushPendingWebSocketMessages(message.id, sendWsEvent);
301
+ });
287
302
  client.on('message', (data) => {
288
- sendWsEvent('message', data.toString('base64'));
303
+ sendWsEvent('message', rawDataToBuffer(data).toString('base64'));
289
304
  });
290
305
  client.on('close', () => {
291
306
  sendWsEvent('close');
292
- this.webSocketClients.delete(message.id);
307
+ this.cleanupWebSocketConnection(message.id);
293
308
  });
294
309
  client.on('error', (error) => {
295
310
  sendWsEvent('error', error.message);
296
- this.webSocketClients.delete(message.id);
311
+ this.cleanupWebSocketConnection(message.id);
297
312
  });
298
313
  this.emit("websocket_connection", { id: message.id, path: message.path });
299
314
  }
@@ -307,25 +322,63 @@ class DainTunnel extends events_1.EventEmitter {
307
322
  return;
308
323
  switch (message.event) {
309
324
  case 'message':
310
- if (message.data && client.readyState === ws_1.default.OPEN) {
325
+ if (!message.data)
326
+ return;
327
+ if (client.readyState === WebSocket.OPEN) {
311
328
  client.send(Buffer.from(message.data, 'base64'));
329
+ return;
330
+ }
331
+ if (client.readyState !== WebSocket.CONNECTING)
332
+ return;
333
+ const pending = this.pendingWebSocketMessages.get(message.id);
334
+ if (!pending)
335
+ return;
336
+ if (pending.length >= LIMITS.MAX_PENDING_WS_MESSAGES_PER_CONNECTION) {
337
+ client.close(1013, "WebSocket upstream overloaded");
338
+ this.cleanupWebSocketConnection(message.id);
339
+ return;
312
340
  }
341
+ pending.push(Buffer.from(message.data, 'base64'));
313
342
  break;
314
343
  case 'close':
315
- if (client.readyState === ws_1.default.OPEN) {
344
+ if (client.readyState === WebSocket.OPEN || client.readyState === WebSocket.CONNECTING) {
316
345
  client.close();
317
346
  }
318
- this.webSocketClients.delete(message.id);
347
+ this.cleanupWebSocketConnection(message.id);
348
+ break;
349
+ case 'error':
350
+ if (client.readyState === WebSocket.OPEN || client.readyState === WebSocket.CONNECTING) {
351
+ client.close(1011, message.data);
352
+ }
353
+ this.cleanupWebSocketConnection(message.id);
319
354
  break;
320
355
  }
321
356
  }
357
+ flushPendingWebSocketMessages(id, sendWsEvent) {
358
+ const client = this.webSocketClients.get(id);
359
+ const pending = this.pendingWebSocketMessages.get(id);
360
+ if (!client || !pending || pending.length === 0)
361
+ return;
362
+ try {
363
+ while (pending.length > 0 && client.readyState === WebSocket.OPEN) {
364
+ client.send(pending.shift());
365
+ }
366
+ }
367
+ catch (error) {
368
+ sendWsEvent("error", error.message);
369
+ this.cleanupWebSocketConnection(id);
370
+ }
371
+ }
372
+ cleanupWebSocketConnection(id) {
373
+ this.pendingWebSocketMessages.delete(id);
374
+ this.webSocketClients.delete(id);
375
+ }
322
376
  handleSSEConnection(message) {
323
377
  const sendSseEvent = (event, data = '') => {
324
378
  this.safeSend({ type: 'sse', id: message.id, event, data });
325
379
  };
326
380
  try {
327
381
  const headers = { ...message.headers };
328
- // Preserve original host for JWT audience validation
329
382
  const originalHost = message.headers.host;
330
383
  if (originalHost && !headers['x-forwarded-host']) {
331
384
  headers['x-forwarded-host'] = originalHost;
@@ -333,29 +386,35 @@ class DainTunnel extends events_1.EventEmitter {
333
386
  }
334
387
  delete headers.host;
335
388
  headers['accept-encoding'] = 'identity';
336
- const req = http_1.default.request({
337
- hostname: 'localhost',
338
- port: this.port,
339
- path: message.path,
389
+ const abortController = new AbortController();
390
+ this.sseAbortControllers.set(message.id, abortController);
391
+ const url = `http://localhost:${this.port}${message.path}`;
392
+ const fetchOptions = {
340
393
  method: message.method || 'GET',
341
394
  headers,
342
- agent: this.httpAgent,
343
- }, (res) => {
344
- if (res.statusCode !== 200) {
345
- let errorBody = '';
346
- res.on('data', (chunk) => { errorBody += chunk.toString(); });
347
- res.on('end', () => {
348
- sendSseEvent('error', `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`);
349
- });
395
+ signal: abortController.signal,
396
+ };
397
+ if (message.body && message.method !== 'GET') {
398
+ fetchOptions.body = Buffer.from(message.body, 'base64');
399
+ }
400
+ fetch(url, fetchOptions)
401
+ .then(async (res) => {
402
+ if (res.status !== 200) {
403
+ const errorBody = await res.text();
404
+ sendSseEvent('error', `Status ${res.status}: ${errorBody.substring(0, 200)}`);
405
+ this.sseAbortControllers.delete(message.id);
350
406
  return;
351
407
  }
352
- const socket = res.socket || res.connection;
353
- if (socket === null || socket === void 0 ? void 0 : socket.setNoDelay)
354
- socket.setNoDelay(true);
355
408
  sendSseEvent('connected');
409
+ if (!res.body) {
410
+ sendSseEvent('close');
411
+ this.sseAbortControllers.delete(message.id);
412
+ return;
413
+ }
414
+ const reader = res.body.getReader();
415
+ const decoder = new TextDecoder();
356
416
  let buffer = '';
357
417
  const processBuffer = () => {
358
- // Normalize line endings once
359
418
  if (buffer.indexOf('\r') >= 0) {
360
419
  buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
361
420
  }
@@ -363,7 +422,6 @@ class DainTunnel extends events_1.EventEmitter {
363
422
  while ((idx = buffer.indexOf('\n\n')) >= 0) {
364
423
  const msgData = buffer.slice(0, idx);
365
424
  buffer = buffer.slice(idx + 2);
366
- // Skip empty messages and comments
367
425
  if (!msgData || msgData[0] === ':')
368
426
  continue;
369
427
  let event = 'message';
@@ -382,30 +440,36 @@ class DainTunnel extends events_1.EventEmitter {
382
440
  sendSseEvent(event, data);
383
441
  }
384
442
  };
385
- res.on('data', (chunk) => {
386
- buffer += chunk.toString();
387
- processBuffer();
388
- });
389
- res.on('end', () => {
443
+ try {
444
+ while (true) {
445
+ const { done, value } = await reader.read();
446
+ if (done)
447
+ break;
448
+ buffer += decoder.decode(value, { stream: true });
449
+ processBuffer();
450
+ }
451
+ // Process remaining buffer
390
452
  if (buffer.trim()) {
391
453
  buffer += '\n\n';
392
454
  processBuffer();
393
455
  }
394
456
  sendSseEvent('close');
395
- });
396
- });
397
- req.on('socket', (socket) => {
398
- socket.setNoDelay(true);
399
- socket.setTimeout(0);
400
- });
401
- req.on('error', (error) => {
402
- sendSseEvent('error', error.message);
457
+ }
458
+ catch (err) {
459
+ if (err.name !== 'AbortError') {
460
+ sendSseEvent('error', err.message);
461
+ }
462
+ }
463
+ finally {
464
+ this.sseAbortControllers.delete(message.id);
465
+ }
466
+ })
467
+ .catch((error) => {
468
+ if (error.name !== 'AbortError') {
469
+ sendSseEvent('error', error.message);
470
+ }
471
+ this.sseAbortControllers.delete(message.id);
403
472
  });
404
- if (message.body && message.method !== 'GET') {
405
- req.write(Buffer.from(message.body, 'base64'));
406
- }
407
- req.end();
408
- this.sseClients.set(message.id, req);
409
473
  this.emit("sse_connection", { id: message.id, path: message.path });
410
474
  }
411
475
  catch (error) {
@@ -413,125 +477,92 @@ class DainTunnel extends events_1.EventEmitter {
413
477
  }
414
478
  }
415
479
  handleSSEClose(message) {
416
- const client = this.sseClients.get(message.id);
417
- if (client) {
418
- client.destroy();
419
- this.sseClients.delete(message.id);
480
+ const controller = this.sseAbortControllers.get(message.id);
481
+ if (controller) {
482
+ controller.abort();
483
+ this.sseAbortControllers.delete(message.id);
420
484
  }
421
485
  }
422
- forwardRequest(request) {
423
- return new Promise((resolve, reject) => {
424
- let resolved = false;
425
- let timeoutId = null;
426
- const finish = (response, error) => {
427
- if (resolved)
428
- return;
429
- resolved = true;
430
- if (timeoutId) {
431
- clearTimeout(timeoutId);
432
- timeoutId = null;
433
- }
434
- if (error) {
435
- reject(error);
436
- }
437
- else {
438
- resolve(response);
439
- }
440
- };
441
- // Preserve original host for JWT audience validation
442
- const headers = { ...request.headers };
443
- const originalHost = request.headers.host;
444
- if (originalHost && !headers['x-forwarded-host']) {
445
- headers['x-forwarded-host'] = originalHost;
446
- headers['x-forwarded-proto'] = 'https';
447
- }
448
- delete headers.host;
449
- const options = {
450
- hostname: 'localhost',
451
- port: this.port,
452
- path: request.path,
453
- method: request.method,
454
- headers,
455
- agent: this.httpAgent,
456
- timeout: TIMEOUTS.REQUEST,
457
- };
458
- const req = http_1.default.request(options, (res) => {
459
- const chunks = [];
460
- res.on('data', (chunk) => chunks.push(chunk));
461
- res.on('end', () => {
462
- const headers = { ...res.headers };
463
- delete headers['transfer-encoding'];
464
- delete headers['content-length'];
465
- finish({
466
- type: 'response',
467
- requestId: request.id,
468
- status: res.statusCode,
469
- headers,
470
- body: Buffer.concat(chunks).toString('base64'),
471
- });
472
- });
473
- res.on('error', (err) => finish(undefined, err));
474
- });
475
- req.on('timeout', () => {
476
- req.destroy();
477
- finish(undefined, new Error(`Request timeout after ${TIMEOUTS.REQUEST}ms`));
478
- });
479
- req.on('error', (err) => finish(undefined, err));
480
- timeoutId = setTimeout(() => {
481
- req.destroy();
482
- finish(undefined, new Error(`Request timeout after ${TIMEOUTS.REQUEST}ms`));
483
- }, TIMEOUTS.REQUEST);
484
- if (request.body && request.method !== 'GET') {
485
- req.write(Buffer.from(request.body, 'base64'));
486
- }
487
- req.end();
486
+ async forwardRequest(request) {
487
+ const headers = { ...request.headers };
488
+ const originalHost = request.headers.host;
489
+ if (originalHost && !headers['x-forwarded-host']) {
490
+ headers['x-forwarded-host'] = originalHost;
491
+ headers['x-forwarded-proto'] = 'https';
492
+ }
493
+ delete headers.host;
494
+ const url = `http://localhost:${this.port}${request.path}`;
495
+ const fetchOptions = {
496
+ method: request.method,
497
+ headers,
498
+ signal: AbortSignal.timeout(TIMEOUTS.REQUEST),
499
+ };
500
+ if (request.body && request.method !== 'GET') {
501
+ fetchOptions.body = Buffer.from(request.body, 'base64');
502
+ }
503
+ const res = await fetch(url, fetchOptions);
504
+ const responseBody = await res.arrayBuffer();
505
+ const responseHeaders = {};
506
+ res.headers.forEach((value, key) => {
507
+ responseHeaders[key] = value;
488
508
  });
509
+ delete responseHeaders['transfer-encoding'];
510
+ delete responseHeaders['content-length'];
511
+ return {
512
+ type: 'response',
513
+ requestId: request.id,
514
+ status: res.status,
515
+ headers: responseHeaders,
516
+ body: Buffer.from(responseBody).toString('base64'),
517
+ };
489
518
  }
490
519
  attemptReconnect() {
520
+ if (this.isStopping)
521
+ return;
491
522
  if (this.reconnectAttempts >= RECONNECT.MAX_ATTEMPTS) {
492
523
  this.emit("max_reconnect_attempts");
493
524
  return;
494
525
  }
495
526
  this.reconnectAttempts++;
496
- // Exponential backoff with jitter
497
527
  const baseDelay = Math.min(RECONNECT.BASE_DELAY * Math.pow(2, this.reconnectAttempts - 1), RECONNECT.MAX_DELAY);
498
528
  const jitter = baseDelay * RECONNECT.JITTER * (Math.random() - 0.5);
499
529
  const delay = Math.round(baseDelay + jitter);
500
530
  this.emit("reconnecting", { attempt: this.reconnectAttempts, delay });
501
- setTimeout(async () => {
531
+ this.reconnectTimer = setTimeout(async () => {
502
532
  try {
503
533
  await this.connect();
504
534
  this.emit("reconnected");
505
535
  }
506
- catch (_a) {
507
- // connect() will call attemptReconnect again on close
536
+ catch {
537
+ if (!this.isStopping)
538
+ this.attemptReconnect();
508
539
  }
509
540
  }, delay);
510
541
  }
511
- /**
512
- * Reset reconnection attempts counter.
513
- * Call this to allow reconnection after max_reconnect_attempts was reached.
514
- */
515
542
  resetReconnection() {
516
543
  this.reconnectAttempts = 0;
517
544
  }
518
545
  async stop() {
546
+ this.isStopping = true;
519
547
  this.stopHeartbeat();
520
548
  this.cleanupAllClients();
549
+ if (this.reconnectTimer) {
550
+ clearTimeout(this.reconnectTimer);
551
+ this.reconnectTimer = null;
552
+ }
521
553
  if (this.ws) {
522
554
  try {
523
- if (this.ws.readyState === ws_1.default.OPEN)
555
+ if (this.ws.readyState === WebSocket.OPEN)
524
556
  this.ws.close();
525
557
  }
526
- catch (_a) { }
558
+ catch { }
527
559
  this.ws = null;
528
560
  }
529
561
  this.tunnelUrl = null;
530
- this.httpAgent.destroy();
531
562
  await new Promise(resolve => setTimeout(resolve, TIMEOUTS.SHUTDOWN_GRACE));
532
563
  }
533
564
  }
534
- exports.DainTunnel = DainTunnel;
535
- exports.default = {
565
+ export { DainTunnel };
566
+ export default {
536
567
  createTunnel: (serverUrl, apiKey) => new DainTunnel(serverUrl, apiKey),
537
568
  };
@@ -3,7 +3,7 @@ declare class DainTunnelServer {
3
3
  private port;
4
4
  private app;
5
5
  private server;
6
- private wss;
6
+ private allowedCorsOrigins;
7
7
  private tunnels;
8
8
  private pendingRequests;
9
9
  private challenges;
@@ -12,11 +12,14 @@ declare class DainTunnelServer {
12
12
  private tunnelRequestCount;
13
13
  private safeSend;
14
14
  private decrementRequestCount;
15
+ private buildForwardedHeaders;
16
+ private buildTunnelUrl;
15
17
  constructor(hostname: string, port: number);
16
- private setupExpressRoutes;
17
- private setupWebSocketServer;
18
- private handleTunnelClientConnection;
19
- private handleProxiedWebSocketConnection;
18
+ private setupRoutes;
19
+ private handleWsOpen;
20
+ private handleWsMessage;
21
+ private handleWsClose;
22
+ private handleProxiedWebSocketClientMessage;
20
23
  private handleChallengeRequest;
21
24
  private handleStartMessage;
22
25
  private handleResponseMessage;