private-connect 0.3.1 → 0.3.4

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 CHANGED
@@ -1,50 +1,78 @@
1
1
  # private-connect
2
2
 
3
- Test connectivity to any service. No signup required.
3
+ Zero-friction connectivity tools. No signup required.
4
4
 
5
- ## Usage
5
+ ## Quick Start
6
6
 
7
7
  ```bash
8
- npx private-connect test <target>
9
- ```
8
+ # Test connectivity to any service
9
+ npx private-connect test db.internal:5432
10
10
 
11
- ## Examples
11
+ # Create a temporary public tunnel
12
+ npx private-connect tunnel 3000
13
+ ```
12
14
 
13
- ```bash
14
- # Test database connectivity
15
- npx private-connect test db.internal:5432
15
+ ## Commands
16
16
 
17
- # Test Redis
18
- npx private-connect test redis:6379
17
+ ### `test` - Test connectivity
19
18
 
20
- # Test an API
21
- npx private-connect test api.internal:8080
19
+ ```bash
20
+ npx private-connect test <target>
22
21
  ```
23
22
 
24
- ## What it checks
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
+ ```
25
29
 
26
- - DNS resolution
30
+ **What it checks:**
27
31
  - TCP connection
28
- - Port accessibility
29
32
  - TLS/SSL (if applicable)
30
- - Response time
33
+ - HTTP response (for web services)
34
+ - Latency
31
35
 
32
- ## Output
36
+ ### `tunnel` - Create a temporary tunnel
33
37
 
38
+ ```bash
39
+ npx private-connect tunnel <port>
34
40
  ```
35
- Testing db.internal:5432...
36
41
 
37
- DNS resolved (12ms)
38
- ✓ TCP connection (45ms)
39
- ✓ Port open
40
- ✓ PostgreSQL detected
42
+ Instantly expose a local service to the internet. No signup required.
41
43
 
42
- Connection successful!
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
43
65
  ```
44
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
+
45
73
  ## Need more?
46
74
 
47
- For tunneling, sharing, and accessing private services:
75
+ For permanent tunnels, sharing with teammates, and AI agent integration:
48
76
 
49
77
  ```bash
50
78
  curl -fsSL https://privateconnect.co/install.sh | bash
@@ -52,4 +80,3 @@ connect up
52
80
  ```
53
81
 
54
82
  → [privateconnect.co](https://privateconnect.co)
55
-
package/dist/index.d.ts CHANGED
@@ -2,10 +2,10 @@
2
2
  /**
3
3
  * Private Connect CLI
4
4
  *
5
- * Zero-friction connectivity testing. No signup required.
5
+ * Zero-friction connectivity testing and temporary tunnels. No signup required.
6
6
  *
7
7
  * Usage:
8
8
  * npx private-connect test vault.internal:8200
9
- * npx private-connect test https://api.example.com
9
+ * npx private-connect tunnel 3000
10
10
  */
11
11
  export {};
package/dist/index.js CHANGED
@@ -3,11 +3,11 @@
3
3
  /**
4
4
  * Private Connect CLI
5
5
  *
6
- * Zero-friction connectivity testing. No signup required.
6
+ * Zero-friction connectivity testing and temporary tunnels. No signup required.
7
7
  *
8
8
  * Usage:
9
9
  * npx private-connect test vault.internal:8200
10
- * npx private-connect test https://api.example.com
10
+ * npx private-connect tunnel 3000
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  const net = require("net");
@@ -15,6 +15,8 @@ const tls = require("tls");
15
15
  const https = require("https");
16
16
  const http = require("http");
17
17
  const url_1 = require("url");
18
+ const crypto_1 = require("crypto");
19
+ const socket_io_client_1 = require("socket.io-client");
18
20
  // Colors (no dependencies)
19
21
  const c = {
20
22
  reset: '\x1b[0m',
@@ -220,41 +222,313 @@ function printCta(success) {
220
222
  }
221
223
  function printHelp() {
222
224
  console.log(`
223
- ${c.bold}Private Connect${c.reset} - Test connectivity to any service
225
+ ${c.bold}Private Connect${c.reset} - Zero-friction connectivity tools
224
226
 
225
- ${c.bold}Usage:${c.reset}
226
- npx private-connect test <target>
227
+ ${c.bold}Commands:${c.reset}
228
+ check <target> Test connectivity to any service
229
+ test <target> Alias for check
230
+ tunnel <port> Create a temporary public tunnel
227
231
 
228
232
  ${c.bold}Examples:${c.reset}
229
233
  npx private-connect test vault.internal:8200
230
234
  npx private-connect test https://api.example.com
231
- npx private-connect test postgres.prod:5432
235
+ npx private-connect tunnel 3000
236
+ npx private-connect tunnel localhost:8080
232
237
 
233
- ${c.bold}What it checks:${c.reset}
238
+ ${c.bold}Tunnel:${c.reset}
239
+ • No signup required
240
+ • Auto-expires in 2 hours
241
+ • Get a public URL instantly
242
+
243
+ ${c.bold}Test:${c.reset}
234
244
  • TCP reachability
235
245
  • TLS validation
236
- • HTTP response (if applicable)
246
+ • HTTP response
237
247
  • Latency
238
248
 
239
- No signup. No account. Just diagnostics.
240
-
241
- ${c.dim}For full features: https://privateconnect.co${c.reset}
249
+ ${c.dim}For permanent tunnels: https://privateconnect.co${c.reset}
242
250
  `);
243
251
  }
252
+ // ─────────────────────────────────────────────────────────────────────────────
253
+ // Temporary Tunnel
254
+ // ─────────────────────────────────────────────────────────────────────────────
255
+ const HUB_URL = process.env.CONNECT_HUB_URL || 'https://api.privateconnect.co';
256
+ const TUNNEL_DOMAIN = process.env.CONNECT_TUNNEL_DOMAIN || 'tunnel.privateconnect.co';
257
+ async function createTemporaryTunnel(options) {
258
+ const { host, port, ttl = 120 } = options;
259
+ console.log();
260
+ console.log(`${c.bold}Private Connect${c.reset} - Temporary Tunnel`);
261
+ console.log(`${c.gray}────────────────────────────────────${c.reset}`);
262
+ console.log();
263
+ // Check if local service is running
264
+ process.stdout.write(` Checking ${c.cyan}${host}:${port}${c.reset}... `);
265
+ const localCheck = await testTcp(host, port, 2000);
266
+ if (!localCheck.ok) {
267
+ console.log(`${fail}`);
268
+ console.log();
269
+ console.log(` ${c.red}Cannot connect to ${host}:${port}${c.reset}`);
270
+ console.log(` ${c.gray}Make sure your service is running${c.reset}`);
271
+ console.log();
272
+ process.exit(1);
273
+ }
274
+ console.log(`${ok}`);
275
+ // Generate a temporary tunnel ID
276
+ const tunnelId = (0, crypto_1.randomBytes)(6).toString('hex');
277
+ const publicUrl = `https://${tunnelId}.${TUNNEL_DOMAIN}`;
278
+ // Request tunnel from hub
279
+ process.stdout.write(` Requesting tunnel... `);
280
+ try {
281
+ const response = await httpRequest(`${HUB_URL}/v1/tunnels/temporary`, {
282
+ method: 'POST',
283
+ headers: { 'Content-Type': 'application/json' },
284
+ body: JSON.stringify({
285
+ tunnelId,
286
+ localHost: host,
287
+ localPort: port,
288
+ ttlMinutes: ttl,
289
+ }),
290
+ });
291
+ if (!response.ok) {
292
+ console.log(`${fail}`);
293
+ console.log();
294
+ if (response.status === 503 || response.status === 404 || response.status === 501) {
295
+ console.log(` ${c.yellow}Temporary tunnels coming soon!${c.reset}`);
296
+ console.log();
297
+ console.log(` For now, use the full CLI:`);
298
+ console.log(` ${c.cyan}curl -fsSL https://privateconnect.co/install.sh | bash${c.reset}`);
299
+ console.log(` ${c.cyan}connect up && connect localhost:${port} --share${c.reset}`);
300
+ }
301
+ else {
302
+ console.log(` ${c.red}Failed to create tunnel: ${response.status}${c.reset}`);
303
+ }
304
+ console.log();
305
+ process.exit(1);
306
+ }
307
+ console.log(`${ok}`);
308
+ console.log();
309
+ console.log(`${c.gray}────────────────────────────────────${c.reset}`);
310
+ console.log();
311
+ const data = JSON.parse(response.body);
312
+ // For local dev, adjust the WS URL
313
+ let wsUrl = data.tunnel.wsUrl;
314
+ if (HUB_URL.includes('localhost')) {
315
+ wsUrl = HUB_URL.replace('http', 'ws') + '/temp-tunnel';
316
+ }
317
+ console.log();
318
+ console.log(`${c.gray}────────────────────────────────────${c.reset}`);
319
+ console.log();
320
+ console.log(` ${c.bold}Local:${c.reset} ${c.cyan}${host}:${port}${c.reset}`);
321
+ console.log(` ${c.bold}Public:${c.reset} ${c.green}${data.tunnel.publicUrl}${c.reset}`);
322
+ console.log(` ${c.bold}Expires:${c.reset} ${data.tunnel.ttlMinutes} minutes`);
323
+ console.log();
324
+ console.log(`${c.gray}────────────────────────────────────${c.reset}`);
325
+ console.log();
326
+ console.log(` ${c.dim}Press Ctrl+C to stop${c.reset}`);
327
+ console.log();
328
+ // Keep connection alive and handle incoming requests
329
+ await runTunnelProxy(data.tunnel.tunnelId, wsUrl, host, port);
330
+ }
331
+ catch (err) {
332
+ console.log(`${fail}`);
333
+ console.log();
334
+ if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
335
+ console.log(` ${c.yellow}Temporary tunnels coming soon!${c.reset}`);
336
+ console.log();
337
+ console.log(` For now, use the full CLI:`);
338
+ console.log(` ${c.cyan}curl -fsSL https://privateconnect.co/install.sh | bash${c.reset}`);
339
+ console.log(` ${c.cyan}connect up && connect localhost:${port} --share${c.reset}`);
340
+ }
341
+ else {
342
+ console.log(` ${c.red}Error: ${err.message}${c.reset}`);
343
+ }
344
+ console.log();
345
+ process.exit(1);
346
+ }
347
+ }
348
+ // Simple HTTP request helper (no dependencies)
349
+ function httpRequest(url, options) {
350
+ return new Promise((resolve, reject) => {
351
+ const parsedUrl = new url_1.URL(url);
352
+ const client = parsedUrl.protocol === 'https:' ? https : http;
353
+ const req = client.request(url, {
354
+ method: options.method || 'GET',
355
+ headers: options.headers,
356
+ }, (res) => {
357
+ let body = '';
358
+ res.on('data', chunk => body += chunk);
359
+ res.on('end', () => {
360
+ resolve({
361
+ ok: res.statusCode >= 200 && res.statusCode < 300,
362
+ status: res.statusCode,
363
+ body,
364
+ });
365
+ });
366
+ });
367
+ req.on('error', reject);
368
+ req.on('timeout', () => reject(new Error('Request timeout')));
369
+ if (options.body) {
370
+ req.write(options.body);
371
+ }
372
+ req.end();
373
+ });
374
+ }
375
+ /**
376
+ * Connect to hub via WebSocket and forward HTTP requests to local service
377
+ */
378
+ async function runTunnelProxy(tunnelId, wsUrl, localHost, localPort) {
379
+ return new Promise((resolve) => {
380
+ // Extract base URL and namespace
381
+ const url = new url_1.URL(wsUrl.replace('ws://', 'http://').replace('wss://', 'https://'));
382
+ const baseUrl = `${url.protocol}//${url.host}`;
383
+ const namespace = url.pathname || '/temp-tunnel';
384
+ const socket = (0, socket_io_client_1.io)(`${baseUrl}${namespace}`, {
385
+ transports: ['websocket'],
386
+ reconnection: true,
387
+ reconnectionAttempts: 10,
388
+ reconnectionDelay: 1000,
389
+ });
390
+ let requestCount = 0;
391
+ socket.on('connect', () => {
392
+ // Register this tunnel
393
+ socket.emit('register', { tunnelId }, (response) => {
394
+ if (!response.success) {
395
+ console.log(` ${c.red}Failed to register: ${response.error}${c.reset}`);
396
+ socket.disconnect();
397
+ resolve();
398
+ }
399
+ });
400
+ });
401
+ socket.on('disconnect', (reason) => {
402
+ if (reason === 'io server disconnect') {
403
+ console.log(` ${c.yellow}Tunnel expired or closed by server${c.reset}`);
404
+ }
405
+ });
406
+ socket.on('tunnel_expired', () => {
407
+ console.log();
408
+ console.log(` ${c.yellow}Tunnel expired${c.reset}`);
409
+ console.log();
410
+ socket.disconnect();
411
+ resolve();
412
+ });
413
+ socket.on('connect_error', (err) => {
414
+ console.log(` ${c.red}Connection error: ${err.message}${c.reset}`);
415
+ });
416
+ // Handle incoming HTTP requests from the hub
417
+ socket.on('http_request', async (data) => {
418
+ requestCount++;
419
+ const timestamp = new Date().toLocaleTimeString();
420
+ console.log(` ${c.gray}[${timestamp}]${c.reset} ${c.cyan}${data.method}${c.reset} ${data.path}`);
421
+ try {
422
+ // Forward request to local service
423
+ const response = await forwardToLocal(localHost, localPort, data);
424
+ // Send response back to hub
425
+ socket.emit('http_response', {
426
+ requestId: data.requestId,
427
+ status: response.status,
428
+ headers: response.headers,
429
+ body: response.body,
430
+ });
431
+ }
432
+ catch (err) {
433
+ // Send error response
434
+ socket.emit('http_response', {
435
+ requestId: data.requestId,
436
+ status: 502,
437
+ headers: { 'content-type': 'application/json' },
438
+ body: JSON.stringify({ error: 'Bad Gateway', message: err.message }),
439
+ });
440
+ }
441
+ });
442
+ // Handle shutdown
443
+ process.on('SIGINT', () => {
444
+ console.log();
445
+ console.log(` ${c.yellow}Tunnel closed${c.reset}`);
446
+ console.log(` ${c.gray}Handled ${requestCount} requests${c.reset}`);
447
+ console.log();
448
+ socket.disconnect();
449
+ resolve();
450
+ });
451
+ });
452
+ }
453
+ /**
454
+ * Forward an HTTP request to the local service
455
+ */
456
+ function forwardToLocal(host, port, request) {
457
+ return new Promise((resolve, reject) => {
458
+ const options = {
459
+ hostname: host,
460
+ port: port,
461
+ path: request.path,
462
+ method: request.method,
463
+ headers: { ...request.headers, host: `${host}:${port}` },
464
+ timeout: 30000,
465
+ };
466
+ const req = http.request(options, (res) => {
467
+ const chunks = [];
468
+ res.on('data', (chunk) => chunks.push(chunk));
469
+ res.on('end', () => {
470
+ const body = Buffer.concat(chunks).toString('utf-8');
471
+ const headers = {};
472
+ for (const [key, value] of Object.entries(res.headers)) {
473
+ if (typeof value === 'string') {
474
+ headers[key] = value;
475
+ }
476
+ else if (Array.isArray(value)) {
477
+ headers[key] = value.join(', ');
478
+ }
479
+ }
480
+ resolve({
481
+ status: res.statusCode || 500,
482
+ headers,
483
+ body,
484
+ });
485
+ });
486
+ });
487
+ req.on('error', reject);
488
+ req.on('timeout', () => reject(new Error('Request timeout')));
489
+ if (request.body) {
490
+ req.write(request.body);
491
+ }
492
+ req.end();
493
+ });
494
+ }
495
+ function parseTunnelTarget(target) {
496
+ // Handle just port number
497
+ if (/^\d+$/.test(target)) {
498
+ return { host: 'localhost', port: parseInt(target, 10) };
499
+ }
500
+ // Handle host:port
501
+ const parts = target.split(':');
502
+ if (parts.length === 2) {
503
+ return { host: parts[0], port: parseInt(parts[1], 10) };
504
+ }
505
+ // Default to localhost with provided port
506
+ return { host: 'localhost', port: parseInt(target, 10) || 3000 };
507
+ }
244
508
  // Main
245
509
  const args = process.argv.slice(2);
246
510
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
247
511
  printHelp();
248
512
  process.exit(0);
249
513
  }
250
- if (args[0] === 'test') {
514
+ if (args[0] === 'test' || args[0] === 'check') {
251
515
  if (!args[1]) {
252
516
  console.error(`${c.red}Error: Target required${c.reset}`);
253
- console.error(`Usage: npx private-connect test <host:port>`);
517
+ console.error(`Usage: npx private-connect ${args[0]} <host:port>`);
254
518
  process.exit(1);
255
519
  }
256
520
  runTest(args[1]).catch(console.error);
257
521
  }
522
+ else if (args[0] === 'tunnel') {
523
+ if (!args[1]) {
524
+ console.error(`${c.red}Error: Port required${c.reset}`);
525
+ console.error(`Usage: npx private-connect tunnel <port>`);
526
+ console.error(` npx private-connect tunnel localhost:3000`);
527
+ process.exit(1);
528
+ }
529
+ const { host, port } = parseTunnelTarget(args[1]);
530
+ createTemporaryTunnel({ host, port }).catch(console.error);
531
+ }
258
532
  else {
259
533
  // Default to test if just a target is provided
260
534
  runTest(args[0]).catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "private-connect",
3
- "version": "0.3.1",
3
+ "version": "0.3.4",
4
4
  "description": "Test connectivity to any service. No signup required.",
5
5
  "bin": {
6
6
  "private-connect": "./dist/index.js"
@@ -26,6 +26,9 @@
26
26
  "url": "https://github.com/treadiehq/private-connect.git",
27
27
  "directory": "packages/cli"
28
28
  },
29
+ "dependencies": {
30
+ "socket.io-client": "^4.7.0"
31
+ },
29
32
  "devDependencies": {
30
33
  "@types/node": "^20.0.0",
31
34
  "typescript": "^5.0.0"