@dainprotocol/tunnel 1.0.7 → 1.1.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.
@@ -9,11 +9,18 @@ declare class DainTunnel extends EventEmitter {
9
9
  private reconnectDelay;
10
10
  private apiKey;
11
11
  private tunnelId;
12
+ private secret;
12
13
  private webSocketClients;
13
14
  private sseClients;
14
15
  constructor(serverUrl: string, apiKey: string);
16
+ /**
17
+ * Sign a challenge using HMAC-SHA256
18
+ * @private
19
+ */
20
+ private signChallenge;
15
21
  start(port: number): Promise<string>;
16
22
  private connect;
23
+ private requestChallenge;
17
24
  private handleMessage;
18
25
  private handleRequest;
19
26
  private handleWebSocketConnection;
@@ -8,6 +8,7 @@ const ws_1 = __importDefault(require("ws"));
8
8
  const http_1 = __importDefault(require("http"));
9
9
  const events_1 = require("events");
10
10
  const crypto_1 = require("crypto");
11
+ const auth_1 = require("@dainprotocol/service-sdk/service/auth");
11
12
  class DainTunnel extends events_1.EventEmitter {
12
13
  constructor(serverUrl, apiKey) {
13
14
  super();
@@ -20,10 +21,23 @@ class DainTunnel extends events_1.EventEmitter {
20
21
  this.reconnectDelay = 5000;
21
22
  this.webSocketClients = new Map();
22
23
  this.sseClients = new Map();
24
+ // Parse API key to extract agentId and secret
25
+ const parsed = (0, auth_1.parseAPIKey)(apiKey);
26
+ if (!parsed) {
27
+ throw new Error('Invalid API key format. Expected: sk_agent_{agentId}_{orgId}_{secret}');
28
+ }
23
29
  this.apiKey = apiKey;
24
- // Create tunnel ID from API key hash
25
- const hash = (0, crypto_1.createHash)('sha256').update(apiKey).digest();
26
- this.tunnelId = hash.toString('base64url').substring(0, 16);
30
+ this.tunnelId = parsed.agentId; // agentId is the tunnel identifier
31
+ this.secret = parsed.secret; // secret for HMAC signatures
32
+ }
33
+ /**
34
+ * Sign a challenge using HMAC-SHA256
35
+ * @private
36
+ */
37
+ signChallenge(challenge) {
38
+ return (0, crypto_1.createHmac)('sha256', this.secret)
39
+ .update(challenge)
40
+ .digest('hex');
27
41
  }
28
42
  async start(port) {
29
43
  this.port = port;
@@ -34,17 +48,26 @@ class DainTunnel extends events_1.EventEmitter {
34
48
  try {
35
49
  console.log(`Connecting to WebSocket server: ${this.serverUrl}`);
36
50
  this.ws = new ws_1.default(this.serverUrl);
37
- this.ws.on("open", () => {
51
+ this.ws.on("open", async () => {
38
52
  console.log('WebSocket connection opened');
39
53
  this.reconnectAttempts = 0;
40
- // Send API key authentication directly (no challenge-response)
41
- this.sendMessage({
42
- type: "start",
43
- port: this.port,
44
- apiKey: this.apiKey,
45
- tunnelId: this.tunnelId
46
- });
47
- this.emit("connected");
54
+ try {
55
+ const challenge = await this.requestChallenge();
56
+ const signature = this.signChallenge(challenge);
57
+ this.sendMessage({
58
+ type: "start",
59
+ port: this.port,
60
+ challenge,
61
+ signature,
62
+ tunnelId: this.tunnelId,
63
+ apiKey: this.apiKey // Send API key for server validation
64
+ });
65
+ this.emit("connected");
66
+ }
67
+ catch (err) {
68
+ console.error('Error during challenge-response:', err);
69
+ reject(err);
70
+ }
48
71
  });
49
72
  this.ws.on("message", (data) => {
50
73
  try {
@@ -86,6 +109,32 @@ class DainTunnel extends events_1.EventEmitter {
86
109
  }
87
110
  });
88
111
  }
112
+ async requestChallenge() {
113
+ return new Promise((resolve, reject) => {
114
+ if (!this.ws) {
115
+ reject(new Error("WebSocket is not connected"));
116
+ return;
117
+ }
118
+ this.ws.send(JSON.stringify({ type: "challenge_request" }));
119
+ const challengeHandler = (message) => {
120
+ const data = JSON.parse(message);
121
+ if (data.type === "challenge") {
122
+ if (this.ws) {
123
+ this.ws.removeListener("message", challengeHandler);
124
+ }
125
+ resolve(data.challenge);
126
+ }
127
+ };
128
+ this.ws.on("message", challengeHandler);
129
+ // Add a timeout for the challenge request
130
+ setTimeout(() => {
131
+ if (this.ws) {
132
+ this.ws.removeListener("message", challengeHandler);
133
+ }
134
+ reject(new Error("Challenge request timeout"));
135
+ }, 5000);
136
+ });
137
+ }
89
138
  handleMessage(message, resolve) {
90
139
  switch (message.type) {
91
140
  case "tunnelUrl":
@@ -6,6 +6,7 @@ declare class DainTunnelServer {
6
6
  private wss;
7
7
  private tunnels;
8
8
  private pendingRequests;
9
+ private challenges;
9
10
  private sseConnections;
10
11
  private wsConnections;
11
12
  constructor(hostname: string, port: number);
@@ -13,6 +14,7 @@ declare class DainTunnelServer {
13
14
  private setupWebSocketServer;
14
15
  private handleTunnelClientConnection;
15
16
  private handleProxiedWebSocketConnection;
17
+ private handleChallengeRequest;
16
18
  private handleStartMessage;
17
19
  private handleResponseMessage;
18
20
  private handleSSEMessage;
@@ -9,6 +9,8 @@ const ws_1 = __importDefault(require("ws"));
9
9
  const uuid_1 = require("uuid");
10
10
  const body_parser_1 = __importDefault(require("body-parser"));
11
11
  const cors_1 = __importDefault(require("cors"));
12
+ const auth_1 = require("@dainprotocol/service-sdk/service/auth");
13
+ const crypto_1 = require("crypto");
12
14
  const url_1 = require("url");
13
15
  class DainTunnelServer {
14
16
  constructor(hostname, port) {
@@ -16,6 +18,7 @@ class DainTunnelServer {
16
18
  this.port = port;
17
19
  this.tunnels = new Map();
18
20
  this.pendingRequests = new Map();
21
+ this.challenges = new Map();
19
22
  this.sseConnections = new Map();
20
23
  this.wsConnections = new Map();
21
24
  this.app = (0, express_1.default)();
@@ -82,7 +85,10 @@ class DainTunnelServer {
82
85
  try {
83
86
  const data = JSON.parse(message);
84
87
  console.log(`Received WebSocket message: ${data.type}`);
85
- if (data.type === "start") {
88
+ if (data.type === "challenge_request") {
89
+ this.handleChallengeRequest(ws);
90
+ }
91
+ else if (data.type === "start") {
86
92
  this.handleStartMessage(ws, data);
87
93
  }
88
94
  else if (data.type === "response") {
@@ -171,16 +177,44 @@ class DainTunnelServer {
171
177
  }));
172
178
  });
173
179
  }
180
+ handleChallengeRequest(ws) {
181
+ const challenge = (0, uuid_1.v4)();
182
+ const challengeObj = { ws, challenge };
183
+ this.challenges.set(challenge, challengeObj);
184
+ ws.send(JSON.stringify({ type: "challenge", challenge }));
185
+ }
174
186
  handleStartMessage(ws, data) {
175
- const { apiKey, tunnelId } = data;
176
- // Validate API key format (sk_agent_{agentId}_{orgId}_{secret})
177
- if (!apiKey || !apiKey.startsWith('sk_agent_')) {
178
- ws.close(1008, "Invalid API key");
187
+ const { challenge, signature, tunnelId, apiKey } = data;
188
+ const challengeObj = this.challenges.get(challenge);
189
+ if (!challengeObj || challengeObj.ws !== ws) {
190
+ ws.close(1008, "Invalid challenge");
179
191
  return;
180
192
  }
181
- // TODO: Validate API key with platform in production
182
- // For now, accept any properly formatted API key (dev mode)
193
+ this.challenges.delete(challenge);
183
194
  try {
195
+ // Parse API key to get the secret for HMAC validation
196
+ if (!apiKey) {
197
+ ws.close(1008, "API key required");
198
+ return;
199
+ }
200
+ const parsed = (0, auth_1.parseAPIKey)(apiKey);
201
+ if (!parsed) {
202
+ ws.close(1008, "Invalid API key format");
203
+ return;
204
+ }
205
+ // Validate HMAC signature
206
+ const expectedSignature = (0, crypto_1.createHmac)('sha256', parsed.secret)
207
+ .update(challenge)
208
+ .digest('hex');
209
+ if (expectedSignature !== signature) {
210
+ ws.close(1008, "Invalid signature");
211
+ return;
212
+ }
213
+ // Verify that tunnelId matches the agentId from the API key
214
+ if (tunnelId !== parsed.agentId) {
215
+ ws.close(1008, "Tunnel ID does not match API key");
216
+ return;
217
+ }
184
218
  // If tunnel already exists, remove old one
185
219
  if (this.tunnels.has(tunnelId)) {
186
220
  console.log(`Tunnel ${tunnelId} already exists, replacing it`);
@@ -442,6 +476,14 @@ class DainTunnelServer {
442
476
  else {
443
477
  console.log(`No tunnel found to remove for the closed WebSocket connection`);
444
478
  }
479
+ // Also remove any pending challenges for this WebSocket
480
+ for (const [challenge, challengeObj] of this.challenges.entries()) {
481
+ if (challengeObj.ws === ws) {
482
+ this.challenges.delete(challenge);
483
+ console.log(`Challenge removed for closed WebSocket`);
484
+ break;
485
+ }
486
+ }
445
487
  }
446
488
  catch (error) {
447
489
  console.error("Error in removeTunnel:", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dainprotocol/tunnel",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "private": false,
@@ -19,7 +19,7 @@
19
19
  "author": "Ryan",
20
20
  "license": "ISC",
21
21
  "dependencies": {
22
- "@dainprotocol/service-sdk": "^1.2.5",
22
+ "@dainprotocol/service-sdk": "^1.3.0",
23
23
  "@types/body-parser": "^1.19.5",
24
24
  "@types/cors": "^2.8.17",
25
25
  "@types/eventsource": "^3.0.0",