private-connect 0.3.0 → 0.3.3

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