pinggy 0.5.3 → 0.5.6

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.
@@ -10,7 +10,7 @@ import {
10
10
  startDaemon,
11
11
  startRemoteManagement,
12
12
  stopDaemon
13
- } from "./chunk-GK2PQ7KX.js";
13
+ } from "./chunk-HN4YLM7U.js";
14
14
  import {
15
15
  readDaemonConfig
16
16
  } from "./chunk-MT44NAXX.js";
@@ -19,7 +19,7 @@ import {
19
19
  Route,
20
20
  SessionMode,
21
21
  ShutdownStatus
22
- } from "./chunk-5YHD6M35.js";
22
+ } from "./chunk-YTJEV27N.js";
23
23
  import {
24
24
  ConfigVerb,
25
25
  SUBCOMMANDS,
@@ -36,7 +36,7 @@ import {
36
36
  updateConfigAutoStart,
37
37
  updateTunnelConfig,
38
38
  validateName
39
- } from "./chunk-WM7I57AA.js";
39
+ } from "./chunk-YWXADADB.js";
40
40
  import {
41
41
  TunnelManager,
42
42
  detachAllTunnelLoggers,
@@ -48,14 +48,14 @@ import {
48
48
  isValidPort,
49
49
  printer_default,
50
50
  setTunnelLoggingEnabled
51
- } from "./chunk-DLNUDW6G.js";
51
+ } from "./chunk-PQSLSIDA.js";
52
52
  import {
53
53
  configureLogger,
54
54
  enablePackageLogging,
55
55
  getLogLevel,
56
56
  logger,
57
57
  setLogLevel
58
- } from "./chunk-7G6SJEEA.js";
58
+ } from "./chunk-P7WNBN7F.js";
59
59
  import {
60
60
  ensurePinggyConfigDir,
61
61
  ensurePinggyLogDir,
@@ -137,6 +137,7 @@ function printHelpMessage() {
137
137
  console.log(" x:noreverseproxy Disable built-in reverse-proxy header injection");
138
138
  console.log(" x:localservertls:host Connect to local HTTPS server with SNI");
139
139
  console.log(" x:passpreflight Pass CORS preflight requests unchanged");
140
+ console.log(" x:haproxy[:v1|:v2] Enable HAProxy PROXY protocol, TCP tunnels only (default v1)");
140
141
  console.log(" a:Key:Val Add header");
141
142
  console.log(" u:Key:Val Update header");
142
143
  console.log(" r:Key Remove header");
@@ -274,6 +275,7 @@ var defaultOptions = {
274
275
  };
275
276
 
276
277
  // src/cli/extendedOptions.ts
278
+ import { HaProxyVersion } from "@pinggy/pinggy";
277
279
  import { isIP } from "net";
278
280
  function parseExtendedOptions(options, config, localServerTls) {
279
281
  if (!options) return localServerTls;
@@ -300,10 +302,20 @@ function parseExtendedOptions(options, config, localServerTls) {
300
302
  case "fullrequesturl":
301
303
  config.originalRequestUrl = true;
302
304
  break;
305
+ case "haproxy":
306
+ case "haproxy:v1":
307
+ config.haProxy = HaProxyVersion.V1;
308
+ break;
309
+ case "haproxy:v2":
310
+ config.haProxy = HaProxyVersion.V2;
311
+ break;
303
312
  default: {
304
313
  if (value && (value.startsWith("localServerTls") || value.startsWith("localservertls"))) {
305
314
  const parts = value.split(/:(.+)/);
306
315
  localServerTls = parts[1] ? parts[1] : "";
316
+ } else if (value && value.startsWith("haproxy:")) {
317
+ printer_default.warn(`Invalid HAProxy version in "${opt}". Use x:haproxy, x:haproxy:v1 or x:haproxy:v2`);
318
+ logger.warn(`Warning: Invalid HAProxy version in "${opt}". Use x:haproxy, x:haproxy:v1 or x:haproxy:v2`);
307
319
  } else {
308
320
  printer_default.warn(`Unknown extended option "${value}"`);
309
321
  logger.warn(`Warning: Unknown extended option "${value}"`);
@@ -828,6 +840,20 @@ function hasRemoteManagement(values) {
828
840
  const token = values["remote-management"];
829
841
  return typeof token === "string" && token.trim().length > 0;
830
842
  }
843
+ function validateHaProxy(finalConfig) {
844
+ if (!finalConfig.haProxy) {
845
+ return null;
846
+ }
847
+ const forwarding = finalConfig.forwarding;
848
+ const entries = Array.isArray(forwarding) ? forwarding : [];
849
+ const primaryType = entries.length > 0 ? entries[0].type : void 0;
850
+ if (primaryType === TunnelType.Tcp) {
851
+ return null;
852
+ }
853
+ return new Error(
854
+ `HAProxy PROXY protocol (x:haproxy) can only be used with TCP tunnels, but this tunnel is "${primaryType ?? TunnelType.Http}". Make it a TCP tunnel (--type tcp, or the tcp@ server prefix) or drop x:haproxy.`
855
+ );
856
+ }
831
857
  function buildFinalConfig(values, positionals, baseConfig) {
832
858
  let token;
833
859
  let server;
@@ -890,6 +916,10 @@ function buildFinalConfig(values, positionals, baseConfig) {
890
916
  }
891
917
  normalizeForwardingToArray(finalConfig, type);
892
918
  parseArgs2(finalConfig, remainingPositionals);
919
+ const haProxyErr = validateHaProxy(finalConfig);
920
+ if (haProxyErr instanceof Error) {
921
+ throw haProxyErr;
922
+ }
893
923
  storeJson(finalConfig, saveconf);
894
924
  return finalConfig;
895
925
  }
@@ -1087,7 +1117,7 @@ async function connectTui(opts) {
1087
1117
  const exitMessage = opts.exitMessage || "Stopping tunnel...";
1088
1118
  if (!noTui && process.stdin.isTTY) {
1089
1119
  try {
1090
- const { TunnelTui } = await import("./TunnelTui-QZEWWH2H.js");
1120
+ const { TunnelTui } = await import("./TunnelTui-S35BNVPO.js");
1091
1121
  const tui = new TunnelTui({
1092
1122
  urls,
1093
1123
  greet,
@@ -1310,7 +1340,7 @@ async function startBackgroundTunnels(configs, values, positionals) {
1310
1340
  client.close();
1311
1341
  }
1312
1342
  async function startAutoStartTunnels() {
1313
- const { getAutoStartConfigs: getAutoStartConfigs2 } = await import("./configStore-HP3NXPH4.js");
1343
+ const { getAutoStartConfigs: getAutoStartConfigs2 } = await import("./configStore-2AZRILVC.js");
1314
1344
  const configs = getAutoStartConfigs2();
1315
1345
  if (configs.length === 0) {
1316
1346
  printer_default.warn("No configs marked for auto-start. Use: pinggy config auto <name>");
@@ -2635,7 +2665,7 @@ var IPCServer = class {
2635
2665
  },
2636
2666
  [Route.RestartTunnel]: async (req) => {
2637
2667
  if (!req.tunnelid) throw new Error("Missing 'tunnelid' field");
2638
- return await this.ops.handleRestart(req.tunnelid);
2668
+ return await this.ops.handleRestart(req.tunnelid, req.noWait);
2639
2669
  },
2640
2670
  [Route.UpdateConfig]: async (req) => {
2641
2671
  if (!req.config) throw new Error("Missing 'config' field");
@@ -3414,7 +3444,7 @@ async function main() {
3414
3444
  const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
3415
3445
  configureLogger(values);
3416
3446
  if (values["_daemon-child"]) {
3417
- const { runDaemonChild: runDaemonChild2 } = await import("./daemonChild-WIRWKBKB.js");
3447
+ const { runDaemonChild: runDaemonChild2 } = await import("./daemonChild-3ONQO2XI.js");
3418
3448
  await runDaemonChild2();
3419
3449
  return;
3420
3450
  }
@@ -16,7 +16,7 @@ function setLogLevel(level) {
16
16
  }
17
17
  import("./daemonConfig-G6S46GPJ.js").then(({ writeDaemonConfig }) => writeDaemonConfig({ logLevel: level })).catch(() => {
18
18
  });
19
- import("./TunnelManager-OPUMAZFX.js").then(({ TunnelManager }) => TunnelManager.getInstance().applyLogLevelToActiveTunnels(level)).catch(() => {
19
+ import("./TunnelManager-ZSVX7U3Z.js").then(({ TunnelManager }) => TunnelManager.getInstance().applyLogLevelToActiveTunnels(level)).catch(() => {
20
20
  });
21
21
  }
22
22
  var _logger = null;
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  getLogLevel,
6
6
  logger
7
- } from "./chunk-7G6SJEEA.js";
7
+ } from "./chunk-P7WNBN7F.js";
8
8
  import {
9
9
  getTunnelLogPath
10
10
  } from "./chunk-GBYF2H4H.js";
@@ -121,7 +121,7 @@ function startSpinner(name = "dots", text = "Loading") {
121
121
  }
122
122
  currentTimer = setInterval(() => {
123
123
  const frame = spinner.frames[i = ++i % spinner.frames.length];
124
- process.stdout.write(`\r${pico.cyan(frame)} ${text}`);
124
+ process.stdout.write(`\r${pico.cyan(frame)} ${text}\x1B[K`);
125
125
  }, spinner.interval);
126
126
  return () => stopSpinner();
127
127
  }
@@ -137,7 +137,7 @@ function stopSpinnerSuccess(message) {
137
137
  clearInterval(currentTimer);
138
138
  currentTimer = null;
139
139
  const finalMessage = message || currentText;
140
- process.stdout.write(`\r${pico.green("\u2714")} ${finalMessage}
140
+ process.stdout.write(`\r${pico.green("\u2714")} ${finalMessage}\x1B[K
141
141
  `);
142
142
  }
143
143
  }
@@ -146,7 +146,7 @@ function stopSpinnerFail(message) {
146
146
  clearInterval(currentTimer);
147
147
  currentTimer = null;
148
148
  const finalMessage = message || currentText;
149
- process.stdout.write(`\r${pico.red("\u2716")} ${finalMessage}
149
+ process.stdout.write(`\r${pico.red("\u2716")} ${finalMessage}\x1B[K
150
150
  `);
151
151
  }
152
152
  }
@@ -724,6 +724,29 @@ var TunnelManager = class _TunnelManager {
724
724
  }
725
725
  return Promise.reject(new Error(`Either configId or tunnelId must be provided`));
726
726
  }
727
+ /**
728
+ * Stop a tunnel instance and wait for the stop to settle, capped by a
729
+ * timeout. Failure is logged, not thrown: the caller replaces the
730
+ * instance either way and a hung stop must not block that forever.
731
+ */
732
+ async stopInstanceGuarded(managed, timeoutMs = 5e3) {
733
+ let timer;
734
+ try {
735
+ await Promise.race([
736
+ managed.instance.stop(),
737
+ new Promise((_, reject) => {
738
+ timer = setTimeout(() => reject(new Error(`Stop timed out after ${timeoutMs}ms`)), timeoutMs);
739
+ })
740
+ ]);
741
+ } catch (e) {
742
+ logger.warn("Failed to stop tunnel instance before replacement", {
743
+ tunnelId: managed.tunnelid,
744
+ error: errorMessage(e)
745
+ });
746
+ } finally {
747
+ if (timer) clearTimeout(timer);
748
+ }
749
+ }
727
750
  /**
728
751
  * Restarts a tunnel with its current configuration.
729
752
  * This function will stop the tunnel if it's running and start it again.
@@ -745,6 +768,9 @@ var TunnelManager = class _TunnelManager {
745
768
  const currentServe = existingTunnel.serve;
746
769
  const autoReconnect = existingTunnel.autoReconnect || false;
747
770
  const currentOrigin = existingTunnel.origin;
771
+ if (!existingTunnel.isStopped) {
772
+ await this.stopInstanceGuarded(existingTunnel);
773
+ }
748
774
  this.tunnelsByTunnelId.delete(tunnelid);
749
775
  this.tunnelsByConfigId.delete(existingTunnel.configId);
750
776
  this.tunnelStats.delete(tunnelid);
@@ -779,9 +805,26 @@ var TunnelManager = class _TunnelManager {
779
805
  throw new Error(`Failed to restart tunnel: ${error instanceof Error ? error.message : "Unknown error"}`);
780
806
  }
781
807
  }
808
+ /**
809
+ * Carry lifecycle state from a replaced tunnel over to its replacement:
810
+ * keep createdAt, start the replacement if the old tunnel was running,
811
+ * otherwise keep it stopped with the original timestamps.
812
+ */
813
+ async applyPreservedLifecycle(tunnel, prior) {
814
+ if (prior.createdAt) {
815
+ tunnel.createdAt = prior.createdAt;
816
+ }
817
+ if (!prior.isStopped) {
818
+ await this.startTunnel(tunnel.tunnelid);
819
+ return;
820
+ }
821
+ tunnel.isStopped = true;
822
+ tunnel.startedAt = prior.startedAt;
823
+ tunnel.stoppedAt = prior.stoppedAt ?? (/* @__PURE__ */ new Date()).toISOString();
824
+ }
782
825
  /**
783
826
  * Updates the configuration of an existing tunnel.
784
- *
827
+ *
785
828
  * This method handles the process of updating a tunnel's configuration while preserving
786
829
  * its state. If the tunnel is running, it will be stopped, updated, and restarted.
787
830
  * In case of failure, it attempts to restore the original configuration.
@@ -808,10 +851,13 @@ var TunnelManager = class _TunnelManager {
808
851
  const currentServe = existingTunnel.serve;
809
852
  const currentAutoReconnect = existingTunnel.autoReconnect || false;
810
853
  const currentOrigin = existingTunnel.origin;
854
+ const currentCreatedAt = existingTunnel.createdAt;
855
+ const currentStartedAt = existingTunnel.startedAt;
856
+ const currentStoppedAt = existingTunnel.stoppedAt;
811
857
  const requestedServe = this.resolveServePath(newConfig);
812
858
  try {
813
859
  if (!isStopped) {
814
- void existingTunnel.instance.stop();
860
+ await this.stopInstanceGuarded(existingTunnel);
815
861
  }
816
862
  this.tunnelsByTunnelId.delete(currentTunnelId);
817
863
  this.tunnelsByConfigId.delete(currentTunnelConfigId);
@@ -823,7 +869,6 @@ var TunnelManager = class _TunnelManager {
823
869
  };
824
870
  const effectiveServe = requestedServe !== void 0 ? requestedServe : currentServe;
825
871
  const effectiveTunnelName = newTunnelName !== void 0 ? newTunnelName : currentTunnelName;
826
- let configWithForwarding;
827
872
  const newTunnel = await this._createTunnelWithProcessedConfig({
828
873
  configId,
829
874
  tunnelid: currentTunnelId,
@@ -833,9 +878,12 @@ var TunnelManager = class _TunnelManager {
833
878
  serve: effectiveServe,
834
879
  autoReconnect: currentAutoReconnect
835
880
  });
836
- if (!isStopped) {
837
- await this.startTunnel(newTunnel.tunnelid);
838
- }
881
+ await this.applyPreservedLifecycle(newTunnel, {
882
+ isStopped,
883
+ createdAt: currentCreatedAt,
884
+ startedAt: currentStartedAt,
885
+ stoppedAt: currentStoppedAt
886
+ });
839
887
  logger.info("Tunnel configuration updated", {
840
888
  tunnelId: newTunnel.tunnelid,
841
889
  configId: newTunnel.configId,
@@ -857,9 +905,12 @@ var TunnelManager = class _TunnelManager {
857
905
  serve: currentServe,
858
906
  autoReconnect: currentAutoReconnect
859
907
  });
860
- if (!isStopped) {
861
- await this.startTunnel(originalTunnel.tunnelid);
862
- }
908
+ await this.applyPreservedLifecycle(originalTunnel, {
909
+ isStopped,
910
+ createdAt: currentCreatedAt,
911
+ startedAt: currentStartedAt,
912
+ stoppedAt: currentStoppedAt
913
+ });
863
914
  logger.warn("Restored original tunnel configuration after update failure", {
864
915
  currentTunnelId,
865
916
  error: errorMessage(error)
@@ -1476,6 +1527,9 @@ var TunnelManager = class _TunnelManager {
1476
1527
  try {
1477
1528
  logger.info("Tunnel reconnection completed", { tunnelId, urls });
1478
1529
  const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1530
+ if (managedTunnel?.isStopping || managedTunnel?.isStopped) {
1531
+ return;
1532
+ }
1479
1533
  if (managedTunnel) {
1480
1534
  managedTunnel.isStopped = false;
1481
1535
  managedTunnel.startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -62,8 +62,8 @@ var IPCClient = class {
62
62
  async stopTunnel(tunnelid) {
63
63
  return this.call(Route.StopTunnel, { tunnelid });
64
64
  }
65
- async restartTunnel(tunnelid) {
66
- return this.call(Route.RestartTunnel, { tunnelid });
65
+ async restartTunnel(tunnelid, noWait) {
66
+ return this.call(Route.RestartTunnel, { tunnelid, noWait });
67
67
  }
68
68
  // v1 operations (used by remote management via daemon)
69
69
  async startTunnelV1(config, mode, noWait) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-7G6SJEEA.js";
3
+ } from "./chunk-P7WNBN7F.js";
4
4
  import {
5
5
  ensureTunnelConfigDir,
6
6
  getTunnelConfigDir
@@ -22,8 +22,8 @@ import {
22
22
  validateName,
23
23
  validateNameForStorage,
24
24
  validateNameStrict
25
- } from "./chunk-WM7I57AA.js";
26
- import "./chunk-7G6SJEEA.js";
25
+ } from "./chunk-YWXADADB.js";
26
+ import "./chunk-P7WNBN7F.js";
27
27
  import "./chunk-GBYF2H4H.js";
28
28
  export {
29
29
  CONFIG_VERBS,
@@ -6,16 +6,16 @@ import {
6
6
  trackIPCTunnelStart,
7
7
  trackTunnelStart,
8
8
  trackTunnelStop
9
- } from "./chunk-BBPR2LJB.js";
10
- import "./chunk-GK2PQ7KX.js";
9
+ } from "./chunk-OH6FSLYF.js";
10
+ import "./chunk-HN4YLM7U.js";
11
11
  import "./chunk-MT44NAXX.js";
12
12
  import {
13
13
  DaemonHost
14
- } from "./chunk-5YHD6M35.js";
15
- import "./chunk-WM7I57AA.js";
16
- import "./chunk-DLNUDW6G.js";
14
+ } from "./chunk-YTJEV27N.js";
15
+ import "./chunk-YWXADADB.js";
16
+ import "./chunk-PQSLSIDA.js";
17
17
  import "./chunk-UB26QJ4T.js";
18
- import "./chunk-7G6SJEEA.js";
18
+ import "./chunk-P7WNBN7F.js";
19
19
  import "./chunk-GBYF2H4H.js";
20
20
  export {
21
21
  DaemonHost,