webhookdrop-tunnel 1.0.0 → 1.0.3

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.
Files changed (2) hide show
  1. package/bin/tunnel.js +123 -76
  2. package/package.json +1 -1
package/bin/tunnel.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * WebhookDrop Tunnel Client
5
5
  *
6
6
  * Forwards webhooks from WebhookDrop to your local development server.
7
+ * Uses connection pooling for high-concurrency SPA tunneling.
7
8
  *
8
9
  * Usage:
9
10
  * npx webhookdrop-tunnel --token YOUR_TOKEN --local http://localhost:3000
@@ -28,12 +29,15 @@ try {
28
29
  blue: (s) => s,
29
30
  gray: (s) => s,
30
31
  bold: (s) => s,
31
- cyan: (s) => s
32
+ cyan: (s) => s,
33
+ magenta: (s) => s
32
34
  };
33
35
  }
34
36
 
35
- const VERSION = '1.0.0';
37
+ const VERSION = '1.0.3';
36
38
  const DEFAULT_SERVER = 'wss://webhookdrop.app';
39
+ const POOL_SIZE = 20; // Number of WebSocket connections
40
+ const PING_INTERVAL = 10000; // 10 seconds
37
41
 
38
42
  // Parse command line arguments
39
43
  program
@@ -44,9 +48,11 @@ program
44
48
  .requiredOption('-l, --local <url>', 'Local server URL to forward webhooks to (e.g., http://localhost:3000)')
45
49
  .option('-e, --endpoint <id>', 'Endpoint ID (usually embedded in token)')
46
50
  .option('-s, --server <url>', 'WebhookDrop server URL', DEFAULT_SERVER)
51
+ .option('-p, --pool <size>', 'Connection pool size', String(POOL_SIZE))
47
52
  .parse(process.argv);
48
53
 
49
54
  const options = program.opts();
55
+ const poolSize = parseInt(options.pool) || POOL_SIZE;
50
56
 
51
57
  // Validate local URL
52
58
  let localUrl;
@@ -75,21 +81,11 @@ if (!endpointId) {
75
81
  process.exit(1);
76
82
  }
77
83
 
78
- // Build WebSocket URL
79
- const wsUrl = `${options.server}/ws/tunnel/${endpointId}?token=${encodeURIComponent(options.token)}&local_url=${encodeURIComponent(options.local)}`;
80
-
81
- // Format timestamp
84
+ // Timestamp helper
82
85
  function timestamp() {
83
86
  return chalk.gray(new Date().toLocaleTimeString());
84
87
  }
85
88
 
86
- // Print banner
87
- console.log('');
88
- console.log(chalk.cyan('╔══════════════════════════════════════════╗'));
89
- console.log(chalk.cyan('║') + chalk.bold(' WebhookDrop Tunnel Client ') + chalk.cyan('║'));
90
- console.log(chalk.cyan('╚══════════════════════════════════════════╝'));
91
- console.log('');
92
-
93
89
  // Forward HTTP request to local server
94
90
  async function forwardRequest(data) {
95
91
  return new Promise((resolve) => {
@@ -113,20 +109,26 @@ async function forwardRequest(data) {
113
109
  method: method,
114
110
  headers: {
115
111
  ...headers,
116
- 'host': targetUrl.host
112
+ 'host': targetUrl.host,
113
+ // Request uncompressed content to avoid encoding issues
114
+ 'accept-encoding': 'identity'
117
115
  },
118
116
  timeout: 30000
119
117
  };
120
118
 
121
119
  const req = httpModule.request(requestOptions, (res) => {
122
- let responseBody = '';
123
- res.on('data', (chunk) => responseBody += chunk);
120
+ // Collect response as Buffer chunks to handle binary data correctly
121
+ const chunks = [];
122
+ res.on('data', (chunk) => chunks.push(chunk));
124
123
  res.on('end', () => {
124
+ const buffer = Buffer.concat(chunks);
125
125
  resolve({
126
126
  success: true,
127
127
  status_code: res.statusCode,
128
128
  headers: res.headers,
129
- body: responseBody
129
+ // Send as base64 to preserve binary data
130
+ body: buffer.toString('base64'),
131
+ body_encoding: 'base64'
130
132
  });
131
133
  });
132
134
  });
@@ -155,29 +157,51 @@ async function forwardRequest(data) {
155
157
  });
156
158
  }
157
159
 
158
- // Main tunnel connection
159
- function connect() {
160
- console.log(`${timestamp()} Connecting to WebhookDrop...`);
161
- console.log(`${timestamp()} Forwarding to: ${chalk.green(options.local)}`);
162
- console.log('');
160
+ // Connection pool
161
+ const connections = new Map();
162
+ let connectedCount = 0;
163
+ let totalRequests = 0;
164
+ let isShuttingDown = false;
163
165
 
164
- const ws = new WebSocket(wsUrl.replace('https://', 'wss://').replace('http://', 'ws://'));
166
+ // Create a single WebSocket connection
167
+ function createConnection(connectionId) {
168
+ const wsUrl = `${options.server}/ws/tunnel/${endpointId}?token=${encodeURIComponent(options.token)}&local_url=${encodeURIComponent(options.local)}&connection_id=${connectionId}`;
165
169
 
170
+ const ws = new WebSocket(wsUrl.replace('https://', 'wss://').replace('http://', 'ws://'));
166
171
  let pingInterval;
172
+ let isConnected = false;
167
173
 
168
174
  ws.on('open', () => {
169
- console.log(`${timestamp()} ${chalk.green('✓')} ${chalk.bold('Tunnel connected!')}`);
170
- console.log(`${timestamp()} Endpoint: ${chalk.cyan(endpointId.substring(0, 8))}...`);
171
- console.log('');
172
- console.log(`${timestamp()} ${chalk.yellow('Waiting for webhooks...')} (Press Ctrl+C to stop)`);
173
- console.log('');
175
+ isConnected = true;
176
+ connectedCount++;
177
+
178
+ if (connectedCount === 1) {
179
+ // First connection - show banner
180
+ console.log('');
181
+ console.log(chalk.cyan('╔══════════════════════════════════════════════════╗'));
182
+ console.log(chalk.cyan('║') + chalk.bold(' WebhookDrop Tunnel Client (Pool Mode) ') + chalk.cyan('║'));
183
+ console.log(chalk.cyan('╚══════════════════════════════════════════════════╝'));
184
+ console.log('');
185
+ console.log(`${timestamp()} Forwarding to: ${chalk.cyan(options.local)}`);
186
+ console.log('');
187
+ }
188
+
189
+ if (connectedCount === poolSize) {
190
+ console.log(`${timestamp()} ${chalk.green('✓')} ${chalk.bold(`All ${poolSize} connections established!`)}`);
191
+ console.log(`${timestamp()} Endpoint: ${chalk.cyan(endpointId.substring(0, 8))}...`);
192
+ console.log('');
193
+ console.log(`${timestamp()} ${chalk.yellow('Waiting for requests...')} (Press Ctrl+C to stop)`);
194
+ console.log('');
195
+ }
174
196
 
175
- // Send ping every 25 seconds to keep connection alive
197
+ // Send ping every 10 seconds to keep connection alive
176
198
  pingInterval = setInterval(() => {
177
199
  if (ws.readyState === WebSocket.OPEN) {
178
200
  ws.send(JSON.stringify({ type: 'ping' }));
179
201
  }
180
- }, 25000);
202
+ }, PING_INTERVAL);
203
+
204
+ connections.set(connectionId, { ws, pingInterval, isConnected: true });
181
205
  });
182
206
 
183
207
  ws.on('message', async (data) => {
@@ -185,82 +209,105 @@ function connect() {
185
209
  const message = JSON.parse(data.toString());
186
210
 
187
211
  if (message.type === 'connected') {
188
- // Initial connection confirmation - already handled in 'open'
212
+ // Initial connection confirmation
189
213
  return;
190
214
  }
191
215
 
192
216
  if (message.type === 'pong') {
193
- // Ignore pong responses
217
+ // Heartbeat response - connection healthy
194
218
  return;
195
219
  }
196
220
 
197
221
  if (message.type === 'webhook_request' || message.type === 'http_request') {
198
222
  const requestId = message.request_id;
199
- const requestData = message.data || message;
200
- const method = requestData.method || 'POST';
201
- const path = requestData.path || '/';
223
+ const requestData = message.data || {};
202
224
 
203
- console.log(`${timestamp()} ${chalk.blue('→')} ${chalk.bold(method)} ${path}`);
225
+ totalRequests++;
226
+ console.log(`${timestamp()} ${chalk.blue('→')} ${requestData.method || 'POST'} ${requestData.path || '/'}`);
204
227
 
205
228
  // Forward to local server
206
229
  const response = await forwardRequest(requestData);
207
230
 
208
- if (response.success) {
209
- console.log(`${timestamp()} ${chalk.green('←')} ${response.status_code} OK`);
210
- } else {
211
- console.log(`${timestamp()} ${chalk.red('←')} ${response.status_code} ${response.error}`);
212
- }
213
-
214
- // Send response back to server
215
- ws.send(JSON.stringify({
216
- type: 'webhook_response',
231
+ // Send response back
232
+ const responseMessage = {
233
+ type: message.type === 'webhook_request' ? 'webhook_response' : 'http_response',
217
234
  request_id: requestId,
218
235
  response: response
219
- }));
220
- }
236
+ };
221
237
 
222
- if (message.type === 'error') {
223
- console.log(`${timestamp()} ${chalk.red('Server error:')} ${message.error}`);
224
- }
238
+ if (ws.readyState === WebSocket.OPEN) {
239
+ ws.send(JSON.stringify(responseMessage));
240
+ }
225
241
 
242
+ const statusColor = response.success && response.status_code < 400 ? chalk.green : chalk.red;
243
+ console.log(`${timestamp()} ${statusColor('←')} ${response.status_code || 502} ${response.success ? 'OK' : response.error || 'Error'}`);
244
+ }
226
245
  } catch (e) {
227
- console.error(`${timestamp()} ${chalk.red('Error parsing message:')} ${e.message}`);
246
+ console.error(`${timestamp()} ${chalk.red('Error processing message:')} ${e.message}`);
228
247
  }
229
248
  });
230
249
 
231
- ws.on('close', (code, reason) => {
232
- clearInterval(pingInterval);
233
- console.log('');
234
- console.log(`${timestamp()} ${chalk.yellow('Connection closed:')} ${reason || 'Unknown reason'}`);
250
+ ws.on('close', () => {
251
+ if (isConnected) {
252
+ connectedCount--;
253
+ }
254
+ isConnected = false;
235
255
 
236
- // Reconnect after 5 seconds
237
- console.log(`${timestamp()} Reconnecting in 5 seconds...`);
238
- setTimeout(connect, 5000);
256
+ if (pingInterval) {
257
+ clearInterval(pingInterval);
258
+ }
259
+
260
+ connections.delete(connectionId);
261
+
262
+ if (!isShuttingDown) {
263
+ // Reconnect after delay
264
+ setTimeout(() => {
265
+ if (!isShuttingDown) {
266
+ createConnection(connectionId);
267
+ }
268
+ }, 2000);
269
+ }
239
270
  });
240
271
 
241
272
  ws.on('error', (error) => {
242
- clearInterval(pingInterval);
243
-
244
- if (error.message.includes('401')) {
245
- console.error(`${timestamp()} ${chalk.red('❌ Authentication failed. Please check your token.')}`);
246
- process.exit(1);
247
- } else if (error.message.includes('404')) {
248
- console.error(`${timestamp()} ${chalk.red('❌ Endpoint not found. Please check your endpoint ID.')}`);
249
- process.exit(1);
250
- } else {
273
+ // Suppress error logging for normal reconnection cases
274
+ if (!isShuttingDown && error.code !== 'ECONNREFUSED') {
251
275
  console.error(`${timestamp()} ${chalk.red('Connection error:')} ${error.message}`);
252
- console.log(`${timestamp()} Reconnecting in 5 seconds...`);
253
- setTimeout(connect, 5000);
254
276
  }
255
277
  });
278
+
279
+ return ws;
256
280
  }
257
281
 
258
- // Handle graceful shutdown
259
- process.on('SIGINT', () => {
282
+ // Start connection pool
283
+ console.log('');
284
+ console.log(`${timestamp()} Starting connection pool (${poolSize} connections)...`);
285
+
286
+ for (let i = 0; i < poolSize; i++) {
287
+ const connectionId = `conn-${i.toString().padStart(2, '0')}`;
288
+ setTimeout(() => createConnection(connectionId), i * 100); // Stagger connections
289
+ }
290
+
291
+ // Handle shutdown
292
+ function shutdown() {
293
+ if (isShuttingDown) return;
294
+ isShuttingDown = true;
295
+
260
296
  console.log('');
261
- console.log(`${timestamp()} ${chalk.yellow('Shutting down tunnel...')}`);
262
- process.exit(0);
263
- });
297
+ console.log(`${timestamp()} Shutting down tunnel...`);
298
+ console.log(`${timestamp()} Total requests handled: ${totalRequests}`);
299
+
300
+ for (const [id, conn] of connections) {
301
+ if (conn.pingInterval) {
302
+ clearInterval(conn.pingInterval);
303
+ }
304
+ if (conn.ws && conn.ws.readyState === WebSocket.OPEN) {
305
+ conn.ws.close();
306
+ }
307
+ }
308
+
309
+ setTimeout(() => process.exit(0), 1000);
310
+ }
264
311
 
265
- // Start the tunnel
266
- connect();
312
+ process.on('SIGINT', shutdown);
313
+ process.on('SIGTERM', shutdown);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webhookdrop-tunnel",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "Forward webhooks from WebhookDrop to your local development server",
5
5
  "main": "index.js",
6
6
  "bin": {