@dainprotocol/tunnel 1.1.26 → 1.1.30

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,6 +9,17 @@ const http_1 = __importDefault(require("http"));
9
9
  const events_1 = require("events");
10
10
  const crypto_1 = require("crypto");
11
11
  const auth_1 = require("@dainprotocol/service-sdk/service/auth");
12
+ const TIMEOUTS = {
13
+ HEARTBEAT: 25000,
14
+ REQUEST: 25000,
15
+ CONNECTION: 10000,
16
+ CHALLENGE: 5000,
17
+ RECONNECT_DELAY: 5000,
18
+ SHUTDOWN_GRACE: 500,
19
+ };
20
+ const RECONNECT = {
21
+ MAX_ATTEMPTS: 5,
22
+ };
12
23
  class DainTunnel extends events_1.EventEmitter {
13
24
  constructor(serverUrl, apiKey) {
14
25
  super();
@@ -17,63 +28,56 @@ class DainTunnel extends events_1.EventEmitter {
17
28
  this.tunnelUrl = null;
18
29
  this.port = null;
19
30
  this.reconnectAttempts = 0;
20
- this.maxReconnectAttempts = 5;
21
- this.reconnectDelay = 5000;
22
31
  this.webSocketClients = new Map();
23
32
  this.sseClients = new Map();
24
- // Heartbeat interval for client-side liveness
25
33
  this.heartbeatInterval = null;
26
- this.HEARTBEAT_INTERVAL = 25000; // 25 seconds (less than server's 30s)
27
- // Client-side timeout (less than server's 30s to respond before server times out)
28
- this.REQUEST_TIMEOUT = 25000;
29
- // Parse API key to extract agentId and secret
34
+ console.log('[DainTunnel] ========================================');
35
+ console.log('[DainTunnel] FIXED VERSION WITH X-FORWARDED-HOST SUPPORT');
36
+ console.log('[DainTunnel] ========================================');
30
37
  const parsed = (0, auth_1.parseAPIKey)(apiKey);
31
38
  if (!parsed) {
32
39
  throw new Error('Invalid API key format. Expected: sk_agent_{agentId}_{orgId}_{secret}');
33
40
  }
34
41
  this.apiKey = apiKey;
35
- this.tunnelId = `${parsed.orgId}_${parsed.agentId}`; // orgId_agentId to prevent collisions
36
- this.secret = parsed.secret; // secret for HMAC signatures
37
- // High-frequency optimization: Create reusable HTTP agent with connection pooling
38
- // Aligned with server's MAX_CONCURRENT_REQUESTS_PER_TUNNEL = 100
42
+ this.tunnelId = `${parsed.orgId}_${parsed.agentId}`;
43
+ this.secret = parsed.secret;
39
44
  this.httpAgent = new http_1.default.Agent({
40
45
  keepAlive: true,
41
- keepAliveMsecs: 30000, // Keep connections alive for 30s
42
- maxSockets: 100, // Match server's concurrent request limit
43
- maxFreeSockets: 20, // Keep more idle connections for burst traffic
46
+ keepAliveMsecs: 30000,
47
+ maxSockets: 100,
48
+ maxFreeSockets: 20,
44
49
  });
45
50
  }
46
- /**
47
- * Sign a challenge using HMAC-SHA256
48
- * @private
49
- */
50
51
  signChallenge(challenge) {
51
- return (0, crypto_1.createHmac)('sha256', this.secret)
52
- .update(challenge)
53
- .digest('hex');
52
+ return (0, crypto_1.createHmac)('sha256', this.secret).update(challenge).digest('hex');
53
+ }
54
+ safeSend(data) {
55
+ var _a;
56
+ try {
57
+ if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
58
+ this.ws.send(JSON.stringify(data));
59
+ return true;
60
+ }
61
+ }
62
+ catch (_b) {
63
+ // Connection lost during send
64
+ }
65
+ return false;
54
66
  }
55
- /**
56
- * Start client-side heartbeat to detect connection issues early
57
- * @private
58
- */
59
67
  startHeartbeat() {
60
- this.stopHeartbeat(); // Clear any existing heartbeat
68
+ this.stopHeartbeat();
61
69
  this.heartbeatInterval = setInterval(() => {
62
- if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
70
+ var _a;
71
+ if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
63
72
  try {
64
73
  this.ws.ping();
65
74
  }
66
- catch (error) {
67
- // Ping failed, connection might be dead
75
+ catch (_b) {
68
76
  this.emit("error", new Error("Heartbeat ping failed"));
69
77
  }
70
78
  }
71
- }, this.HEARTBEAT_INTERVAL);
79
+ }, TIMEOUTS.HEARTBEAT);
72
80
  }
73
- /**
74
- * Stop the client-side heartbeat
75
- * @private
76
- */
77
81
  stopHeartbeat() {
78
82
  if (this.heartbeatInterval) {
79
83
  clearInterval(this.heartbeatInterval);
@@ -88,25 +92,20 @@ class DainTunnel extends events_1.EventEmitter {
88
92
  return new Promise((resolve, reject) => {
89
93
  let resolved = false;
90
94
  let connectionTimeoutId = null;
91
- const cleanup = () => {
95
+ const finish = (value, error) => {
96
+ if (resolved)
97
+ return;
98
+ resolved = true;
92
99
  if (connectionTimeoutId) {
93
100
  clearTimeout(connectionTimeoutId);
94
101
  connectionTimeoutId = null;
95
102
  }
96
- };
97
- const safeResolve = (value) => {
98
- if (!resolved) {
99
- resolved = true;
100
- cleanup();
101
- resolve(value);
102
- }
103
- };
104
- const safeReject = (error) => {
105
- if (!resolved) {
106
- resolved = true;
107
- cleanup();
103
+ if (error) {
108
104
  reject(error);
109
105
  }
106
+ else {
107
+ resolve(value);
108
+ }
110
109
  };
111
110
  try {
112
111
  this.ws = new ws_1.default(this.serverUrl);
@@ -115,78 +114,74 @@ class DainTunnel extends events_1.EventEmitter {
115
114
  try {
116
115
  const challenge = await this.requestChallenge();
117
116
  const signature = this.signChallenge(challenge);
118
- this.sendMessage({
117
+ this.safeSend({
119
118
  type: "start",
120
119
  port: this.port,
121
120
  challenge,
122
121
  signature,
123
122
  tunnelId: this.tunnelId,
124
- apiKey: this.apiKey // Send API key for server validation
123
+ apiKey: this.apiKey
125
124
  });
126
- // Start heartbeat after successful authentication
127
125
  this.startHeartbeat();
128
126
  this.emit("connected");
129
127
  }
130
128
  catch (err) {
131
- safeReject(err);
129
+ finish(undefined, err);
132
130
  }
133
131
  });
134
132
  this.ws.on("message", (data) => {
135
133
  try {
136
- this.handleMessage(JSON.parse(data), safeResolve);
134
+ this.handleMessage(JSON.parse(data), (url) => finish(url));
137
135
  }
138
136
  catch (err) {
139
- safeReject(err);
137
+ finish(undefined, err);
140
138
  }
141
139
  });
142
140
  this.ws.on("close", () => {
143
- // Stop heartbeat on disconnect
144
141
  this.stopHeartbeat();
145
- // Clean up all active SSE connections
146
- for (const [, client] of this.sseClients) {
147
- try {
148
- client.destroy();
149
- }
150
- catch (e) { /* ignore */ }
151
- }
152
- this.sseClients.clear();
153
- // Clean up all WebSocket clients
154
- for (const [, client] of this.webSocketClients) {
155
- try {
156
- if (client.readyState === ws_1.default.OPEN)
157
- client.close(1001);
158
- }
159
- catch (e) { /* ignore */ }
160
- }
161
- this.webSocketClients.clear();
142
+ this.cleanupAllClients();
162
143
  if (this.tunnelUrl) {
163
144
  this.emit("disconnected");
164
145
  this.attemptReconnect();
165
146
  }
166
147
  else {
167
- safeReject(new Error("Connection closed before tunnel established"));
148
+ finish(undefined, new Error("Connection closed before tunnel established"));
168
149
  }
169
150
  });
170
151
  this.ws.on("error", (error) => this.emit("error", error));
171
- // Connection timeout with proper cleanup
172
152
  connectionTimeoutId = setTimeout(() => {
153
+ var _a;
173
154
  if (!resolved && (!this.ws || this.ws.readyState !== ws_1.default.OPEN)) {
174
- safeReject(new Error("Connection timeout"));
175
- // Close the WebSocket if it's still trying to connect
176
- if (this.ws) {
177
- try {
178
- this.ws.terminate();
179
- }
180
- catch (e) { /* ignore */ }
155
+ finish(undefined, new Error("Connection timeout"));
156
+ try {
157
+ (_a = this.ws) === null || _a === void 0 ? void 0 : _a.terminate();
181
158
  }
159
+ catch (_b) { }
182
160
  }
183
- }, 10000);
161
+ }, TIMEOUTS.CONNECTION);
184
162
  }
185
163
  catch (err) {
186
- safeReject(err);
164
+ finish(undefined, err);
187
165
  }
188
166
  });
189
167
  }
168
+ cleanupAllClients() {
169
+ for (const client of this.sseClients.values()) {
170
+ try {
171
+ client.destroy();
172
+ }
173
+ catch (_a) { }
174
+ }
175
+ this.sseClients.clear();
176
+ for (const client of this.webSocketClients.values()) {
177
+ try {
178
+ if (client.readyState === ws_1.default.OPEN)
179
+ client.close(1001);
180
+ }
181
+ catch (_b) { }
182
+ }
183
+ this.webSocketClients.clear();
184
+ }
190
185
  async requestChallenge() {
191
186
  return new Promise((resolve, reject) => {
192
187
  if (!this.ws) {
@@ -195,38 +190,37 @@ class DainTunnel extends events_1.EventEmitter {
195
190
  }
196
191
  let resolved = false;
197
192
  let timeoutId = null;
198
- const cleanup = () => {
199
- if (this.ws) {
200
- this.ws.removeListener("message", challengeHandler);
201
- }
193
+ const finish = (challenge, error) => {
194
+ var _a;
195
+ if (resolved)
196
+ return;
197
+ resolved = true;
198
+ (_a = this.ws) === null || _a === void 0 ? void 0 : _a.removeListener("message", challengeHandler);
202
199
  if (timeoutId) {
203
200
  clearTimeout(timeoutId);
204
201
  timeoutId = null;
205
202
  }
203
+ if (error) {
204
+ reject(error);
205
+ }
206
+ else {
207
+ resolve(challenge);
208
+ }
206
209
  };
207
210
  const challengeHandler = (message) => {
208
211
  try {
209
212
  const data = JSON.parse(message);
210
- if (data.type === "challenge" && !resolved) {
211
- resolved = true;
212
- cleanup();
213
- resolve(data.challenge);
213
+ if (data.type === "challenge") {
214
+ finish(data.challenge);
214
215
  }
215
216
  }
216
- catch (e) {
217
- // Ignore parse errors for non-challenge messages
218
- }
217
+ catch (_a) { }
219
218
  };
220
219
  this.ws.on("message", challengeHandler);
221
220
  this.ws.send(JSON.stringify({ type: "challenge_request" }));
222
- // Add a timeout for the challenge request
223
221
  timeoutId = setTimeout(() => {
224
- if (!resolved) {
225
- resolved = true;
226
- cleanup();
227
- reject(new Error("Challenge request timeout"));
228
- }
229
- }, 5000);
222
+ finish(undefined, new Error("Challenge request timeout"));
223
+ }, TIMEOUTS.CHALLENGE);
230
224
  });
231
225
  }
232
226
  handleMessage(message, resolve) {
@@ -256,141 +250,138 @@ class DainTunnel extends events_1.EventEmitter {
256
250
  async handleRequest(request) {
257
251
  try {
258
252
  const response = await this.forwardRequest(request);
259
- this.sendMessage(response);
253
+ this.safeSend(response);
260
254
  this.emit("request_handled", { request, response });
261
255
  }
262
256
  catch (error) {
263
- // Send error response back to server instead of just emitting an event
264
- // This prevents 30-second timeouts when the local service is unreachable
265
257
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
266
- const errorResponse = {
258
+ this.safeSend({
267
259
  type: "response",
268
260
  requestId: request.id,
269
- status: 502, // Bad Gateway - indicates upstream error
261
+ status: 502,
270
262
  headers: { "content-type": "application/json" },
271
263
  body: Buffer.from(JSON.stringify({
272
264
  error: "Bad Gateway",
273
265
  message: `Failed to forward request to local service: ${errorMessage}`
274
266
  })).toString("base64")
275
- };
276
- this.sendMessage(errorResponse);
267
+ });
277
268
  this.emit("request_error", { request, error });
278
269
  }
279
270
  }
280
271
  handleWebSocketConnection(message) {
281
- var _a;
272
+ const sendWsEvent = (event, data) => {
273
+ this.safeSend({ type: 'websocket', id: message.id, event, data });
274
+ };
282
275
  try {
276
+ // Preserve original host for JWT audience validation
277
+ const headers = { ...message.headers };
278
+ const originalHost = message.headers.host;
279
+ if (originalHost && !headers['x-forwarded-host']) {
280
+ headers['x-forwarded-host'] = originalHost;
281
+ headers['x-forwarded-proto'] = 'https';
282
+ }
283
+ delete headers.host; // Remove so WebSocket library doesn't send tunnel host to localhost
283
284
  const client = new ws_1.default(`ws://localhost:${this.port}${message.path}`, {
284
- headers: message.headers
285
+ headers
285
286
  });
286
287
  this.webSocketClients.set(message.id, client);
287
288
  client.on('message', (data) => {
288
- var _a;
289
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
290
- this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'message', data: data.toString('base64') }));
291
- }
289
+ sendWsEvent('message', data.toString('base64'));
292
290
  });
293
291
  client.on('close', () => {
294
- var _a;
295
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
296
- this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'close' }));
297
- }
292
+ sendWsEvent('close');
298
293
  this.webSocketClients.delete(message.id);
299
294
  });
300
295
  client.on('error', (error) => {
301
- var _a;
302
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
303
- this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'error', data: error.message }));
304
- }
296
+ sendWsEvent('error', error.message);
305
297
  this.webSocketClients.delete(message.id);
306
298
  });
307
299
  this.emit("websocket_connection", { id: message.id, path: message.path });
308
300
  }
309
301
  catch (error) {
310
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
311
- this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'error', data: error.message }));
312
- }
302
+ sendWsEvent('error', error.message);
313
303
  }
314
304
  }
315
305
  handleWebSocketMessage(message) {
316
306
  const client = this.webSocketClients.get(message.id);
317
307
  if (!client)
318
308
  return;
319
- if (message.event === 'message' && message.data) {
320
- const data = Buffer.from(message.data, 'base64');
321
- if (client.readyState === ws_1.default.OPEN) {
322
- client.send(data);
323
- }
324
- }
325
- else if (message.event === 'close') {
326
- if (client.readyState === ws_1.default.OPEN) {
327
- client.close();
328
- }
329
- this.webSocketClients.delete(message.id);
309
+ switch (message.event) {
310
+ case 'message':
311
+ if (message.data && client.readyState === ws_1.default.OPEN) {
312
+ client.send(Buffer.from(message.data, 'base64'));
313
+ }
314
+ break;
315
+ case 'close':
316
+ if (client.readyState === ws_1.default.OPEN) {
317
+ client.close();
318
+ }
319
+ this.webSocketClients.delete(message.id);
320
+ break;
330
321
  }
331
322
  }
332
323
  handleSSEConnection(message) {
333
- var _a;
324
+ const sendSseEvent = (event, data = '') => {
325
+ this.safeSend({ type: 'sse', id: message.id, event, data });
326
+ };
334
327
  try {
328
+ const headers = { ...message.headers };
329
+ // Preserve original host for JWT audience validation
330
+ const originalHost = message.headers.host;
331
+ console.log(`[TunnelClient SSE] Original host from request: ${originalHost}`);
332
+ if (originalHost && !headers['x-forwarded-host']) {
333
+ headers['x-forwarded-host'] = originalHost;
334
+ headers['x-forwarded-proto'] = 'https';
335
+ console.log(`[TunnelClient SSE] Set x-forwarded-host: ${originalHost}`);
336
+ }
337
+ delete headers.host;
338
+ headers['accept-encoding'] = 'identity';
339
+ console.log(`[TunnelClient SSE] Forwarding to localhost:${this.port}${message.path} with x-forwarded-host: ${headers['x-forwarded-host']}`);
335
340
  const req = http_1.default.request({
336
341
  hostname: 'localhost',
337
342
  port: this.port,
338
343
  path: message.path,
339
344
  method: message.method || 'GET',
340
- headers: message.headers,
345
+ headers,
341
346
  agent: this.httpAgent,
342
347
  }, (res) => {
343
- var _a;
344
- // Non-200 response - forward error to tunnel server
345
348
  if (res.statusCode !== 200) {
349
+ console.log(`[TunnelClient SSE] Got non-200 status: ${res.statusCode}`);
346
350
  let errorBody = '';
347
351
  res.on('data', (chunk) => { errorBody += chunk.toString(); });
348
352
  res.on('end', () => {
349
- var _a;
350
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
351
- this.ws.send(JSON.stringify({
352
- type: 'sse', id: message.id, event: 'error',
353
- data: `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`
354
- }));
355
- }
353
+ console.log(`[TunnelClient SSE] Error body: ${errorBody.substring(0, 500)}`);
354
+ sendSseEvent('error', `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`);
356
355
  });
357
356
  return;
358
357
  }
359
- // Optimize TCP for streaming
360
358
  const socket = res.socket || res.connection;
361
359
  if (socket === null || socket === void 0 ? void 0 : socket.setNoDelay)
362
360
  socket.setNoDelay(true);
363
- // Notify tunnel server that local service accepted
364
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
365
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'connected', data: '' }));
366
- }
367
- // Stream SSE events to tunnel server
361
+ sendSseEvent('connected');
368
362
  let buffer = '';
369
- // Helper to process SSE events from buffer
370
363
  const processBuffer = () => {
371
- var _a;
372
- // FIX: Normalize CRLF to LF for cross-platform SSE compatibility
373
- // Some SSE implementations use \r\n\r\n instead of \n\n
374
364
  buffer = buffer.replace(/\r\n/g, '\n');
375
365
  while (buffer.includes('\n\n')) {
376
366
  const idx = buffer.indexOf('\n\n');
377
367
  const msgData = buffer.substring(0, idx);
378
368
  buffer = buffer.substring(idx + 2);
379
- // Skip empty messages and keepalive comments
380
369
  if (!msgData.trim() || msgData.startsWith(':'))
381
370
  continue;
382
- let event = 'message', data = '';
371
+ let event = 'message';
372
+ let data = '';
383
373
  for (const line of msgData.split('\n')) {
384
- if (line.startsWith('event:'))
374
+ if (line.startsWith('event:')) {
385
375
  event = line.substring(6).trim();
386
- else if (line.startsWith('data:'))
376
+ }
377
+ else if (line.startsWith('data:')) {
387
378
  data += line.substring(5).trim() + '\n';
379
+ }
388
380
  }
389
381
  if (data.endsWith('\n'))
390
382
  data = data.slice(0, -1);
391
- // Only send if we have actual data
392
- if (data && ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
393
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event, data }));
383
+ if (data) {
384
+ sendSseEvent(event, data);
394
385
  }
395
386
  }
396
387
  };
@@ -399,34 +390,29 @@ class DainTunnel extends events_1.EventEmitter {
399
390
  processBuffer();
400
391
  });
401
392
  res.on('end', () => {
402
- var _a;
403
- // Process any remaining data in buffer (might be missing final \n\n)
404
393
  if (buffer.trim()) {
405
- // Add missing newlines to ensure processing
406
394
  buffer += '\n\n';
407
395
  processBuffer();
408
396
  }
409
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
410
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'close', data: '' }));
411
- }
397
+ sendSseEvent('close');
412
398
  });
413
399
  });
400
+ req.on('socket', (socket) => {
401
+ socket.setNoDelay(true);
402
+ socket.setTimeout(0);
403
+ });
414
404
  req.on('error', (error) => {
415
- var _a;
416
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
417
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: error.message }));
418
- }
405
+ sendSseEvent('error', error.message);
419
406
  });
420
- if (message.body && message.method !== 'GET')
407
+ if (message.body && message.method !== 'GET') {
421
408
  req.write(Buffer.from(message.body, 'base64'));
409
+ }
422
410
  req.end();
423
411
  this.sseClients.set(message.id, req);
424
412
  this.emit("sse_connection", { id: message.id, path: message.path });
425
413
  }
426
414
  catch (error) {
427
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
428
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: error.message }));
429
- }
415
+ sendSseEvent('error', error.message);
430
416
  }
431
417
  }
432
418
  handleSSEClose(message) {
@@ -440,146 +426,101 @@ class DainTunnel extends events_1.EventEmitter {
440
426
  return new Promise((resolve, reject) => {
441
427
  let resolved = false;
442
428
  let timeoutId = null;
443
- const cleanup = () => {
429
+ const finish = (response, error) => {
430
+ if (resolved)
431
+ return;
432
+ resolved = true;
444
433
  if (timeoutId) {
445
434
  clearTimeout(timeoutId);
446
435
  timeoutId = null;
447
436
  }
448
- };
449
- const safeResolve = (response) => {
450
- if (!resolved) {
451
- resolved = true;
452
- cleanup();
453
- resolve(response);
454
- }
455
- };
456
- const safeReject = (error) => {
457
- if (!resolved) {
458
- resolved = true;
459
- cleanup();
437
+ if (error) {
460
438
  reject(error);
461
439
  }
440
+ else {
441
+ resolve(response);
442
+ }
462
443
  };
444
+ // Preserve original host for JWT audience validation
445
+ const headers = { ...request.headers };
446
+ const originalHost = request.headers.host;
447
+ console.log(`[TunnelClient] Original host from request: ${originalHost}`);
448
+ if (originalHost && !headers['x-forwarded-host']) {
449
+ headers['x-forwarded-host'] = originalHost;
450
+ headers['x-forwarded-proto'] = 'https';
451
+ console.log(`[TunnelClient] Set x-forwarded-host: ${originalHost}`);
452
+ }
453
+ delete headers.host; // Remove so Node.js doesn't send the tunnel host to localhost
454
+ console.log(`[TunnelClient] Forwarding to localhost:${this.port}${request.path} with x-forwarded-host: ${headers['x-forwarded-host']}`);
463
455
  const options = {
464
456
  hostname: 'localhost',
465
457
  port: this.port,
466
458
  path: request.path,
467
459
  method: request.method,
468
- headers: request.headers,
469
- agent: this.httpAgent, // Use connection pooling
470
- timeout: this.REQUEST_TIMEOUT, // Socket timeout
460
+ headers,
461
+ agent: this.httpAgent,
462
+ timeout: TIMEOUTS.REQUEST,
471
463
  };
472
464
  const req = http_1.default.request(options, (res) => {
473
- let body = Buffer.from([]);
474
- res.on('data', (chunk) => {
475
- body = Buffer.concat([body, chunk]);
476
- });
465
+ const chunks = [];
466
+ res.on('data', (chunk) => chunks.push(chunk));
477
467
  res.on('end', () => {
478
468
  const headers = { ...res.headers };
479
469
  delete headers['transfer-encoding'];
480
470
  delete headers['content-length'];
481
- safeResolve({
471
+ finish({
482
472
  type: 'response',
483
473
  requestId: request.id,
484
474
  status: res.statusCode,
485
475
  headers,
486
- body: body.toString('base64'),
476
+ body: Buffer.concat(chunks).toString('base64'),
487
477
  });
488
478
  });
489
- res.on('error', safeReject);
479
+ res.on('error', (err) => finish(undefined, err));
490
480
  });
491
- // Handle request timeout
492
481
  req.on('timeout', () => {
493
482
  req.destroy();
494
- safeReject(new Error(`Request timeout after ${this.REQUEST_TIMEOUT}ms`));
483
+ finish(undefined, new Error(`Request timeout after ${TIMEOUTS.REQUEST}ms`));
495
484
  });
496
- req.on('error', safeReject);
497
- // Additional safety timeout
485
+ req.on('error', (err) => finish(undefined, err));
498
486
  timeoutId = setTimeout(() => {
499
- if (!resolved) {
500
- req.destroy();
501
- safeReject(new Error(`Request timeout after ${this.REQUEST_TIMEOUT}ms`));
502
- }
503
- }, this.REQUEST_TIMEOUT);
487
+ req.destroy();
488
+ finish(undefined, new Error(`Request timeout after ${TIMEOUTS.REQUEST}ms`));
489
+ }, TIMEOUTS.REQUEST);
504
490
  if (request.body && request.method !== 'GET') {
505
491
  req.write(Buffer.from(request.body, 'base64'));
506
492
  }
507
493
  req.end();
508
494
  });
509
495
  }
510
- sendMessage(message) {
511
- if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
512
- this.ws.send(JSON.stringify(message));
513
- }
514
- }
515
496
  attemptReconnect() {
516
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
497
+ if (this.reconnectAttempts < RECONNECT.MAX_ATTEMPTS) {
517
498
  this.reconnectAttempts++;
518
499
  setTimeout(async () => {
519
500
  try {
520
501
  await this.connect();
521
502
  }
522
- catch (error) { /* ignore */ }
523
- }, this.reconnectDelay);
503
+ catch (_a) { }
504
+ }, TIMEOUTS.RECONNECT_DELAY);
524
505
  }
525
506
  else {
526
507
  this.emit("max_reconnect_attempts");
527
508
  }
528
509
  }
529
510
  async stop() {
530
- return new Promise(async (resolve) => {
511
+ this.stopHeartbeat();
512
+ this.cleanupAllClients();
513
+ if (this.ws) {
531
514
  try {
532
- // Stop heartbeat first
533
- this.stopHeartbeat();
534
- // Close all WebSocket clients
535
- for (const [id, client] of this.webSocketClients.entries()) {
536
- try {
537
- if (client.readyState === ws_1.default.OPEN) {
538
- client.close();
539
- }
540
- }
541
- catch (error) {
542
- console.error(`Error closing WebSocket client ${id}:`, error);
543
- }
544
- }
545
- this.webSocketClients.clear();
546
- // Close all SSE clients
547
- for (const [id, client] of this.sseClients.entries()) {
548
- try {
549
- client.destroy();
550
- }
551
- catch (error) {
552
- console.error(`Error destroying SSE client ${id}:`, error);
553
- }
554
- }
555
- this.sseClients.clear();
556
- // Close main WebSocket connection
557
- if (this.ws) {
558
- try {
559
- if (this.ws.readyState === ws_1.default.OPEN) {
560
- this.ws.close();
561
- }
562
- }
563
- catch (error) {
564
- console.error('Error closing main WebSocket:', error);
565
- }
566
- this.ws = null;
567
- }
568
- // Reset tunnel URL so reconnect doesn't happen
569
- this.tunnelUrl = null;
570
- // Destroy the HTTP agent to close pooled connections
571
- if (this.httpAgent) {
572
- this.httpAgent.destroy();
573
- }
574
- // Wait for all connections to close properly
575
- await new Promise(resolve => setTimeout(resolve, 500));
576
- resolve();
577
- }
578
- catch (error) {
579
- console.error('Error in stop method:', error);
580
- resolve(); // Resolve anyway to not block cleaning up
515
+ if (this.ws.readyState === ws_1.default.OPEN)
516
+ this.ws.close();
581
517
  }
582
- });
518
+ catch (_a) { }
519
+ this.ws = null;
520
+ }
521
+ this.tunnelUrl = null;
522
+ this.httpAgent.destroy();
523
+ await new Promise(resolve => setTimeout(resolve, TIMEOUTS.SHUTDOWN_GRACE));
583
524
  }
584
525
  }
585
526
  exports.DainTunnel = DainTunnel;