@dainprotocol/tunnel 1.1.35 → 2.0.1

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.
@@ -1,20 +1,12 @@
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
- const express_1 = __importDefault(require("express"));
7
- const http_1 = __importDefault(require("http"));
8
- const ws_1 = __importDefault(require("ws"));
9
- const body_parser_1 = __importDefault(require("body-parser"));
10
- const auth_1 = require("@dainprotocol/service-sdk/service/auth");
11
- const crypto_1 = require("crypto");
1
+ import { Hono } from "hono";
2
+ import { parseAPIKey } from "@dainprotocol/service-sdk/service/auth";
3
+ import { createHmac, timingSafeEqual } from "crypto";
12
4
  const TIMEOUTS = {
13
5
  CHALLENGE_TTL: 30000,
14
6
  PING_INTERVAL: 30000,
15
7
  REQUEST_TIMEOUT: 30000,
16
8
  SSE_KEEPALIVE: 5000,
17
- TUNNEL_RETRY_DELAY: 20, // Reduced from 100ms
9
+ TUNNEL_RETRY_DELAY: 20,
18
10
  SERVER_KEEPALIVE: 65000,
19
11
  SERVER_HEADERS: 66000,
20
12
  };
@@ -23,17 +15,16 @@ const LIMITS = {
23
15
  MAX_MISSED_PONGS: 2,
24
16
  MAX_PAYLOAD_BYTES: 100 * 1024 * 1024,
25
17
  BACKPRESSURE_THRESHOLD: 1024 * 1024,
18
+ MAX_PENDING_WS_EVENTS_PER_CONNECTION: 256,
26
19
  SERVER_MAX_HEADERS: 100,
27
20
  WS_BACKLOG: 100,
28
- TUNNEL_RETRY_COUNT: 2, // Reduced from 3
21
+ TUNNEL_RETRY_COUNT: 2,
29
22
  };
30
- // Fast ID generator - no crypto needed for internal IDs
31
23
  let idCounter = 0;
32
24
  const ID_PREFIX = Date.now().toString(36);
33
25
  function fastId() {
34
26
  return `${ID_PREFIX}-${(++idCounter).toString(36)}`;
35
27
  }
36
- // Fast path extraction - avoids URL parsing overhead
37
28
  function extractPathParts(url) {
38
29
  if (!url)
39
30
  return [];
@@ -41,31 +32,45 @@ function extractPathParts(url) {
41
32
  const path = qIdx >= 0 ? url.slice(0, qIdx) : url;
42
33
  return path.split('/');
43
34
  }
44
- function rawDataToString(message) {
45
- if (typeof message === "string")
46
- return message;
47
- if (Buffer.isBuffer(message))
48
- return message.toString("utf8");
49
- if (Array.isArray(message))
50
- return Buffer.concat(message).toString("utf8");
51
- return Buffer.from(message).toString("utf8");
52
- }
53
- function rawDataToBase64(data) {
54
- if (typeof data === "string")
55
- return Buffer.from(data, "utf8").toString("base64");
56
- if (Buffer.isBuffer(data))
57
- return data.toString("base64");
58
- if (Array.isArray(data))
59
- return Buffer.concat(data).toString("base64");
60
- return Buffer.from(data).toString("base64");
61
- }
62
35
  function normalizeHostToBaseUrl(hostname) {
63
36
  return /^https?:\/\//i.test(hostname) ? hostname : `http://${hostname}`;
64
37
  }
38
+ function parseAllowedCorsOrigins(raw) {
39
+ if (!raw)
40
+ return null;
41
+ const origins = raw
42
+ .split(",")
43
+ .map((origin) => origin.trim().toLowerCase())
44
+ .filter(Boolean);
45
+ return origins.length > 0 ? new Set(origins) : null;
46
+ }
47
+ function isCorsOriginAllowed(origin, allowedOrigins) {
48
+ if (!allowedOrigins)
49
+ return true;
50
+ if (allowedOrigins.has("*"))
51
+ return true;
52
+ return allowedOrigins.has(origin.trim().toLowerCase());
53
+ }
54
+ const ALLOWED_CORS_ORIGINS = parseAllowedCorsOrigins(process.env.TUNNEL_ALLOWED_ORIGINS || process.env.CORS_ALLOWED_ORIGINS);
65
55
  class DainTunnelServer {
56
+ buildCorsHeaders(origin) {
57
+ const headers = {
58
+ "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
59
+ "access-control-allow-headers": "X-DAIN-SIGNATURE, X-DAIN-SMART-ACCOUNT-PDA, X-DAIN-AGENT-ID, X-DAIN-ORG-ID, X-DAIN-ADDRESS, X-DAIN-TIMESTAMP, X-DAIN-WEBHOOK-URL, X-Service, X-Service-Token, Content-Type, Authorization, Accept, Origin, X-Requested-With",
60
+ };
61
+ if (origin && isCorsOriginAllowed(origin, this.allowedCorsOrigins)) {
62
+ headers["access-control-allow-origin"] = origin;
63
+ headers["access-control-allow-credentials"] = "true";
64
+ headers["vary"] = "Origin";
65
+ }
66
+ else if (!origin) {
67
+ headers["access-control-allow-origin"] = "*";
68
+ }
69
+ return headers;
70
+ }
66
71
  safeSend(ws, data) {
67
72
  try {
68
- if (ws.readyState === ws_1.default.OPEN) {
73
+ if (ws.readyState === 1) { // WebSocket.OPEN
69
74
  ws.send(JSON.stringify(data));
70
75
  return true;
71
76
  }
@@ -103,55 +108,43 @@ class DainTunnelServer {
103
108
  constructor(hostname, port) {
104
109
  this.hostname = hostname;
105
110
  this.port = port;
111
+ this.server = null;
112
+ this.allowedCorsOrigins = ALLOWED_CORS_ORIGINS;
106
113
  this.tunnels = new Map();
107
114
  this.pendingRequests = new Map();
108
115
  this.challenges = new Map();
109
116
  this.sseConnections = new Map();
110
117
  this.wsConnections = new Map();
118
+ this.pendingProxiedWSEvents = new Map();
111
119
  this.tunnelRequestCount = new Map();
112
- this.app = (0, express_1.default)();
113
- this.server = http_1.default.createServer(this.app);
114
- this.server.keepAliveTimeout = TIMEOUTS.SERVER_KEEPALIVE;
115
- this.server.headersTimeout = TIMEOUTS.SERVER_HEADERS;
116
- this.server.maxHeadersCount = LIMITS.SERVER_MAX_HEADERS;
117
- this.wss = new ws_1.default.Server({
118
- server: this.server,
119
- path: undefined,
120
- backlog: LIMITS.WS_BACKLOG,
121
- perMessageDeflate: false,
122
- maxPayload: LIMITS.MAX_PAYLOAD_BYTES,
123
- });
124
- this.app.use((req, res, next) => {
125
- const origin = req.headers.origin;
120
+ this.app = new Hono();
121
+ this.setupRoutes();
122
+ }
123
+ setupRoutes() {
124
+ // CORS middleware
125
+ this.app.use("*", async (c, next) => {
126
+ const origin = c.req.header("origin");
126
127
  if (origin) {
127
- res.header("Access-Control-Allow-Origin", origin);
128
- res.header("Vary", "Origin");
129
- res.header("Access-Control-Allow-Credentials", "true");
128
+ if (!isCorsOriginAllowed(origin, this.allowedCorsOrigins)) {
129
+ return c.json({ error: "Origin not allowed" }, 403);
130
+ }
131
+ c.header("Access-Control-Allow-Origin", origin);
132
+ c.header("Vary", "Origin");
133
+ c.header("Access-Control-Allow-Credentials", "true");
130
134
  }
131
135
  else {
132
- res.header("Access-Control-Allow-Origin", "*");
136
+ c.header("Access-Control-Allow-Origin", "*");
133
137
  }
134
- res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
135
- res.header("Access-Control-Allow-Headers", "X-DAIN-SIGNATURE, X-DAIN-SMART-ACCOUNT-PDA, X-DAIN-AGENT-ID, X-DAIN-ORG-ID, X-DAIN-ADDRESS, X-DAIN-TIMESTAMP, X-DAIN-WEBHOOK-URL, Content-Type, Authorization, Accept, Origin, X-Requested-With");
136
- if (req.method === "OPTIONS")
137
- return res.sendStatus(204);
138
- next();
139
- });
140
- this.app.use((req, res, next) => {
141
- if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
142
- return next();
138
+ c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
139
+ c.header("Access-Control-Allow-Headers", "X-DAIN-SIGNATURE, X-DAIN-SMART-ACCOUNT-PDA, X-DAIN-AGENT-ID, X-DAIN-ORG-ID, X-DAIN-ADDRESS, X-DAIN-TIMESTAMP, X-DAIN-WEBHOOK-URL, X-Service, X-Service-Token, Content-Type, Authorization, Accept, Origin, X-Requested-With");
140
+ if (c.req.method === "OPTIONS") {
141
+ return c.body(null, 204);
143
142
  }
144
- body_parser_1.default.raw({ type: "*/*", limit: "100mb" })(req, res, next);
143
+ await next();
145
144
  });
146
- this.setupExpressRoutes();
147
- this.setupWebSocketServer();
148
- }
149
- setupExpressRoutes() {
150
- this.app.get("/", (req, res, next) => {
151
- var _a;
152
- if (((_a = req.headers.upgrade) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'websocket')
153
- return next();
154
- res.status(200).json({
145
+ // Health/status at root
146
+ this.app.get("/", (c) => {
147
+ return c.json({
155
148
  status: "healthy",
156
149
  tunnels: this.tunnels.size,
157
150
  pendingRequests: this.pendingRequests.size,
@@ -160,102 +153,87 @@ class DainTunnelServer {
160
153
  uptime: process.uptime()
161
154
  });
162
155
  });
163
- this.app.get("/health", (req, res) => {
164
- res.status(200).json({
156
+ this.app.get("/health", (c) => {
157
+ return c.json({
165
158
  status: "ok",
166
159
  tunnels: this.tunnels.size,
167
160
  pendingRequests: this.pendingRequests.size,
168
161
  uptime: process.uptime()
169
162
  });
170
163
  });
171
- this.app.use("/:tunnelId", this.handleRequest.bind(this));
164
+ // Tunnel request handler - all methods
165
+ this.app.all("/:tunnelId/*", (c) => this.handleRequest(c));
166
+ this.app.all("/:tunnelId", (c) => this.handleRequest(c));
172
167
  }
173
- setupWebSocketServer() {
174
- this.wss.on("connection", (ws, req) => {
175
- try {
176
- const pathParts = extractPathParts(req.url);
177
- const isRootConnection = pathParts.length <= 1 || !pathParts[1];
178
- if (isRootConnection) {
179
- this.handleTunnelClientConnection(ws, req);
180
- }
181
- else {
182
- this.handleProxiedWebSocketConnection(ws, req);
183
- }
184
- }
185
- catch (error) {
186
- console.error("[Tunnel] WebSocket setup error:", error);
187
- ws.close(1011, "Internal server error");
168
+ // --- WebSocket handlers for Bun.serve ---
169
+ handleWsOpen(ws) {
170
+ // Nothing needed on open for tunnel clients; proxied WS connections
171
+ // are set up via the data attached during upgrade
172
+ }
173
+ handleWsMessage(ws, message) {
174
+ const data = ws.data;
175
+ if (data.isProxiedWebSocket) {
176
+ // This is a proxied WebSocket client connection
177
+ this.handleProxiedWebSocketClientMessage(ws, message);
178
+ return;
179
+ }
180
+ // This is a tunnel client connection
181
+ try {
182
+ const msgStr = typeof message === "string" ? message : Buffer.from(message).toString("utf8");
183
+ const parsed = JSON.parse(msgStr);
184
+ switch (parsed.type) {
185
+ case "challenge_request":
186
+ this.handleChallengeRequest(ws);
187
+ break;
188
+ case "start":
189
+ this.handleStartMessage(ws, parsed);
190
+ break;
191
+ case "response":
192
+ this.handleResponseMessage(parsed);
193
+ break;
194
+ case "sse":
195
+ this.handleSSEMessage(parsed);
196
+ break;
197
+ case "websocket":
198
+ this.handleWebSocketMessage(parsed);
199
+ break;
188
200
  }
189
- });
201
+ }
202
+ catch (error) {
203
+ console.error("[Tunnel] Message error:", error);
204
+ ws.close(1008, "Invalid message");
205
+ }
190
206
  }
191
- handleTunnelClientConnection(ws, req) {
192
- ws.on("message", (message) => {
193
- try {
194
- const data = JSON.parse(rawDataToString(message));
195
- switch (data.type) {
196
- case "challenge_request":
197
- this.handleChallengeRequest(ws);
198
- break;
199
- case "start":
200
- this.handleStartMessage(ws, data);
201
- break;
202
- case "response":
203
- this.handleResponseMessage(data);
204
- break;
205
- case "sse":
206
- this.handleSSEMessage(data);
207
- break;
208
- case "websocket":
209
- this.handleWebSocketMessage(data);
210
- break;
207
+ handleWsClose(ws, code, reason) {
208
+ const data = ws.data;
209
+ if (data.isProxiedWebSocket) {
210
+ // Proxied WS client disconnected
211
+ if (data.proxiedWsId && data.proxiedTunnelId) {
212
+ const tunnel = this.tunnels.get(data.proxiedTunnelId);
213
+ if (tunnel) {
214
+ this.safeSend(tunnel.ws, { type: "websocket", id: data.proxiedWsId, event: "close" });
211
215
  }
216
+ this.wsConnections.delete(data.proxiedWsId);
212
217
  }
213
- catch (error) {
214
- console.error("[Tunnel] Message error:", error);
215
- ws.close(1008, "Invalid message");
216
- }
217
- });
218
- ws.on("close", () => this.removeTunnel(ws));
219
- ws.on("error", (error) => console.error("[Tunnel] WS error:", error));
220
- }
221
- handleProxiedWebSocketConnection(ws, req) {
222
- const pathParts = extractPathParts(req.url);
223
- if (pathParts.length < 2) {
224
- ws.close(1008, "Invalid tunnel ID");
225
218
  return;
226
219
  }
227
- const tunnelId = pathParts[1];
228
- const remainingPath = '/' + pathParts.slice(2).join('/');
229
- const tunnel = this.tunnels.get(tunnelId);
230
- if (!tunnel) {
231
- ws.close(1008, "Tunnel not found");
220
+ // Tunnel client disconnected
221
+ this.removeTunnel(ws);
222
+ }
223
+ // --- Proxied WebSocket handling ---
224
+ handleProxiedWebSocketClientMessage(ws, message) {
225
+ const { proxiedWsId, proxiedTunnelId } = ws.data;
226
+ if (!proxiedWsId || !proxiedTunnelId)
232
227
  return;
233
- }
234
- const wsConnectionId = fastId();
235
- const forwardedHeaders = this.buildForwardedHeaders(req.headers, tunnelId);
236
- this.wsConnections.set(wsConnectionId, {
237
- clientSocket: ws,
238
- id: wsConnectionId,
239
- path: remainingPath,
240
- headers: forwardedHeaders,
241
- tunnelId
242
- });
243
- this.safeSend(tunnel.ws, {
244
- type: "websocket_connection",
245
- id: wsConnectionId,
246
- path: remainingPath,
247
- headers: forwardedHeaders
248
- });
249
- const sendToTunnel = (event, data) => {
250
- const currentTunnel = this.tunnels.get(tunnelId);
251
- if (currentTunnel) {
252
- this.safeSend(currentTunnel.ws, { type: "websocket", id: wsConnectionId, event, data });
253
- }
254
- };
255
- ws.on("message", (data) => sendToTunnel("message", rawDataToBase64(data)));
256
- ws.on("close", () => { sendToTunnel("close"); this.wsConnections.delete(wsConnectionId); });
257
- ws.on("error", (error) => sendToTunnel("error", error.message));
228
+ const tunnel = this.tunnels.get(proxiedTunnelId);
229
+ if (!tunnel)
230
+ return;
231
+ const base64Data = typeof message === "string"
232
+ ? Buffer.from(message, "utf8").toString("base64")
233
+ : Buffer.from(message).toString("base64");
234
+ this.safeSend(tunnel.ws, { type: "websocket", id: proxiedWsId, event: "message", data: base64Data });
258
235
  }
236
+ // --- Tunnel client message handlers ---
259
237
  handleChallengeRequest(ws) {
260
238
  const challenge = fastId();
261
239
  this.challenges.set(challenge, { ws, challenge, timestamp: Date.now() });
@@ -275,16 +253,16 @@ class DainTunnelServer {
275
253
  ws.close(1008, "API key required");
276
254
  return;
277
255
  }
278
- const parsed = (0, auth_1.parseAPIKey)(apiKey);
256
+ const parsed = parseAPIKey(apiKey);
279
257
  if (!parsed) {
280
258
  ws.close(1008, "Invalid API key format");
281
259
  return;
282
260
  }
283
- const expectedSignature = (0, crypto_1.createHmac)('sha256', parsed.secret).update(challenge).digest('hex');
261
+ const expectedSignature = createHmac('sha256', parsed.secret).update(challenge).digest('hex');
284
262
  const expectedSigBuffer = Buffer.from(expectedSignature, 'hex');
285
263
  const receivedSigBuffer = Buffer.from(signature, 'hex');
286
264
  if (expectedSigBuffer.length !== receivedSigBuffer.length ||
287
- !(0, crypto_1.timingSafeEqual)(expectedSigBuffer, receivedSigBuffer)) {
265
+ !timingSafeEqual(expectedSigBuffer, receivedSigBuffer)) {
288
266
  ws.close(1008, "Invalid signature");
289
267
  return;
290
268
  }
@@ -298,19 +276,20 @@ class DainTunnelServer {
298
276
  existingTunnel.ws.close(1000, "Replaced by new connection");
299
277
  }
300
278
  this.tunnels.set(tunnelId, { id: tunnelId, ws });
301
- ws.tunnelId = tunnelId;
279
+ ws.data.tunnelId = tunnelId;
302
280
  let isAlive = true;
303
281
  let missedPongs = 0;
304
- ws.on("pong", () => { isAlive = true; missedPongs = 0; });
282
+ // Bun ServerWebSocket doesn't have pong events or ping() method the same way.
283
+ // Use a simple send-based keepalive check instead.
305
284
  const intervalId = setInterval(() => {
306
285
  if (!this.tunnels.has(tunnelId)) {
307
286
  clearInterval(intervalId);
308
287
  return;
309
288
  }
310
289
  const tunnel = this.tunnels.get(tunnelId);
311
- if (!tunnel || tunnel.ws !== ws || ws.readyState !== ws_1.default.OPEN) {
290
+ if (!tunnel || tunnel.ws !== ws || ws.readyState !== 1) {
312
291
  clearInterval(intervalId);
313
- if ((tunnel === null || tunnel === void 0 ? void 0 : tunnel.ws) === ws)
292
+ if (tunnel?.ws === ws)
314
293
  this.tunnels.delete(tunnelId);
315
294
  return;
316
295
  }
@@ -319,24 +298,29 @@ class DainTunnelServer {
319
298
  if (missedPongs >= LIMITS.MAX_MISSED_PONGS) {
320
299
  console.log(`[Tunnel] ${tunnelId} failed liveness check (${missedPongs} missed pongs), terminating`);
321
300
  clearInterval(intervalId);
322
- ws.terminate();
301
+ ws.close(1001, "Liveness check failed");
323
302
  return;
324
303
  }
325
304
  }
326
305
  isAlive = false;
327
306
  try {
307
+ // Bun supports ws.ping() on ServerWebSocket
328
308
  ws.ping();
329
309
  }
330
- catch (_a) {
310
+ catch {
331
311
  clearInterval(intervalId);
332
- ws.terminate();
312
+ ws.close(1001, "Ping failed");
333
313
  }
334
314
  }, TIMEOUTS.PING_INTERVAL);
335
- ws.keepAliveInterval = intervalId;
336
- ws.on("close", () => clearInterval(intervalId));
315
+ ws.data.keepAliveInterval = intervalId;
316
+ // Bun's pong is received via the websocket handler pong callback
317
+ // We track isAlive through the pong handler set up in Bun.serve websocket config
337
318
  const tunnelUrl = this.buildTunnelUrl(tunnelId);
338
319
  ws.send(JSON.stringify({ type: "tunnelUrl", url: tunnelUrl }));
339
320
  console.log(`[Tunnel] Created: ${tunnelUrl}`);
321
+ // Store isAlive/missedPongs on ws.data for the pong handler
322
+ ws.data._isAlive = true;
323
+ ws.data._missedPongsReset = () => { isAlive = true; missedPongs = 0; };
340
324
  }
341
325
  catch (error) {
342
326
  console.error(`Error in handleStartMessage for tunnel ${tunnelId}:`, error);
@@ -347,66 +331,91 @@ class DainTunnelServer {
347
331
  const pendingRequest = this.pendingRequests.get(data.requestId);
348
332
  if (!pendingRequest)
349
333
  return;
350
- const { res, tunnelId, timeoutId } = pendingRequest;
334
+ const { resolve, tunnelId, timeoutId, origin } = pendingRequest;
351
335
  this.pendingRequests.delete(data.requestId);
352
336
  if (timeoutId)
353
337
  clearTimeout(timeoutId);
354
338
  this.decrementRequestCount(tunnelId);
355
- // Modify headers in place instead of spreading
356
339
  const headers = data.headers;
357
340
  delete headers["transfer-encoding"];
358
341
  delete headers["content-length"];
342
+ // Inject CORS headers — Hono middleware doesn't apply to Promise-resolved responses
343
+ Object.assign(headers, this.buildCorsHeaders(origin));
359
344
  const bodyBuffer = Buffer.from(data.body, "base64");
360
- res.status(data.status).set(headers).set("Content-Length", bodyBuffer.length.toString()).send(bodyBuffer);
345
+ headers["content-length"] = bodyBuffer.length.toString();
346
+ resolve(new Response(bodyBuffer, {
347
+ status: data.status,
348
+ headers,
349
+ }));
361
350
  }
362
351
  handleSSEMessage(data) {
363
352
  const connection = this.sseConnections.get(data.id);
364
- if (!connection)
353
+ if (!connection || connection.closed)
365
354
  return;
366
- const { res, tunnelId } = connection;
367
355
  if (data.event === 'connected')
368
356
  return;
369
357
  if (data.event === 'close') {
370
358
  try {
371
- res.end();
359
+ connection.controller.close();
372
360
  }
373
- catch (_a) { }
374
- this.cleanupSSEConnection(data.id, tunnelId);
361
+ catch { }
362
+ connection.closed = true;
363
+ this.cleanupSSEConnection(data.id, connection.tunnelId);
375
364
  return;
376
365
  }
377
366
  if (data.event === 'error') {
378
367
  try {
379
- res.write(`event: error\ndata: ${data.data}\n\n`);
380
- res.end();
368
+ connection.controller.enqueue(new TextEncoder().encode(`event: error\ndata: ${data.data}\n\n`));
369
+ connection.controller.close();
381
370
  }
382
- catch (_b) { }
383
- this.cleanupSSEConnection(data.id, tunnelId);
371
+ catch { }
372
+ connection.closed = true;
373
+ this.cleanupSSEConnection(data.id, connection.tunnelId);
384
374
  return;
385
375
  }
386
376
  try {
387
- // Batch write SSE event in single call
388
377
  const lines = data.data.split('\n');
389
378
  const message = (data.event ? `event: ${data.event}\n` : '') +
390
- lines.map(line => `data: ${line}`).join('\n') + '\n\n';
391
- res.write(message);
379
+ lines.map((line) => `data: ${line}`).join('\n') + '\n\n';
380
+ connection.controller.enqueue(new TextEncoder().encode(message));
392
381
  }
393
- catch (_c) {
394
- this.cleanupSSEConnection(data.id, tunnelId);
382
+ catch {
383
+ this.cleanupSSEConnection(data.id, connection.tunnelId);
395
384
  }
396
385
  }
397
386
  cleanupSSEConnection(id, tunnelId) {
398
387
  const connection = this.sseConnections.get(id);
399
- if (connection === null || connection === void 0 ? void 0 : connection.keepAliveInterval)
388
+ if (connection?.keepAliveInterval)
400
389
  clearInterval(connection.keepAliveInterval);
401
390
  this.decrementRequestCount(tunnelId);
402
391
  this.sseConnections.delete(id);
403
392
  }
404
393
  handleWebSocketMessage(data) {
405
394
  const connection = this.wsConnections.get(data.id);
406
- if (!connection)
395
+ if (!connection) {
396
+ const pendingEvents = this.pendingProxiedWSEvents.get(data.id);
397
+ if (!pendingEvents)
398
+ return;
399
+ if (data.event === "message" &&
400
+ pendingEvents.length >= LIMITS.MAX_PENDING_WS_EVENTS_PER_CONNECTION) {
401
+ pendingEvents.push({
402
+ type: "websocket",
403
+ id: data.id,
404
+ event: "error",
405
+ data: "WebSocket downstream overloaded",
406
+ });
407
+ pendingEvents.push({
408
+ type: "websocket",
409
+ id: data.id,
410
+ event: "close",
411
+ });
412
+ return;
413
+ }
414
+ pendingEvents.push(data);
407
415
  return;
416
+ }
408
417
  const { clientSocket } = connection;
409
- const isOpen = clientSocket.readyState === ws_1.default.OPEN;
418
+ const isOpen = clientSocket.readyState === 1;
410
419
  switch (data.event) {
411
420
  case 'message':
412
421
  if (data.data && isOpen)
@@ -424,17 +433,24 @@ class DainTunnelServer {
424
433
  break;
425
434
  }
426
435
  }
427
- async handleRequest(req, res) {
428
- var _a, _b;
429
- const tunnelId = req.params.tunnelId;
430
- if (((_a = req.headers.upgrade) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'websocket')
436
+ flushPendingWebSocketEvents(id) {
437
+ const pendingEvents = this.pendingProxiedWSEvents.get(id);
438
+ if (!pendingEvents || pendingEvents.length === 0) {
439
+ this.pendingProxiedWSEvents.delete(id);
431
440
  return;
441
+ }
442
+ this.pendingProxiedWSEvents.delete(id);
443
+ for (const event of pendingEvents) {
444
+ this.handleWebSocketMessage(event);
445
+ }
446
+ }
447
+ async handleRequest(c) {
448
+ const tunnelId = c.req.param("tunnelId");
432
449
  if (!tunnelId || !tunnelId.includes("_")) {
433
- res.status(404).json({
450
+ return c.json({
434
451
  error: "Not Found",
435
452
  message: `Tunnel "${tunnelId}" does not exist.`,
436
- });
437
- return;
453
+ }, 404);
438
454
  }
439
455
  let tunnel;
440
456
  let retries = LIMITS.TUNNEL_RETRY_COUNT;
@@ -446,142 +462,180 @@ class DainTunnelServer {
446
462
  }
447
463
  }
448
464
  if (!tunnel) {
449
- res.status(502).json({
465
+ return c.json({
450
466
  error: "Bad Gateway",
451
467
  message: `Tunnel "${tunnelId}" not connected. The service may be offline or reconnecting.`,
452
468
  availableTunnels: this.tunnels.size
453
- });
454
- return;
455
- }
456
- if (tunnel.ws.bufferedAmount > LIMITS.BACKPRESSURE_THRESHOLD) {
457
- res.status(503).json({ error: "Service Unavailable", message: "Tunnel high load" });
458
- return;
469
+ }, 502);
459
470
  }
460
471
  const currentCount = this.tunnelRequestCount.get(tunnelId) || 0;
461
472
  if (currentCount >= LIMITS.MAX_CONCURRENT_REQUESTS_PER_TUNNEL) {
462
- res.status(503).json({ error: "Service Unavailable", message: "Too many concurrent requests" });
463
- return;
473
+ return c.json({ error: "Service Unavailable", message: "Too many concurrent requests" }, 503);
464
474
  }
465
475
  this.tunnelRequestCount.set(tunnelId, currentCount + 1);
466
- if ((_b = req.headers.accept) === null || _b === void 0 ? void 0 : _b.includes('text/event-stream')) {
467
- this.handleSSERequest(req, res, tunnelId, tunnel);
468
- return;
476
+ // Check for SSE
477
+ if (c.req.header("accept")?.includes('text/event-stream')) {
478
+ return this.handleSSERequest(c, tunnelId, tunnel);
469
479
  }
480
+ // Regular HTTP request
470
481
  const requestId = fastId();
471
- const startTime = Date.now();
472
- const timeoutId = setTimeout(() => {
473
- const pendingRequest = this.pendingRequests.get(requestId);
474
- if (pendingRequest) {
475
- this.decrementRequestCount(tunnelId);
476
- this.pendingRequests.delete(requestId);
477
- if (!res.headersSent) {
478
- res.status(504).json({ error: "Gateway Timeout", message: "Request timed out" });
479
- }
480
- }
481
- }, TIMEOUTS.REQUEST_TIMEOUT);
482
- this.pendingRequests.set(requestId, { res, startTime, tunnelId, timeoutId });
483
- const hasBody = req.method !== "GET" && req.method !== "HEAD" &&
484
- req.body && Buffer.isBuffer(req.body) && req.body.length > 0;
485
- const sent = this.safeSend(tunnel.ws, {
486
- type: "request",
487
- id: requestId,
488
- method: req.method,
489
- path: req.url,
490
- headers: this.buildForwardedHeaders(req.headers, tunnelId),
491
- body: hasBody ? req.body.toString("base64") : undefined,
482
+ // Extract headers as a plain object
483
+ const reqHeaders = {};
484
+ c.req.raw.headers.forEach((value, key) => {
485
+ reqHeaders[key] = value;
492
486
  });
493
- if (!sent) {
494
- clearTimeout(timeoutId);
495
- this.pendingRequests.delete(requestId);
496
- this.decrementRequestCount(tunnelId);
497
- if (!res.headersSent) {
498
- res.status(502).json({ error: "Bad Gateway", message: "Tunnel connection lost" });
487
+ const forwardedHeaders = this.buildForwardedHeaders(reqHeaders, tunnelId);
488
+ // Get the path relative to the tunnel ID (everything after /:tunnelId)
489
+ const fullUrl = new URL(c.req.url);
490
+ const tunnelPrefix = `/${tunnelId}`;
491
+ let path = fullUrl.pathname.slice(tunnelPrefix.length) || '/';
492
+ if (fullUrl.search)
493
+ path += fullUrl.search;
494
+ // Read body for non-GET/HEAD methods
495
+ let bodyBase64;
496
+ const method = c.req.method;
497
+ if (method !== "GET" && method !== "HEAD") {
498
+ try {
499
+ const bodyBuffer = await c.req.arrayBuffer();
500
+ if (bodyBuffer.byteLength > 0) {
501
+ bodyBase64 = Buffer.from(bodyBuffer).toString("base64");
502
+ }
499
503
  }
504
+ catch { }
500
505
  }
506
+ const origin = c.req.header("origin");
507
+ const corsHeaders = this.buildCorsHeaders(origin);
508
+ return new Promise((resolve) => {
509
+ const timeoutId = setTimeout(() => {
510
+ const pendingRequest = this.pendingRequests.get(requestId);
511
+ if (pendingRequest) {
512
+ this.decrementRequestCount(tunnelId);
513
+ this.pendingRequests.delete(requestId);
514
+ resolve(new Response(JSON.stringify({ error: "Gateway Timeout", message: "Request timed out" }), {
515
+ status: 504,
516
+ headers: { "content-type": "application/json", ...corsHeaders },
517
+ }));
518
+ }
519
+ }, TIMEOUTS.REQUEST_TIMEOUT);
520
+ this.pendingRequests.set(requestId, { resolve, startTime: Date.now(), tunnelId, timeoutId, origin });
521
+ const sent = this.safeSend(tunnel.ws, {
522
+ type: "request",
523
+ id: requestId,
524
+ method,
525
+ path,
526
+ headers: forwardedHeaders,
527
+ body: bodyBase64,
528
+ });
529
+ if (!sent) {
530
+ clearTimeout(timeoutId);
531
+ this.pendingRequests.delete(requestId);
532
+ this.decrementRequestCount(tunnelId);
533
+ resolve(new Response(JSON.stringify({ error: "Bad Gateway", message: "Tunnel connection lost" }), {
534
+ status: 502,
535
+ headers: { "content-type": "application/json", ...corsHeaders },
536
+ }));
537
+ }
538
+ });
501
539
  }
502
- handleSSERequest(req, res, tunnelId, tunnel) {
503
- var _a, _b;
540
+ async handleSSERequest(c, tunnelId, tunnel) {
504
541
  const sseId = fastId();
505
- if (req.socket) {
506
- req.socket.setNoDelay(true);
507
- req.socket.setTimeout(0);
542
+ const requestUrl = new URL(c.req.url);
543
+ let path = requestUrl.pathname.slice(`/${tunnelId}`.length) || "/";
544
+ if (requestUrl.search)
545
+ path += requestUrl.search;
546
+ // Extract headers
547
+ const reqHeaders = {};
548
+ c.req.raw.headers.forEach((value, key) => {
549
+ reqHeaders[key] = value;
550
+ });
551
+ const forwardedHeaders = this.buildForwardedHeaders(reqHeaders, tunnelId);
552
+ let bodyBase64;
553
+ if (c.req.method !== "GET" && c.req.method !== "HEAD") {
554
+ try {
555
+ const bodyBuffer = await c.req.arrayBuffer();
556
+ if (bodyBuffer.byteLength > 0) {
557
+ bodyBase64 = Buffer.from(bodyBuffer).toString("base64");
558
+ }
559
+ }
560
+ catch {
561
+ // Best-effort body forwarding for streaming requests
562
+ }
508
563
  }
509
- const origin = req.headers.origin;
510
- const sseHeaders = {
564
+ const origin = c.req.header("origin");
565
+ const responseHeaders = {
511
566
  'Content-Type': 'text/event-stream',
512
567
  'Cache-Control': 'no-cache, no-transform',
513
568
  'Connection': 'keep-alive',
514
569
  'X-Accel-Buffering': 'no',
515
570
  'X-Content-Type-Options': 'nosniff',
516
571
  'Content-Encoding': 'identity',
517
- 'Transfer-Encoding': 'chunked',
518
572
  };
519
- if (typeof origin === 'string') {
520
- sseHeaders['Access-Control-Allow-Origin'] = origin;
521
- sseHeaders['Access-Control-Allow-Credentials'] = 'true';
522
- sseHeaders['Vary'] = 'Origin';
573
+ if (origin) {
574
+ responseHeaders['Access-Control-Allow-Origin'] = origin;
575
+ responseHeaders['Access-Control-Allow-Credentials'] = 'true';
576
+ responseHeaders['Vary'] = 'Origin';
523
577
  }
524
578
  else {
525
- sseHeaders['Access-Control-Allow-Origin'] = '*';
526
- }
527
- res.writeHead(200, sseHeaders);
528
- const flush = () => {
529
- if (typeof res.flush === 'function')
530
- res.flush();
531
- };
532
- (_a = res.flushHeaders) === null || _a === void 0 ? void 0 : _a.call(res);
533
- res.write(':keepalive\n\n');
534
- flush();
535
- const keepAliveInterval = setInterval(() => {
536
- try {
537
- if (!res.writableEnded) {
538
- res.write(':keepalive\n\n');
539
- flush();
579
+ responseHeaders['Access-Control-Allow-Origin'] = '*';
580
+ }
581
+ const encoder = new TextEncoder();
582
+ let sseController;
583
+ const stream = new ReadableStream({
584
+ start: (controller) => {
585
+ sseController = controller;
586
+ // Send initial keepalive
587
+ controller.enqueue(encoder.encode(':keepalive\n\n'));
588
+ const keepAliveInterval = setInterval(() => {
589
+ const conn = this.sseConnections.get(sseId);
590
+ if (!conn || conn.closed) {
591
+ clearInterval(keepAliveInterval);
592
+ return;
593
+ }
594
+ try {
595
+ controller.enqueue(encoder.encode(':keepalive\n\n'));
596
+ }
597
+ catch {
598
+ clearInterval(keepAliveInterval);
599
+ }
600
+ }, TIMEOUTS.SSE_KEEPALIVE);
601
+ this.sseConnections.set(sseId, {
602
+ controller,
603
+ id: sseId,
604
+ tunnelId,
605
+ keepAliveInterval,
606
+ closed: false,
607
+ });
608
+ const sent = this.safeSend(tunnel.ws, {
609
+ type: "sse_connection",
610
+ id: sseId,
611
+ path,
612
+ method: c.req.method,
613
+ headers: forwardedHeaders,
614
+ body: bodyBase64,
615
+ });
616
+ if (!sent) {
617
+ this.cleanupSSEConnection(sseId, tunnelId);
618
+ try {
619
+ controller.close();
620
+ }
621
+ catch { }
540
622
  }
541
- else
542
- clearInterval(keepAliveInterval);
543
- }
544
- catch (_a) {
545
- clearInterval(keepAliveInterval);
546
- }
547
- }, TIMEOUTS.SSE_KEEPALIVE);
548
- this.sseConnections.set(sseId, { req, res, id: sseId, tunnelId, keepAliveInterval });
549
- const hasBody = req.method !== "GET" && req.body &&
550
- Buffer.isBuffer(req.body) && req.body.length > 0;
551
- const sent = this.safeSend(tunnel.ws, {
552
- type: "sse_connection",
553
- id: sseId,
554
- path: req.url,
555
- method: req.method,
556
- headers: this.buildForwardedHeaders(req.headers, tunnelId),
557
- body: hasBody ? req.body.toString("base64") : undefined
623
+ },
624
+ cancel: () => {
625
+ // Client disconnected
626
+ this.safeSend(tunnel.ws, { type: "sse_close", id: sseId });
627
+ this.cleanupSSEConnection(sseId, tunnelId);
628
+ },
558
629
  });
559
- if (!sent) {
560
- this.cleanupSSEConnection(sseId, tunnelId);
561
- res.end();
562
- return;
563
- }
564
- let cleanedUp = false;
565
- const doCleanup = () => {
566
- if (cleanedUp)
567
- return;
568
- cleanedUp = true;
569
- this.safeSend(tunnel.ws, { type: "sse_close", id: sseId });
570
- this.cleanupSSEConnection(sseId, tunnelId);
571
- };
572
- req.on('close', () => {
573
- var _a;
574
- if (((_a = req.socket) === null || _a === void 0 ? void 0 : _a.destroyed) || res.writableEnded)
575
- doCleanup();
630
+ return new Response(stream, {
631
+ status: 200,
632
+ headers: responseHeaders,
576
633
  });
577
- (_b = req.socket) === null || _b === void 0 ? void 0 : _b.on('close', doCleanup);
578
- req.on('error', doCleanup);
579
- res.on('error', doCleanup);
580
634
  }
581
635
  removeTunnel(ws) {
582
636
  try {
583
- if (ws.keepAliveInterval)
584
- clearInterval(ws.keepAliveInterval);
637
+ if (ws.data.keepAliveInterval)
638
+ clearInterval(ws.data.keepAliveInterval);
585
639
  const removedTunnelId = this.findAndRemoveTunnel(ws);
586
640
  if (removedTunnelId) {
587
641
  this.tunnelRequestCount.delete(removedTunnelId);
@@ -596,10 +650,10 @@ class DainTunnelServer {
596
650
  }
597
651
  }
598
652
  findAndRemoveTunnel(ws) {
599
- const tunnelId = ws.tunnelId;
653
+ const tunnelId = ws.data.tunnelId;
600
654
  if (tunnelId && this.tunnels.has(tunnelId)) {
601
655
  const tunnel = this.tunnels.get(tunnelId);
602
- if ((tunnel === null || tunnel === void 0 ? void 0 : tunnel.ws) === ws) {
656
+ if (tunnel?.ws === ws) {
603
657
  this.tunnels.delete(tunnelId);
604
658
  return tunnelId;
605
659
  }
@@ -615,14 +669,15 @@ class DainTunnelServer {
615
669
  cleanupPendingRequests(tunnelId) {
616
670
  for (const [requestId, pending] of this.pendingRequests.entries()) {
617
671
  if (pending.tunnelId === tunnelId) {
618
- try {
619
- if (pending.timeoutId)
620
- clearTimeout(pending.timeoutId);
621
- if (!pending.res.headersSent) {
622
- pending.res.status(502).json({ error: "Bad Gateway", message: "Tunnel closed" });
623
- }
624
- }
625
- catch (_a) { }
672
+ if (pending.timeoutId)
673
+ clearTimeout(pending.timeoutId);
674
+ pending.resolve(new Response(JSON.stringify({ error: "Bad Gateway", message: "Tunnel closed" }), {
675
+ status: 502,
676
+ headers: {
677
+ "content-type": "application/json",
678
+ ...this.buildCorsHeaders(pending.origin),
679
+ },
680
+ }));
626
681
  this.pendingRequests.delete(requestId);
627
682
  }
628
683
  }
@@ -633,9 +688,10 @@ class DainTunnelServer {
633
688
  if (conn.keepAliveInterval)
634
689
  clearInterval(conn.keepAliveInterval);
635
690
  try {
636
- conn.res.end();
691
+ conn.controller.close();
637
692
  }
638
- catch (_a) { }
693
+ catch { }
694
+ conn.closed = true;
639
695
  this.sseConnections.delete(sseId);
640
696
  }
641
697
  }
@@ -646,7 +702,7 @@ class DainTunnelServer {
646
702
  try {
647
703
  conn.clientSocket.close(1001, "Tunnel closed");
648
704
  }
649
- catch (_a) { }
705
+ catch { }
650
706
  this.wsConnections.delete(wsId);
651
707
  }
652
708
  }
@@ -659,78 +715,168 @@ class DainTunnelServer {
659
715
  }
660
716
  }
661
717
  async start() {
662
- return new Promise((resolve) => {
663
- this.server.listen(this.port, () => {
664
- console.log(`DainTunnel Server is running on ${this.hostname}:${this.port}`);
665
- resolve();
666
- });
667
- });
668
- }
669
- async stop() {
670
- return new Promise((resolve) => {
671
- try {
672
- for (const tunnel of this.tunnels.values()) {
673
- if (tunnel.ws.keepAliveInterval)
674
- clearInterval(tunnel.ws.keepAliveInterval);
675
- try {
676
- tunnel.ws.terminate();
718
+ const self = this;
719
+ this.server = Bun.serve({
720
+ port: this.port,
721
+ hostname: "0.0.0.0",
722
+ fetch(req, server) {
723
+ const url = new URL(req.url);
724
+ // Check for WebSocket upgrade
725
+ if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
726
+ const pathParts = url.pathname.split('/').filter(Boolean);
727
+ if (pathParts.length === 0) {
728
+ // Root WebSocket = tunnel client connection
729
+ const success = server.upgrade(req, {
730
+ data: {
731
+ isProxiedWebSocket: false,
732
+ tunnelId: undefined,
733
+ keepAliveInterval: undefined,
734
+ },
735
+ });
736
+ if (success)
737
+ return undefined;
738
+ return new Response("WebSocket upgrade failed", { status: 400 });
739
+ }
740
+ // Proxied WebSocket = /:tunnelId/...
741
+ const tunnelId = pathParts[0];
742
+ const remainingPath = '/' + pathParts.slice(1).join('/');
743
+ const tunnel = self.tunnels.get(tunnelId);
744
+ if (!tunnel) {
745
+ return new Response("Tunnel not found", { status: 404 });
677
746
  }
678
- catch (error) {
679
- console.error(`Error closing tunnel ${tunnel.id}:`, error);
747
+ const wsConnectionId = fastId();
748
+ const reqHeaders = {};
749
+ req.headers.forEach((value, key) => {
750
+ reqHeaders[key] = value;
751
+ });
752
+ const forwardedHeaders = self.buildForwardedHeaders(reqHeaders, tunnelId);
753
+ self._pendingProxiedWs = self._pendingProxiedWs || new Map();
754
+ self._pendingProxiedWs.set(wsConnectionId, {
755
+ tunnelId,
756
+ path: remainingPath,
757
+ headers: forwardedHeaders,
758
+ });
759
+ self.pendingProxiedWSEvents.set(wsConnectionId, []);
760
+ const success = server.upgrade(req, {
761
+ data: {
762
+ isProxiedWebSocket: true,
763
+ proxiedWsId: wsConnectionId,
764
+ proxiedTunnelId: tunnelId,
765
+ },
766
+ });
767
+ if (success) {
768
+ self.safeSend(tunnel.ws, {
769
+ type: "websocket_connection",
770
+ id: wsConnectionId,
771
+ path: remainingPath,
772
+ headers: forwardedHeaders,
773
+ });
774
+ return undefined;
680
775
  }
776
+ self._pendingProxiedWs?.delete(wsConnectionId);
777
+ self.pendingProxiedWSEvents.delete(wsConnectionId);
778
+ return new Response("WebSocket upgrade failed", { status: 400 });
681
779
  }
682
- this.tunnels.clear();
683
- this.tunnelRequestCount.clear();
684
- for (const [requestId, pending] of this.pendingRequests.entries()) {
685
- if (pending.timeoutId)
686
- clearTimeout(pending.timeoutId);
687
- if (!pending.res.headersSent) {
688
- try {
689
- pending.res.status(503).json({ error: "Service Unavailable", message: "Server shutting down" });
780
+ // Regular HTTP request - pass to Hono
781
+ return self.app.fetch(req);
782
+ },
783
+ websocket: {
784
+ open(ws) {
785
+ if (ws.data.isProxiedWebSocket && ws.data.proxiedWsId) {
786
+ const pendingMap = self._pendingProxiedWs;
787
+ const pending = pendingMap?.get(ws.data.proxiedWsId);
788
+ if (pending) {
789
+ self.wsConnections.set(ws.data.proxiedWsId, {
790
+ clientSocket: ws,
791
+ id: ws.data.proxiedWsId,
792
+ path: pending.path,
793
+ headers: pending.headers,
794
+ tunnelId: pending.tunnelId,
795
+ });
796
+ pendingMap.delete(ws.data.proxiedWsId);
797
+ self.flushPendingWebSocketEvents(ws.data.proxiedWsId);
690
798
  }
691
- catch (_a) { }
692
799
  }
693
- this.pendingRequests.delete(requestId);
800
+ self.handleWsOpen(ws);
801
+ },
802
+ message(ws, message) {
803
+ self.handleWsMessage(ws, message);
804
+ },
805
+ close(ws, code, reason) {
806
+ self.handleWsClose(ws, code, reason);
807
+ },
808
+ pong(ws) {
809
+ // Reset liveness tracking when pong received
810
+ const resetFn = ws.data?._missedPongsReset;
811
+ if (resetFn)
812
+ resetFn();
813
+ },
814
+ perMessageDeflate: false,
815
+ maxPayloadLength: LIMITS.MAX_PAYLOAD_BYTES,
816
+ backpressureLimit: LIMITS.BACKPRESSURE_THRESHOLD,
817
+ },
818
+ });
819
+ console.log(`DainTunnel Server is running on ${this.hostname}:${this.port}`);
820
+ }
821
+ async stop() {
822
+ try {
823
+ for (const tunnel of this.tunnels.values()) {
824
+ if (tunnel.ws.data.keepAliveInterval)
825
+ clearInterval(tunnel.ws.data.keepAliveInterval);
826
+ try {
827
+ tunnel.ws.close(1001, "Server shutting down");
694
828
  }
695
- this.challenges.clear();
696
- for (const [sseId, conn] of this.sseConnections.entries()) {
697
- if (conn.keepAliveInterval)
698
- clearInterval(conn.keepAliveInterval);
699
- try {
700
- conn.res.end();
701
- }
702
- catch (error) {
703
- console.error(`Error closing SSE ${sseId}:`, error);
704
- }
705
- this.sseConnections.delete(sseId);
829
+ catch (error) {
830
+ console.error(`Error closing tunnel ${tunnel.id}:`, error);
706
831
  }
707
- for (const [wsId, conn] of this.wsConnections.entries()) {
708
- try {
709
- conn.clientSocket.close(1001, "Server shutting down");
710
- }
711
- catch (error) {
712
- console.error(`Error closing WS ${wsId}:`, error);
713
- }
714
- this.wsConnections.delete(wsId);
832
+ }
833
+ this.tunnels.clear();
834
+ this.tunnelRequestCount.clear();
835
+ for (const [requestId, pending] of this.pendingRequests.entries()) {
836
+ if (pending.timeoutId)
837
+ clearTimeout(pending.timeoutId);
838
+ pending.resolve(new Response(JSON.stringify({
839
+ error: "Service Unavailable",
840
+ message: "Server shutting down",
841
+ }), {
842
+ status: 503,
843
+ headers: {
844
+ "content-type": "application/json",
845
+ ...this.buildCorsHeaders(pending.origin),
846
+ },
847
+ }));
848
+ this.pendingRequests.delete(requestId);
849
+ }
850
+ this.challenges.clear();
851
+ for (const [sseId, conn] of this.sseConnections.entries()) {
852
+ if (conn.keepAliveInterval)
853
+ clearInterval(conn.keepAliveInterval);
854
+ try {
855
+ conn.controller.close();
715
856
  }
716
- for (const socket of this.wss.clients) {
717
- try {
718
- socket.terminate();
719
- }
720
- catch (_b) { }
857
+ catch (error) {
858
+ console.error(`Error closing SSE ${sseId}:`, error);
721
859
  }
722
- this.wss.close(() => {
723
- var _a, _b, _c, _d;
724
- (_b = (_a = this.server).closeIdleConnections) === null || _b === void 0 ? void 0 : _b.call(_a);
725
- (_d = (_c = this.server).closeAllConnections) === null || _d === void 0 ? void 0 : _d.call(_c);
726
- this.server.close(() => resolve());
727
- });
860
+ conn.closed = true;
861
+ this.sseConnections.delete(sseId);
728
862
  }
729
- catch (error) {
730
- console.error('Error during server shutdown:', error);
731
- resolve();
863
+ for (const [wsId, conn] of this.wsConnections.entries()) {
864
+ try {
865
+ conn.clientSocket.close(1001, "Server shutting down");
866
+ }
867
+ catch (error) {
868
+ console.error(`Error closing WS ${wsId}:`, error);
869
+ }
870
+ this.wsConnections.delete(wsId);
732
871
  }
733
- });
872
+ if (this.server) {
873
+ this.server.stop();
874
+ this.server = null;
875
+ }
876
+ }
877
+ catch (error) {
878
+ console.error('Error during server shutdown:', error);
879
+ }
734
880
  }
735
881
  }
736
- exports.default = DainTunnelServer;
882
+ export default DainTunnelServer;