private-connect 0.4.2 → 0.4.7

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 +88 -82
  2. package/dist/index.js +245 -4
  3. package/package.json +18 -6
package/README.md CHANGED
@@ -1,82 +1,88 @@
1
- # private-connect
2
-
3
- Zero-friction connectivity tools. No signup required.
4
-
5
- ## Quick Start
6
-
7
- ```bash
8
- # Test connectivity to any service
9
- npx private-connect test db.internal:5432
10
-
11
- # Create a temporary public tunnel
12
- npx private-connect tunnel 3000
13
- ```
14
-
15
- ## Commands
16
-
17
- ### `test` - Test connectivity
18
-
19
- ```bash
20
- npx private-connect test <target>
21
- ```
22
-
23
- **Examples:**
24
- ```bash
25
- npx private-connect test db.internal:5432 # Database
26
- npx private-connect test redis:6379 # Redis
27
- npx private-connect test https://api.internal # API
28
- ```
29
-
30
- **What it checks:**
31
- - TCP connection
32
- - TLS/SSL (if applicable)
33
- - HTTP response (for web services)
34
- - Latency
35
-
36
- ### `tunnel` - Create a temporary tunnel
37
-
38
- ```bash
39
- npx private-connect tunnel <port>
40
- ```
41
-
42
- Instantly expose a local service to the internet. No signup required.
43
-
44
- **Examples:**
45
- ```bash
46
- npx private-connect tunnel 3000 # Expose localhost:3000
47
- npx private-connect tunnel localhost:8080 # Specify host and port
48
- ```
49
-
50
- **Output:**
51
- ```
52
- Private Connect - Temporary Tunnel
53
- ────────────────────────────────────
54
-
55
- Local: localhost:3000
56
- Public: https://api.privateconnect.co/t/abc123
57
- Expires: 120 minutes
58
-
59
- ────────────────────────────────────
60
-
61
- Press Ctrl+C to stop
62
-
63
- [12:00:01] GET /api/users
64
- [12:00:02] POST /api/login
65
- ```
66
-
67
- **Features:**
68
- - No signup or account required
69
- - Auto-expires in 2 hours
70
- - Real-time request logging
71
- - Works with any HTTP service
72
-
73
- ## Need more?
74
-
75
- For permanent tunnels, sharing with teammates, and AI agent integration:
76
-
77
- ```bash
78
- curl -fsSL https://privateconnect.co/install.sh | bash
79
- connect up
80
- ```
81
-
82
- → [privateconnect.co](https://privateconnect.co)
1
+ # Private Connect
2
+
3
+ Zero-friction connectivity tools. No signup required.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Test connectivity to any service
9
+ npx private-connect test db.internal:5432
10
+
11
+ # Create a temporary public tunnel
12
+ npx private-connect tunnel 3000
13
+ ```
14
+
15
+ ## Commands
16
+
17
+ ### `test` - Test connectivity
18
+
19
+ ```bash
20
+ npx private-connect test <target>
21
+ ```
22
+
23
+ **Examples:**
24
+ ```bash
25
+ npx private-connect test db.internal:5432 # Database
26
+ npx private-connect test redis:6379 # Redis
27
+ npx private-connect test https://api.internal # API
28
+ ```
29
+
30
+ **What it checks:**
31
+ - TCP connection
32
+ - TLS/SSL (if applicable)
33
+ - HTTP response (for web services)
34
+ - Latency
35
+
36
+ ### `tunnel` - Create a temporary tunnel
37
+
38
+ ```bash
39
+ npx private-connect tunnel <port>
40
+ ```
41
+
42
+ Instantly expose a local service to the internet. No signup required.
43
+
44
+ **Examples:**
45
+ ```bash
46
+ npx private-connect tunnel 3000 # Expose localhost:3000
47
+ npx private-connect tunnel localhost:8080 # Specify host and port
48
+ ```
49
+
50
+ **Output:**
51
+ ```
52
+ Private Connect - Temporary Tunnel
53
+ ────────────────────────────────────
54
+
55
+ Local: localhost:3000
56
+ Public: https://privateconnect.co/w/abc12345
57
+ Anyone can access this URL
58
+ Inspector: https://privateconnect.co/debug/s-xyz789
59
+ Live traffic monitoring & request replay
60
+ Expires: 120 minutes
61
+
62
+ ────────────────────────────────────
63
+
64
+ Press Ctrl+C to stop
65
+ ```
66
+
67
+ **Sharing:**
68
+ - The public URL shows your actual website (like ngrok)
69
+ - Perfect for demos, testing, and sharing with teammates
70
+ - Works immediately - no landing page, just your app
71
+
72
+ **Features:**
73
+ - No signup or account required
74
+ - Auto-expires in 2 hours
75
+ - Real-time request logging
76
+ - Works with any HTTP service
77
+ - **Shareable URLs** - The public URL shows your actual website, perfect for demos and testing
78
+
79
+ ## Need more?
80
+
81
+ For permanent tunnels, sharing with teammates, and AI agent integration:
82
+
83
+ ```bash
84
+ curl -fsSL https://privateconnect.co/install.sh | bash
85
+ connect up
86
+ ```
87
+
88
+ → [privateconnect.co](https://privateconnect.co)
package/dist/index.js CHANGED
@@ -248,6 +248,9 @@ ${c.bold}Commands:${c.reset}
248
248
  check <target> Test connectivity to any service
249
249
  test <target> Alias for check
250
250
  tunnel <port> Create a temporary public tunnel
251
+ list List all active tunnels
252
+ close <id> Close a tunnel by ID
253
+ close --all Close all active tunnels
251
254
 
252
255
  ${c.bold}Examples:${c.reset}
253
256
  npx private-connect test vault.internal:8200
@@ -255,6 +258,8 @@ ${c.bold}Examples:${c.reset}
255
258
  npx private-connect tunnel 3000
256
259
  npx private-connect tunnel localhost:8080
257
260
  npx private-connect tunnel 4096 --tcp
261
+ npx private-connect list
262
+ npx private-connect close abc123
258
263
 
259
264
  ${c.bold}Tunnel:${c.reset}
260
265
  • No signup required
@@ -336,11 +341,41 @@ async function createTemporaryTunnel(options) {
336
341
  if (HUB_URL.includes('localhost')) {
337
342
  wsUrl = HUB_URL.replace('http', 'ws') + '/temp-tunnel';
338
343
  }
344
+ // Auto-create debug session for HTTP tunnels
345
+ let debugSession = null;
346
+ if (data.tunnel.type === 'http') {
347
+ try {
348
+ const debugResponse = await httpRequest(`${HUB_URL}/v1/tunnels/temporary/${data.tunnel.tunnelId}/debug`, {
349
+ method: 'POST',
350
+ headers: { 'Content-Type': 'application/json' },
351
+ body: JSON.stringify({ aiEnabled: false }),
352
+ });
353
+ if (debugResponse.ok) {
354
+ const debugData = JSON.parse(debugResponse.body);
355
+ debugSession = { token: debugData.session.token, url: debugData.session.url };
356
+ }
357
+ }
358
+ catch (err) {
359
+ // Silently fail - debug is optional
360
+ }
361
+ }
339
362
  console.log();
340
363
  console.log(`${c.gray}────────────────────────────────────${c.reset}`);
341
364
  console.log();
342
365
  console.log(` ${c.bold}Local:${c.reset} ${c.cyan}${host}:${port}${c.reset}`);
343
- console.log(` ${c.bold}Public:${c.reset} ${c.green}${data.tunnel.publicUrl}${c.reset}`);
366
+ // Show public URL prominently
367
+ if (data.tunnel.subdomain) {
368
+ console.log(` ${c.bold}Public:${c.reset} ${c.green}${c.bold}${data.tunnel.publicUrl}${c.reset}`);
369
+ console.log(` ${c.gray} Anyone can access this URL${c.reset}`);
370
+ }
371
+ else {
372
+ console.log(` ${c.bold}Public:${c.reset} ${c.green}${data.tunnel.publicUrl}${c.reset}`);
373
+ }
374
+ // Show debug inspector link
375
+ if (debugSession) {
376
+ console.log(` ${c.bold}Inspector:${c.reset} ${c.cyan}${debugSession.url}${c.reset}`);
377
+ console.log(` ${c.gray} Live traffic monitoring & request replay${c.reset}`);
378
+ }
344
379
  if (data.tunnel.type === 'tcp' && data.tunnel.tcpHost && data.tunnel.tcpPort) {
345
380
  console.log(` ${c.bold}Connect:${c.reset} ${c.cyan}${data.tunnel.tcpHost}:${data.tunnel.tcpPort}${c.reset}`);
346
381
  }
@@ -440,6 +475,17 @@ async function runTunnelProxy(tunnelId, wsUrl, localHost, localPort) {
440
475
  socket.disconnect();
441
476
  resolve();
442
477
  });
478
+ socket.on('server_shutdown', (data) => {
479
+ console.log();
480
+ console.log(` ${c.yellow}⚠ Server shutting down${c.reset}`);
481
+ if (data.message) {
482
+ console.log(` ${c.dim}${data.message}${c.reset}`);
483
+ }
484
+ if (data.reconnectIn) {
485
+ console.log(` ${c.dim}Reconnect in ${data.reconnectIn} seconds...${c.reset}`);
486
+ }
487
+ console.log();
488
+ });
443
489
  socket.on('connect_error', (err) => {
444
490
  console.log(` ${c.red}Connection error: ${err.message}${c.reset}`);
445
491
  });
@@ -565,15 +611,32 @@ async function runTcpTunnelProxy(tunnelId, wsUrl, localHost, localPort) {
565
611
  socket.disconnect();
566
612
  resolve();
567
613
  });
614
+ socket.on('server_shutdown', (data) => {
615
+ console.log();
616
+ console.log(` ${c.yellow}⚠ Server shutting down${c.reset}`);
617
+ if (data.message) {
618
+ console.log(` ${c.dim}${data.message}${c.reset}`);
619
+ }
620
+ if (data.reconnectIn) {
621
+ console.log(` ${c.dim}Reconnect in ${data.reconnectIn} seconds...${c.reset}`);
622
+ }
623
+ console.log();
624
+ // Close all TCP connections gracefully
625
+ for (const [, conn] of tcpConnections) {
626
+ conn.end();
627
+ }
628
+ tcpConnections.clear();
629
+ });
568
630
  // Handle TCP dial request from hub
569
631
  socket.on('tcp_dial', (data) => {
570
632
  connectionCount++;
571
633
  const timestamp = new Date().toLocaleTimeString();
572
634
  console.log(` ${c.gray}[${timestamp}]${c.reset} ${c.cyan}TCP${c.reset} connection ${data.connectionId.slice(0, 8)}`);
573
- // Connect to local service
635
+ // Connect to local service - use validated localHost/localPort, not server-provided values
636
+ // This prevents SSRF attacks where a compromised server could redirect connections
574
637
  const localSocket = net.createConnection({
575
- host: data.targetHost,
576
- port: data.targetPort,
638
+ host: localHost,
639
+ port: localPort,
577
640
  });
578
641
  tcpConnections.set(data.connectionId, localSocket);
579
642
  localSocket.on('connect', () => {
@@ -639,6 +702,167 @@ function parseTunnelTarget(target) {
639
702
  // Default to localhost with provided port
640
703
  return { host: 'localhost', port: parseInt(target, 10) || 3000 };
641
704
  }
705
+ /**
706
+ * List all active tunnels
707
+ */
708
+ async function listTunnels() {
709
+ console.log();
710
+ console.log(` ${c.cyan}${c.bold}Private Connect${c.reset} ${c.dim}Active Tunnels${c.reset}`);
711
+ console.log();
712
+ const hubUrl = process.env.CONNECT_HUB_URL || 'https://api.privateconnect.co';
713
+ try {
714
+ const response = await new Promise((resolve, reject) => {
715
+ const url = new url_1.URL(`${hubUrl}/v1/tunnels/temporary`);
716
+ const protocol = url.protocol === 'https:' ? https : http;
717
+ const req = protocol.get(url.href, (res) => {
718
+ let data = '';
719
+ res.on('data', chunk => data += chunk);
720
+ res.on('end', () => {
721
+ try {
722
+ resolve(JSON.parse(data));
723
+ }
724
+ catch {
725
+ reject(new Error('Invalid response'));
726
+ }
727
+ });
728
+ });
729
+ req.on('error', reject);
730
+ req.setTimeout(10000, () => {
731
+ req.destroy();
732
+ reject(new Error('Request timeout'));
733
+ });
734
+ });
735
+ if (response.count === 0) {
736
+ console.log(` ${c.dim}No active tunnels${c.reset}`);
737
+ }
738
+ else {
739
+ console.log(` ${c.green}${response.count}${c.reset} active tunnel${response.count > 1 ? 's' : ''}`);
740
+ console.log();
741
+ for (const tunnel of response.tunnels) {
742
+ const status = tunnel.connected ? `${c.green}connected${c.reset}` : `${c.yellow}waiting${c.reset}`;
743
+ const expiresIn = Math.max(0, Math.floor((new Date(tunnel.expiresAt).getTime() - Date.now()) / 60000));
744
+ console.log(` ${c.cyan}${tunnel.tunnelId}${c.reset}`);
745
+ console.log(` Type: ${tunnel.type} Status: ${status} Requests: ${tunnel.requestCount} Expires: ${expiresIn}m`);
746
+ if (tunnel.subdomain) {
747
+ console.log(` Subdomain: ${c.dim}${tunnel.subdomain}${c.reset}`);
748
+ }
749
+ console.log();
750
+ }
751
+ }
752
+ }
753
+ catch (err) {
754
+ console.error(` ${fail} Failed to list tunnels: ${err.message}`);
755
+ }
756
+ console.log();
757
+ }
758
+ /**
759
+ * Close a tunnel by ID
760
+ */
761
+ async function closeTunnel(tunnelId) {
762
+ console.log();
763
+ console.log(` ${c.cyan}${c.bold}Private Connect${c.reset} ${c.dim}Close Tunnel${c.reset}`);
764
+ console.log();
765
+ const hubUrl = process.env.CONNECT_HUB_URL || 'https://api.privateconnect.co';
766
+ try {
767
+ await new Promise((resolve, reject) => {
768
+ const url = new url_1.URL(`${hubUrl}/v1/tunnels/temporary/${tunnelId}`);
769
+ const protocol = url.protocol === 'https:' ? https : http;
770
+ const req = protocol.request(url.href, { method: 'DELETE' }, (res) => {
771
+ let data = '';
772
+ res.on('data', chunk => data += chunk);
773
+ res.on('end', () => {
774
+ if (res.statusCode === 200) {
775
+ resolve();
776
+ }
777
+ else if (res.statusCode === 404) {
778
+ reject(new Error('Tunnel not found or already expired'));
779
+ }
780
+ else {
781
+ reject(new Error(`Server error: ${res.statusCode}`));
782
+ }
783
+ });
784
+ });
785
+ req.on('error', reject);
786
+ req.setTimeout(10000, () => {
787
+ req.destroy();
788
+ reject(new Error('Request timeout'));
789
+ });
790
+ req.end();
791
+ });
792
+ console.log(` ${ok} Tunnel ${c.cyan}${tunnelId}${c.reset} closed`);
793
+ }
794
+ catch (err) {
795
+ console.error(` ${fail} ${err.message}`);
796
+ }
797
+ console.log();
798
+ }
799
+ /**
800
+ * Close all active tunnels
801
+ */
802
+ async function closeAllTunnels() {
803
+ console.log();
804
+ console.log(` ${c.cyan}${c.bold}Private Connect${c.reset} ${c.dim}Close All Tunnels${c.reset}`);
805
+ console.log();
806
+ const hubUrl = process.env.CONNECT_HUB_URL || 'https://api.privateconnect.co';
807
+ try {
808
+ // First list all tunnels
809
+ const response = await new Promise((resolve, reject) => {
810
+ const url = new url_1.URL(`${hubUrl}/v1/tunnels/temporary`);
811
+ const protocol = url.protocol === 'https:' ? https : http;
812
+ const req = protocol.get(url.href, (res) => {
813
+ let data = '';
814
+ res.on('data', chunk => data += chunk);
815
+ res.on('end', () => {
816
+ try {
817
+ resolve(JSON.parse(data));
818
+ }
819
+ catch {
820
+ reject(new Error('Invalid response'));
821
+ }
822
+ });
823
+ });
824
+ req.on('error', reject);
825
+ });
826
+ if (response.count === 0) {
827
+ console.log(` ${c.dim}No active tunnels to close${c.reset}`);
828
+ }
829
+ else {
830
+ let closed = 0;
831
+ for (const tunnel of response.tunnels) {
832
+ try {
833
+ await new Promise((resolve, reject) => {
834
+ const url = new url_1.URL(`${hubUrl}/v1/tunnels/temporary/${tunnel.tunnelId}`);
835
+ const protocol = url.protocol === 'https:' ? https : http;
836
+ const req = protocol.request(url.href, { method: 'DELETE' }, (res) => {
837
+ res.on('data', () => { });
838
+ res.on('end', () => {
839
+ if (res.statusCode === 200) {
840
+ resolve();
841
+ }
842
+ else {
843
+ reject(new Error(`Failed: ${res.statusCode}`));
844
+ }
845
+ });
846
+ });
847
+ req.on('error', reject);
848
+ req.end();
849
+ });
850
+ console.log(` ${ok} Closed ${c.cyan}${tunnel.tunnelId}${c.reset}`);
851
+ closed++;
852
+ }
853
+ catch (err) {
854
+ console.log(` ${fail} Failed to close ${tunnel.tunnelId}: ${err.message}`);
855
+ }
856
+ }
857
+ console.log();
858
+ console.log(` ${c.green}Closed ${closed}/${response.count} tunnels${c.reset}`);
859
+ }
860
+ }
861
+ catch (err) {
862
+ console.error(` ${fail} Failed to close tunnels: ${err.message}`);
863
+ }
864
+ console.log();
865
+ }
642
866
  // Main
643
867
  const args = process.argv.slice(2);
644
868
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
@@ -667,6 +891,23 @@ else if (args[0] === 'tunnel') {
667
891
  const tcp = args.includes('--tcp') || args.includes('-t');
668
892
  createTemporaryTunnel({ host, port, tcp }).catch(console.error);
669
893
  }
894
+ else if (args[0] === 'list' || args[0] === 'ls') {
895
+ listTunnels().catch(console.error);
896
+ }
897
+ else if (args[0] === 'close' || args[0] === 'kill') {
898
+ if (!args[1]) {
899
+ console.error(`${c.red}Error: Tunnel ID required${c.reset}`);
900
+ console.error(`Usage: npx private-connect close <tunnelId>`);
901
+ console.error(` npx private-connect close --all`);
902
+ process.exit(1);
903
+ }
904
+ if (args[1] === '--all' || args[1] === '-a') {
905
+ closeAllTunnels().catch(console.error);
906
+ }
907
+ else {
908
+ closeTunnel(args[1]).catch(console.error);
909
+ }
910
+ }
670
911
  else {
671
912
  // Default to test if just a target is provided
672
913
  runTest(args[0]).catch(console.error);
package/package.json CHANGED
@@ -1,23 +1,35 @@
1
1
  {
2
2
  "name": "private-connect",
3
- "version": "0.4.2",
4
- "description": "Test connectivity to any service. No signup required.",
3
+ "version": "0.4.7",
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"
7
7
  },
8
8
  "main": "dist/index.js",
9
9
  "files": [
10
- "dist"
10
+ "dist",
11
+ "README.md"
11
12
  ],
12
13
  "scripts": {
13
14
  "build": "tsc",
14
15
  "dev": "tsc -w"
15
16
  },
16
17
  "keywords": [
17
- "connectivity",
18
- "diagnostics",
19
- "network",
18
+ "vpn-alternative",
19
+ "ngrok-alternative",
20
+ "tailscale-alternative",
21
+ "secure-tunneling",
22
+ "private-services",
23
+ "service-connectivity",
24
+ "zero-trust",
25
+ "networking",
26
+ "developer-tools",
27
+ "cli-tool",
20
28
  "tunnel",
29
+ "connectivity",
30
+ "ssh-tunnel-alternative",
31
+ "firewall-bypass",
32
+ "service-mesh",
21
33
  "private-connect"
22
34
  ],
23
35
  "license": "FSL-1.1-MIT",