pinggy 0.5.3 → 0.5.5

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.
@@ -3,7 +3,7 @@ import {
3
3
  IPCClient,
4
4
  SessionMode,
5
5
  ShutdownStatus
6
- } from "./chunk-5YHD6M35.js";
6
+ } from "./chunk-YTJEV27N.js";
7
7
  import {
8
8
  TunnelAlreadyRunningError,
9
9
  TunnelManager,
@@ -12,10 +12,10 @@ import {
12
12
  getVersion,
13
13
  isValidPort,
14
14
  printer_default
15
- } from "./chunk-DLNUDW6G.js";
15
+ } from "./chunk-PQSLSIDA.js";
16
16
  import {
17
17
  logger
18
- } from "./chunk-7G6SJEEA.js";
18
+ } from "./chunk-P7WNBN7F.js";
19
19
  import {
20
20
  getDaemonInfoPath,
21
21
  getDaemonLogPath
@@ -129,7 +129,7 @@ var RemoteManagementStatus = {
129
129
  };
130
130
 
131
131
  // src/remote_management/remote_schema.ts
132
- import { TunnelType } from "@pinggy/pinggy";
132
+ import { HaProxyVersion, TunnelType } from "@pinggy/pinggy";
133
133
  import { z } from "zod";
134
134
  var HeaderModificationSchema = z.object({
135
135
  key: z.string(),
@@ -209,7 +209,7 @@ var UpdateConfigSchema = z.object({
209
209
  var ForwardingEntryV2Schema = z.object({
210
210
  listenAddress: z.string().optional(),
211
211
  address: z.string(),
212
- type: z.enum([TunnelType.Http, TunnelType.Tcp, TunnelType.Udp, TunnelType.Tls, TunnelType.TlsTcp])
212
+ type: z.enum([TunnelType.Http, TunnelType.Tcp, TunnelType.Udp, TunnelType.Tls, TunnelType.TlsTcp]).default(TunnelType.Http)
213
213
  });
214
214
  var OptionalSchema = z.object({
215
215
  sniServerName: z.string().optional(),
@@ -253,7 +253,9 @@ var TunnelConfigV1Schema = z.object({
253
253
  httpsOnly: z.boolean().optional(),
254
254
  originalRequestUrl: z.boolean().optional(),
255
255
  allowPreflight: z.boolean().optional(),
256
- optional: OptionalSchema.optional()
256
+ haProxy: z.enum([HaProxyVersion.V1, HaProxyVersion.V2]).optional(),
257
+ serve: z.string().optional(),
258
+ optional: z.record(z.string(), z.unknown()).optional()
257
259
  });
258
260
  var TUNNEL_CONFIG_V1_KEYS = Object.keys(
259
261
  TunnelConfigV1Schema.shape
@@ -286,6 +288,7 @@ function pinggyOptionsToTunnelConfigV1(opts, configStoredInCli) {
286
288
  httpsOnly: opts.httpsOnly ?? false,
287
289
  originalRequestUrl: opts.originalRequestUrl ?? false,
288
290
  allowPreflight: opts.allowPreflight ?? false,
291
+ haProxy: opts.haProxy,
289
292
  optional: opts.optional || {}
290
293
  };
291
294
  }
@@ -506,11 +509,11 @@ var TunnelOperations = class {
506
509
  startPromise.catch((err) => {
507
510
  logger.error("No-wait startTunnel failed", { tunnelid, err: String(err) });
508
511
  });
509
- return this.buildPendingTunnelResponseV2(tunnelid, tunnelConfig, config, config.configId, config.name, config.optional?.serve);
512
+ return this.buildPendingTunnelResponseV2(tunnelid, tunnelConfig, config, config.configId, config.name, config.serve);
510
513
  }
511
514
  await startPromise;
512
515
  const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
513
- return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.optional?.serve);
516
+ return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.serve);
514
517
  } catch (err) {
515
518
  if (err instanceof TunnelAlreadyRunningError) {
516
519
  return this.error(ErrorCode.TunnelAlreadyRunningError, err, err.message);
@@ -532,10 +535,12 @@ var TunnelOperations = class {
532
535
  if (noWait) {
533
536
  const existing = this.tunnelManager.getManagedTunnel(config.configid);
534
537
  if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
535
- this.tunnelManager.updateConfig(updateOpts).catch((err) => {
536
- logger.error("No-wait updateConfig failed", { configid: config.configid, err: String(err) });
537
- });
538
- return this.buildPendingTunnelResponse(existing.tunnelid, existing.tunnelConfig, config.configid, existing.tunnelName, existing.serve);
538
+ if (!existing.isStopped) {
539
+ this.tunnelManager.updateConfig(updateOpts).catch((err) => {
540
+ logger.error("No-wait updateConfig failed", { configid: config.configid, err: String(err) });
541
+ });
542
+ return this.buildPendingTunnelResponse(existing.tunnelid, existing.tunnelConfig, config.configid, existing.tunnelName, existing.serve);
543
+ }
539
544
  }
540
545
  const tunnel = await this.tunnelManager.updateConfig(updateOpts);
541
546
  if (!tunnel.instance || !tunnel.tunnelConfig)
@@ -550,10 +555,12 @@ var TunnelOperations = class {
550
555
  if (noWait) {
551
556
  const existing = this.tunnelManager.getManagedTunnel(config.configId);
552
557
  if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
553
- this.tunnelManager.updateConfig(config).catch((err) => {
554
- logger.error("No-wait updateConfigV2 failed", { configId: config.configId, err: String(err) });
555
- });
556
- return this.buildPendingTunnelResponseV2(existing.tunnelid, existing.tunnelConfig, config, config.configId, existing.tunnelName, existing.serve);
558
+ if (!existing.isStopped) {
559
+ this.tunnelManager.updateConfig(config).catch((err) => {
560
+ logger.error("No-wait updateConfigV2 failed", { configId: config.configId, err: String(err) });
561
+ });
562
+ return this.buildPendingTunnelResponseV2(existing.tunnelid, existing.tunnelConfig, config, config.configId, existing.tunnelName, existing.serve);
563
+ }
557
564
  }
558
565
  const tunnel = await this.tunnelManager.updateConfig(config);
559
566
  if (!tunnel.instance || !tunnel.tunnelConfig)
@@ -700,13 +707,71 @@ import WebSocket from "ws";
700
707
 
701
708
  // src/remote_management/websocket_printer.ts
702
709
  import pico from "picocolors";
703
- var PENDING_START_TIMEOUT_MS = 5 * 60 * 1e3;
704
- var RemoteManagementWebSocketPrinter = class {
710
+ var PENDING_TIMEOUT_MS = 5 * 60 * 1e3;
711
+ var PendingTracker = class {
705
712
  constructor() {
706
- this.pendingStarts = /* @__PURE__ */ new Map();
713
+ this.entries = /* @__PURE__ */ new Map();
714
+ }
715
+ queue(entry) {
716
+ this.latestConfigId = entry.configId;
717
+ this.entries.set(entry.configId, entry);
718
+ }
719
+ /** Store an entry without claiming the latest cursor. */
720
+ upsert(entry) {
721
+ this.entries.set(entry.configId, entry);
722
+ }
723
+ get(configId) {
724
+ return this.entries.get(configId);
725
+ }
726
+ find(configId, tunnelId) {
727
+ const byConfigId = this.entries.get(configId);
728
+ if (byConfigId) {
729
+ return byConfigId;
730
+ }
731
+ if (!tunnelId) {
732
+ return void 0;
733
+ }
734
+ for (const entry of this.entries.values()) {
735
+ if (entry.tunnelId === tunnelId) {
736
+ return entry;
737
+ }
738
+ }
739
+ return void 0;
740
+ }
741
+ delete(configId) {
742
+ this.entries.delete(configId);
743
+ if (this.latestConfigId === configId) {
744
+ this.latestConfigId = void 0;
745
+ }
746
+ }
747
+ get latest() {
748
+ return this.latestConfigId;
749
+ }
750
+ get hasLatest() {
751
+ return this.entries.size > 0 && this.latestConfigId !== void 0;
752
+ }
753
+ isLatest(configId) {
754
+ return configId === this.latestConfigId;
755
+ }
756
+ cleanupExpired(onTimeout) {
757
+ const now = Date.now();
758
+ for (const [configId, entry] of this.entries) {
759
+ if (now - entry.queuedAt <= PENDING_TIMEOUT_MS) {
760
+ continue;
761
+ }
762
+ const wasLatest = this.isLatest(configId);
763
+ this.delete(configId);
764
+ onTimeout(entry, wasLatest);
765
+ }
707
766
  }
708
- setTunnelHandler(tunnelHandler) {
709
- this.tunnelHandler = tunnelHandler;
767
+ };
768
+ var RemoteManagementWebSocketPrinter = class {
769
+ constructor() {
770
+ this.pendingStarts = new PendingTracker();
771
+ this.pendingActions = new PendingTracker();
772
+ // tunnelId -> details, fed by monitorList. Stop/restart requests only carry
773
+ // a tunnelId; without this the spinner shows the raw id.
774
+ this.tunnelDetailsCache = /* @__PURE__ */ new Map();
710
775
  }
711
776
  queueStart(config) {
712
777
  this.cleanupExpiredPendingStarts();
@@ -715,24 +780,23 @@ var RemoteManagementWebSocketPrinter = class {
715
780
  configName: this.getConfigNameFromRequest(config),
716
781
  queuedAt: Date.now()
717
782
  };
718
- this.latestPendingConfigId = entry.configId;
719
- this.pendingStarts.set(entry.configId, entry);
783
+ this.pendingStarts.queue(entry);
720
784
  printer_default.startSpinner("Starting tunnel with config name: " + entry.configName);
721
785
  }
722
786
  failQueuedStart(config, reason) {
723
787
  const configId = this.getConfigIdFromRequest(config);
724
788
  const pending = this.pendingStarts.get(configId);
725
789
  const configName = pending?.configName || this.getConfigNameFromRequest(config);
790
+ const wasLatest = this.pendingStarts.isLatest(configId);
726
791
  this.pendingStarts.delete(configId);
727
- if (this.latestPendingConfigId === configId) {
728
- this.latestPendingConfigId = void 0;
792
+ if (wasLatest) {
729
793
  printer_default.stopSpinnerFail(`Failed to start tunnel with config name: ${configName}. ${reason}`);
730
794
  }
731
795
  }
732
796
  handleStartResult(config, result) {
733
797
  this.cleanupExpiredPendingStarts();
734
798
  const requestedConfigId = this.getConfigIdFromRequest(config);
735
- if (this.latestPendingConfigId && requestedConfigId !== this.latestPendingConfigId) {
799
+ if (this.pendingStarts.latest && !this.pendingStarts.isLatest(requestedConfigId)) {
736
800
  this.pendingStarts.delete(requestedConfigId);
737
801
  return;
738
802
  }
@@ -740,20 +804,20 @@ var RemoteManagementWebSocketPrinter = class {
740
804
  this.failQueuedStart(config, result.message);
741
805
  return;
742
806
  }
743
- const configId = this.getConfigIdFromTunnel(result);
744
807
  const pending = this.pendingStarts.get(requestedConfigId) || {
745
808
  configId: requestedConfigId,
746
809
  configName: this.getConfigNameFromRequest(config),
747
810
  queuedAt: Date.now()
748
811
  };
749
812
  pending.tunnelId = result.tunnelid;
750
- this.pendingStarts.set(requestedConfigId, pending);
813
+ this.pendingStarts.upsert(pending);
751
814
  if (result.remoteurls.length > 0) {
752
815
  this.completePendingStart(pending, result.remoteurls);
753
816
  }
754
817
  }
755
818
  printStopRequested(tunnelId) {
756
819
  const details = this.resolveTunnelDetails(tunnelId);
820
+ this.clearPendingAction(details.configId, tunnelId);
757
821
  printer_default.startSpinner("Stopping tunnel with config name: " + details.configName);
758
822
  }
759
823
  handleStopResult(tunnelId, result) {
@@ -767,6 +831,7 @@ var RemoteManagementWebSocketPrinter = class {
767
831
  }
768
832
  printRestartRequested(tunnelId) {
769
833
  const details = this.resolveTunnelDetails(tunnelId);
834
+ this.clearPendingAction(details.configId, tunnelId);
770
835
  printer_default.startSpinner("Restarting tunnel with config name: " + details.configName);
771
836
  }
772
837
  handleRestartResult(tunnelId, result) {
@@ -776,48 +841,101 @@ var RemoteManagementWebSocketPrinter = class {
776
841
  printer_default.stopSpinnerFail("Failed to restart tunnel with config name: " + details.configName);
777
842
  return;
778
843
  }
779
- printer_default.stopSpinnerSuccess("Restarted tunnel with config name: " + details.configName);
780
- if (result.remoteurls?.length > 0) {
781
- printer_default.info(pico.cyanBright("Remote URLs:"));
782
- (result.remoteurls ?? []).forEach(
783
- (url) => printer_default.print(" " + pico.magentaBright(url))
784
- );
844
+ this.queuePendingAction("restart", details.configId, details.configName, result.tunnelid);
845
+ }
846
+ printUpdateConfigRequested(config) {
847
+ const configName = this.getConfigNameFromRequest(config);
848
+ this.clearPendingAction(this.getConfigIdFromRequest(config));
849
+ printer_default.startSpinner("Updating tunnel configuration for: " + configName);
850
+ }
851
+ handleUpdateConfigResult(config, result) {
852
+ const configId = this.getConfigIdFromRequest(config);
853
+ const configName = this.getConfigNameFromRequest(config);
854
+ if (isErrorResponse(result)) {
855
+ printer_default.stopSpinnerFail(`Failed to update tunnel configuration for: ${configName}. ${result.message}`);
856
+ return;
785
857
  }
858
+ if (result.status.state === "exited" /* Exited */) {
859
+ printer_default.stopSpinnerSuccess(`Tunnel configuration updated: ${configName}.`);
860
+ return;
861
+ }
862
+ this.queuePendingAction("update", configId, configName, result.tunnelid);
863
+ }
864
+ clearPendingAction(configId, tunnelId) {
865
+ const entry = this.pendingActions.find(configId, tunnelId);
866
+ if (!entry) {
867
+ return;
868
+ }
869
+ this.pendingActions.delete(entry.configId);
870
+ }
871
+ queuePendingAction(action, configId, configName, tunnelId) {
872
+ this.cleanupExpiredPendingActions();
873
+ this.pendingActions.queue({ action, configId, configName, queuedAt: Date.now(), tunnelId });
874
+ }
875
+ printActionComplete(action, configName, urls) {
876
+ const message = action === "restart" ? `Restarted tunnel with config name: ${configName}` : `Tunnel configuration updated and restarted: ${configName}`;
877
+ printer_default.stopSpinnerSuccess(message);
878
+ printer_default.info(pico.cyanBright("Remote URLs:"));
879
+ urls.forEach((url) => printer_default.print(" " + pico.magentaBright(url)));
786
880
  }
787
881
  monitorList(result) {
788
882
  this.cleanupExpiredPendingStarts();
789
- if (!Array.isArray(result) || this.pendingStarts.size === 0 || !this.latestPendingConfigId) {
883
+ this.cleanupExpiredPendingActions();
884
+ if (!Array.isArray(result)) {
790
885
  return;
791
886
  }
792
887
  for (const tunnel of result) {
793
- const pending = this.findPendingStart(tunnel);
794
- if (!pending) {
795
- continue;
888
+ this.tunnelDetailsCache.set(tunnel.tunnelid, {
889
+ configId: this.getConfigIdFromTunnel(tunnel),
890
+ configName: this.getConfigNameFromTunnel(tunnel)
891
+ });
892
+ }
893
+ this.resolvePendingFromList(
894
+ result,
895
+ this.pendingStarts,
896
+ (entry, urls) => this.completePendingStart(entry, urls),
897
+ (entry, reason) => printer_default.stopSpinnerFail(`Tunnel start did not complete for config name: ${entry.configName}. ${reason}`)
898
+ );
899
+ this.resolvePendingFromList(
900
+ result,
901
+ this.pendingActions,
902
+ (entry, urls) => {
903
+ this.pendingActions.delete(entry.configId);
904
+ this.printActionComplete(entry.action, entry.configName, urls);
905
+ },
906
+ (entry, reason) => {
907
+ const verb = entry.action === "restart" ? "Restart" : "Configuration update";
908
+ printer_default.stopSpinnerFail(`${verb} did not complete for config name: ${entry.configName}. ${reason}`);
796
909
  }
797
- if (pending.configId !== this.latestPendingConfigId) {
910
+ );
911
+ }
912
+ resolvePendingFromList(tunnels, tracker, onComplete, onExited) {
913
+ if (!tracker.hasLatest) {
914
+ return;
915
+ }
916
+ for (const tunnel of tunnels) {
917
+ const entry = tracker.find(this.getConfigIdFromTunnel(tunnel), tunnel.tunnelid);
918
+ if (!entry || !tracker.isLatest(entry.configId)) {
798
919
  continue;
799
920
  }
800
- pending.tunnelId = tunnel.tunnelid;
801
- this.pendingStarts.set(pending.configId, pending);
921
+ entry.tunnelId = tunnel.tunnelid;
922
+ tracker.upsert(entry);
802
923
  if (tunnel.remoteurls.length > 0) {
803
- this.completePendingStart(pending, tunnel.remoteurls);
924
+ onComplete(entry, tunnel.remoteurls);
804
925
  continue;
805
926
  }
806
927
  if (tunnel.status.state === "exited" /* Exited */) {
807
- const reason = tunnel.status.errormsg || "Tunnel exited before a public URL was assigned";
808
- this.pendingStarts.delete(pending.configId);
809
- this.latestPendingConfigId = void 0;
810
- printer_default.stopSpinnerFail(`Tunnel start did not complete for config name: ${pending.configName}. ${reason}`);
928
+ tracker.delete(entry.configId);
929
+ onExited(entry, tunnel.status.errormsg || "Tunnel exited before a public URL was assigned");
811
930
  }
812
931
  }
813
932
  }
814
933
  completePendingStart(entry, urls) {
815
- if (this.latestPendingConfigId && entry.configId !== this.latestPendingConfigId) {
934
+ if (this.pendingStarts.latest && !this.pendingStarts.isLatest(entry.configId)) {
816
935
  this.pendingStarts.delete(entry.configId);
817
936
  return;
818
937
  }
819
938
  this.pendingStarts.delete(entry.configId);
820
- this.latestPendingConfigId = void 0;
821
939
  printer_default.stopSpinnerSuccess(`Tunnel started with config name: ${entry.configName}.`);
822
940
  printer_default.info(pico.cyanBright("Remote URLs:"));
823
941
  (urls ?? []).forEach(
@@ -825,28 +943,27 @@ var RemoteManagementWebSocketPrinter = class {
825
943
  );
826
944
  }
827
945
  cleanupExpiredPendingStarts() {
828
- const now = Date.now();
829
- for (const [configId, entry] of this.pendingStarts.entries()) {
830
- if (now - entry.queuedAt <= PENDING_START_TIMEOUT_MS) {
831
- continue;
946
+ this.pendingStarts.cleanupExpired((entry, wasLatest) => {
947
+ const message = `Timed out while waiting for tunnel URL for config name: ${entry.configName}`;
948
+ if (wasLatest) {
949
+ printer_default.stopSpinnerFail(message);
950
+ } else {
951
+ printer_default.warn(message);
832
952
  }
833
- this.pendingStarts.delete(configId);
834
- printer_default.warn(`Timed out while waiting for tunnel URL for config name: ${entry.configName}`);
835
- logger.warn("Pending websocket start entry expired", { configId, tunnelId: entry.tunnelId });
836
- }
953
+ logger.warn("Pending websocket start entry expired", { configId: entry.configId, tunnelId: entry.tunnelId });
954
+ });
837
955
  }
838
- findPendingStart(tunnel) {
839
- const configId = this.getConfigIdFromTunnel(tunnel);
840
- const byConfigId = this.pendingStarts.get(configId);
841
- if (byConfigId) {
842
- return byConfigId;
843
- }
844
- for (const entry of this.pendingStarts.values()) {
845
- if (entry.tunnelId === tunnel.tunnelid) {
846
- return entry;
956
+ cleanupExpiredPendingActions() {
957
+ this.pendingActions.cleanupExpired((entry, wasLatest) => {
958
+ const verb = entry.action === "restart" ? "restart" : "configuration update";
959
+ const message = `Timed out while waiting for tunnel URL after ${verb} for config name: ${entry.configName}`;
960
+ if (wasLatest) {
961
+ printer_default.stopSpinnerFail(message);
962
+ } else {
963
+ printer_default.warn(message);
847
964
  }
848
- }
849
- return void 0;
965
+ logger.warn("Pending websocket action entry expired", { configId: entry.configId, tunnelId: entry.tunnelId, action: entry.action });
966
+ });
850
967
  }
851
968
  resolveTunnelDetails(tunnelId, result) {
852
969
  if (result && !isErrorResponse(result)) {
@@ -855,7 +972,7 @@ var RemoteManagementWebSocketPrinter = class {
855
972
  configName: this.getConfigNameFromTunnel(result)
856
973
  };
857
974
  }
858
- return {
975
+ return this.tunnelDetailsCache.get(tunnelId) ?? {
859
976
  configId: tunnelId,
860
977
  configName: tunnelId
861
978
  };
@@ -892,7 +1009,6 @@ var WsCommand = {
892
1009
  var WebSocketCommandHandler = class {
893
1010
  constructor(handler) {
894
1011
  this.tunnelHandler = handler ?? new TunnelOperations();
895
- remoteManagementWebSocketPrinter.setTunnelHandler(this.tunnelHandler);
896
1012
  }
897
1013
  safeParse(text) {
898
1014
  if (!text) return void 0;
@@ -959,8 +1075,10 @@ var WebSocketCommandHandler = class {
959
1075
  }
960
1076
  }
961
1077
  async handleStopReq(req, raw) {
1078
+ let tunnelID;
962
1079
  try {
963
1080
  const dc = StopSchema.parse(raw);
1081
+ tunnelID = dc.tunnelID;
964
1082
  remoteManagementWebSocketPrinter.printStopRequested(dc.tunnelID);
965
1083
  const result = await this.tunnelHandler.handleStop(dc.tunnelID);
966
1084
  remoteManagementWebSocketPrinter.handleStopResult(dc.tunnelID, result);
@@ -970,6 +1088,9 @@ var WebSocketCommandHandler = class {
970
1088
  printer_default.warn("Validation failed for stop request");
971
1089
  return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
972
1090
  }
1091
+ if (tunnelID) {
1092
+ remoteManagementWebSocketPrinter.handleStopResult(tunnelID, newErrorResponse({ code: ErrorCode.InternalServerError, message: errorMessage(e) }));
1093
+ }
973
1094
  printer_default.warn(`Error in handleStopReq error: ${String(e)}`);
974
1095
  return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
975
1096
  }
@@ -989,8 +1110,10 @@ var WebSocketCommandHandler = class {
989
1110
  }
990
1111
  }
991
1112
  async handleRestartReq(req, raw) {
1113
+ let tunnelID;
992
1114
  try {
993
1115
  const dc = RestartSchema.parse(raw);
1116
+ tunnelID = dc.tunnelID;
994
1117
  remoteManagementWebSocketPrinter.printRestartRequested(dc.tunnelID);
995
1118
  const result = await this.tunnelHandler.handleRestart(dc.tunnelID, true);
996
1119
  remoteManagementWebSocketPrinter.handleRestartResult(dc.tunnelID, result);
@@ -1000,34 +1123,51 @@ var WebSocketCommandHandler = class {
1000
1123
  printer_default.warn("Validation failed for restart request");
1001
1124
  return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
1002
1125
  }
1126
+ if (tunnelID) {
1127
+ remoteManagementWebSocketPrinter.handleRestartResult(tunnelID, newErrorResponse({ code: ErrorCode.InternalServerError, message: errorMessage(e) }));
1128
+ }
1003
1129
  printer_default.warn(`Error in handleRestartReq error: ${String(e)}`);
1004
1130
  return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
1005
1131
  }
1006
1132
  }
1007
1133
  async handleUpdateConfigReq(req, raw) {
1134
+ let parsedConfig;
1008
1135
  try {
1009
1136
  const dc = UpdateConfigSchema.parse(raw);
1137
+ parsedConfig = dc.tunnelConfig;
1138
+ remoteManagementWebSocketPrinter.printUpdateConfigRequested(dc.tunnelConfig);
1010
1139
  const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig, true);
1140
+ remoteManagementWebSocketPrinter.handleUpdateConfigResult(dc.tunnelConfig, result);
1011
1141
  return this.wrapResponse(result, req);
1012
1142
  } catch (e) {
1013
1143
  if (e instanceof z2.ZodError) {
1014
1144
  printer_default.warn("Validation failed for updateconfig request");
1015
1145
  return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
1016
1146
  }
1147
+ if (parsedConfig) {
1148
+ remoteManagementWebSocketPrinter.handleUpdateConfigResult(parsedConfig, newErrorResponse({ code: ErrorCode.InternalServerError, message: errorMessage(e) }));
1149
+ }
1017
1150
  printer_default.warn(`Error in handleUpdateConfigReq error: ${String(e)}`);
1018
1151
  return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
1019
1152
  }
1020
1153
  }
1021
1154
  async handleUpdateConfigV2Req(req, raw) {
1155
+ let parsedConfig;
1022
1156
  try {
1023
1157
  const dc = UpdateConfigV2Schema.parse(raw);
1158
+ parsedConfig = dc.tunnelConfig;
1159
+ remoteManagementWebSocketPrinter.printUpdateConfigRequested(dc.tunnelConfig);
1024
1160
  const result = await this.tunnelHandler.handleUpdateConfigV2(dc.tunnelConfig, true);
1161
+ remoteManagementWebSocketPrinter.handleUpdateConfigResult(dc.tunnelConfig, result);
1025
1162
  return this.wrapResponse(result, req);
1026
1163
  } catch (e) {
1027
1164
  if (e instanceof z2.ZodError) {
1028
1165
  printer_default.warn("Validation failed for update-config-v2 request");
1029
1166
  return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
1030
1167
  }
1168
+ if (parsedConfig) {
1169
+ remoteManagementWebSocketPrinter.handleUpdateConfigResult(parsedConfig, newErrorResponse({ code: ErrorCode.InternalServerError, message: errorMessage(e) }));
1170
+ }
1031
1171
  printer_default.warn(`Error in handleUpdateConfigV2Req error: ${String(e)}`);
1032
1172
  return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
1033
1173
  }
@@ -1507,7 +1647,7 @@ async function ensureDaemonRunning() {
1507
1647
  const existing = getDaemonInfo();
1508
1648
  if (existing) return existing;
1509
1649
  if (process.versions.electron) {
1510
- const { runDaemonChild } = await import("./daemonChild-WIRWKBKB.js");
1650
+ const { runDaemonChild } = await import("./daemonChild-ZACX7V6J.js");
1511
1651
  const handle = await runDaemonChild({
1512
1652
  installSignalHandlers: false,
1513
1653
  exitOnFailure: false,
@@ -1529,7 +1669,7 @@ function getInProcessDaemonHandle() {
1529
1669
  async function getActiveTunnelSummaries(origin = "app") {
1530
1670
  const info = getDaemonInfo();
1531
1671
  if (!info) return [];
1532
- const { IPCClient: IPCClient2 } = await import("./ipcClient-Z2RXDQUQ.js");
1672
+ const { IPCClient: IPCClient2 } = await import("./ipcClient-XRL2WUUM.js");
1533
1673
  const client = new IPCClient2(info.port, origin);
1534
1674
  let tunnels;
1535
1675
  try {
@@ -1576,7 +1716,7 @@ async function stopDaemon() {
1576
1716
  if (!info) return { ok: false, error: "No daemon is running." };
1577
1717
  let daemonErrors = [];
1578
1718
  try {
1579
- const { IPCClient: IPCClient2 } = await import("./ipcClient-Z2RXDQUQ.js");
1719
+ const { IPCClient: IPCClient2 } = await import("./ipcClient-XRL2WUUM.js");
1580
1720
  const client = new IPCClient2(info.port);
1581
1721
  const result = await client.shutdown();
1582
1722
  logger.debug("Sent shutdown command to daemon", { result });
@@ -2048,9 +2188,9 @@ var TunnelClient = class _TunnelClient {
2048
2188
  this.assertClient();
2049
2189
  return this.ipc.getTunnel(tunnelId);
2050
2190
  }
2051
- async handleRestart(tunnelId) {
2191
+ async handleRestart(tunnelId, noWait) {
2052
2192
  this.assertClient();
2053
- return this.ipc.restartTunnel(tunnelId);
2193
+ return this.ipc.restartTunnel(tunnelId, noWait);
2054
2194
  }
2055
2195
  async shutdown() {
2056
2196
  this.assertClient();
@@ -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-NBR2L6LW.js";
10
+ import "./chunk-UX64XCC2.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,