webhookdrop-tunnel 1.0.1 → 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 +113 -72
  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) => {
@@ -161,29 +157,51 @@ async function forwardRequest(data) {
161
157
  });
162
158
  }
163
159
 
164
- // Main tunnel connection
165
- function connect() {
166
- console.log(`${timestamp()} Connecting to WebhookDrop...`);
167
- console.log(`${timestamp()} Forwarding to: ${chalk.green(options.local)}`);
168
- console.log('');
160
+ // Connection pool
161
+ const connections = new Map();
162
+ let connectedCount = 0;
163
+ let totalRequests = 0;
164
+ let isShuttingDown = false;
169
165
 
170
- 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}`;
171
169
 
170
+ const ws = new WebSocket(wsUrl.replace('https://', 'wss://').replace('http://', 'ws://'));
172
171
  let pingInterval;
172
+ let isConnected = false;
173
173
 
174
174
  ws.on('open', () => {
175
- console.log(`${timestamp()} ${chalk.green('✓')} ${chalk.bold('Tunnel connected!')}`);
176
- console.log(`${timestamp()} Endpoint: ${chalk.cyan(endpointId.substring(0, 8))}...`);
177
- console.log('');
178
- console.log(`${timestamp()} ${chalk.yellow('Waiting for webhooks...')} (Press Ctrl+C to stop)`);
179
- 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
+ }
180
196
 
181
- // Send ping every 25 seconds to keep connection alive
197
+ // Send ping every 10 seconds to keep connection alive
182
198
  pingInterval = setInterval(() => {
183
199
  if (ws.readyState === WebSocket.OPEN) {
184
200
  ws.send(JSON.stringify({ type: 'ping' }));
185
201
  }
186
- }, 25000);
202
+ }, PING_INTERVAL);
203
+
204
+ connections.set(connectionId, { ws, pingInterval, isConnected: true });
187
205
  });
188
206
 
189
207
  ws.on('message', async (data) => {
@@ -191,82 +209,105 @@ function connect() {
191
209
  const message = JSON.parse(data.toString());
192
210
 
193
211
  if (message.type === 'connected') {
194
- // Initial connection confirmation - already handled in 'open'
212
+ // Initial connection confirmation
195
213
  return;
196
214
  }
197
215
 
198
216
  if (message.type === 'pong') {
199
- // Ignore pong responses
217
+ // Heartbeat response - connection healthy
200
218
  return;
201
219
  }
202
220
 
203
221
  if (message.type === 'webhook_request' || message.type === 'http_request') {
204
222
  const requestId = message.request_id;
205
- const requestData = message.data || message;
206
- const method = requestData.method || 'POST';
207
- const path = requestData.path || '/';
223
+ const requestData = message.data || {};
208
224
 
209
- console.log(`${timestamp()} ${chalk.blue('→')} ${chalk.bold(method)} ${path}`);
225
+ totalRequests++;
226
+ console.log(`${timestamp()} ${chalk.blue('→')} ${requestData.method || 'POST'} ${requestData.path || '/'}`);
210
227
 
211
228
  // Forward to local server
212
229
  const response = await forwardRequest(requestData);
213
230
 
214
- if (response.success) {
215
- console.log(`${timestamp()} ${chalk.green('←')} ${response.status_code} OK`);
216
- } else {
217
- console.log(`${timestamp()} ${chalk.red('←')} ${response.status_code} ${response.error}`);
218
- }
219
-
220
- // Send response back to server
221
- ws.send(JSON.stringify({
222
- type: 'webhook_response',
231
+ // Send response back
232
+ const responseMessage = {
233
+ type: message.type === 'webhook_request' ? 'webhook_response' : 'http_response',
223
234
  request_id: requestId,
224
235
  response: response
225
- }));
226
- }
236
+ };
227
237
 
228
- if (message.type === 'error') {
229
- console.log(`${timestamp()} ${chalk.red('Server error:')} ${message.error}`);
230
- }
238
+ if (ws.readyState === WebSocket.OPEN) {
239
+ ws.send(JSON.stringify(responseMessage));
240
+ }
231
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
+ }
232
245
  } catch (e) {
233
- console.error(`${timestamp()} ${chalk.red('Error parsing message:')} ${e.message}`);
246
+ console.error(`${timestamp()} ${chalk.red('Error processing message:')} ${e.message}`);
234
247
  }
235
248
  });
236
249
 
237
- ws.on('close', (code, reason) => {
238
- clearInterval(pingInterval);
239
- console.log('');
240
- console.log(`${timestamp()} ${chalk.yellow('Connection closed:')} ${reason || 'Unknown reason'}`);
250
+ ws.on('close', () => {
251
+ if (isConnected) {
252
+ connectedCount--;
253
+ }
254
+ isConnected = false;
241
255
 
242
- // Reconnect after 5 seconds
243
- console.log(`${timestamp()} Reconnecting in 5 seconds...`);
244
- 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
+ }
245
270
  });
246
271
 
247
272
  ws.on('error', (error) => {
248
- clearInterval(pingInterval);
249
-
250
- if (error.message.includes('401')) {
251
- console.error(`${timestamp()} ${chalk.red('❌ Authentication failed. Please check your token.')}`);
252
- process.exit(1);
253
- } else if (error.message.includes('404')) {
254
- console.error(`${timestamp()} ${chalk.red('❌ Endpoint not found. Please check your endpoint ID.')}`);
255
- process.exit(1);
256
- } else {
273
+ // Suppress error logging for normal reconnection cases
274
+ if (!isShuttingDown && error.code !== 'ECONNREFUSED') {
257
275
  console.error(`${timestamp()} ${chalk.red('Connection error:')} ${error.message}`);
258
- console.log(`${timestamp()} Reconnecting in 5 seconds...`);
259
- setTimeout(connect, 5000);
260
276
  }
261
277
  });
278
+
279
+ return ws;
262
280
  }
263
281
 
264
- // Handle graceful shutdown
265
- 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
+
266
296
  console.log('');
267
- console.log(`${timestamp()} ${chalk.yellow('Shutting down tunnel...')}`);
268
- process.exit(0);
269
- });
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
+ }
270
311
 
271
- // Start the tunnel
272
- 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.1",
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": {