opentunnel-cli 1.0.23 → 1.0.25

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 (50) hide show
  1. package/README.md +39 -2
  2. package/dist/cli/index.js +678 -84
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/client/CloudflareTunnelClient.d.ts +110 -0
  5. package/dist/client/CloudflareTunnelClient.d.ts.map +1 -0
  6. package/dist/client/CloudflareTunnelClient.js +531 -0
  7. package/dist/client/CloudflareTunnelClient.js.map +1 -0
  8. package/dist/client/NgrokClient.d.ts +18 -1
  9. package/dist/client/NgrokClient.d.ts.map +1 -1
  10. package/dist/client/NgrokClient.js +130 -4
  11. package/dist/client/NgrokClient.js.map +1 -1
  12. package/dist/client/TunnelClient.d.ts +0 -1
  13. package/dist/client/TunnelClient.d.ts.map +1 -1
  14. package/dist/client/TunnelClient.js +2 -96
  15. package/dist/client/TunnelClient.js.map +1 -1
  16. package/dist/client/index.d.ts +1 -0
  17. package/dist/client/index.d.ts.map +1 -1
  18. package/dist/client/index.js +3 -1
  19. package/dist/client/index.js.map +1 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +3 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/lib/index.d.ts +2 -0
  25. package/dist/lib/index.d.ts.map +1 -0
  26. package/dist/lib/index.js +18 -0
  27. package/dist/lib/index.js.map +1 -0
  28. package/dist/lib/pages/index.d.ts +2 -0
  29. package/dist/lib/pages/index.d.ts.map +1 -0
  30. package/dist/lib/pages/index.js +6 -0
  31. package/dist/lib/pages/index.js.map +1 -0
  32. package/dist/lib/pages/not-running.d.ts +10 -0
  33. package/dist/lib/pages/not-running.d.ts.map +1 -0
  34. package/dist/lib/pages/not-running.js +117 -0
  35. package/dist/lib/pages/not-running.js.map +1 -0
  36. package/dist/server/TunnelServer.d.ts +1 -3
  37. package/dist/server/TunnelServer.d.ts.map +1 -1
  38. package/dist/server/TunnelServer.js +7 -58
  39. package/dist/server/TunnelServer.js.map +1 -1
  40. package/dist/shared/credentials.d.ts +101 -0
  41. package/dist/shared/credentials.d.ts.map +1 -0
  42. package/dist/shared/credentials.js +302 -0
  43. package/dist/shared/credentials.js.map +1 -0
  44. package/dist/shared/ip-filter.d.ts +75 -0
  45. package/dist/shared/ip-filter.d.ts.map +1 -0
  46. package/dist/shared/ip-filter.js +203 -0
  47. package/dist/shared/ip-filter.js.map +1 -0
  48. package/dist/shared/types.d.ts +34 -0
  49. package/dist/shared/types.d.ts.map +1 -1
  50. package/package.json +6 -3
package/dist/cli/index.js CHANGED
@@ -42,6 +42,8 @@ const chalk_1 = __importDefault(require("chalk"));
42
42
  const ora_1 = __importDefault(require("ora"));
43
43
  const TunnelClient_1 = require("../client/TunnelClient");
44
44
  const NgrokClient_1 = require("../client/NgrokClient");
45
+ const CloudflareTunnelClient_1 = require("../client/CloudflareTunnelClient");
46
+ const credentials_1 = require("../shared/credentials");
45
47
  const utils_1 = require("../shared/utils");
46
48
  const yaml_1 = require("yaml");
47
49
  const fs = __importStar(require("fs"));
@@ -220,8 +222,8 @@ const program = new commander_1.Command();
220
222
  program
221
223
  .name("opentunnel")
222
224
  .alias("ot")
223
- .description("Expose local ports to the internet via custom domains or ngrok")
224
- .version("1.0.23");
225
+ .description("Expose local ports to the internet via custom domains, ngrok, or Cloudflare Tunnel")
226
+ .version("1.0.25");
225
227
  // Helper function to build WebSocket URL from domain
226
228
  // User only provides base domain (e.g., fjrg2007.com), system handles the rest
227
229
  // Note: --insecure flag only affects certificate verification, not the protocol
@@ -618,8 +620,28 @@ program
618
620
  .option("--insecure", "Skip SSL verification (for self-signed certs)")
619
621
  .option("--ngrok", "Use ngrok instead of OpenTunnel server")
620
622
  .option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
623
+ .option("--cloudflare, --cf", "Use Cloudflare Tunnel instead of OpenTunnel server")
624
+ .option("--cf-hostname <hostname>", "Custom hostname for Cloudflare Tunnel")
625
+ .option("--provider <provider>", "Tunnel provider (opentunnel, ngrok, cloudflare)")
621
626
  .action(async (port, options) => {
622
- if (options.ngrok || options.domain === "ngrok") {
627
+ // Determine provider
628
+ const provider = options.provider?.toLowerCase() ||
629
+ (options.cloudflare || options.cf ? "cloudflare" : null) ||
630
+ (options.ngrok ? "ngrok" : null);
631
+ // Cloudflare Tunnel
632
+ if (provider === "cloudflare" || provider === "cf") {
633
+ await createCloudflareTunnel({
634
+ protocol: options.https ? "https" : "http",
635
+ localHost: options.host,
636
+ localPort: parseInt(port),
637
+ hostname: options.cfHostname,
638
+ tunnelName: options.subdomain, // -n works as tunnel name for CF
639
+ noTlsVerify: options.insecure,
640
+ });
641
+ return;
642
+ }
643
+ // ngrok
644
+ if (provider === "ngrok") {
623
645
  await createNgrokTunnel({
624
646
  protocol: options.https ? "https" : "http",
625
647
  localHost: options.host,
@@ -630,8 +652,8 @@ program
630
652
  });
631
653
  return;
632
654
  }
633
- // If remote server domain provided, just connect to it
634
- if (options.domain) {
655
+ // If remote server domain provided, connect to OpenTunnel server
656
+ if (options.domain && options.domain !== "cloudflare" && options.domain !== "ngrok") {
635
657
  const { url: serverUrl } = buildServerUrl(options.domain, options.basePath);
636
658
  await createTunnel({
637
659
  protocol: options.https ? "https" : "http",
@@ -649,6 +671,8 @@ program
649
671
  console.log(chalk_1.default.gray("\nExamples:"));
650
672
  console.log(chalk_1.default.cyan(" opentunnel http 3000 -s example.com"));
651
673
  console.log(chalk_1.default.cyan(" opentunnel http 3000 --domain example.com -n myapp"));
674
+ console.log(chalk_1.default.cyan(" opentunnel http 3000 --cloudflare"));
675
+ console.log(chalk_1.default.cyan(" opentunnel http 3000 --ngrok"));
652
676
  process.exit(1);
653
677
  });
654
678
  // TCP tunnel command
@@ -664,8 +688,33 @@ program
664
688
  .option("--insecure", "Skip SSL verification (for self-signed certs)")
665
689
  .option("--ngrok", "Use ngrok instead of OpenTunnel server")
666
690
  .option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
691
+ .option("--cloudflare, --cf", "Use Cloudflare Tunnel (TCP requires named tunnel)")
692
+ .option("--cf-hostname <hostname>", "Custom hostname for Cloudflare Tunnel")
693
+ .option("--provider <provider>", "Tunnel provider (opentunnel, ngrok, cloudflare)")
667
694
  .action(async (port, options) => {
668
- if (options.ngrok || options.domain === "ngrok") {
695
+ // Determine provider
696
+ const provider = options.provider?.toLowerCase() ||
697
+ (options.cloudflare || options.cf ? "cloudflare" : null) ||
698
+ (options.ngrok ? "ngrok" : null);
699
+ // Cloudflare Tunnel - Note: TCP requires named tunnel
700
+ if (provider === "cloudflare" || provider === "cf") {
701
+ if (!options.subdomain) {
702
+ console.log(chalk_1.default.yellow("Note: Cloudflare TCP tunnels require a named tunnel."));
703
+ console.log(chalk_1.default.gray("Use -n <tunnel-name> to specify a named tunnel."));
704
+ console.log(chalk_1.default.gray("\nExample: opentunnel tcp 5432 --cf -n my-tunnel"));
705
+ }
706
+ await createCloudflareTunnel({
707
+ protocol: "tcp",
708
+ localHost: options.host,
709
+ localPort: parseInt(port),
710
+ hostname: options.cfHostname,
711
+ tunnelName: options.subdomain,
712
+ noTlsVerify: options.insecure,
713
+ });
714
+ return;
715
+ }
716
+ // ngrok
717
+ if (provider === "ngrok") {
669
718
  await createNgrokTunnel({
670
719
  protocol: "tcp",
671
720
  localHost: options.host,
@@ -676,8 +725,8 @@ program
676
725
  });
677
726
  return;
678
727
  }
679
- // If remote server domain provided, just connect to it
680
- if (options.domain) {
728
+ // If remote server domain provided, connect to OpenTunnel server
729
+ if (options.domain && options.domain !== "cloudflare" && options.domain !== "ngrok") {
681
730
  const { url: serverUrl } = buildServerUrl(options.domain, options.basePath);
682
731
  await createTunnel({
683
732
  protocol: "tcp",
@@ -695,6 +744,7 @@ program
695
744
  console.log(chalk_1.default.gray("\nExamples:"));
696
745
  console.log(chalk_1.default.cyan(" opentunnel tcp 5432 -s example.com"));
697
746
  console.log(chalk_1.default.cyan(" opentunnel tcp 5432 --domain example.com -r 15432"));
747
+ console.log(chalk_1.default.cyan(" opentunnel tcp 5432 --ngrok"));
698
748
  process.exit(1);
699
749
  });
700
750
  // Quick expose command
@@ -709,6 +759,7 @@ program
709
759
  .option("--domain <domain>", "Server domain (e.g., domain.com)")
710
760
  .option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
711
761
  .option("--ngrok", "Use ngrok instead of OpenTunnel server")
762
+ .option("--cloudflare, --cf", "Use Cloudflare Tunnel instead of OpenTunnel server")
712
763
  .action(async (port, options) => {
713
764
  const serverUrl = options.server || (options.domain
714
765
  ? `wss://${options.domain}/_tunnel`
@@ -717,26 +768,37 @@ program
717
768
  await runTunnelInBackground("expose", port, { ...options, server: serverUrl });
718
769
  return;
719
770
  }
720
- if (options.ngrok || options.server === "ngrok") {
721
- await createNgrokTunnel({
771
+ // Cloudflare Tunnel
772
+ if (options.cloudflare || options.cf || options.server === "cloudflare") {
773
+ await createCloudflareTunnel({
722
774
  protocol: options.protocol,
723
775
  localHost: "localhost",
724
776
  localPort: parseInt(port),
725
- subdomain: options.subdomain,
726
- authtoken: options.token
777
+ noTlsVerify: options.insecure,
727
778
  });
779
+ return;
728
780
  }
729
- else {
730
- await createTunnel({
781
+ // ngrok
782
+ if (options.ngrok || options.server === "ngrok") {
783
+ await createNgrokTunnel({
731
784
  protocol: options.protocol,
732
785
  localHost: "localhost",
733
786
  localPort: parseInt(port),
734
787
  subdomain: options.subdomain,
735
- serverUrl,
736
- token: options.token,
737
- insecure: options.insecure
788
+ authtoken: options.token
738
789
  });
790
+ return;
739
791
  }
792
+ // OpenTunnel server
793
+ await createTunnel({
794
+ protocol: options.protocol,
795
+ localHost: "localhost",
796
+ localPort: parseInt(port),
797
+ subdomain: options.subdomain,
798
+ serverUrl,
799
+ token: options.token,
800
+ insecure: options.insecure
801
+ });
740
802
  });
741
803
  // Server command
742
804
  program
@@ -1613,7 +1675,7 @@ program
1613
1675
  console.log(chalk_1.default.cyan(`Connecting to ${remote}...\n`));
1614
1676
  console.log(chalk_1.default.cyan(`Starting ${tunnelsToStart.length} tunnel(s)...\n`));
1615
1677
  try {
1616
- await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true);
1678
+ await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true, config);
1617
1679
  console.log(chalk_1.default.gray("\nPress Ctrl+C to stop"));
1618
1680
  // Keep running
1619
1681
  await new Promise(() => { });
@@ -1665,7 +1727,7 @@ program
1665
1727
  const serverUrl = `${wsProtocol}://localhost:${port}/_tunnel`;
1666
1728
  // Small delay to ensure server is fully ready
1667
1729
  await new Promise(resolve => setTimeout(resolve, 500));
1668
- await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true);
1730
+ await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true, config);
1669
1731
  }
1670
1732
  else if (isServerMode) {
1671
1733
  console.log(chalk_1.default.gray("\nServer ready. Waiting for connections..."));
@@ -2301,79 +2363,175 @@ async function runTunnelInBackgroundFromConfig(name, protocol, port, options) {
2301
2363
  console.log(chalk_1.default.green(` ✓ ${name}: started (PID: ${child.pid})`));
2302
2364
  }
2303
2365
  ;
2304
- async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure) {
2305
- const spinner = (0, ora_1.default)("Connecting to server...").start();
2306
- const client = new TunnelClient_1.TunnelClient({
2307
- serverUrl,
2308
- token,
2309
- reconnect: true,
2310
- silent: true,
2311
- rejectUnauthorized: !insecure,
2312
- });
2313
- try {
2314
- await client.connect();
2315
- spinner.succeed("Connected to server");
2316
- const activeTunnels = [];
2317
- for (const tunnel of tunnels) {
2318
- const tunnelSpinner = (0, ora_1.default)(`Creating tunnel: ${tunnel.name}...`).start();
2319
- try {
2320
- const { tunnelId, publicUrl } = await client.createTunnel({
2321
- protocol: tunnel.protocol,
2322
- localHost: tunnel.host || "localhost",
2323
- localPort: tunnel.port,
2324
- subdomain: tunnel.subdomain,
2325
- remotePort: tunnel.remotePort,
2326
- });
2327
- activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl });
2328
- tunnelSpinner.succeed(`${tunnel.name}: ${publicUrl}`);
2329
- }
2330
- catch (err) {
2331
- tunnelSpinner.fail(`${tunnel.name}: ${err.message}`);
2332
- }
2366
+ async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure, globalConfig) {
2367
+ // Determine global provider
2368
+ const globalProvider = globalConfig?.provider || "opentunnel";
2369
+ // Group tunnels by provider
2370
+ const tunnelsByProvider = new Map();
2371
+ for (const tunnel of tunnels) {
2372
+ const provider = tunnel.provider || globalProvider;
2373
+ if (!tunnelsByProvider.has(provider)) {
2374
+ tunnelsByProvider.set(provider, []);
2333
2375
  }
2334
- if (activeTunnels.length === 0) {
2335
- console.log(chalk_1.default.red("\nNo tunnels created"));
2336
- process.exit(1);
2376
+ tunnelsByProvider.get(provider).push(tunnel);
2377
+ }
2378
+ const activeTunnels = [];
2379
+ const clients = [];
2380
+ // Process OpenTunnel tunnels
2381
+ const opentunnelTunnels = tunnelsByProvider.get("opentunnel") || [];
2382
+ if (opentunnelTunnels.length > 0) {
2383
+ const spinner = (0, ora_1.default)("Connecting to OpenTunnel server...").start();
2384
+ const client = new TunnelClient_1.TunnelClient({
2385
+ serverUrl,
2386
+ token,
2387
+ reconnect: true,
2388
+ silent: true,
2389
+ rejectUnauthorized: !insecure,
2390
+ });
2391
+ try {
2392
+ await client.connect();
2393
+ spinner.succeed("Connected to OpenTunnel server");
2394
+ clients.push({ provider: "opentunnel", client });
2395
+ for (const tunnel of opentunnelTunnels) {
2396
+ const tunnelSpinner = (0, ora_1.default)(`Creating tunnel: ${tunnel.name}...`).start();
2397
+ try {
2398
+ const { tunnelId, publicUrl } = await client.createTunnel({
2399
+ protocol: tunnel.protocol,
2400
+ localHost: tunnel.host || "localhost",
2401
+ localPort: tunnel.port,
2402
+ subdomain: tunnel.subdomain,
2403
+ remotePort: tunnel.remotePort,
2404
+ });
2405
+ activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "opentunnel", client });
2406
+ tunnelSpinner.succeed(`${tunnel.name}: ${publicUrl}`);
2407
+ }
2408
+ catch (err) {
2409
+ tunnelSpinner.fail(`${tunnel.name}: ${err.message}`);
2410
+ }
2411
+ }
2412
+ // Handle reconnection events
2413
+ client.on("disconnected", () => {
2414
+ console.log(chalk_1.default.yellow("\n OpenTunnel disconnected, reconnecting..."));
2415
+ });
2416
+ client.on("connected", () => {
2417
+ console.log(chalk_1.default.green(" OpenTunnel reconnected!"));
2418
+ });
2337
2419
  }
2338
- console.log(chalk_1.default.cyan("\n─────────────────────────────────────────"));
2339
- console.log(chalk_1.default.green(` ${activeTunnels.length} tunnel(s) active`));
2340
- console.log(chalk_1.default.cyan("─────────────────────────────────────────\n"));
2341
- for (const t of activeTunnels) {
2342
- console.log(` ${chalk_1.default.white(t.name.padEnd(15))} ${chalk_1.default.green(t.publicUrl)}`);
2420
+ catch (err) {
2421
+ spinner.fail(`OpenTunnel connection failed: ${err.message}`);
2343
2422
  }
2344
- console.log(chalk_1.default.gray("\n Press Ctrl+C to stop all tunnels\n"));
2345
- // Keep alive with uptime counter
2346
- const startTime = Date.now();
2347
- const statsInterval = setInterval(() => {
2348
- const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
2349
- process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
2350
- }, 1000);
2351
- // Handle exit
2352
- const cleanup = async () => {
2353
- clearInterval(statsInterval);
2354
- console.log("\n");
2355
- const closeSpinner = (0, ora_1.default)("Closing tunnels...").start();
2356
- for (const t of activeTunnels) {
2357
- await client.closeTunnel(t.tunnelId);
2358
- }
2359
- await client.disconnect();
2360
- closeSpinner.succeed("All tunnels closed");
2361
- process.exit(0);
2362
- };
2363
- process.on("SIGINT", cleanup);
2364
- process.on("SIGTERM", cleanup);
2365
- // Handle reconnection
2366
- client.on("disconnected", () => {
2367
- console.log(chalk_1.default.yellow("\n Disconnected, reconnecting..."));
2423
+ }
2424
+ // Process ngrok tunnels (one at a time, ngrok limitation)
2425
+ const ngrokTunnels = tunnelsByProvider.get("ngrok") || [];
2426
+ for (const tunnel of ngrokTunnels) {
2427
+ const spinner = (0, ora_1.default)(`Creating ngrok tunnel: ${tunnel.name}...`).start();
2428
+ const ngrokToken = tunnel.ngrokToken || globalConfig?.ngrok?.token;
2429
+ const ngrokRegion = tunnel.ngrokRegion || globalConfig?.ngrok?.region || "us";
2430
+ // Merge IP access config: tunnel-specific > global security > none
2431
+ const { IpFilter } = await Promise.resolve().then(() => __importStar(require("../shared/ip-filter")));
2432
+ const ipAccess = IpFilter.mergeConfigs(globalConfig?.security?.ipAccess, tunnel.ipAccess);
2433
+ const client = new NgrokClient_1.NgrokClient({
2434
+ authtoken: ngrokToken,
2435
+ region: ngrokRegion,
2436
+ ipAccess,
2368
2437
  });
2369
- client.on("connected", () => {
2370
- console.log(chalk_1.default.green(" Reconnected!"));
2438
+ try {
2439
+ await client.connect();
2440
+ const { tunnelId, publicUrl } = await client.createTunnel({
2441
+ protocol: tunnel.protocol,
2442
+ localHost: tunnel.host || "localhost",
2443
+ localPort: tunnel.port,
2444
+ subdomain: tunnel.subdomain,
2445
+ remotePort: tunnel.remotePort,
2446
+ });
2447
+ activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "ngrok", client });
2448
+ clients.push({ provider: "ngrok", client });
2449
+ spinner.succeed(`${tunnel.name}: ${publicUrl} (ngrok)`);
2450
+ }
2451
+ catch (err) {
2452
+ spinner.fail(`${tunnel.name}: ${err.message}`);
2453
+ console.log(chalk_1.default.yellow(" Make sure ngrok is installed: https://ngrok.com/download"));
2454
+ }
2455
+ }
2456
+ // Process Cloudflare tunnels (one at a time)
2457
+ const cloudflareTunnels = tunnelsByProvider.get("cloudflare") || [];
2458
+ for (const tunnel of cloudflareTunnels) {
2459
+ const spinner = (0, ora_1.default)(`Creating Cloudflare tunnel: ${tunnel.name}...`).start();
2460
+ const cfHostname = tunnel.cfHostname || globalConfig?.cloudflare?.hostname;
2461
+ // For Cloudflare: subdomain = tunnel name
2462
+ const cfTunnelName = tunnel.subdomain || globalConfig?.cloudflare?.tunnelName;
2463
+ // Merge IP access config: tunnel-specific > global security > none
2464
+ const { IpFilter } = await Promise.resolve().then(() => __importStar(require("../shared/ip-filter")));
2465
+ const ipAccess = IpFilter.mergeConfigs(globalConfig?.security?.ipAccess, tunnel.ipAccess);
2466
+ const client = new CloudflareTunnelClient_1.CloudflareTunnelClient({
2467
+ hostname: cfHostname,
2468
+ tunnelName: cfTunnelName,
2469
+ protocol: tunnel.protocol === "https" ? "https" : "http",
2470
+ ipAccess,
2371
2471
  });
2472
+ try {
2473
+ await client.connect();
2474
+ const { tunnelId, publicUrl } = await client.createTunnel({
2475
+ protocol: tunnel.protocol,
2476
+ localHost: tunnel.host || "localhost",
2477
+ localPort: tunnel.port,
2478
+ });
2479
+ activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "cloudflare", client });
2480
+ clients.push({ provider: "cloudflare", client });
2481
+ const tunnelMode = cfTunnelName ? ` [${cfTunnelName}]` : "";
2482
+ spinner.succeed(`${tunnel.name}: ${publicUrl}${tunnelMode} (cloudflare)`);
2483
+ }
2484
+ catch (err) {
2485
+ spinner.fail(`${tunnel.name}: ${err.message}`);
2486
+ console.log(chalk_1.default.yellow(" cloudflared installs automatically. Check internet connection or try: npm rebuild cloudflared"));
2487
+ }
2372
2488
  }
2373
- catch (err) {
2374
- spinner.fail(`Failed: ${err.message}`);
2489
+ if (activeTunnels.length === 0) {
2490
+ console.log(chalk_1.default.red("\nNo tunnels created"));
2375
2491
  process.exit(1);
2376
2492
  }
2493
+ console.log(chalk_1.default.cyan("\n─────────────────────────────────────────"));
2494
+ console.log(chalk_1.default.green(` ${activeTunnels.length} tunnel(s) active`));
2495
+ console.log(chalk_1.default.cyan("─────────────────────────────────────────\n"));
2496
+ for (const t of activeTunnels) {
2497
+ const providerTag = t.provider !== "opentunnel" ? chalk_1.default.gray(` (${t.provider})`) : "";
2498
+ console.log(` ${chalk_1.default.white(t.name.padEnd(15))} ${chalk_1.default.green(t.publicUrl)}${providerTag}`);
2499
+ }
2500
+ console.log(chalk_1.default.gray("\n Press Ctrl+C to stop all tunnels\n"));
2501
+ // Keep alive with uptime counter
2502
+ const startTime = Date.now();
2503
+ const statsInterval = setInterval(() => {
2504
+ const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
2505
+ process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
2506
+ }, 1000);
2507
+ // Handle exit
2508
+ const cleanup = async () => {
2509
+ clearInterval(statsInterval);
2510
+ console.log("\n");
2511
+ const closeSpinner = (0, ora_1.default)("Closing tunnels...").start();
2512
+ for (const t of activeTunnels) {
2513
+ try {
2514
+ await t.client.closeTunnel(t.tunnelId);
2515
+ }
2516
+ catch {
2517
+ // Ignore errors during cleanup
2518
+ }
2519
+ }
2520
+ for (const { client } of clients) {
2521
+ try {
2522
+ await client.disconnect();
2523
+ }
2524
+ catch {
2525
+ // Ignore errors during cleanup
2526
+ }
2527
+ }
2528
+ closeSpinner.succeed("All tunnels closed");
2529
+ process.exit(0);
2530
+ };
2531
+ process.on("SIGINT", cleanup);
2532
+ process.on("SIGTERM", cleanup);
2533
+ // Keep process alive
2534
+ await new Promise(() => { });
2377
2535
  }
2378
2536
  ;
2379
2537
  async function runTunnelInBackground(command, port, options) {
@@ -2565,6 +2723,61 @@ async function createNgrokTunnel(options) {
2565
2723
  }
2566
2724
  }
2567
2725
  ;
2726
+ async function createCloudflareTunnel(options) {
2727
+ const tunnelType = options.tunnelName ? `named tunnel '${options.tunnelName}'` : "quick tunnel";
2728
+ const spinner = (0, ora_1.default)(`Starting Cloudflare ${tunnelType}...`).start();
2729
+ const client = new CloudflareTunnelClient_1.CloudflareTunnelClient({
2730
+ hostname: options.hostname,
2731
+ tunnelName: options.tunnelName,
2732
+ noTlsVerify: options.noTlsVerify,
2733
+ protocol: options.protocol === "https" ? "https" : "http",
2734
+ });
2735
+ try {
2736
+ await client.connect();
2737
+ spinner.text = "Creating tunnel...";
2738
+ const { tunnelId, publicUrl } = await client.createTunnel({
2739
+ protocol: options.protocol,
2740
+ localHost: options.localHost,
2741
+ localPort: options.localPort,
2742
+ });
2743
+ spinner.succeed("Tunnel established!");
2744
+ const providerInfo = options.tunnelName
2745
+ ? `Cloudflare [${options.tunnelName}]`
2746
+ : "Cloudflare";
2747
+ printTunnelInfo({
2748
+ status: "Online",
2749
+ protocol: options.protocol,
2750
+ localHost: options.localHost,
2751
+ localPort: options.localPort,
2752
+ publicUrl,
2753
+ provider: providerInfo,
2754
+ });
2755
+ // Keep alive
2756
+ const startTime = Date.now();
2757
+ const statsInterval = setInterval(() => {
2758
+ const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
2759
+ process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
2760
+ }, 1000);
2761
+ // Handle exit
2762
+ const cleanup = async () => {
2763
+ clearInterval(statsInterval);
2764
+ console.log("\n");
2765
+ spinner.start("Closing tunnel...");
2766
+ await client.closeTunnel(tunnelId);
2767
+ await client.disconnect();
2768
+ spinner.succeed("Tunnel closed");
2769
+ process.exit(0);
2770
+ };
2771
+ process.on("SIGINT", cleanup);
2772
+ process.on("SIGTERM", cleanup);
2773
+ }
2774
+ catch (err) {
2775
+ spinner.fail(`Failed: ${err.message}`);
2776
+ console.log(chalk_1.default.yellow("\ncloudflared installs automatically. Check your internet connection or try: npm rebuild cloudflared"));
2777
+ process.exit(1);
2778
+ }
2779
+ }
2780
+ ;
2568
2781
  function printTunnelInfo(info) {
2569
2782
  console.log("");
2570
2783
  console.log(chalk_1.default.cyan(` OpenTunnel ${chalk_1.default.gray(`(via ${info.provider})`)}`));
@@ -2579,5 +2792,386 @@ function printTunnelInfo(info) {
2579
2792
  console.log("");
2580
2793
  }
2581
2794
  ;
2795
+ // ============================================================================
2796
+ // Unified Provider Commands
2797
+ // ============================================================================
2798
+ // Login command - unified for all providers
2799
+ program
2800
+ .command("login <provider>")
2801
+ .description("Authenticate with a provider (cloudflare, ngrok)")
2802
+ .option("-t, --token <token>", "Authentication token (required for ngrok)")
2803
+ .action(async (provider, options) => {
2804
+ const credManager = new credentials_1.CredentialsManager();
2805
+ const normalizedProvider = provider.toLowerCase();
2806
+ if (normalizedProvider === "cloudflare" || normalizedProvider === "cf") {
2807
+ const spinner = (0, ora_1.default)("Running cloudflared login...").start();
2808
+ try {
2809
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
2810
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
2811
+ spinner.stop();
2812
+ console.log(chalk_1.default.cyan("\n Opening browser for Cloudflare authentication...\n"));
2813
+ const proc = spawn(binPath, ["login"], {
2814
+ stdio: "inherit",
2815
+ shell: process.platform === "win32",
2816
+ });
2817
+ await new Promise((resolve, reject) => {
2818
+ proc.on("close", (code) => {
2819
+ if (code === 0) {
2820
+ const os = require("os");
2821
+ const defaultCertPath = path.join(os.homedir(), ".cloudflared", "cert.pem");
2822
+ credManager.setCloudflare({ certPath: defaultCertPath });
2823
+ console.log(chalk_1.default.green("\n Cloudflare credentials saved"));
2824
+ console.log(chalk_1.default.gray(` Cert path: ${defaultCertPath}`));
2825
+ resolve();
2826
+ }
2827
+ else {
2828
+ reject(new Error(`cloudflared login exited with code ${code}`));
2829
+ }
2830
+ });
2831
+ proc.on("error", reject);
2832
+ });
2833
+ }
2834
+ catch (err) {
2835
+ spinner.fail(`Login failed: ${err.message}`);
2836
+ process.exit(1);
2837
+ }
2838
+ }
2839
+ else if (normalizedProvider === "ngrok") {
2840
+ if (!options.token) {
2841
+ console.log(chalk_1.default.red("Error: ngrok requires --token <token>"));
2842
+ console.log(chalk_1.default.gray("\nGet your token from: https://dashboard.ngrok.com/get-started/your-authtoken"));
2843
+ console.log(chalk_1.default.cyan("\nUsage: opentunnel login ngrok --token <your-token>"));
2844
+ process.exit(1);
2845
+ }
2846
+ credManager.setNgrok({ token: options.token });
2847
+ console.log(chalk_1.default.green("\n ngrok credentials saved"));
2848
+ console.log(chalk_1.default.gray(` Token: ${options.token.slice(0, 8)}...`));
2849
+ console.log(chalk_1.default.gray(` Stored at: ${credentials_1.CredentialsManager.getCredentialsFile()}\n`));
2850
+ }
2851
+ else {
2852
+ console.log(chalk_1.default.red(`Unknown provider: ${provider}`));
2853
+ console.log(chalk_1.default.gray("\nSupported providers: cloudflare (cf), ngrok"));
2854
+ process.exit(1);
2855
+ }
2856
+ });
2857
+ // Logout command - unified for all providers
2858
+ program
2859
+ .command("logout <provider>")
2860
+ .description("Remove stored credentials for a provider")
2861
+ .action(async (provider) => {
2862
+ const credManager = new credentials_1.CredentialsManager();
2863
+ const normalizedProvider = provider.toLowerCase();
2864
+ if (normalizedProvider === "cloudflare" || normalizedProvider === "cf") {
2865
+ const removed = credManager.removeProvider("cloudflare");
2866
+ if (removed) {
2867
+ console.log(chalk_1.default.green("\n Cloudflare credentials removed\n"));
2868
+ }
2869
+ else {
2870
+ console.log(chalk_1.default.yellow("\n No Cloudflare credentials found\n"));
2871
+ }
2872
+ }
2873
+ else if (normalizedProvider === "ngrok") {
2874
+ const removed = credManager.removeProvider("ngrok");
2875
+ if (removed) {
2876
+ console.log(chalk_1.default.green("\n ngrok credentials removed\n"));
2877
+ }
2878
+ else {
2879
+ console.log(chalk_1.default.yellow("\n No ngrok credentials found\n"));
2880
+ }
2881
+ }
2882
+ else {
2883
+ console.log(chalk_1.default.red(`Unknown provider: ${provider}`));
2884
+ console.log(chalk_1.default.gray("\nSupported providers: cloudflare (cf), ngrok"));
2885
+ process.exit(1);
2886
+ }
2887
+ });
2888
+ // Create tunnel command (for named tunnels)
2889
+ program
2890
+ .command("create <name>")
2891
+ .description("Create a named tunnel")
2892
+ .option("--cf, --cloudflare", "Create Cloudflare named tunnel")
2893
+ .option("--provider <provider>", "Provider (cloudflare)")
2894
+ .action(async (name, options) => {
2895
+ const provider = options.provider?.toLowerCase() ||
2896
+ (options.cloudflare || options.cf ? "cloudflare" : null);
2897
+ if (!provider) {
2898
+ console.log(chalk_1.default.red("Error: Provider required"));
2899
+ console.log(chalk_1.default.gray("\nUsage: opentunnel create <name> --cf"));
2900
+ console.log(chalk_1.default.gray(" opentunnel create <name> --provider cloudflare"));
2901
+ process.exit(1);
2902
+ }
2903
+ if (provider === "cloudflare" || provider === "cf") {
2904
+ const spinner = (0, ora_1.default)(`Creating Cloudflare tunnel '${name}'...`).start();
2905
+ try {
2906
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
2907
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
2908
+ const proc = spawn(binPath, ["tunnel", "create", name], {
2909
+ stdio: ["ignore", "pipe", "pipe"],
2910
+ shell: process.platform === "win32",
2911
+ });
2912
+ let output = "";
2913
+ let errorOutput = "";
2914
+ proc.stdout?.on("data", (data) => { output += data.toString(); });
2915
+ proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
2916
+ await new Promise((resolve, reject) => {
2917
+ proc.on("close", (code) => {
2918
+ if (code === 0) {
2919
+ spinner.succeed(`Tunnel '${name}' created`);
2920
+ if (output)
2921
+ console.log(chalk_1.default.gray(output.trim()));
2922
+ resolve();
2923
+ }
2924
+ else {
2925
+ reject(new Error(errorOutput || `Exit code ${code}`));
2926
+ }
2927
+ });
2928
+ proc.on("error", reject);
2929
+ });
2930
+ }
2931
+ catch (err) {
2932
+ spinner.fail(`Failed to create tunnel: ${err.message}`);
2933
+ if (err.message.includes("login")) {
2934
+ console.log(chalk_1.default.yellow("\nRun 'opentunnel login cloudflare' first"));
2935
+ }
2936
+ process.exit(1);
2937
+ }
2938
+ }
2939
+ else {
2940
+ console.log(chalk_1.default.red(`Provider '${provider}' doesn't support named tunnels`));
2941
+ process.exit(1);
2942
+ }
2943
+ });
2944
+ // Delete tunnel command
2945
+ program
2946
+ .command("delete <name>")
2947
+ .description("Delete a named tunnel")
2948
+ .option("--cf, --cloudflare", "Delete Cloudflare named tunnel")
2949
+ .option("--provider <provider>", "Provider (cloudflare)")
2950
+ .option("-f, --force", "Force deletion without confirmation")
2951
+ .action(async (name, options) => {
2952
+ const provider = options.provider?.toLowerCase() ||
2953
+ (options.cloudflare || options.cf ? "cloudflare" : null);
2954
+ if (!provider) {
2955
+ console.log(chalk_1.default.red("Error: Provider required"));
2956
+ console.log(chalk_1.default.gray("\nUsage: opentunnel delete <name> --cf"));
2957
+ process.exit(1);
2958
+ }
2959
+ if (provider === "cloudflare" || provider === "cf") {
2960
+ if (!options.force) {
2961
+ console.log(chalk_1.default.yellow(`\n This will delete tunnel '${name}' permanently.`));
2962
+ console.log(chalk_1.default.gray(" Use --force to skip this confirmation.\n"));
2963
+ const readline = await Promise.resolve().then(() => __importStar(require("readline")));
2964
+ const rl = readline.createInterface({
2965
+ input: process.stdin,
2966
+ output: process.stdout,
2967
+ });
2968
+ const answer = await new Promise((resolve) => {
2969
+ rl.question(" Type the tunnel name to confirm: ", resolve);
2970
+ });
2971
+ rl.close();
2972
+ if (answer !== name) {
2973
+ console.log(chalk_1.default.red("\n Cancelled: name didn't match\n"));
2974
+ return;
2975
+ }
2976
+ }
2977
+ const spinner = (0, ora_1.default)(`Deleting tunnel '${name}'...`).start();
2978
+ try {
2979
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
2980
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
2981
+ const proc = spawn(binPath, ["tunnel", "delete", name], {
2982
+ stdio: ["ignore", "pipe", "pipe"],
2983
+ shell: process.platform === "win32",
2984
+ });
2985
+ let errorOutput = "";
2986
+ proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
2987
+ await new Promise((resolve, reject) => {
2988
+ proc.on("close", (code) => {
2989
+ if (code === 0) {
2990
+ spinner.succeed(`Tunnel '${name}' deleted`);
2991
+ resolve();
2992
+ }
2993
+ else {
2994
+ reject(new Error(errorOutput || `Exit code ${code}`));
2995
+ }
2996
+ });
2997
+ proc.on("error", reject);
2998
+ });
2999
+ }
3000
+ catch (err) {
3001
+ spinner.fail(`Failed to delete tunnel: ${err.message}`);
3002
+ process.exit(1);
3003
+ }
3004
+ }
3005
+ });
3006
+ // Route DNS command (Cloudflare specific)
3007
+ program
3008
+ .command("route <name> <hostname>")
3009
+ .description("Route a hostname to a tunnel (Cloudflare DNS)")
3010
+ .option("--cf, --cloudflare", "Route via Cloudflare")
3011
+ .option("--provider <provider>", "Provider (cloudflare)")
3012
+ .action(async (name, hostname, options) => {
3013
+ const provider = options.provider?.toLowerCase() ||
3014
+ (options.cloudflare || options.cf ? "cloudflare" : "cloudflare"); // Default to cloudflare
3015
+ if (provider === "cloudflare" || provider === "cf") {
3016
+ const spinner = (0, ora_1.default)(`Routing ${hostname} to tunnel '${name}'...`).start();
3017
+ try {
3018
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
3019
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
3020
+ const proc = spawn(binPath, ["tunnel", "route", "dns", name, hostname], {
3021
+ stdio: ["ignore", "pipe", "pipe"],
3022
+ shell: process.platform === "win32",
3023
+ });
3024
+ let output = "";
3025
+ let errorOutput = "";
3026
+ proc.stdout?.on("data", (data) => { output += data.toString(); });
3027
+ proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
3028
+ await new Promise((resolve, reject) => {
3029
+ proc.on("close", (code) => {
3030
+ if (code === 0) {
3031
+ spinner.succeed(`DNS route created: ${hostname} -> ${name}`);
3032
+ if (output)
3033
+ console.log(chalk_1.default.gray(output.trim()));
3034
+ resolve();
3035
+ }
3036
+ else {
3037
+ reject(new Error(errorOutput || `Exit code ${code}`));
3038
+ }
3039
+ });
3040
+ proc.on("error", reject);
3041
+ });
3042
+ }
3043
+ catch (err) {
3044
+ spinner.fail(`Failed to create route: ${err.message}`);
3045
+ process.exit(1);
3046
+ }
3047
+ }
3048
+ });
3049
+ // List tunnels command - extend existing list command
3050
+ program
3051
+ .command("tunnels")
3052
+ .description("List named tunnels")
3053
+ .option("--cf, --cloudflare", "List Cloudflare tunnels")
3054
+ .option("--provider <provider>", "Provider (cloudflare)")
3055
+ .action(async (options) => {
3056
+ const provider = options.provider?.toLowerCase() ||
3057
+ (options.cloudflare || options.cf ? "cloudflare" : null);
3058
+ if (!provider) {
3059
+ console.log(chalk_1.default.red("Error: Provider required"));
3060
+ console.log(chalk_1.default.gray("\nUsage: opentunnel tunnels --cf"));
3061
+ process.exit(1);
3062
+ }
3063
+ if (provider === "cloudflare" || provider === "cf") {
3064
+ const spinner = (0, ora_1.default)("Listing Cloudflare tunnels...").start();
3065
+ try {
3066
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
3067
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
3068
+ const proc = spawn(binPath, ["tunnel", "list"], {
3069
+ stdio: ["ignore", "pipe", "pipe"],
3070
+ shell: process.platform === "win32",
3071
+ });
3072
+ let output = "";
3073
+ let errorOutput = "";
3074
+ proc.stdout?.on("data", (data) => { output += data.toString(); });
3075
+ proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
3076
+ await new Promise((resolve, reject) => {
3077
+ proc.on("close", (code) => {
3078
+ if (code === 0) {
3079
+ spinner.stop();
3080
+ console.log(chalk_1.default.cyan("\n Cloudflare Tunnels"));
3081
+ console.log(chalk_1.default.gray(" " + "─".repeat(60)));
3082
+ if (output.trim()) {
3083
+ console.log(output);
3084
+ }
3085
+ else {
3086
+ console.log(chalk_1.default.yellow(" No tunnels found"));
3087
+ console.log(chalk_1.default.gray("\n Create one with: opentunnel create <name> --cf"));
3088
+ }
3089
+ resolve();
3090
+ }
3091
+ else {
3092
+ reject(new Error(errorOutput || `Exit code ${code}`));
3093
+ }
3094
+ });
3095
+ proc.on("error", reject);
3096
+ });
3097
+ }
3098
+ catch (err) {
3099
+ spinner.fail(`Failed to list tunnels: ${err.message}`);
3100
+ if (err.message.includes("login")) {
3101
+ console.log(chalk_1.default.yellow("\nRun 'opentunnel login cloudflare' first"));
3102
+ }
3103
+ process.exit(1);
3104
+ }
3105
+ }
3106
+ });
3107
+ // ============================================================================
3108
+ // Config Commands
3109
+ // ============================================================================
3110
+ const configCommand = program
3111
+ .command("config")
3112
+ .description("Manage OpenTunnel configuration");
3113
+ configCommand
3114
+ .command("set <key> <value>")
3115
+ .description("Set a configuration value (e.g., ngrok.token, cloudflare.accountId)")
3116
+ .action(async (key, value) => {
3117
+ const credManager = new credentials_1.CredentialsManager();
3118
+ const validKeys = [
3119
+ "ngrok.token",
3120
+ "cloudflare.accountId",
3121
+ "cloudflare.tunnelToken",
3122
+ "cloudflare.certPath",
3123
+ ];
3124
+ if (!validKeys.includes(key)) {
3125
+ console.log(chalk_1.default.red(`Invalid key: ${key}`));
3126
+ console.log(chalk_1.default.gray("\nValid keys:"));
3127
+ for (const k of validKeys) {
3128
+ console.log(chalk_1.default.cyan(` ${k}`));
3129
+ }
3130
+ process.exit(1);
3131
+ }
3132
+ credManager.set(key, value);
3133
+ console.log(chalk_1.default.green(`\n ${key} = ${value.slice(0, 20)}${value.length > 20 ? "..." : ""}\n`));
3134
+ });
3135
+ configCommand
3136
+ .command("get <key>")
3137
+ .description("Get a configuration value")
3138
+ .action(async (key) => {
3139
+ const credManager = new credentials_1.CredentialsManager();
3140
+ const value = credManager.get(key);
3141
+ if (value) {
3142
+ // Mask sensitive values
3143
+ const masked = key.includes("token") || key.includes("Token")
3144
+ ? value.slice(0, 8) + "..." + value.slice(-4)
3145
+ : value;
3146
+ console.log(chalk_1.default.cyan(`\n ${key} = ${masked}\n`));
3147
+ }
3148
+ else {
3149
+ console.log(chalk_1.default.yellow(`\n ${key} is not set\n`));
3150
+ }
3151
+ });
3152
+ configCommand
3153
+ .command("list")
3154
+ .description("List all stored configuration")
3155
+ .action(async () => {
3156
+ const credManager = new credentials_1.CredentialsManager();
3157
+ const keys = credManager.listKeys();
3158
+ console.log(chalk_1.default.cyan("\n Stored Configuration"));
3159
+ console.log(chalk_1.default.gray(" " + "─".repeat(40)));
3160
+ if (keys.length === 0) {
3161
+ console.log(chalk_1.default.yellow(" No configuration stored"));
3162
+ console.log(chalk_1.default.gray("\n Use 'opentunnel login' or 'opentunnel config set' to add credentials"));
3163
+ }
3164
+ else {
3165
+ for (const key of keys) {
3166
+ const value = credManager.get(key);
3167
+ const masked = (key.includes("token") || key.includes("Token")) && value
3168
+ ? value.slice(0, 8) + "..." + value.slice(-4)
3169
+ : value;
3170
+ console.log(` ${chalk_1.default.white(key.padEnd(25))} ${chalk_1.default.gray(masked || "")}`);
3171
+ }
3172
+ }
3173
+ console.log(chalk_1.default.gray(" " + "─".repeat(40)));
3174
+ console.log(chalk_1.default.gray(`\n Config file: ${credentials_1.CredentialsManager.getCredentialsFile()}\n`));
3175
+ });
2582
3176
  program.parse();
2583
3177
  //# sourceMappingURL=index.js.map