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.
Files changed (3) hide show
  1. package/README.md +53 -2
  2. package/dist/index.js +254 -16
  3. 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/w/abc12345
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 any HTTP service
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 or raw TCP (--tcp flag)
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
- console.log(`${c.bold}Private Connect${c.reset} - Temporary ${tcp ? 'TCP ' : ''}Tunnel`);
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
- const localCheck = await testTcp(host, port, 2000);
302
- if (!localCheck.ok) {
303
- console.log(`${fail}`);
304
- console.log();
305
- console.log(` ${c.red}Cannot connect to ${host}:${port}${c.reset}`);
306
- console.log(` ${c.gray}Make sure your service is running${c.reset}`);
307
- console.log();
308
- process.exit(1);
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
- console.log(` ${c.dim}Press Ctrl+C to stop${c.reset}`);
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 === 'tcp') {
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
- createTemporaryTunnel({ host, port, tcp }).catch(console.error);
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.7",
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"