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.
- package/README.md +74 -0
- package/bin/tunnel.js +266 -0
- 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
|
+
}
|