pinggy 0.4.5 → 0.4.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.
@@ -83,7 +83,9 @@ function enablePackageLogging(opts) {
83
83
  return applyLoggingConfig(opts ?? {});
84
84
  }
85
85
  function enableLoggingByLogLevelInSdk(loglevel, logFilePath) {
86
- if (!loglevel) return;
86
+ if (!loglevel) {
87
+ return;
88
+ }
87
89
  const l = loglevel.toUpperCase();
88
90
  if (loglevel === "DEBUG") {
89
91
  pinggy.setDebugLogging(true, LogLevel.DEBUG, logFilePath);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-HUN2MRZO.js";
3
+ } from "./chunk-3RTRUYNW.js";
4
4
 
5
5
  // src/utils/printer.ts
6
6
  import pico2 from "picocolors";
@@ -66,6 +66,11 @@ var _CLIPrinter = class _CLIPrinter {
66
66
  const def = this.errorDefinitions.find((d) => d.match(err));
67
67
  const msg = def.message(err);
68
68
  console.error(pico2.red(pico2.bold("\u2716 Error:")), pico2.red(msg));
69
+ }
70
+ static fatal(err) {
71
+ const def = this.errorDefinitions.find((d) => d.match(err));
72
+ const msg = def.message(err);
73
+ console.error(pico2.red(pico2.bold("\u2716 Fatal Error:")), pico2.red(msg));
69
74
  process.exit(1);
70
75
  }
71
76
  static red(message) {
@@ -259,7 +264,9 @@ var TunnelManager = class _TunnelManager {
259
264
  */
260
265
  async startTunnel(tunnelId) {
261
266
  const managed = this.tunnelsByTunnelId.get(tunnelId);
262
- if (!managed) throw new Error(`Tunnel with id "${tunnelId}" not found`);
267
+ if (!managed) {
268
+ throw new Error(`Tunnel with id "${tunnelId}" not found`);
269
+ }
263
270
  logger.info("Starting tunnel", { tunnelId });
264
271
  let urls;
265
272
  try {
@@ -307,7 +314,9 @@ var TunnelManager = class _TunnelManager {
307
314
  */
308
315
  stopTunnel(tunnelId) {
309
316
  const managed = this.tunnelsByTunnelId.get(tunnelId);
310
- if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
317
+ if (!managed) {
318
+ throw new Error(`Tunnel "${tunnelId}" not found`);
319
+ }
311
320
  logger.info("Stopping tunnel", { tunnelId, configId: managed.configId });
312
321
  try {
313
322
  managed.instance.stop();
@@ -488,12 +497,16 @@ var TunnelManager = class _TunnelManager {
488
497
  getTunnelInstance(configId, tunnelId) {
489
498
  if (configId) {
490
499
  const managed = this.tunnelsByConfigId.get(configId);
491
- if (!managed) throw new Error(`Tunnel "${configId}" not found`);
500
+ if (!managed) {
501
+ throw new Error(`Tunnel "${configId}" not found`);
502
+ }
492
503
  return managed.instance;
493
504
  }
494
505
  if (tunnelId) {
495
506
  const managed = this.tunnelsByTunnelId.get(tunnelId);
496
- if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
507
+ if (!managed) {
508
+ throw new Error(`Tunnel "${tunnelId}" not found`);
509
+ }
497
510
  return managed.instance;
498
511
  }
499
512
  throw new Error(`Either configId or tunnelId must be provided`);
@@ -672,12 +685,16 @@ var TunnelManager = class _TunnelManager {
672
685
  getManagedTunnel(configId, tunnelId) {
673
686
  if (configId) {
674
687
  const managed = this.tunnelsByConfigId.get(configId);
675
- if (!managed) throw new Error(`Tunnel "${configId}" not found`);
688
+ if (!managed) {
689
+ throw new Error(`Tunnel "${configId}" not found`);
690
+ }
676
691
  return managed;
677
692
  }
678
693
  if (tunnelId) {
679
694
  const managed = this.tunnelsByTunnelId.get(tunnelId);
680
- if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
695
+ if (!managed) {
696
+ throw new Error(`Tunnel "${tunnelId}" not found`);
697
+ }
681
698
  return managed;
682
699
  }
683
700
  throw new Error(`Either configId or tunnelId must be provided`);
@@ -1066,7 +1083,9 @@ var TunnelManager = class _TunnelManager {
1066
1083
  notifyPollingErrorListeners(tunnelId, errorMsg) {
1067
1084
  try {
1068
1085
  const listeners = this.tunnelPollingErrorListeners.get(tunnelId);
1069
- if (!listeners) return;
1086
+ if (!listeners) {
1087
+ return;
1088
+ }
1070
1089
  for (const [id, listener] of listeners) {
1071
1090
  try {
1072
1091
  listener(tunnelId, errorMsg);
@@ -1081,7 +1100,9 @@ var TunnelManager = class _TunnelManager {
1081
1100
  notifyErrorListeners(tunnelId, errorMsg, isFatal) {
1082
1101
  try {
1083
1102
  const listeners = this.tunnelErrorListeners.get(tunnelId);
1084
- if (!listeners) return;
1103
+ if (!listeners) {
1104
+ return;
1105
+ }
1085
1106
  for (const [id, listener] of listeners) {
1086
1107
  try {
1087
1108
  listener(tunnelId, errorMsg, isFatal);
@@ -1130,7 +1151,9 @@ var TunnelManager = class _TunnelManager {
1130
1151
  managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
1131
1152
  }
1132
1153
  const listeners = this.tunnelDisconnectListeners.get(tunnelId);
1133
- if (!listeners) return;
1154
+ if (!listeners) {
1155
+ return;
1156
+ }
1134
1157
  for (const [id, listener] of listeners) {
1135
1158
  try {
1136
1159
  listener(tunnelId, error, messages);
@@ -1158,7 +1181,9 @@ var TunnelManager = class _TunnelManager {
1158
1181
  try {
1159
1182
  logger.info("Tunnel will reconnect", { tunnelId, error, messages });
1160
1183
  const listeners = this.tunnelWillReconnectListeners.get(tunnelId);
1161
- if (!listeners) return;
1184
+ if (!listeners) {
1185
+ return;
1186
+ }
1162
1187
  for (const [id, listener] of listeners) {
1163
1188
  try {
1164
1189
  listener(tunnelId, error, messages);
@@ -1186,7 +1211,9 @@ var TunnelManager = class _TunnelManager {
1186
1211
  try {
1187
1212
  logger.info("Tunnel reconnecting", { tunnelId, retryCnt });
1188
1213
  const listeners = this.tunnelReconnectingListeners.get(tunnelId);
1189
- if (!listeners) return;
1214
+ if (!listeners) {
1215
+ return;
1216
+ }
1190
1217
  for (const [id, listener] of listeners) {
1191
1218
  try {
1192
1219
  listener(tunnelId, retryCnt);
@@ -1264,7 +1291,9 @@ var TunnelManager = class _TunnelManager {
1264
1291
  managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
1265
1292
  }
1266
1293
  const listeners = this.tunnelReconnectionFailedListeners.get(tunnelId);
1267
- if (!listeners) return;
1294
+ if (!listeners) {
1295
+ return;
1296
+ }
1268
1297
  for (const [id, listener] of listeners) {
1269
1298
  try {
1270
1299
  listener(tunnelId, retryCnt);
@@ -1288,7 +1317,9 @@ var TunnelManager = class _TunnelManager {
1288
1317
  try {
1289
1318
  logger.debug("Error in Tunnel Worker", { tunnelId, errorMessage: error.message });
1290
1319
  const listeners = this.tunnelWorkerErrorListeners.get(tunnelId);
1291
- if (!listeners) return;
1320
+ if (!listeners) {
1321
+ return;
1322
+ }
1292
1323
  for (const [id, listener] of listeners) {
1293
1324
  try {
1294
1325
  listener(tunnelId, error);
@@ -1771,6 +1802,26 @@ var TunnelOperations = class {
1771
1802
  }
1772
1803
  return status;
1773
1804
  }
1805
+ // --- Placeholder response ---
1806
+ buildPendingTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, serve) {
1807
+ return {
1808
+ tunnelid,
1809
+ remoteurls: [],
1810
+ tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, false, void 0, serve),
1811
+ status: this.buildStatus(tunnelid, "starting" /* Starting */, "" /* NoError */),
1812
+ stats: newStats()
1813
+ };
1814
+ }
1815
+ buildPendingTunnelResponseV2(tunnelid, tunnelConfig, configFromCli, configid, tunnelName, serve) {
1816
+ return {
1817
+ tunnelid,
1818
+ remoteurls: [],
1819
+ tunnelconfig: pinggyOptionsToTunnelConfigV1(tunnelConfig, configFromCli),
1820
+ status: this.buildStatus(tunnelid, "starting" /* Starting */, "" /* NoError */),
1821
+ stats: newStats(),
1822
+ greetmsg: ""
1823
+ };
1824
+ }
1774
1825
  // --- Helper to construct TunnelResponse ---
1775
1826
  async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, serve) {
1776
1827
  const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
@@ -1811,10 +1862,10 @@ var TunnelOperations = class {
1811
1862
  });
1812
1863
  }
1813
1864
  // --- Operations ---
1814
- async handleStart(config) {
1865
+ async handleStart(config, noWait = false) {
1815
1866
  try {
1816
1867
  const opts = tunnelConfigToPinggyOptions(config);
1817
- const { tunnelid, instance, tunnelName, serve, tunnelConfig } = await this.tunnelManager.createTunnel({
1868
+ const managed = await this.tunnelManager.createTunnel({
1818
1869
  ...opts,
1819
1870
  configId: config.configid,
1820
1871
  name: config.configname,
@@ -1822,36 +1873,52 @@ var TunnelOperations = class {
1822
1873
  serve: config.serve
1823
1874
  }
1824
1875
  });
1825
- await this.tunnelManager.startTunnel(tunnelid);
1876
+ const { tunnelid, tunnelName, serve, tunnelConfig } = managed;
1877
+ const startPromise = this.tunnelManager.startTunnel(tunnelid);
1878
+ if (noWait) {
1879
+ startPromise.catch((err) => {
1880
+ logger.error("No-wait startTunnel failed", { tunnelid, err: String(err) });
1881
+ });
1882
+ return this.buildPendingTunnelResponse(tunnelid, tunnelConfig, config.configid, tunnelName, serve);
1883
+ }
1884
+ await startPromise;
1826
1885
  const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
1827
- const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, serve);
1828
- return resp;
1886
+ return this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, serve);
1829
1887
  } catch (err) {
1830
1888
  return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
1831
1889
  }
1832
1890
  }
1833
- async handleStartV2(config) {
1891
+ async handleStartV2(config, noWait = false) {
1834
1892
  try {
1835
- const { tunnelid, instance, serve } = await this.tunnelManager.createTunnel(config);
1893
+ const managed = await this.tunnelManager.createTunnel(config);
1894
+ const { tunnelid, serve, tunnelConfig } = managed;
1836
1895
  await this.tunnelManager.startTunnel(tunnelid);
1837
1896
  const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
1838
- const resp = this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.serve);
1839
- return resp;
1897
+ return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.serve);
1840
1898
  } catch (err) {
1841
1899
  return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
1842
1900
  }
1843
1901
  }
1844
- async handleUpdateConfig(config) {
1902
+ async handleUpdateConfig(config, noWait = false) {
1845
1903
  try {
1846
1904
  const opts = tunnelConfigToPinggyOptions(config);
1847
- const tunnel = await this.tunnelManager.updateConfig({
1905
+ const updateOpts = {
1848
1906
  ...opts,
1849
1907
  configId: config.configid,
1850
1908
  name: config.configname,
1851
1909
  optional: {
1852
1910
  serve: config.serve
1853
1911
  }
1854
- });
1912
+ };
1913
+ if (noWait) {
1914
+ const existing = this.tunnelManager.getManagedTunnel(config.configid);
1915
+ if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
1916
+ this.tunnelManager.updateConfig(updateOpts).catch((err) => {
1917
+ logger.error("No-wait updateConfig failed", { configid: config.configid, err: String(err) });
1918
+ });
1919
+ return this.buildPendingTunnelResponse(existing.tunnelid, existing.tunnelConfig, config.configid, existing.tunnelName, existing.serve);
1920
+ }
1921
+ const tunnel = await this.tunnelManager.updateConfig(updateOpts);
1855
1922
  if (!tunnel.instance || !tunnel.tunnelConfig)
1856
1923
  throw new Error("Invalid tunnel state after configuration update");
1857
1924
  return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.serve);
@@ -1859,8 +1926,17 @@ var TunnelOperations = class {
1859
1926
  return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
1860
1927
  }
1861
1928
  }
1862
- async handleUpdateConfigV2(config) {
1929
+ async handleUpdateConfigV2(config, noWait = false) {
1863
1930
  try {
1931
+ if (noWait) {
1932
+ const existing = this.tunnelManager.getManagedTunnel(config.configId);
1933
+ console.log(existing);
1934
+ if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
1935
+ this.tunnelManager.updateConfig(config).catch((err) => {
1936
+ logger.error("No-wait updateConfigV2 failed", { configId: config.configId, err: String(err) });
1937
+ });
1938
+ return this.buildPendingTunnelResponseV2(existing.tunnelid, existing.tunnelConfig, config, config.configId, existing.tunnelName, existing.serve);
1939
+ }
1864
1940
  const tunnel = await this.tunnelManager.updateConfig(config);
1865
1941
  if (!tunnel.instance || !tunnel.tunnelConfig)
1866
1942
  throw new Error("Invalid tunnel state after configuration update");
@@ -1947,8 +2023,16 @@ var TunnelOperations = class {
1947
2023
  return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
1948
2024
  }
1949
2025
  }
1950
- async handleRestart(tunnelid) {
2026
+ async handleRestart(tunnelid, noWait = false) {
1951
2027
  try {
2028
+ if (noWait) {
2029
+ const managed2 = this.tunnelManager.getManagedTunnel("", tunnelid);
2030
+ if (!managed2?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2031
+ this.tunnelManager.restartTunnel(tunnelid).catch((err) => {
2032
+ logger.error("No-wait restartTunnel failed", { tunnelid, err: String(err) });
2033
+ });
2034
+ return this.buildPendingTunnelResponse(tunnelid, managed2.tunnelConfig, managed2.configId, managed2.tunnelName, managed2.serve);
2035
+ }
1952
2036
  await this.tunnelManager.restartTunnel(tunnelid);
1953
2037
  const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1954
2038
  if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
@@ -2217,7 +2301,7 @@ var WebSocketCommandHandler = class {
2217
2301
  const dc = StartSchema.parse(raw);
2218
2302
  queuedConfig = dc.tunnelConfig;
2219
2303
  remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
2220
- const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
2304
+ const result = await this.tunnelHandler.handleStart(dc.tunnelConfig, true);
2221
2305
  remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
2222
2306
  return this.wrapResponse(result, req);
2223
2307
  } catch (e) {
@@ -2238,7 +2322,7 @@ var WebSocketCommandHandler = class {
2238
2322
  const dc = StartV2Schema.parse(raw);
2239
2323
  queuedConfig = dc.tunnelConfig;
2240
2324
  remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
2241
- const result = await this.tunnelHandler.handleStartV2(dc.tunnelConfig);
2325
+ const result = await this.tunnelHandler.handleStartV2(dc.tunnelConfig, true);
2242
2326
  remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
2243
2327
  return this.wrapResponse(result, req);
2244
2328
  } catch (e) {
@@ -2287,7 +2371,7 @@ var WebSocketCommandHandler = class {
2287
2371
  try {
2288
2372
  const dc = RestartSchema.parse(raw);
2289
2373
  remoteManagementWebSocketPrinter.printRestartRequested(dc.tunnelID);
2290
- const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
2374
+ const result = await this.tunnelHandler.handleRestart(dc.tunnelID, true);
2291
2375
  remoteManagementWebSocketPrinter.handleRestartResult(dc.tunnelID, result);
2292
2376
  return this.wrapResponse(result, req);
2293
2377
  } catch (e) {
@@ -2302,7 +2386,7 @@ var WebSocketCommandHandler = class {
2302
2386
  async handleUpdateConfigReq(req, raw) {
2303
2387
  try {
2304
2388
  const dc = UpdateConfigSchema.parse(raw);
2305
- const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
2389
+ const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig, true);
2306
2390
  return this.wrapResponse(result, req);
2307
2391
  } catch (e) {
2308
2392
  if (e instanceof z2.ZodError) {
@@ -2316,7 +2400,7 @@ var WebSocketCommandHandler = class {
2316
2400
  async handleUpdateConfigV2Req(req, raw) {
2317
2401
  try {
2318
2402
  const dc = UpdateConfigV2Schema.parse(raw);
2319
- const result = await this.tunnelHandler.handleUpdateConfigV2(dc.tunnelConfig);
2403
+ const result = await this.tunnelHandler.handleUpdateConfigV2(dc.tunnelConfig, true);
2320
2404
  return this.wrapResponse(result, req);
2321
2405
  } catch (e) {
2322
2406
  if (e instanceof z2.ZodError) {
@@ -2482,6 +2566,12 @@ function handleConnectionStatusMessage(firstMessage) {
2482
2566
  // src/remote_management/remoteManagement.ts
2483
2567
  var RECONNECT_SLEEP_MS = 5e3;
2484
2568
  var PING_INTERVAL_MS = 3e4;
2569
+ var RemoteManagementUnauthorizedError = class extends Error {
2570
+ constructor() {
2571
+ super("Unauthorized. Please enter a valid token.");
2572
+ this.name = "RemoteManagementUnauthorizedError";
2573
+ }
2574
+ };
2485
2575
  var _remoteManagementState = {
2486
2576
  status: "NOT_RUNNING",
2487
2577
  errorMessage: ""
@@ -2546,9 +2636,14 @@ async function initiateRemoteManagement(remoteManagementConfig) {
2546
2636
  try {
2547
2637
  await handleWebSocketConnection(wsUrl, wsHost, remoteManagementConfig.apiKey);
2548
2638
  } catch (error) {
2639
+ if (error instanceof RemoteManagementUnauthorizedError) {
2640
+ throw error;
2641
+ }
2549
2642
  logger.warn("Remote management connection error", { error: String(error) });
2550
2643
  }
2551
- if (_stopRequested) break;
2644
+ if (_stopRequested) {
2645
+ break;
2646
+ }
2552
2647
  printer_default.warn(`Remote management disconnected. Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds...`);
2553
2648
  logger.info("Reconnecting to remote management after disconnect");
2554
2649
  await sleep(RECONNECT_SLEEP_MS);
@@ -2557,22 +2652,34 @@ async function initiateRemoteManagement(remoteManagementConfig) {
2557
2652
  logger.info("Remote management stopped.");
2558
2653
  return getRemoteManagementState();
2559
2654
  }
2560
- async function handleWebSocketConnection(wsUrl, wsHost, token) {
2561
- return new Promise((resolve) => {
2655
+ async function handleWebSocketConnection(wsUrl, wsHost, token, onOpenCallback) {
2656
+ return new Promise((resolve, reject) => {
2562
2657
  const ws = new WebSocket(wsUrl, {
2563
2658
  headers: { Authorization: `Bearer ${token}` }
2564
2659
  });
2565
2660
  currentWs = ws;
2566
2661
  let heartbeat = null;
2567
2662
  let firstMessage = true;
2568
- const cleanup = () => {
2569
- if (heartbeat) clearInterval(heartbeat);
2663
+ let settled = false;
2664
+ const cleanup = (err) => {
2665
+ if (settled) {
2666
+ return;
2667
+ }
2668
+ settled = true;
2669
+ if (heartbeat) {
2670
+ clearInterval(heartbeat);
2671
+ }
2570
2672
  currentWs = null;
2571
- resolve();
2673
+ if (err) {
2674
+ reject(err);
2675
+ } else {
2676
+ resolve();
2677
+ }
2572
2678
  };
2573
2679
  ws.once("open", () => {
2574
2680
  printer_default.success(`Connected to ${wsHost}`);
2575
2681
  setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
2682
+ onOpenCallback?.();
2576
2683
  heartbeat = setInterval(() => {
2577
2684
  if (ws.readyState === WebSocket.OPEN) ws.ping();
2578
2685
  }, PING_INTERVAL_MS);
@@ -2600,13 +2707,14 @@ async function handleWebSocketConnection(wsUrl, wsHost, token) {
2600
2707
  ws.on("unexpected-response", (_, res) => {
2601
2708
  if (res.statusCode === 401) {
2602
2709
  setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
2603
- printer_default.error("Unauthorized. Please enter a valid token.");
2604
2710
  logger.error("Unauthorized (401) on remote management connect");
2711
+ cleanup(new RemoteManagementUnauthorizedError());
2605
2712
  ws.close();
2606
2713
  } else {
2607
2714
  logger.warn("Unexpected HTTP response ", { statusCode: res.statusCode });
2608
2715
  printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
2609
2716
  cleanup();
2717
+ ws.close();
2610
2718
  }
2611
2719
  });
2612
2720
  ws.on("close", (code, reason) => {
@@ -2618,7 +2726,7 @@ async function handleWebSocketConnection(wsUrl, wsHost, token) {
2618
2726
  ws.on("error", (err) => {
2619
2727
  setRemoteManagementState({ status: RemoteManagementStatus.Error, errorMessage: err.message });
2620
2728
  logger.warn("WebSocket error", { error: err.message });
2621
- printer_default.error(err);
2729
+ printer_default.warn(err.message);
2622
2730
  cleanup();
2623
2731
  });
2624
2732
  });
@@ -2648,6 +2756,58 @@ async function closeRemoteManagement(timeoutMs = 1e4) {
2648
2756
  return getRemoteManagementState();
2649
2757
  }
2650
2758
  }
2759
+ function startRemoteManagement(remoteManagementConfig) {
2760
+ if (!remoteManagementConfig.apiKey || remoteManagementConfig.apiKey.trim().length === 0) {
2761
+ return Promise.reject(new Error("Remote management token is required"));
2762
+ }
2763
+ const wsUrl = remoteManagementConfig.serverUrl;
2764
+ const wsHost = extractHostname(wsUrl);
2765
+ logger.info("Remote management mode enabled.");
2766
+ _stopRequested = false;
2767
+ return new Promise((resolve, reject) => {
2768
+ let firstSettled = false;
2769
+ const settleOnce = (err) => {
2770
+ if (firstSettled) {
2771
+ return;
2772
+ }
2773
+ firstSettled = true;
2774
+ if (err) {
2775
+ reject(err);
2776
+ } else {
2777
+ resolve(getRemoteManagementState());
2778
+ }
2779
+ };
2780
+ const runLoop = async () => {
2781
+ const sigintHandler = () => {
2782
+ _stopRequested = true;
2783
+ };
2784
+ process.once("SIGINT", sigintHandler);
2785
+ while (!_stopRequested) {
2786
+ logger.info("Connecting to remote management", { wsUrl });
2787
+ setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
2788
+ try {
2789
+ await handleWebSocketConnection(wsUrl, wsHost, remoteManagementConfig.apiKey, () => settleOnce());
2790
+ } catch (error) {
2791
+ if (error instanceof RemoteManagementUnauthorizedError) {
2792
+ settleOnce(error);
2793
+ process.removeListener("SIGINT", sigintHandler);
2794
+ return;
2795
+ }
2796
+ settleOnce();
2797
+ logger.warn("Remote management connection error", { error: String(error) });
2798
+ }
2799
+ if (_stopRequested) {
2800
+ break;
2801
+ }
2802
+ logger.info("Reconnecting to remote management after disconnect");
2803
+ await sleep(RECONNECT_SLEEP_MS);
2804
+ }
2805
+ process.removeListener("SIGINT", sigintHandler);
2806
+ logger.info("Remote management stopped.");
2807
+ };
2808
+ runLoop().catch((err) => settleOnce(err instanceof Error ? err : new Error(String(err))));
2809
+ });
2810
+ }
2651
2811
  function getRemoteManagementState() {
2652
2812
  return _remoteManagementState;
2653
2813
  }
@@ -2668,8 +2828,10 @@ export {
2668
2828
  TunnelErrorCodeType,
2669
2829
  TunnelWarningCode,
2670
2830
  TunnelOperations,
2831
+ RemoteManagementUnauthorizedError,
2671
2832
  parseRemoteManagement,
2672
2833
  initiateRemoteManagement,
2673
2834
  closeRemoteManagement,
2835
+ startRemoteManagement,
2674
2836
  getRemoteManagementState
2675
2837
  };