webhookdrop-tunnel 1.0.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.
Files changed (3) hide show
  1. package/README.md +74 -0
  2. package/bin/tunnel.js +266 -0
  3. package/package.json +36 -0
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # WebhookDrop Tunnel
2
+
3
+ Forward webhooks from WebhookDrop to your local development server.
4
+
5
+ **Works on:** Windows, macOS, Linux
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npx webhookdrop-tunnel --token YOUR_TOKEN --local http://localhost:3000
11
+ ```
12
+
13
+ That's it! No installation required.
14
+
15
+ ## Get Your Token
16
+
17
+ 1. Login to [WebhookDrop](https://webhookdrop.app)
18
+ 2. Go to **Tunnels** page
19
+ 3. Click **"Connect Tunnel"** on your endpoint
20
+ 4. Copy the token from the popup
21
+
22
+ ## Usage
23
+
24
+ ```bash
25
+ npx webhookdrop-tunnel --token <TOKEN> --local <LOCAL_URL>
26
+ ```
27
+
28
+ ### Options
29
+
30
+ | Option | Description | Required |
31
+ |--------|-------------|----------|
32
+ | `-t, --token` | Tunnel authentication token | ✅ Yes |
33
+ | `-l, --local` | Local server URL (e.g., http://localhost:3000) | ✅ Yes |
34
+ | `-e, --endpoint` | Endpoint ID (usually in token) | No |
35
+ | `-s, --server` | WebhookDrop server URL | No |
36
+ | `-V, --version` | Show version | No |
37
+ | `-h, --help` | Show help | No |
38
+
39
+ ### Examples
40
+
41
+ Forward to Express server on port 3000:
42
+ ```bash
43
+ npx webhookdrop-tunnel -t abc123... -l http://localhost:3000
44
+ ```
45
+
46
+ Forward to n8n on port 5678:
47
+ ```bash
48
+ npx webhookdrop-tunnel -t abc123... -l http://localhost:5678
49
+ ```
50
+
51
+ Forward to Django on port 8000:
52
+ ```bash
53
+ npx webhookdrop-tunnel -t abc123... -l http://127.0.0.1:8000
54
+ ```
55
+
56
+ ## How It Works
57
+
58
+ 1. **Connect**: The client establishes a WebSocket connection to WebhookDrop
59
+ 2. **Listen**: When a webhook arrives at your WebhookDrop URL, it's sent through the tunnel
60
+ 3. **Forward**: The client forwards the webhook to your local server
61
+ 4. **Respond**: Your local server's response is sent back through the tunnel
62
+
63
+ ```
64
+ External Service → WebhookDrop → Tunnel → Your Local Server
65
+ ← Response ←
66
+ ```
67
+
68
+ ## Requirements
69
+
70
+ - Node.js 14 or higher
71
+
72
+ ## License
73
+
74
+ MIT License - see [LICENSE](LICENSE) for details.
package/bin/tunnel.js ADDED
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * WebhookDrop Tunnel Client
5
+ *
6
+ * Forwards webhooks from WebhookDrop to your local development server.
7
+ *
8
+ * Usage:
9
+ * npx webhookdrop-tunnel --token YOUR_TOKEN --local http://localhost:3000
10
+ */
11
+
12
+ const WebSocket = require('ws');
13
+ const http = require('http');
14
+ const https = require('https');
15
+ const { URL } = require('url');
16
+ const { program } = require('commander');
17
+
18
+ // Use chalk v4 (CommonJS compatible)
19
+ let chalk;
20
+ try {
21
+ chalk = require('chalk');
22
+ } catch {
23
+ // Fallback if chalk not available
24
+ chalk = {
25
+ green: (s) => s,
26
+ red: (s) => s,
27
+ yellow: (s) => s,
28
+ blue: (s) => s,
29
+ gray: (s) => s,
30
+ bold: (s) => s,
31
+ cyan: (s) => s
32
+ };
33
+ }
34
+
35
+ const VERSION = '1.0.0';
36
+ const DEFAULT_SERVER = 'wss://webhookdrop.app';
37
+
38
+ // Parse command line arguments
39
+ program
40
+ .name('webhookdrop-tunnel')
41
+ .description('Forward webhooks from WebhookDrop to your local server')
42
+ .version(VERSION)
43
+ .requiredOption('-t, --token <token>', 'Tunnel authentication token (from WebhookDrop dashboard)')
44
+ .requiredOption('-l, --local <url>', 'Local server URL to forward webhooks to (e.g., http://localhost:3000)')
45
+ .option('-e, --endpoint <id>', 'Endpoint ID (usually embedded in token)')
46
+ .option('-s, --server <url>', 'WebhookDrop server URL', DEFAULT_SERVER)
47
+ .parse(process.argv);
48
+
49
+ const options = program.opts();
50
+
51
+ // Validate local URL
52
+ let localUrl;
53
+ try {
54
+ localUrl = new URL(options.local);
55
+ } catch (e) {
56
+ console.error(chalk.red(`❌ Invalid local URL: ${options.local}`));
57
+ process.exit(1);
58
+ }
59
+
60
+ // Extract endpoint ID from token if not provided
61
+ function getEndpointIdFromToken(token) {
62
+ try {
63
+ const parts = token.split('.');
64
+ if (parts.length !== 3) return null;
65
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
66
+ return payload.endpoint_id;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ const endpointId = options.endpoint || getEndpointIdFromToken(options.token);
73
+ if (!endpointId) {
74
+ console.error(chalk.red('❌ Could not determine endpoint ID. Please provide --endpoint flag.'));
75
+ process.exit(1);
76
+ }
77
+
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
82
+ function timestamp() {
83
+ return chalk.gray(new Date().toLocaleTimeString());
84
+ }
85
+
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
+ // Forward HTTP request to local server
94
+ async function forwardRequest(data) {
95
+ return new Promise((resolve) => {
96
+ const method = data.method || 'POST';
97
+ const path = data.path || '/';
98
+ const headers = data.headers || {};
99
+ const body = data.body || '';
100
+
101
+ const targetUrl = new URL(path, options.local);
102
+ if (data.query_string) {
103
+ targetUrl.search = data.query_string;
104
+ }
105
+
106
+ const isHttps = targetUrl.protocol === 'https:';
107
+ const httpModule = isHttps ? https : http;
108
+
109
+ const requestOptions = {
110
+ hostname: targetUrl.hostname,
111
+ port: targetUrl.port || (isHttps ? 443 : 80),
112
+ path: targetUrl.pathname + targetUrl.search,
113
+ method: method,
114
+ headers: {
115
+ ...headers,
116
+ 'host': targetUrl.host
117
+ },
118
+ timeout: 30000
119
+ };
120
+
121
+ const req = httpModule.request(requestOptions, (res) => {
122
+ let responseBody = '';
123
+ res.on('data', (chunk) => responseBody += chunk);
124
+ res.on('end', () => {
125
+ resolve({
126
+ success: true,
127
+ status_code: res.statusCode,
128
+ headers: res.headers,
129
+ body: responseBody
130
+ });
131
+ });
132
+ });
133
+
134
+ req.on('error', (error) => {
135
+ resolve({
136
+ success: false,
137
+ status_code: 502,
138
+ error: error.message
139
+ });
140
+ });
141
+
142
+ req.on('timeout', () => {
143
+ req.destroy();
144
+ resolve({
145
+ success: false,
146
+ status_code: 504,
147
+ error: 'Request timeout'
148
+ });
149
+ });
150
+
151
+ if (body) {
152
+ req.write(body);
153
+ }
154
+ req.end();
155
+ });
156
+ }
157
+
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('');
163
+
164
+ const ws = new WebSocket(wsUrl.replace('https://', 'wss://').replace('http://', 'ws://'));
165
+
166
+ let pingInterval;
167
+
168
+ 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('');
174
+
175
+ // Send ping every 25 seconds to keep connection alive
176
+ pingInterval = setInterval(() => {
177
+ if (ws.readyState === WebSocket.OPEN) {
178
+ ws.send(JSON.stringify({ type: 'ping' }));
179
+ }
180
+ }, 25000);
181
+ });
182
+
183
+ ws.on('message', async (data) => {
184
+ try {
185
+ const message = JSON.parse(data.toString());
186
+
187
+ if (message.type === 'connected') {
188
+ // Initial connection confirmation - already handled in 'open'
189
+ return;
190
+ }
191
+
192
+ if (message.type === 'pong') {
193
+ // Ignore pong responses
194
+ return;
195
+ }
196
+
197
+ if (message.type === 'webhook_request' || message.type === 'http_request') {
198
+ const requestId = message.request_id;
199
+ const requestData = message.data || message;
200
+ const method = requestData.method || 'POST';
201
+ const path = requestData.path || '/';
202
+
203
+ console.log(`${timestamp()} ${chalk.blue('→')} ${chalk.bold(method)} ${path}`);
204
+
205
+ // Forward to local server
206
+ const response = await forwardRequest(requestData);
207
+
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',
217
+ request_id: requestId,
218
+ response: response
219
+ }));
220
+ }
221
+
222
+ if (message.type === 'error') {
223
+ console.log(`${timestamp()} ${chalk.red('Server error:')} ${message.error}`);
224
+ }
225
+
226
+ } catch (e) {
227
+ console.error(`${timestamp()} ${chalk.red('Error parsing message:')} ${e.message}`);
228
+ }
229
+ });
230
+
231
+ ws.on('close', (code, reason) => {
232
+ clearInterval(pingInterval);
233
+ console.log('');
234
+ console.log(`${timestamp()} ${chalk.yellow('Connection closed:')} ${reason || 'Unknown reason'}`);
235
+
236
+ // Reconnect after 5 seconds
237
+ console.log(`${timestamp()} Reconnecting in 5 seconds...`);
238
+ setTimeout(connect, 5000);
239
+ });
240
+
241
+ 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 {
251
+ console.error(`${timestamp()} ${chalk.red('Connection error:')} ${error.message}`);
252
+ console.log(`${timestamp()} Reconnecting in 5 seconds...`);
253
+ setTimeout(connect, 5000);
254
+ }
255
+ });
256
+ }
257
+
258
+ // Handle graceful shutdown
259
+ process.on('SIGINT', () => {
260
+ console.log('');
261
+ console.log(`${timestamp()} ${chalk.yellow('Shutting down tunnel...')}`);
262
+ process.exit(0);
263
+ });
264
+
265
+ // Start the tunnel
266
+ connect();
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "webhookdrop-tunnel",
3
+ "version": "1.0.0",
4
+ "description": "Forward webhooks from WebhookDrop to your local development server",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "webhookdrop-tunnel": "./bin/tunnel.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"No tests yet\""
11
+ },
12
+ "keywords": [
13
+ "webhook",
14
+ "tunnel",
15
+ "localhost",
16
+ "development",
17
+ "webhookdrop",
18
+ "ngrok-alternative",
19
+ "local-development"
20
+ ],
21
+ "author": "WebhookDrop",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/webhookdrop/tunnel"
26
+ },
27
+ "homepage": "https://webhookdrop.app",
28
+ "engines": {
29
+ "node": ">=14.0.0"
30
+ },
31
+ "dependencies": {
32
+ "ws": "^8.14.0",
33
+ "chalk": "^4.1.2",
34
+ "commander": "^11.1.0"
35
+ }
36
+ }