@dainprotocol/tunnel 1.1.30 → 1.1.31

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.
@@ -31,9 +31,6 @@ class DainTunnel extends events_1.EventEmitter {
31
31
  this.webSocketClients = new Map();
32
32
  this.sseClients = new Map();
33
33
  this.heartbeatInterval = null;
34
- console.log('[DainTunnel] ========================================');
35
- console.log('[DainTunnel] FIXED VERSION WITH X-FORWARDED-HOST SUPPORT');
36
- console.log('[DainTunnel] ========================================');
37
34
  const parsed = (0, auth_1.parseAPIKey)(apiKey);
38
35
  if (!parsed) {
39
36
  throw new Error('Invalid API key format. Expected: sk_agent_{agentId}_{orgId}_{secret}');
@@ -328,15 +325,12 @@ class DainTunnel extends events_1.EventEmitter {
328
325
  const headers = { ...message.headers };
329
326
  // Preserve original host for JWT audience validation
330
327
  const originalHost = message.headers.host;
331
- console.log(`[TunnelClient SSE] Original host from request: ${originalHost}`);
332
328
  if (originalHost && !headers['x-forwarded-host']) {
333
329
  headers['x-forwarded-host'] = originalHost;
334
330
  headers['x-forwarded-proto'] = 'https';
335
- console.log(`[TunnelClient SSE] Set x-forwarded-host: ${originalHost}`);
336
331
  }
337
332
  delete headers.host;
338
333
  headers['accept-encoding'] = 'identity';
339
- console.log(`[TunnelClient SSE] Forwarding to localhost:${this.port}${message.path} with x-forwarded-host: ${headers['x-forwarded-host']}`);
340
334
  const req = http_1.default.request({
341
335
  hostname: 'localhost',
342
336
  port: this.port,
@@ -346,11 +340,9 @@ class DainTunnel extends events_1.EventEmitter {
346
340
  agent: this.httpAgent,
347
341
  }, (res) => {
348
342
  if (res.statusCode !== 200) {
349
- console.log(`[TunnelClient SSE] Got non-200 status: ${res.statusCode}`);
350
343
  let errorBody = '';
351
344
  res.on('data', (chunk) => { errorBody += chunk.toString(); });
352
345
  res.on('end', () => {
353
- console.log(`[TunnelClient SSE] Error body: ${errorBody.substring(0, 500)}`);
354
346
  sendSseEvent('error', `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`);
355
347
  });
356
348
  return;
@@ -361,28 +353,31 @@ class DainTunnel extends events_1.EventEmitter {
361
353
  sendSseEvent('connected');
362
354
  let buffer = '';
363
355
  const processBuffer = () => {
364
- buffer = buffer.replace(/\r\n/g, '\n');
365
- while (buffer.includes('\n\n')) {
366
- const idx = buffer.indexOf('\n\n');
367
- const msgData = buffer.substring(0, idx);
368
- buffer = buffer.substring(idx + 2);
369
- if (!msgData.trim() || msgData.startsWith(':'))
356
+ // Normalize line endings once
357
+ if (buffer.indexOf('\r') >= 0) {
358
+ buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
359
+ }
360
+ let idx;
361
+ while ((idx = buffer.indexOf('\n\n')) >= 0) {
362
+ const msgData = buffer.slice(0, idx);
363
+ buffer = buffer.slice(idx + 2);
364
+ // Skip empty messages and comments
365
+ if (!msgData || msgData[0] === ':')
370
366
  continue;
371
367
  let event = 'message';
372
368
  let data = '';
373
- for (const line of msgData.split('\n')) {
374
- if (line.startsWith('event:')) {
375
- event = line.substring(6).trim();
369
+ const lines = msgData.split('\n');
370
+ for (let i = 0; i < lines.length; i++) {
371
+ const line = lines[i];
372
+ if (line.length > 6 && line[0] === 'e' && line.startsWith('event:')) {
373
+ event = line.slice(6).trim();
376
374
  }
377
- else if (line.startsWith('data:')) {
378
- data += line.substring(5).trim() + '\n';
375
+ else if (line.length > 5 && line[0] === 'd' && line.startsWith('data:')) {
376
+ data += (data ? '\n' : '') + line.slice(5).trim();
379
377
  }
380
378
  }
381
- if (data.endsWith('\n'))
382
- data = data.slice(0, -1);
383
- if (data) {
379
+ if (data)
384
380
  sendSseEvent(event, data);
385
- }
386
381
  }
387
382
  };
388
383
  res.on('data', (chunk) => {
@@ -444,14 +439,11 @@ class DainTunnel extends events_1.EventEmitter {
444
439
  // Preserve original host for JWT audience validation
445
440
  const headers = { ...request.headers };
446
441
  const originalHost = request.headers.host;
447
- console.log(`[TunnelClient] Original host from request: ${originalHost}`);
448
442
  if (originalHost && !headers['x-forwarded-host']) {
449
443
  headers['x-forwarded-host'] = originalHost;
450
444
  headers['x-forwarded-proto'] = 'https';
451
- console.log(`[TunnelClient] Set x-forwarded-host: ${originalHost}`);
452
445
  }
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']}`);
446
+ delete headers.host;
455
447
  const options = {
456
448
  hostname: 'localhost',
457
449
  port: this.port,
@@ -9,13 +9,12 @@ const ws_1 = __importDefault(require("ws"));
9
9
  const body_parser_1 = __importDefault(require("body-parser"));
10
10
  const auth_1 = require("@dainprotocol/service-sdk/service/auth");
11
11
  const crypto_1 = require("crypto");
12
- const url_1 = require("url");
13
12
  const TIMEOUTS = {
14
13
  CHALLENGE_TTL: 30000,
15
14
  PING_INTERVAL: 30000,
16
15
  REQUEST_TIMEOUT: 30000,
17
16
  SSE_KEEPALIVE: 5000,
18
- TUNNEL_RETRY_DELAY: 100,
17
+ TUNNEL_RETRY_DELAY: 20, // Reduced from 100ms
19
18
  SERVER_KEEPALIVE: 65000,
20
19
  SERVER_HEADERS: 66000,
21
20
  };
@@ -26,11 +25,21 @@ const LIMITS = {
26
25
  BACKPRESSURE_THRESHOLD: 1024 * 1024,
27
26
  SERVER_MAX_HEADERS: 100,
28
27
  WS_BACKLOG: 100,
29
- TUNNEL_RETRY_COUNT: 3,
28
+ TUNNEL_RETRY_COUNT: 2, // Reduced from 3
30
29
  };
30
+ // Fast ID generator - no crypto needed for internal IDs
31
31
  let idCounter = 0;
32
+ const ID_PREFIX = Date.now().toString(36);
32
33
  function fastId() {
33
- return `${Date.now().toString(36)}-${(idCounter++).toString(36)}-${(0, crypto_1.randomBytes)(4).toString('hex')}`;
34
+ return `${ID_PREFIX}-${(++idCounter).toString(36)}`;
35
+ }
36
+ // Fast path extraction - avoids URL parsing overhead
37
+ function extractPathParts(url) {
38
+ if (!url)
39
+ return [];
40
+ const qIdx = url.indexOf('?');
41
+ const path = qIdx >= 0 ? url.slice(0, qIdx) : url;
42
+ return path.split('/');
34
43
  }
35
44
  class DainTunnelServer {
36
45
  safeSend(ws, data) {
@@ -121,11 +130,9 @@ class DainTunnelServer {
121
130
  }
122
131
  setupWebSocketServer() {
123
132
  this.wss.on("connection", (ws, req) => {
124
- var _a;
125
133
  try {
126
- const url = (0, url_1.parse)(req.url || '', true);
127
- const pathParts = ((_a = url.pathname) === null || _a === void 0 ? void 0 : _a.split('/')) || [];
128
- const isRootConnection = !url.pathname || url.pathname === '/' || pathParts.length <= 1 || !pathParts[1];
134
+ const pathParts = extractPathParts(req.url);
135
+ const isRootConnection = pathParts.length <= 1 || !pathParts[1];
129
136
  if (isRootConnection) {
130
137
  this.handleTunnelClientConnection(ws, req);
131
138
  }
@@ -170,9 +177,7 @@ class DainTunnelServer {
170
177
  ws.on("error", (error) => console.error("[Tunnel] WS error:", error));
171
178
  }
172
179
  handleProxiedWebSocketConnection(ws, req) {
173
- var _a;
174
- const url = (0, url_1.parse)(req.url || '', true);
175
- const pathParts = ((_a = url.pathname) === null || _a === void 0 ? void 0 : _a.split('/')) || [];
180
+ const pathParts = extractPathParts(req.url);
176
181
  if (pathParts.length < 2) {
177
182
  ws.close(1008, "Invalid tunnel ID");
178
183
  return;
@@ -299,21 +304,20 @@ class DainTunnelServer {
299
304
  }
300
305
  }
301
306
  handleResponseMessage(data) {
302
- console.log(`[Response] Received for: ${data.requestId}, status: ${data.status}`);
303
307
  const pendingRequest = this.pendingRequests.get(data.requestId);
304
308
  if (!pendingRequest)
305
309
  return;
306
- const { res, startTime, tunnelId, timeoutId } = pendingRequest;
307
- console.log(`[Response] Completed in ${Date.now() - startTime}ms`);
310
+ const { res, tunnelId, timeoutId } = pendingRequest;
311
+ this.pendingRequests.delete(data.requestId);
308
312
  if (timeoutId)
309
313
  clearTimeout(timeoutId);
310
314
  this.decrementRequestCount(tunnelId);
311
- const headers = { ...data.headers };
315
+ // Modify headers in place instead of spreading
316
+ const headers = data.headers;
312
317
  delete headers["transfer-encoding"];
313
318
  delete headers["content-length"];
314
319
  const bodyBuffer = Buffer.from(data.body, "base64");
315
320
  res.status(data.status).set(headers).set("Content-Length", bodyBuffer.length.toString()).send(bodyBuffer);
316
- this.pendingRequests.delete(data.requestId);
317
321
  }
318
322
  handleSSEMessage(data) {
319
323
  const connection = this.sseConnections.get(data.id);
@@ -340,12 +344,11 @@ class DainTunnelServer {
340
344
  return;
341
345
  }
342
346
  try {
343
- if (data.event)
344
- res.write(`event: ${data.event}\n`);
345
- for (const line of data.data.split('\n')) {
346
- res.write(`data: ${line}\n`);
347
- }
348
- res.write('\n');
347
+ // Batch write SSE event in single call
348
+ const lines = data.data.split('\n');
349
+ const message = (data.event ? `event: ${data.event}\n` : '') +
350
+ lines.map(line => `data: ${line}`).join('\n') + '\n\n';
351
+ res.write(message);
349
352
  }
350
353
  catch (_c) {
351
354
  this.cleanupSSEConnection(data.id, tunnelId);
@@ -382,10 +385,9 @@ class DainTunnelServer {
382
385
  }
383
386
  }
384
387
  async handleRequest(req, res) {
385
- var _a, _b, _c;
388
+ var _a, _b;
386
389
  const tunnelId = req.params.tunnelId;
387
- console.log(`[Request] ${req.method} /${tunnelId}${req.url}, Accept: ${(_a = req.headers.accept) === null || _a === void 0 ? void 0 : _a.substring(0, 30)}`);
388
- if (((_b = req.headers.upgrade) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === 'websocket')
390
+ if (((_a = req.headers.upgrade) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'websocket')
389
391
  return;
390
392
  let tunnel;
391
393
  let retries = LIMITS.TUNNEL_RETRY_COUNT;
@@ -397,17 +399,14 @@ class DainTunnelServer {
397
399
  }
398
400
  }
399
401
  if (!tunnel) {
400
- const available = Array.from(this.tunnels.keys());
401
- console.log(`[Request] Tunnel not found: ${tunnelId}, available tunnels: [${available.join(', ')}], count: ${available.length}`);
402
402
  this.decrementRequestCount(tunnelId);
403
403
  res.status(502).json({
404
404
  error: "Bad Gateway",
405
405
  message: `Tunnel "${tunnelId}" not connected. The service may be offline or reconnecting.`,
406
- availableTunnels: available.length
406
+ availableTunnels: this.tunnels.size
407
407
  });
408
408
  return;
409
409
  }
410
- console.log(`[Request] Tunnel found: ${tunnelId}, wsState: ${tunnel.ws.readyState}`);
411
410
  if (tunnel.ws.bufferedAmount > LIMITS.BACKPRESSURE_THRESHOLD) {
412
411
  res.status(503).json({ error: "Service Unavailable", message: "Tunnel high load" });
413
412
  return;
@@ -418,7 +417,7 @@ class DainTunnelServer {
418
417
  return;
419
418
  }
420
419
  this.tunnelRequestCount.set(tunnelId, currentCount + 1);
421
- if ((_c = req.headers.accept) === null || _c === void 0 ? void 0 : _c.includes('text/event-stream')) {
420
+ if ((_b = req.headers.accept) === null || _b === void 0 ? void 0 : _b.includes('text/event-stream')) {
422
421
  this.handleSSERequest(req, res, tunnelId, tunnel);
423
422
  return;
424
423
  }
@@ -435,7 +434,6 @@ class DainTunnelServer {
435
434
  }
436
435
  }, TIMEOUTS.REQUEST_TIMEOUT);
437
436
  this.pendingRequests.set(requestId, { res, startTime, tunnelId, timeoutId });
438
- console.log(`[Request] Sending to tunnel client: ${requestId}, wsState: ${tunnel.ws.readyState}, buffered: ${tunnel.ws.bufferedAmount}`);
439
437
  const hasBody = req.method !== "GET" && req.method !== "HEAD" &&
440
438
  req.body && Buffer.isBuffer(req.body) && req.body.length > 0;
441
439
  const sent = this.safeSend(tunnel.ws, {
@@ -446,7 +444,6 @@ class DainTunnelServer {
446
444
  headers: req.headers,
447
445
  body: hasBody ? req.body.toString("base64") : undefined,
448
446
  });
449
- console.log(`[Request] Sent to tunnel client: ${sent}`);
450
447
  if (!sent) {
451
448
  clearTimeout(timeoutId);
452
449
  this.pendingRequests.delete(requestId);
@@ -459,8 +456,6 @@ class DainTunnelServer {
459
456
  handleSSERequest(req, res, tunnelId, tunnel) {
460
457
  var _a, _b;
461
458
  const sseId = fastId();
462
- const startTime = Date.now();
463
- console.log(`[SSE] ${sseId} started: ${req.url}`);
464
459
  if (req.socket) {
465
460
  req.socket.setNoDelay(true);
466
461
  req.socket.setTimeout(0);
@@ -525,7 +520,6 @@ class DainTunnelServer {
525
520
  if (cleanedUp)
526
521
  return;
527
522
  cleanedUp = true;
528
- console.log(`[SSE] ${sseId} completed in ${Date.now() - startTime}ms`);
529
523
  this.safeSend(tunnel.ws, { type: "sse_close", id: sseId });
530
524
  this.cleanupSSEConnection(sseId, tunnelId);
531
525
  };
package/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "@dainprotocol/tunnel",
3
- "version": "1.1.30",
3
+ "version": "1.1.31",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "build:types": "tsc --emitDeclarationOnly",
13
+ "test": "jest",
14
+ "test:watch": "jest --watch",
15
+ "prepublishOnly": "npm run build && npm run build:types",
16
+ "start": "node dist/server/start.js",
17
+ "start-server": "ts-node src/server/start.ts"
18
+ },
10
19
  "keywords": [],
11
20
  "author": "Ryan",
12
21
  "license": "ISC",
@@ -68,13 +77,5 @@
68
77
  "./dist/server/*.d.ts"
69
78
  ]
70
79
  }
71
- },
72
- "scripts": {
73
- "build": "tsc",
74
- "build:types": "tsc --emitDeclarationOnly",
75
- "test": "jest",
76
- "test:watch": "jest --watch",
77
- "start": "node dist/server/start.js",
78
- "start-server": "ts-node src/server/start.ts"
79
80
  }
80
- }
81
+ }