private-connect 0.5.7 → 0.5.9
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 +53 -2
- package/dist/index.js +254 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Zero-friction connectivity tools. No signup required.
|
|
4
4
|
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i private-connect # add to your project
|
|
9
|
+
npm i -g private-connect # install globally
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
**Run it:** after a global install use `private-connect`; after a local install use `npx private-connect`. Or run without installing: `npx private-connect <command>`.
|
|
13
|
+
|
|
5
14
|
## Quick Start
|
|
6
15
|
|
|
7
16
|
```bash
|
|
@@ -10,6 +19,10 @@ npx private-connect test db.internal:5432
|
|
|
10
19
|
|
|
11
20
|
# Create a temporary public tunnel
|
|
12
21
|
npx private-connect tunnel 3000
|
|
22
|
+
|
|
23
|
+
# Test webhooks locally (Polar, Stripe, GitHub, or any provider)
|
|
24
|
+
npx private-connect polar 3000
|
|
25
|
+
npx private-connect stripe 3000
|
|
13
26
|
```
|
|
14
27
|
|
|
15
28
|
## Commands
|
|
@@ -53,7 +66,7 @@ Private Connect - Temporary Tunnel
|
|
|
53
66
|
────────────────────────────────────
|
|
54
67
|
|
|
55
68
|
Local: localhost:3000
|
|
56
|
-
Public: https://privateconnect.co
|
|
69
|
+
Public: https://abc12345.privateconnect.co
|
|
57
70
|
Anyone can access this URL
|
|
58
71
|
Inspector: https://privateconnect.co/debug/s-xyz789
|
|
59
72
|
Live traffic monitoring & request replay
|
|
@@ -64,6 +77,20 @@ Private Connect - Temporary Tunnel
|
|
|
64
77
|
Press Ctrl+C to stop
|
|
65
78
|
```
|
|
66
79
|
|
|
80
|
+
**TCP/UDP Tunnels:**
|
|
81
|
+
```bash
|
|
82
|
+
npx private-connect tunnel 5432 --tcp # TCP tunnel (databases, etc.)
|
|
83
|
+
npx private-connect tunnel 27015 --udp # UDP tunnel (game servers, etc.)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
TCP/UDP output shows connection details:
|
|
87
|
+
```
|
|
88
|
+
Local: localhost:5432
|
|
89
|
+
Public: tcp://api.privateconnect.co:40001
|
|
90
|
+
Connect: api.privateconnect.co:40001
|
|
91
|
+
Expires: 120 minutes
|
|
92
|
+
```
|
|
93
|
+
|
|
67
94
|
**Sharing:**
|
|
68
95
|
- The public URL shows your actual website (like ngrok)
|
|
69
96
|
- Perfect for demos, testing, and sharing with teammates
|
|
@@ -73,9 +100,33 @@ Private Connect - Temporary Tunnel
|
|
|
73
100
|
- No signup or account required
|
|
74
101
|
- Auto-expires in 2 hours
|
|
75
102
|
- Real-time request logging
|
|
76
|
-
- Works with
|
|
103
|
+
- Works with HTTP, TCP, and UDP services
|
|
77
104
|
- **Shareable URLs** - The public URL shows your actual website, perfect for demos and testing
|
|
78
105
|
|
|
106
|
+
### `<provider> <port>` - Webhook tunnel with provider setup
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npx private-connect <provider> <port>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Create a tunnel and get provider-specific webhook setup instructions.
|
|
113
|
+
|
|
114
|
+
**Known providers** (with tailored instructions):
|
|
115
|
+
- `polar` — [Polar](https://polar.sh) webhooks
|
|
116
|
+
- `stripe` — [Stripe](https://stripe.com) webhooks
|
|
117
|
+
- `github` — [GitHub](https://github.com) webhooks
|
|
118
|
+
- `shopify` — [Shopify](https://shopify.dev) webhooks
|
|
119
|
+
|
|
120
|
+
**Any provider name works** — unknown names get generic webhook guidance.
|
|
121
|
+
|
|
122
|
+
**Examples:**
|
|
123
|
+
```bash
|
|
124
|
+
npx private-connect polar 3000 # Polar webhooks → localhost:3000
|
|
125
|
+
npx private-connect stripe 3000 # Stripe webhooks → localhost:3000
|
|
126
|
+
npx private-connect github 8080 # GitHub webhooks → localhost:8080
|
|
127
|
+
npx private-connect myapp 3000 # Generic webhooks → localhost:3000
|
|
128
|
+
```
|
|
129
|
+
|
|
79
130
|
### `setup-openclaw` - One-command OpenClaw setup
|
|
80
131
|
|
|
81
132
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
const net = require("net");
|
|
14
|
+
const dgram = require("dgram");
|
|
14
15
|
const tls = require("tls");
|
|
15
16
|
const https = require("https");
|
|
16
17
|
const http = require("http");
|
|
@@ -248,6 +249,7 @@ ${c.bold}Commands:${c.reset}
|
|
|
248
249
|
check <target> Test connectivity to any service
|
|
249
250
|
test <target> Alias for check
|
|
250
251
|
tunnel <port> Create a temporary public tunnel
|
|
252
|
+
<provider> <port> Webhook tunnel with provider-specific setup
|
|
251
253
|
list List all active tunnels
|
|
252
254
|
close <id> Close a tunnel by ID
|
|
253
255
|
close --all Close all active tunnels
|
|
@@ -260,6 +262,11 @@ ${c.bold}Examples:${c.reset}
|
|
|
260
262
|
npx private-connect tunnel 3000
|
|
261
263
|
npx private-connect tunnel localhost:8080
|
|
262
264
|
npx private-connect tunnel 4096 --tcp
|
|
265
|
+
npx private-connect tunnel 27015 --udp
|
|
266
|
+
npx private-connect polar 3000
|
|
267
|
+
npx private-connect stripe 3000
|
|
268
|
+
npx private-connect github 8080
|
|
269
|
+
npx private-connect myapp 3000
|
|
263
270
|
npx private-connect list
|
|
264
271
|
npx private-connect close abc123
|
|
265
272
|
npx private-connect setup-openclaw
|
|
@@ -268,7 +275,12 @@ ${c.bold}Examples:${c.reset}
|
|
|
268
275
|
${c.bold}Tunnel:${c.reset}
|
|
269
276
|
• No signup required
|
|
270
277
|
• Auto-expires in 2 hours
|
|
271
|
-
• HTTP
|
|
278
|
+
• HTTP, TCP (--tcp), or UDP (--udp)
|
|
279
|
+
|
|
280
|
+
${c.bold}Webhooks:${c.reset}
|
|
281
|
+
• Use any provider name: polar, stripe, github, shopify, or your own
|
|
282
|
+
• Known providers get tailored setup instructions
|
|
283
|
+
• Unknown providers get generic webhook guidance
|
|
272
284
|
|
|
273
285
|
${c.bold}Test:${c.reset}
|
|
274
286
|
• TCP reachability
|
|
@@ -284,30 +296,112 @@ ${c.bold}OpenClaw:${c.reset}
|
|
|
284
296
|
${c.dim}For permanent tunnels: https://privateconnect.co${c.reset}
|
|
285
297
|
`);
|
|
286
298
|
}
|
|
299
|
+
const WEBHOOK_PROVIDERS = {
|
|
300
|
+
polar: {
|
|
301
|
+
name: 'Polar',
|
|
302
|
+
dashboardUrl: 'https://polar.sh',
|
|
303
|
+
docsUrl: 'https://polar.sh/docs/integrate/webhooks',
|
|
304
|
+
secretEnvVar: 'POLAR_WEBHOOK_SECRET',
|
|
305
|
+
instructions: [
|
|
306
|
+
'Go to ${dashboardUrl} → Settings → Webhooks',
|
|
307
|
+
'Add the public URL above as your webhook endpoint',
|
|
308
|
+
'Copy the signing secret and set it as ${secretEnvVar}',
|
|
309
|
+
'Docs: ${docsUrl}',
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
stripe: {
|
|
313
|
+
name: 'Stripe',
|
|
314
|
+
dashboardUrl: 'https://dashboard.stripe.com',
|
|
315
|
+
docsUrl: 'https://docs.stripe.com/webhooks',
|
|
316
|
+
secretEnvVar: 'STRIPE_WEBHOOK_SECRET',
|
|
317
|
+
instructions: [
|
|
318
|
+
'Go to ${dashboardUrl} → Developers → Webhooks',
|
|
319
|
+
'Add the public URL above as your webhook endpoint',
|
|
320
|
+
'Copy the signing secret (whsec_...) and set it as ${secretEnvVar}',
|
|
321
|
+
'Docs: ${docsUrl}',
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
github: {
|
|
325
|
+
name: 'GitHub',
|
|
326
|
+
dashboardUrl: 'https://github.com',
|
|
327
|
+
docsUrl: 'https://docs.github.com/en/webhooks',
|
|
328
|
+
secretEnvVar: 'GITHUB_WEBHOOK_SECRET',
|
|
329
|
+
instructions: [
|
|
330
|
+
'Go to your repo → Settings → Webhooks → Add webhook',
|
|
331
|
+
'Set the Payload URL to the public URL above',
|
|
332
|
+
'Set a secret and store it as ${secretEnvVar}',
|
|
333
|
+
'Docs: ${docsUrl}',
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
shopify: {
|
|
337
|
+
name: 'Shopify',
|
|
338
|
+
dashboardUrl: 'https://admin.shopify.com',
|
|
339
|
+
docsUrl: 'https://shopify.dev/docs/apps/build/webhooks',
|
|
340
|
+
secretEnvVar: 'SHOPIFY_WEBHOOK_SECRET',
|
|
341
|
+
instructions: [
|
|
342
|
+
'Go to ${dashboardUrl} → Settings → Notifications → Webhooks',
|
|
343
|
+
'Add the public URL above as your webhook endpoint',
|
|
344
|
+
'Use the signing secret from your app settings as ${secretEnvVar}',
|
|
345
|
+
'Docs: ${docsUrl}',
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
// Reserved CLI commands that should NOT be treated as provider names
|
|
350
|
+
const RESERVED_COMMANDS = ['test', 'check', 'tunnel', 'list', 'ls', 'close', 'kill', 'setup-openclaw', 'openclaw-setup', 'setup-moltbot', 'moltbot-setup', 'pair', 'qr', '--help', '-h'];
|
|
351
|
+
function getProviderInstructions(provider) {
|
|
352
|
+
return provider.instructions.map(line => line
|
|
353
|
+
.replace('${dashboardUrl}', provider.dashboardUrl)
|
|
354
|
+
.replace('${docsUrl}', provider.docsUrl)
|
|
355
|
+
.replace('${secretEnvVar}', provider.secretEnvVar || 'WEBHOOK_SECRET'));
|
|
356
|
+
}
|
|
287
357
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
288
358
|
// Temporary Tunnel
|
|
289
359
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
290
360
|
const HUB_URL = process.env.CONNECT_HUB_URL || 'https://api.privateconnect.co';
|
|
291
361
|
const TUNNEL_DOMAIN = process.env.CONNECT_TUNNEL_DOMAIN || 'tunnel.privateconnect.co';
|
|
292
362
|
async function createTemporaryTunnel(options) {
|
|
293
|
-
const { host, port, ttl = 120, tcp = false } = options;
|
|
294
|
-
const tunnelType = tcp ? 'tcp' : 'http';
|
|
363
|
+
const { host, port, ttl = 120, tcp = false, udp = false, provider: providerName } = options;
|
|
364
|
+
const tunnelType = udp ? 'udp' : (tcp ? 'tcp' : 'http');
|
|
365
|
+
const provider = providerName ? WEBHOOK_PROVIDERS[providerName.toLowerCase()] : undefined;
|
|
295
366
|
console.log();
|
|
296
|
-
|
|
367
|
+
if (provider) {
|
|
368
|
+
console.log(`${c.bold}Private Connect${c.reset} - ${provider.name} Webhooks → localhost:${port}`);
|
|
369
|
+
}
|
|
370
|
+
else if (providerName) {
|
|
371
|
+
// Unknown provider — generic webhook mode
|
|
372
|
+
console.log(`${c.bold}Private Connect${c.reset} - ${providerName} Webhooks → localhost:${port}`);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
console.log(`${c.bold}Private Connect${c.reset} - Temporary ${udp ? 'UDP ' : tcp ? 'TCP ' : ''}Tunnel`);
|
|
376
|
+
}
|
|
297
377
|
console.log(`${c.gray}────────────────────────────────────${c.reset}`);
|
|
298
378
|
console.log();
|
|
299
379
|
// Check if local service is running
|
|
300
380
|
process.stdout.write(` Checking ${c.cyan}${host}:${port}${c.reset}... `);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
381
|
+
if (udp) {
|
|
382
|
+
// For UDP, we can't really test connectivity without sending data
|
|
383
|
+
// Just verify it's a valid port
|
|
384
|
+
if (port < 1 || port > 65535) {
|
|
385
|
+
console.log(`${fail}`);
|
|
386
|
+
console.log();
|
|
387
|
+
console.log(` ${c.red}Invalid port: ${port}${c.reset}`);
|
|
388
|
+
console.log();
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
console.log(`${ok} ${c.dim}(UDP - assuming service is ready)${c.reset}`);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
const localCheck = await testTcp(host, port, 2000);
|
|
395
|
+
if (!localCheck.ok) {
|
|
396
|
+
console.log(`${fail}`);
|
|
397
|
+
console.log();
|
|
398
|
+
console.log(` ${c.red}Cannot connect to ${host}:${port}${c.reset}`);
|
|
399
|
+
console.log(` ${c.gray}Make sure your service is running${c.reset}`);
|
|
400
|
+
console.log();
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
console.log(`${ok}`);
|
|
309
404
|
}
|
|
310
|
-
console.log(`${ok}`);
|
|
311
405
|
// Generate a temporary tunnel ID
|
|
312
406
|
const tunnelId = (0, crypto_1.randomBytes)(6).toString('hex');
|
|
313
407
|
// Request tunnel from hub
|
|
@@ -388,14 +482,46 @@ async function createTemporaryTunnel(options) {
|
|
|
388
482
|
if (data.tunnel.type === 'tcp' && data.tunnel.tcpHost && data.tunnel.tcpPort) {
|
|
389
483
|
console.log(` ${c.bold}Connect:${c.reset} ${c.cyan}${data.tunnel.tcpHost}:${data.tunnel.tcpPort}${c.reset}`);
|
|
390
484
|
}
|
|
485
|
+
if (data.tunnel.type === 'udp' && data.tunnel.udpHost && data.tunnel.udpPort) {
|
|
486
|
+
console.log(` ${c.bold}Connect:${c.reset} ${c.cyan}${data.tunnel.udpHost}:${data.tunnel.udpPort}${c.reset} ${c.dim}(UDP)${c.reset}`);
|
|
487
|
+
}
|
|
391
488
|
console.log(` ${c.bold}Expires:${c.reset} ${data.tunnel.ttlMinutes} minutes`);
|
|
489
|
+
// Show provider-specific webhook instructions
|
|
490
|
+
if (provider) {
|
|
491
|
+
console.log();
|
|
492
|
+
console.log(`${c.gray}────────────────────────────────────${c.reset}`);
|
|
493
|
+
console.log();
|
|
494
|
+
console.log(` ${c.bold}${provider.name} Setup:${c.reset}`);
|
|
495
|
+
const lines = getProviderInstructions(provider);
|
|
496
|
+
lines.forEach((line, i) => {
|
|
497
|
+
console.log(` ${c.dim}${i + 1}.${c.reset} ${line}`);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
else if (providerName) {
|
|
501
|
+
// Generic webhook instructions for unknown providers
|
|
502
|
+
console.log();
|
|
503
|
+
console.log(`${c.gray}────────────────────────────────────${c.reset}`);
|
|
504
|
+
console.log();
|
|
505
|
+
console.log(` ${c.bold}Webhook Setup:${c.reset}`);
|
|
506
|
+
console.log(` ${c.dim}1.${c.reset} Add the public URL above as your webhook endpoint in ${providerName}`);
|
|
507
|
+
console.log(` ${c.dim}2.${c.reset} Copy the signing secret from your ${providerName} dashboard`);
|
|
508
|
+
console.log(` ${c.dim}3.${c.reset} Verify webhook signatures in your local handler`);
|
|
509
|
+
}
|
|
392
510
|
console.log();
|
|
393
511
|
console.log(`${c.gray}────────────────────────────────────${c.reset}`);
|
|
394
512
|
console.log();
|
|
395
|
-
|
|
513
|
+
if (providerName) {
|
|
514
|
+
console.log(` ${c.dim}Waiting for webhooks... Press Ctrl+C to stop${c.reset}`);
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
console.log(` ${c.dim}Press Ctrl+C to stop${c.reset}`);
|
|
518
|
+
}
|
|
396
519
|
console.log();
|
|
397
520
|
// Keep connection alive and handle incoming requests
|
|
398
|
-
if (data.tunnel.type === '
|
|
521
|
+
if (data.tunnel.type === 'udp') {
|
|
522
|
+
await runUdpTunnelProxy(data.tunnel.tunnelId, wsUrl, host, port);
|
|
523
|
+
}
|
|
524
|
+
else if (data.tunnel.type === 'tcp') {
|
|
399
525
|
await runTcpTunnelProxy(data.tunnel.tunnelId, wsUrl, host, port);
|
|
400
526
|
}
|
|
401
527
|
else {
|
|
@@ -698,6 +824,106 @@ async function runTcpTunnelProxy(tunnelId, wsUrl, localHost, localPort) {
|
|
|
698
824
|
});
|
|
699
825
|
});
|
|
700
826
|
}
|
|
827
|
+
/**
|
|
828
|
+
* Run UDP tunnel proxy - forward UDP datagrams
|
|
829
|
+
*/
|
|
830
|
+
async function runUdpTunnelProxy(tunnelId, wsUrl, localHost, localPort) {
|
|
831
|
+
return new Promise((resolve) => {
|
|
832
|
+
const url = new url_1.URL(wsUrl.replace('ws://', 'http://').replace('wss://', 'https://'));
|
|
833
|
+
const baseUrl = `${url.protocol}//${url.host}`;
|
|
834
|
+
const namespace = url.pathname || '/temp-tunnel';
|
|
835
|
+
const socket = (0, socket_io_client_1.io)(`${baseUrl}${namespace}`, {
|
|
836
|
+
transports: ['websocket'],
|
|
837
|
+
reconnection: true,
|
|
838
|
+
reconnectionAttempts: 10,
|
|
839
|
+
reconnectionDelay: 1000,
|
|
840
|
+
});
|
|
841
|
+
// Create local UDP socket for forwarding to local service
|
|
842
|
+
const localUdpSocket = dgram.createSocket('udp4');
|
|
843
|
+
// Track UDP "sessions" by remote address for response routing
|
|
844
|
+
// sessionId -> { remoteInfo from server }
|
|
845
|
+
const sessions = new Map();
|
|
846
|
+
let packetCount = 0;
|
|
847
|
+
socket.on('connect', () => {
|
|
848
|
+
socket.emit('register', { tunnelId }, (response) => {
|
|
849
|
+
if (!response.success) {
|
|
850
|
+
console.log(` ${c.red}Failed to register: ${response.error}${c.reset}`);
|
|
851
|
+
socket.disconnect();
|
|
852
|
+
resolve();
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
socket.on('disconnect', (reason) => {
|
|
857
|
+
localUdpSocket.close();
|
|
858
|
+
if (reason === 'io server disconnect') {
|
|
859
|
+
console.log(` ${c.yellow}Tunnel expired or closed by server${c.reset}`);
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
socket.on('tunnel_expired', () => {
|
|
863
|
+
console.log();
|
|
864
|
+
console.log(` ${c.yellow}Tunnel expired${c.reset}`);
|
|
865
|
+
console.log();
|
|
866
|
+
localUdpSocket.close();
|
|
867
|
+
socket.disconnect();
|
|
868
|
+
resolve();
|
|
869
|
+
});
|
|
870
|
+
socket.on('server_shutdown', (data) => {
|
|
871
|
+
console.log();
|
|
872
|
+
console.log(` ${c.yellow}⚠ Server shutting down${c.reset}`);
|
|
873
|
+
if (data.message) {
|
|
874
|
+
console.log(` ${c.dim}${data.message}${c.reset}`);
|
|
875
|
+
}
|
|
876
|
+
if (data.reconnectIn) {
|
|
877
|
+
console.log(` ${c.dim}Reconnect in ${data.reconnectIn} seconds...${c.reset}`);
|
|
878
|
+
}
|
|
879
|
+
console.log();
|
|
880
|
+
});
|
|
881
|
+
// Handle incoming UDP datagram from server (from remote client)
|
|
882
|
+
socket.on('udp_datagram', (data) => {
|
|
883
|
+
packetCount++;
|
|
884
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
885
|
+
console.log(` ${c.gray}[${timestamp}]${c.reset} ${c.cyan}UDP${c.reset} ← ${data.remoteAddress}:${data.remotePort} (${Buffer.from(data.data, 'base64').length} bytes)`);
|
|
886
|
+
// Store session info for response routing
|
|
887
|
+
sessions.set(data.sessionId, { address: data.remoteAddress, port: data.remotePort });
|
|
888
|
+
// Forward to local UDP service
|
|
889
|
+
const buffer = Buffer.from(data.data, 'base64');
|
|
890
|
+
localUdpSocket.send(buffer, localPort, localHost, (err) => {
|
|
891
|
+
if (err) {
|
|
892
|
+
console.log(` ${c.red}UDP send error: ${err.message}${c.reset}`);
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
});
|
|
896
|
+
// Handle response from local UDP service
|
|
897
|
+
localUdpSocket.on('message', (msg, rinfo) => {
|
|
898
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
899
|
+
console.log(` ${c.gray}[${timestamp}]${c.reset} ${c.cyan}UDP${c.reset} → response (${msg.length} bytes)`);
|
|
900
|
+
// Find the most recent session to send response to
|
|
901
|
+
// In practice, you might need more sophisticated session tracking
|
|
902
|
+
const lastSession = Array.from(sessions.entries()).pop();
|
|
903
|
+
if (lastSession) {
|
|
904
|
+
socket.emit('udp_response', {
|
|
905
|
+
sessionId: lastSession[0],
|
|
906
|
+
data: msg.toString('base64'),
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
localUdpSocket.on('error', (err) => {
|
|
911
|
+
console.log(` ${c.red}Local UDP socket error: ${err.message}${c.reset}`);
|
|
912
|
+
});
|
|
913
|
+
// Bind local socket to receive responses
|
|
914
|
+
localUdpSocket.bind();
|
|
915
|
+
// Handle shutdown
|
|
916
|
+
process.on('SIGINT', () => {
|
|
917
|
+
console.log();
|
|
918
|
+
console.log(` ${c.yellow}Tunnel closed${c.reset}`);
|
|
919
|
+
console.log(` ${c.gray}Handled ${packetCount} UDP packets${c.reset}`);
|
|
920
|
+
console.log();
|
|
921
|
+
localUdpSocket.close();
|
|
922
|
+
socket.disconnect();
|
|
923
|
+
resolve();
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
}
|
|
701
927
|
function parseTunnelTarget(target) {
|
|
702
928
|
// Handle just port number
|
|
703
929
|
if (/^\d+$/.test(target)) {
|
|
@@ -989,11 +1215,17 @@ else if (args[0] === 'tunnel') {
|
|
|
989
1215
|
console.error(`Usage: npx private-connect tunnel <port>`);
|
|
990
1216
|
console.error(` npx private-connect tunnel localhost:3000`);
|
|
991
1217
|
console.error(` npx private-connect tunnel 4096 --tcp`);
|
|
1218
|
+
console.error(` npx private-connect tunnel 27015 --udp`);
|
|
992
1219
|
process.exit(1);
|
|
993
1220
|
}
|
|
994
1221
|
const { host, port } = parseTunnelTarget(args[1]);
|
|
995
1222
|
const tcp = args.includes('--tcp') || args.includes('-t');
|
|
996
|
-
|
|
1223
|
+
const udp = args.includes('--udp') || args.includes('-u');
|
|
1224
|
+
if (tcp && udp) {
|
|
1225
|
+
console.error(`${c.red}Error: Cannot use both --tcp and --udp${c.reset}`);
|
|
1226
|
+
process.exit(1);
|
|
1227
|
+
}
|
|
1228
|
+
createTemporaryTunnel({ host, port, tcp, udp }).catch(console.error);
|
|
997
1229
|
}
|
|
998
1230
|
else if (args[0] === 'list' || args[0] === 'ls') {
|
|
999
1231
|
listTunnels().catch(console.error);
|
|
@@ -1018,6 +1250,12 @@ else if (args[0] === 'setup-openclaw' || args[0] === 'openclaw-setup' || args[0]
|
|
|
1018
1250
|
else if (args[0] === 'pair' || args[0] === 'qr') {
|
|
1019
1251
|
showPairingQR().catch(console.error);
|
|
1020
1252
|
}
|
|
1253
|
+
else if (!RESERVED_COMMANDS.includes(args[0]) && args[1] && /^\d+$/.test(args[1].split(':').pop() || '')) {
|
|
1254
|
+
// <product-name> <port|host:port> — webhook tunnel for a specific provider
|
|
1255
|
+
const providerName = args[0];
|
|
1256
|
+
const { host, port } = parseTunnelTarget(args[1]);
|
|
1257
|
+
createTemporaryTunnel({ host, port, provider: providerName }).catch(console.error);
|
|
1258
|
+
}
|
|
1021
1259
|
else {
|
|
1022
1260
|
// Default to test if just a target is provided
|
|
1023
1261
|
runTest(args[0]).catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "private-connect",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "Access private services by name from anywhere. No VPN setup, no firewall rules. Open source alternative to ngrok and Tailscale for service-level connectivity.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"private-connect": "./dist/index.js"
|