@dainprotocol/tunnel 2.0.0 → 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.
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +98 -17
- package/package.json +3 -2
package/dist/server/index.d.ts
CHANGED
|
@@ -9,7 +9,9 @@ declare class DainTunnelServer {
|
|
|
9
9
|
private challenges;
|
|
10
10
|
private sseConnections;
|
|
11
11
|
private wsConnections;
|
|
12
|
+
private pendingProxiedWSEvents;
|
|
12
13
|
private tunnelRequestCount;
|
|
14
|
+
private buildCorsHeaders;
|
|
13
15
|
private safeSend;
|
|
14
16
|
private decrementRequestCount;
|
|
15
17
|
private buildForwardedHeaders;
|
|
@@ -26,6 +28,7 @@ declare class DainTunnelServer {
|
|
|
26
28
|
private handleSSEMessage;
|
|
27
29
|
private cleanupSSEConnection;
|
|
28
30
|
private handleWebSocketMessage;
|
|
31
|
+
private flushPendingWebSocketEvents;
|
|
29
32
|
private handleRequest;
|
|
30
33
|
private handleSSERequest;
|
|
31
34
|
private removeTunnel;
|
package/dist/server/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const LIMITS = {
|
|
|
15
15
|
MAX_MISSED_PONGS: 2,
|
|
16
16
|
MAX_PAYLOAD_BYTES: 100 * 1024 * 1024,
|
|
17
17
|
BACKPRESSURE_THRESHOLD: 1024 * 1024,
|
|
18
|
+
MAX_PENDING_WS_EVENTS_PER_CONNECTION: 256,
|
|
18
19
|
SERVER_MAX_HEADERS: 100,
|
|
19
20
|
WS_BACKLOG: 100,
|
|
20
21
|
TUNNEL_RETRY_COUNT: 2,
|
|
@@ -52,6 +53,21 @@ function isCorsOriginAllowed(origin, allowedOrigins) {
|
|
|
52
53
|
}
|
|
53
54
|
const ALLOWED_CORS_ORIGINS = parseAllowedCorsOrigins(process.env.TUNNEL_ALLOWED_ORIGINS || process.env.CORS_ALLOWED_ORIGINS);
|
|
54
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
|
+
}
|
|
55
71
|
safeSend(ws, data) {
|
|
56
72
|
try {
|
|
57
73
|
if (ws.readyState === 1) { // WebSocket.OPEN
|
|
@@ -99,6 +115,7 @@ class DainTunnelServer {
|
|
|
99
115
|
this.challenges = new Map();
|
|
100
116
|
this.sseConnections = new Map();
|
|
101
117
|
this.wsConnections = new Map();
|
|
118
|
+
this.pendingProxiedWSEvents = new Map();
|
|
102
119
|
this.tunnelRequestCount = new Map();
|
|
103
120
|
this.app = new Hono();
|
|
104
121
|
this.setupRoutes();
|
|
@@ -314,7 +331,7 @@ class DainTunnelServer {
|
|
|
314
331
|
const pendingRequest = this.pendingRequests.get(data.requestId);
|
|
315
332
|
if (!pendingRequest)
|
|
316
333
|
return;
|
|
317
|
-
const { resolve, tunnelId, timeoutId } = pendingRequest;
|
|
334
|
+
const { resolve, tunnelId, timeoutId, origin } = pendingRequest;
|
|
318
335
|
this.pendingRequests.delete(data.requestId);
|
|
319
336
|
if (timeoutId)
|
|
320
337
|
clearTimeout(timeoutId);
|
|
@@ -322,6 +339,8 @@ class DainTunnelServer {
|
|
|
322
339
|
const headers = data.headers;
|
|
323
340
|
delete headers["transfer-encoding"];
|
|
324
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));
|
|
325
344
|
const bodyBuffer = Buffer.from(data.body, "base64");
|
|
326
345
|
headers["content-length"] = bodyBuffer.length.toString();
|
|
327
346
|
resolve(new Response(bodyBuffer, {
|
|
@@ -373,8 +392,28 @@ class DainTunnelServer {
|
|
|
373
392
|
}
|
|
374
393
|
handleWebSocketMessage(data) {
|
|
375
394
|
const connection = this.wsConnections.get(data.id);
|
|
376
|
-
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);
|
|
377
415
|
return;
|
|
416
|
+
}
|
|
378
417
|
const { clientSocket } = connection;
|
|
379
418
|
const isOpen = clientSocket.readyState === 1;
|
|
380
419
|
switch (data.event) {
|
|
@@ -394,6 +433,17 @@ class DainTunnelServer {
|
|
|
394
433
|
break;
|
|
395
434
|
}
|
|
396
435
|
}
|
|
436
|
+
flushPendingWebSocketEvents(id) {
|
|
437
|
+
const pendingEvents = this.pendingProxiedWSEvents.get(id);
|
|
438
|
+
if (!pendingEvents || pendingEvents.length === 0) {
|
|
439
|
+
this.pendingProxiedWSEvents.delete(id);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
this.pendingProxiedWSEvents.delete(id);
|
|
443
|
+
for (const event of pendingEvents) {
|
|
444
|
+
this.handleWebSocketMessage(event);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
397
447
|
async handleRequest(c) {
|
|
398
448
|
const tunnelId = c.req.param("tunnelId");
|
|
399
449
|
if (!tunnelId || !tunnelId.includes("_")) {
|
|
@@ -453,6 +503,8 @@ class DainTunnelServer {
|
|
|
453
503
|
}
|
|
454
504
|
catch { }
|
|
455
505
|
}
|
|
506
|
+
const origin = c.req.header("origin");
|
|
507
|
+
const corsHeaders = this.buildCorsHeaders(origin);
|
|
456
508
|
return new Promise((resolve) => {
|
|
457
509
|
const timeoutId = setTimeout(() => {
|
|
458
510
|
const pendingRequest = this.pendingRequests.get(requestId);
|
|
@@ -461,11 +513,11 @@ class DainTunnelServer {
|
|
|
461
513
|
this.pendingRequests.delete(requestId);
|
|
462
514
|
resolve(new Response(JSON.stringify({ error: "Gateway Timeout", message: "Request timed out" }), {
|
|
463
515
|
status: 504,
|
|
464
|
-
headers: { "content-type": "application/json" },
|
|
516
|
+
headers: { "content-type": "application/json", ...corsHeaders },
|
|
465
517
|
}));
|
|
466
518
|
}
|
|
467
519
|
}, TIMEOUTS.REQUEST_TIMEOUT);
|
|
468
|
-
this.pendingRequests.set(requestId, { resolve, startTime: Date.now(), tunnelId, timeoutId });
|
|
520
|
+
this.pendingRequests.set(requestId, { resolve, startTime: Date.now(), tunnelId, timeoutId, origin });
|
|
469
521
|
const sent = this.safeSend(tunnel.ws, {
|
|
470
522
|
type: "request",
|
|
471
523
|
id: requestId,
|
|
@@ -480,19 +532,35 @@ class DainTunnelServer {
|
|
|
480
532
|
this.decrementRequestCount(tunnelId);
|
|
481
533
|
resolve(new Response(JSON.stringify({ error: "Bad Gateway", message: "Tunnel connection lost" }), {
|
|
482
534
|
status: 502,
|
|
483
|
-
headers: { "content-type": "application/json" },
|
|
535
|
+
headers: { "content-type": "application/json", ...corsHeaders },
|
|
484
536
|
}));
|
|
485
537
|
}
|
|
486
538
|
});
|
|
487
539
|
}
|
|
488
|
-
handleSSERequest(c, tunnelId, tunnel) {
|
|
540
|
+
async handleSSERequest(c, tunnelId, tunnel) {
|
|
489
541
|
const sseId = fastId();
|
|
542
|
+
const requestUrl = new URL(c.req.url);
|
|
543
|
+
let path = requestUrl.pathname.slice(`/${tunnelId}`.length) || "/";
|
|
544
|
+
if (requestUrl.search)
|
|
545
|
+
path += requestUrl.search;
|
|
490
546
|
// Extract headers
|
|
491
547
|
const reqHeaders = {};
|
|
492
548
|
c.req.raw.headers.forEach((value, key) => {
|
|
493
549
|
reqHeaders[key] = value;
|
|
494
550
|
});
|
|
495
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
|
+
}
|
|
563
|
+
}
|
|
496
564
|
const origin = c.req.header("origin");
|
|
497
565
|
const responseHeaders = {
|
|
498
566
|
'Content-Type': 'text/event-stream',
|
|
@@ -540,9 +608,10 @@ class DainTunnelServer {
|
|
|
540
608
|
const sent = this.safeSend(tunnel.ws, {
|
|
541
609
|
type: "sse_connection",
|
|
542
610
|
id: sseId,
|
|
543
|
-
path
|
|
611
|
+
path,
|
|
544
612
|
method: c.req.method,
|
|
545
613
|
headers: forwardedHeaders,
|
|
614
|
+
body: bodyBase64,
|
|
546
615
|
});
|
|
547
616
|
if (!sent) {
|
|
548
617
|
this.cleanupSSEConnection(sseId, tunnelId);
|
|
@@ -604,7 +673,10 @@ class DainTunnelServer {
|
|
|
604
673
|
clearTimeout(pending.timeoutId);
|
|
605
674
|
pending.resolve(new Response(JSON.stringify({ error: "Bad Gateway", message: "Tunnel closed" }), {
|
|
606
675
|
status: 502,
|
|
607
|
-
headers: {
|
|
676
|
+
headers: {
|
|
677
|
+
"content-type": "application/json",
|
|
678
|
+
...this.buildCorsHeaders(pending.origin),
|
|
679
|
+
},
|
|
608
680
|
}));
|
|
609
681
|
this.pendingRequests.delete(requestId);
|
|
610
682
|
}
|
|
@@ -678,6 +750,13 @@ class DainTunnelServer {
|
|
|
678
750
|
reqHeaders[key] = value;
|
|
679
751
|
});
|
|
680
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, []);
|
|
681
760
|
const success = server.upgrade(req, {
|
|
682
761
|
data: {
|
|
683
762
|
isProxiedWebSocket: true,
|
|
@@ -686,13 +765,6 @@ class DainTunnelServer {
|
|
|
686
765
|
},
|
|
687
766
|
});
|
|
688
767
|
if (success) {
|
|
689
|
-
// Store setup info so open handler can register the connection
|
|
690
|
-
self._pendingProxiedWs = self._pendingProxiedWs || new Map();
|
|
691
|
-
self._pendingProxiedWs.set(wsConnectionId, {
|
|
692
|
-
tunnelId,
|
|
693
|
-
path: remainingPath,
|
|
694
|
-
headers: forwardedHeaders,
|
|
695
|
-
});
|
|
696
768
|
self.safeSend(tunnel.ws, {
|
|
697
769
|
type: "websocket_connection",
|
|
698
770
|
id: wsConnectionId,
|
|
@@ -701,6 +773,8 @@ class DainTunnelServer {
|
|
|
701
773
|
});
|
|
702
774
|
return undefined;
|
|
703
775
|
}
|
|
776
|
+
self._pendingProxiedWs?.delete(wsConnectionId);
|
|
777
|
+
self.pendingProxiedWSEvents.delete(wsConnectionId);
|
|
704
778
|
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
705
779
|
}
|
|
706
780
|
// Regular HTTP request - pass to Hono
|
|
@@ -720,6 +794,7 @@ class DainTunnelServer {
|
|
|
720
794
|
tunnelId: pending.tunnelId,
|
|
721
795
|
});
|
|
722
796
|
pendingMap.delete(ws.data.proxiedWsId);
|
|
797
|
+
self.flushPendingWebSocketEvents(ws.data.proxiedWsId);
|
|
723
798
|
}
|
|
724
799
|
}
|
|
725
800
|
self.handleWsOpen(ws);
|
|
@@ -760,9 +835,15 @@ class DainTunnelServer {
|
|
|
760
835
|
for (const [requestId, pending] of this.pendingRequests.entries()) {
|
|
761
836
|
if (pending.timeoutId)
|
|
762
837
|
clearTimeout(pending.timeoutId);
|
|
763
|
-
pending.resolve(new Response(JSON.stringify({
|
|
838
|
+
pending.resolve(new Response(JSON.stringify({
|
|
839
|
+
error: "Service Unavailable",
|
|
840
|
+
message: "Server shutting down",
|
|
841
|
+
}), {
|
|
764
842
|
status: 503,
|
|
765
|
-
headers: {
|
|
843
|
+
headers: {
|
|
844
|
+
"content-type": "application/json",
|
|
845
|
+
...this.buildCorsHeaders(pending.origin),
|
|
846
|
+
},
|
|
766
847
|
}));
|
|
767
848
|
this.pendingRequests.delete(requestId);
|
|
768
849
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dainprotocol/tunnel",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"private": false,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
|
+
"build:production": "tsc && bun build dist/server/start.js --outdir dist-prod --target bun --minify --sourcemap=external",
|
|
12
13
|
"build:types": "tsc --emitDeclarationOnly",
|
|
13
14
|
"test": "bun test",
|
|
14
15
|
"prepublishOnly": "npm run build && npm run build:types",
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
"author": "Ryan",
|
|
20
21
|
"license": "ISC",
|
|
21
22
|
"dependencies": {
|
|
22
|
-
"@dainprotocol/service-sdk": "2.0.
|
|
23
|
+
"@dainprotocol/service-sdk": "2.0.91",
|
|
23
24
|
"@types/node": "^22.5.4",
|
|
24
25
|
"@types/ws": "^8.5.12",
|
|
25
26
|
"hono": "^4.7.0",
|