@dainprotocol/tunnel 1.1.35 → 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.
@@ -10,8 +10,7 @@ declare class DainTunnel extends EventEmitter {
10
10
  private secret;
11
11
  private webSocketClients;
12
12
  private pendingWebSocketMessages;
13
- private sseClients;
14
- private httpAgent;
13
+ private sseAbortControllers;
15
14
  private heartbeatInterval;
16
15
  private reconnectTimer;
17
16
  private isStopping;
@@ -34,10 +33,6 @@ declare class DainTunnel extends EventEmitter {
34
33
  private handleSSEClose;
35
34
  private forwardRequest;
36
35
  private attemptReconnect;
37
- /**
38
- * Reset reconnection attempts counter.
39
- * Call this to allow reconnection after max_reconnect_attempts was reached.
40
- */
41
36
  resetReconnection(): void;
42
37
  stop(): Promise<void>;
43
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,10 +10,10 @@ 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,
24
17
  };
25
18
  const LIMITS = {
26
19
  MAX_PENDING_WS_MESSAGES_PER_CONNECTION: 256,
@@ -43,7 +36,7 @@ function rawDataToBuffer(data) {
43
36
  return Buffer.from(data, "utf8");
44
37
  return Buffer.from(data);
45
38
  }
46
- class DainTunnel extends events_1.EventEmitter {
39
+ class DainTunnel extends EventEmitter {
47
40
  constructor(serverUrl, apiKey) {
48
41
  super();
49
42
  this.serverUrl = serverUrl;
@@ -53,36 +46,29 @@ class DainTunnel extends events_1.EventEmitter {
53
46
  this.reconnectAttempts = 0;
54
47
  this.webSocketClients = new Map();
55
48
  this.pendingWebSocketMessages = new Map();
56
- this.sseClients = new Map();
49
+ this.sseAbortControllers = new Map();
57
50
  this.heartbeatInterval = null;
58
51
  this.reconnectTimer = null;
59
52
  this.isStopping = false;
60
- const parsed = (0, auth_1.parseAPIKey)(apiKey);
53
+ const parsed = parseAPIKey(apiKey);
61
54
  if (!parsed) {
62
55
  throw new Error('Invalid API key format. Expected: sk_agent_{agentId}_{orgId}_{secret}');
63
56
  }
64
57
  this.apiKey = apiKey;
65
58
  this.tunnelId = `${parsed.orgId}_${parsed.agentId}`;
66
59
  this.secret = parsed.secret;
67
- this.httpAgent = new http_1.default.Agent({
68
- keepAlive: true,
69
- keepAliveMsecs: 30000,
70
- maxSockets: 100,
71
- maxFreeSockets: 20,
72
- });
73
60
  }
74
61
  signChallenge(challenge) {
75
- return (0, crypto_1.createHmac)('sha256', this.secret).update(challenge).digest('hex');
62
+ return createHmac('sha256', this.secret).update(challenge).digest('hex');
76
63
  }
77
64
  safeSend(data) {
78
- var _a;
79
65
  try {
80
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
66
+ if (this.ws?.readyState === WebSocket.OPEN) {
81
67
  this.ws.send(JSON.stringify(data));
82
68
  return true;
83
69
  }
84
70
  }
85
- catch (_b) {
71
+ catch {
86
72
  // Connection lost during send
87
73
  }
88
74
  return false;
@@ -90,12 +76,11 @@ class DainTunnel extends events_1.EventEmitter {
90
76
  startHeartbeat() {
91
77
  this.stopHeartbeat();
92
78
  this.heartbeatInterval = setInterval(() => {
93
- var _a;
94
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
79
+ if (this.ws?.readyState === WebSocket.OPEN) {
95
80
  try {
96
81
  this.ws.ping();
97
82
  }
98
- catch (_b) {
83
+ catch {
99
84
  this.emit("error", new Error("Heartbeat ping failed"));
100
85
  }
101
86
  }
@@ -132,7 +117,7 @@ class DainTunnel extends events_1.EventEmitter {
132
117
  }
133
118
  };
134
119
  try {
135
- this.ws = new ws_1.default(this.serverUrl);
120
+ this.ws = new WebSocket(this.serverUrl);
136
121
  this.ws.on("open", async () => {
137
122
  this.reconnectAttempts = 0;
138
123
  try {
@@ -177,13 +162,12 @@ class DainTunnel extends events_1.EventEmitter {
177
162
  });
178
163
  this.ws.on("error", (error) => this.emit("error", error));
179
164
  connectionTimeoutId = setTimeout(() => {
180
- var _a;
181
- if (!resolved && (!this.ws || this.ws.readyState !== ws_1.default.OPEN)) {
165
+ if (!resolved && (!this.ws || this.ws.readyState !== WebSocket.OPEN)) {
182
166
  finish(undefined, new Error("Connection timeout"));
183
167
  try {
184
- (_a = this.ws) === null || _a === void 0 ? void 0 : _a.terminate();
168
+ this.ws?.terminate();
185
169
  }
186
- catch (_b) { }
170
+ catch { }
187
171
  }
188
172
  }, TIMEOUTS.CONNECTION);
189
173
  }
@@ -193,19 +177,19 @@ class DainTunnel extends events_1.EventEmitter {
193
177
  });
194
178
  }
195
179
  cleanupAllClients() {
196
- for (const client of this.sseClients.values()) {
180
+ for (const controller of this.sseAbortControllers.values()) {
197
181
  try {
198
- client.destroy();
182
+ controller.abort();
199
183
  }
200
- catch (_a) { }
184
+ catch { }
201
185
  }
202
- this.sseClients.clear();
186
+ this.sseAbortControllers.clear();
203
187
  for (const client of this.webSocketClients.values()) {
204
188
  try {
205
- if (client.readyState === ws_1.default.OPEN)
189
+ if (client.readyState === WebSocket.OPEN)
206
190
  client.close(1001);
207
191
  }
208
- catch (_b) { }
192
+ catch { }
209
193
  }
210
194
  this.webSocketClients.clear();
211
195
  this.pendingWebSocketMessages.clear();
@@ -219,11 +203,10 @@ class DainTunnel extends events_1.EventEmitter {
219
203
  let resolved = false;
220
204
  let timeoutId = null;
221
205
  const finish = (challenge, error) => {
222
- var _a;
223
206
  if (resolved)
224
207
  return;
225
208
  resolved = true;
226
- (_a = this.ws) === null || _a === void 0 ? void 0 : _a.removeListener("message", challengeHandler);
209
+ this.ws?.removeListener("message", challengeHandler);
227
210
  if (timeoutId) {
228
211
  clearTimeout(timeoutId);
229
212
  timeoutId = null;
@@ -242,7 +225,7 @@ class DainTunnel extends events_1.EventEmitter {
242
225
  finish(data.challenge);
243
226
  }
244
227
  }
245
- catch (_a) { }
228
+ catch { }
246
229
  };
247
230
  this.ws.on("message", challengeHandler);
248
231
  this.ws.send(JSON.stringify({ type: "challenge_request" }));
@@ -301,15 +284,14 @@ class DainTunnel extends events_1.EventEmitter {
301
284
  this.safeSend({ type: 'websocket', id: message.id, event, data });
302
285
  };
303
286
  try {
304
- // Preserve original host for JWT audience validation
305
287
  const headers = { ...message.headers };
306
288
  const originalHost = message.headers.host;
307
289
  if (originalHost && !headers['x-forwarded-host']) {
308
290
  headers['x-forwarded-host'] = originalHost;
309
291
  headers['x-forwarded-proto'] = 'https';
310
292
  }
311
- delete headers.host; // Remove so WebSocket library doesn't send tunnel host to localhost
312
- 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}`, {
313
295
  headers
314
296
  });
315
297
  this.pendingWebSocketMessages.set(message.id, []);
@@ -342,11 +324,11 @@ class DainTunnel extends events_1.EventEmitter {
342
324
  case 'message':
343
325
  if (!message.data)
344
326
  return;
345
- if (client.readyState === ws_1.default.OPEN) {
327
+ if (client.readyState === WebSocket.OPEN) {
346
328
  client.send(Buffer.from(message.data, 'base64'));
347
329
  return;
348
330
  }
349
- if (client.readyState !== ws_1.default.CONNECTING)
331
+ if (client.readyState !== WebSocket.CONNECTING)
350
332
  return;
351
333
  const pending = this.pendingWebSocketMessages.get(message.id);
352
334
  if (!pending)
@@ -359,13 +341,13 @@ class DainTunnel extends events_1.EventEmitter {
359
341
  pending.push(Buffer.from(message.data, 'base64'));
360
342
  break;
361
343
  case 'close':
362
- if (client.readyState === ws_1.default.OPEN || client.readyState === ws_1.default.CONNECTING) {
344
+ if (client.readyState === WebSocket.OPEN || client.readyState === WebSocket.CONNECTING) {
363
345
  client.close();
364
346
  }
365
347
  this.cleanupWebSocketConnection(message.id);
366
348
  break;
367
349
  case 'error':
368
- if (client.readyState === ws_1.default.OPEN || client.readyState === ws_1.default.CONNECTING) {
350
+ if (client.readyState === WebSocket.OPEN || client.readyState === WebSocket.CONNECTING) {
369
351
  client.close(1011, message.data);
370
352
  }
371
353
  this.cleanupWebSocketConnection(message.id);
@@ -378,7 +360,7 @@ class DainTunnel extends events_1.EventEmitter {
378
360
  if (!client || !pending || pending.length === 0)
379
361
  return;
380
362
  try {
381
- while (pending.length > 0 && client.readyState === ws_1.default.OPEN) {
363
+ while (pending.length > 0 && client.readyState === WebSocket.OPEN) {
382
364
  client.send(pending.shift());
383
365
  }
384
366
  }
@@ -397,7 +379,6 @@ class DainTunnel extends events_1.EventEmitter {
397
379
  };
398
380
  try {
399
381
  const headers = { ...message.headers };
400
- // Preserve original host for JWT audience validation
401
382
  const originalHost = message.headers.host;
402
383
  if (originalHost && !headers['x-forwarded-host']) {
403
384
  headers['x-forwarded-host'] = originalHost;
@@ -405,34 +386,35 @@ class DainTunnel extends events_1.EventEmitter {
405
386
  }
406
387
  delete headers.host;
407
388
  headers['accept-encoding'] = 'identity';
408
- const req = http_1.default.request({
409
- hostname: 'localhost',
410
- port: this.port,
411
- 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 = {
412
393
  method: message.method || 'GET',
413
394
  headers,
414
- agent: this.httpAgent,
415
- }, (res) => {
416
- const cleanup = () => {
417
- this.sseClients.delete(message.id);
418
- };
419
- if (res.statusCode !== 200) {
420
- let errorBody = '';
421
- res.on('data', (chunk) => { errorBody += chunk.toString(); });
422
- res.on('end', () => {
423
- sendSseEvent('error', `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`);
424
- cleanup();
425
- });
426
- res.on('close', cleanup);
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);
427
406
  return;
428
407
  }
429
- const socket = res.socket || res.connection;
430
- if (socket === null || socket === void 0 ? void 0 : socket.setNoDelay)
431
- socket.setNoDelay(true);
432
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();
433
416
  let buffer = '';
434
417
  const processBuffer = () => {
435
- // Normalize line endings once
436
418
  if (buffer.indexOf('\r') >= 0) {
437
419
  buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
438
420
  }
@@ -440,7 +422,6 @@ class DainTunnel extends events_1.EventEmitter {
440
422
  while ((idx = buffer.indexOf('\n\n')) >= 0) {
441
423
  const msgData = buffer.slice(0, idx);
442
424
  buffer = buffer.slice(idx + 2);
443
- // Skip empty messages and comments
444
425
  if (!msgData || msgData[0] === ':')
445
426
  continue;
446
427
  let event = 'message';
@@ -459,40 +440,36 @@ class DainTunnel extends events_1.EventEmitter {
459
440
  sendSseEvent(event, data);
460
441
  }
461
442
  };
462
- res.on('data', (chunk) => {
463
- buffer += chunk.toString();
464
- processBuffer();
465
- });
466
- 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
467
452
  if (buffer.trim()) {
468
453
  buffer += '\n\n';
469
454
  processBuffer();
470
455
  }
471
456
  sendSseEvent('close');
472
- cleanup();
473
- });
474
- res.on('close', cleanup);
475
- res.on('error', (error) => {
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') {
476
469
  sendSseEvent('error', error.message);
477
- cleanup();
478
- });
479
- });
480
- req.on('socket', (socket) => {
481
- socket.setNoDelay(true);
482
- socket.setTimeout(0);
483
- });
484
- req.on('error', (error) => {
485
- sendSseEvent('error', error.message);
486
- this.sseClients.delete(message.id);
487
- });
488
- req.on('close', () => {
489
- this.sseClients.delete(message.id);
470
+ }
471
+ this.sseAbortControllers.delete(message.id);
490
472
  });
491
- this.sseClients.set(message.id, req);
492
- if (message.body && message.method !== 'GET') {
493
- req.write(Buffer.from(message.body, 'base64'));
494
- }
495
- req.end();
496
473
  this.emit("sse_connection", { id: message.id, path: message.path });
497
474
  }
498
475
  catch (error) {
@@ -500,79 +477,44 @@ class DainTunnel extends events_1.EventEmitter {
500
477
  }
501
478
  }
502
479
  handleSSEClose(message) {
503
- const client = this.sseClients.get(message.id);
504
- if (client) {
505
- client.destroy();
506
- 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);
507
484
  }
508
485
  }
509
- forwardRequest(request) {
510
- return new Promise((resolve, reject) => {
511
- let resolved = false;
512
- let timeoutId = null;
513
- const finish = (response, error) => {
514
- if (resolved)
515
- return;
516
- resolved = true;
517
- if (timeoutId) {
518
- clearTimeout(timeoutId);
519
- timeoutId = null;
520
- }
521
- if (error) {
522
- reject(error);
523
- }
524
- else {
525
- resolve(response);
526
- }
527
- };
528
- // Preserve original host for JWT audience validation
529
- const headers = { ...request.headers };
530
- const originalHost = request.headers.host;
531
- if (originalHost && !headers['x-forwarded-host']) {
532
- headers['x-forwarded-host'] = originalHost;
533
- headers['x-forwarded-proto'] = 'https';
534
- }
535
- delete headers.host;
536
- const options = {
537
- hostname: 'localhost',
538
- port: this.port,
539
- path: request.path,
540
- method: request.method,
541
- headers,
542
- agent: this.httpAgent,
543
- timeout: TIMEOUTS.REQUEST,
544
- };
545
- const req = http_1.default.request(options, (res) => {
546
- const chunks = [];
547
- res.on('data', (chunk) => chunks.push(chunk));
548
- res.on('end', () => {
549
- const headers = { ...res.headers };
550
- delete headers['transfer-encoding'];
551
- delete headers['content-length'];
552
- finish({
553
- type: 'response',
554
- requestId: request.id,
555
- status: res.statusCode,
556
- headers,
557
- body: Buffer.concat(chunks).toString('base64'),
558
- });
559
- });
560
- res.on('error', (err) => finish(undefined, err));
561
- });
562
- req.on('timeout', () => {
563
- req.destroy();
564
- finish(undefined, new Error(`Request timeout after ${TIMEOUTS.REQUEST}ms`));
565
- });
566
- req.on('error', (err) => finish(undefined, err));
567
- timeoutId = setTimeout(() => {
568
- req.destroy();
569
- finish(undefined, new Error(`Request timeout after ${TIMEOUTS.REQUEST}ms`));
570
- }, TIMEOUTS.REQUEST);
571
- if (request.body && request.method !== 'GET') {
572
- req.write(Buffer.from(request.body, 'base64'));
573
- }
574
- 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;
575
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
+ };
576
518
  }
577
519
  attemptReconnect() {
578
520
  if (this.isStopping)
@@ -582,7 +524,6 @@ class DainTunnel extends events_1.EventEmitter {
582
524
  return;
583
525
  }
584
526
  this.reconnectAttempts++;
585
- // Exponential backoff with jitter
586
527
  const baseDelay = Math.min(RECONNECT.BASE_DELAY * Math.pow(2, this.reconnectAttempts - 1), RECONNECT.MAX_DELAY);
587
528
  const jitter = baseDelay * RECONNECT.JITTER * (Math.random() - 0.5);
588
529
  const delay = Math.round(baseDelay + jitter);
@@ -592,16 +533,12 @@ class DainTunnel extends events_1.EventEmitter {
592
533
  await this.connect();
593
534
  this.emit("reconnected");
594
535
  }
595
- catch (_a) {
536
+ catch {
596
537
  if (!this.isStopping)
597
538
  this.attemptReconnect();
598
539
  }
599
540
  }, delay);
600
541
  }
601
- /**
602
- * Reset reconnection attempts counter.
603
- * Call this to allow reconnection after max_reconnect_attempts was reached.
604
- */
605
542
  resetReconnection() {
606
543
  this.reconnectAttempts = 0;
607
544
  }
@@ -615,18 +552,17 @@ class DainTunnel extends events_1.EventEmitter {
615
552
  }
616
553
  if (this.ws) {
617
554
  try {
618
- if (this.ws.readyState === ws_1.default.OPEN)
555
+ if (this.ws.readyState === WebSocket.OPEN)
619
556
  this.ws.close();
620
557
  }
621
- catch (_a) { }
558
+ catch { }
622
559
  this.ws = null;
623
560
  }
624
561
  this.tunnelUrl = null;
625
- this.httpAgent.destroy();
626
562
  await new Promise(resolve => setTimeout(resolve, TIMEOUTS.SHUTDOWN_GRACE));
627
563
  }
628
564
  }
629
- exports.DainTunnel = DainTunnel;
630
- exports.default = {
565
+ export { DainTunnel };
566
+ export default {
631
567
  createTunnel: (serverUrl, apiKey) => new DainTunnel(serverUrl, apiKey),
632
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;
@@ -15,10 +15,11 @@ declare class DainTunnelServer {
15
15
  private buildForwardedHeaders;
16
16
  private buildTunnelUrl;
17
17
  constructor(hostname: string, port: number);
18
- private setupExpressRoutes;
19
- private setupWebSocketServer;
20
- private handleTunnelClientConnection;
21
- private handleProxiedWebSocketConnection;
18
+ private setupRoutes;
19
+ private handleWsOpen;
20
+ private handleWsMessage;
21
+ private handleWsClose;
22
+ private handleProxiedWebSocketClientMessage;
22
23
  private handleChallengeRequest;
23
24
  private handleStartMessage;
24
25
  private handleResponseMessage;