pinggy 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -847,7 +847,10 @@ var index_exports = {};
847
847
  __export(index_exports, {
848
848
  TunnelManager: () => TunnelManager,
849
849
  TunnelOperations: () => TunnelOperations,
850
- enablePackageLogging: () => enablePackageLogging
850
+ closeRemoteManagement: () => closeRemoteManagement,
851
+ enablePackageLogging: () => enablePackageLogging,
852
+ getRemoteManagementState: () => getRemoteManagementState,
853
+ initiateRemoteManagement: () => initiateRemoteManagement
851
854
  });
852
855
  module.exports = __toCommonJS(index_exports);
853
856
  init_cjs_shims();
@@ -971,6 +974,7 @@ var TunnelManager = class _TunnelManager {
971
974
  this.tunnelErrorListeners = /* @__PURE__ */ new Map();
972
975
  this.tunnelDisconnectListeners = /* @__PURE__ */ new Map();
973
976
  this.tunnelWorkerErrorListeners = /* @__PURE__ */ new Map();
977
+ this.tunnelStartListeners = /* @__PURE__ */ new Map();
974
978
  }
975
979
  static getInstance() {
976
980
  if (!_TunnelManager.instance) {
@@ -1001,7 +1005,15 @@ var TunnelManager = class _TunnelManager {
1001
1005
  throw new Error(`Tunnel with configId "${configid}" already exists`);
1002
1006
  }
1003
1007
  const tunnelid = config.tunnelid || await getUuid();
1004
- const instance = import_pinggy2.pinggy.createTunnel(config);
1008
+ let instance;
1009
+ try {
1010
+ instance = await import_pinggy2.pinggy.createTunnel(config);
1011
+ } catch (e) {
1012
+ logger.error("Error creating tunnel instance:", e);
1013
+ throw e;
1014
+ }
1015
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1016
+ let autoReconnect = config.autoReconnect !== void 0 ? config.autoReconnect : false;
1005
1017
  const managed = {
1006
1018
  tunnelid,
1007
1019
  configid,
@@ -1011,8 +1023,15 @@ var TunnelManager = class _TunnelManager {
1011
1023
  additionalForwarding,
1012
1024
  serve: config.serve,
1013
1025
  warnings: [],
1014
- isStopped: false
1026
+ isStopped: false,
1027
+ createdAt: now,
1028
+ startedAt: null,
1029
+ stoppedAt: null,
1030
+ autoReconnect
1015
1031
  };
1032
+ instance.setPrimaryForwardingCallback(({}) => {
1033
+ managed.startedAt = (/* @__PURE__ */ new Date()).toISOString();
1034
+ });
1016
1035
  this.setupStatsCallback(tunnelid, managed);
1017
1036
  this.setupErrorCallback(tunnelid, managed);
1018
1037
  this.setupDisconnectCallback(tunnelid, managed);
@@ -1056,6 +1075,20 @@ var TunnelManager = class _TunnelManager {
1056
1075
  if (managed.serve) {
1057
1076
  this.startStaticFileServer(managed);
1058
1077
  }
1078
+ try {
1079
+ const startListeners = this.tunnelStartListeners.get(tunnelId);
1080
+ if (startListeners) {
1081
+ for (const [id, listener] of startListeners) {
1082
+ try {
1083
+ listener(tunnelId, urls);
1084
+ } catch (err) {
1085
+ logger.debug("Error in start-listener callback", { listenerId: id, tunnelId, err });
1086
+ }
1087
+ }
1088
+ }
1089
+ } catch (e) {
1090
+ logger.warn("Failed to notify start listeners", { tunnelId, e });
1091
+ }
1059
1092
  return urls;
1060
1093
  }
1061
1094
  /**
@@ -1085,9 +1118,11 @@ var TunnelManager = class _TunnelManager {
1085
1118
  this.tunnelErrorListeners.delete(tunnelId);
1086
1119
  this.tunnelDisconnectListeners.delete(tunnelId);
1087
1120
  this.tunnelWorkerErrorListeners.delete(tunnelId);
1121
+ this.tunnelStartListeners.delete(tunnelId);
1088
1122
  managed.serveWorker = null;
1089
1123
  managed.warnings = managed.warnings ?? [];
1090
1124
  managed.isStopped = true;
1125
+ managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
1091
1126
  logger.info("Tunnel stopped", { tunnelId, configId: managed.configid });
1092
1127
  return { configid: managed.configid, tunnelid: managed.tunnelid };
1093
1128
  } catch (error) {
@@ -1169,6 +1204,60 @@ var TunnelManager = class _TunnelManager {
1169
1204
  this.tunnelStatsListeners.clear();
1170
1205
  logger.info("All tunnels stopped and cleared");
1171
1206
  }
1207
+ /**
1208
+ * Remove a stopped tunnel's records so it will no longer be returned by list methods.
1209
+ *
1210
+ *
1211
+ * @param tunnelId - the tunnel id to remove
1212
+ * @returns true if the record was removed, false otherwise
1213
+ */
1214
+ removeStoppedTunnelByTunnelId(tunnelId) {
1215
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
1216
+ if (!managed) {
1217
+ logger.debug("Attempted to remove non-existent tunnel", { tunnelId });
1218
+ return false;
1219
+ }
1220
+ if (!managed.isStopped) {
1221
+ logger.warn("Attempted to remove tunnel that is not stopped", { tunnelId });
1222
+ return false;
1223
+ }
1224
+ this._cleanupTunnelRecords(managed);
1225
+ logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.configid });
1226
+ return true;
1227
+ }
1228
+ /**
1229
+ * Remove a stopped tunnel by its config id.
1230
+ * @param configId - the config id to remove
1231
+ * @returns true if the record was removed, false otherwise
1232
+ */
1233
+ removeStoppedTunnelByConfigId(configId) {
1234
+ const managed = this.tunnelsByConfigId.get(configId);
1235
+ if (!managed) {
1236
+ logger.debug("Attempted to remove non-existent tunnel by configId", { configId });
1237
+ return false;
1238
+ }
1239
+ return this.removeStoppedTunnelByTunnelId(managed.tunnelid);
1240
+ }
1241
+ _cleanupTunnelRecords(managed) {
1242
+ if (!managed.isStopped) {
1243
+ throw new Error(`Active tunnel "${managed.tunnelid}" cannot be removed`);
1244
+ }
1245
+ try {
1246
+ if (managed.serveWorker) {
1247
+ managed.serveWorker = null;
1248
+ }
1249
+ this.tunnelStats.delete(managed.tunnelid);
1250
+ this.tunnelStatsListeners.delete(managed.tunnelid);
1251
+ this.tunnelErrorListeners.delete(managed.tunnelid);
1252
+ this.tunnelDisconnectListeners.delete(managed.tunnelid);
1253
+ this.tunnelWorkerErrorListeners.delete(managed.tunnelid);
1254
+ this.tunnelStartListeners.delete(managed.tunnelid);
1255
+ this.tunnelsByTunnelId.delete(managed.tunnelid);
1256
+ this.tunnelsByConfigId.delete(managed.configid);
1257
+ } catch (e) {
1258
+ logger.warn("Failed cleaning up tunnel records", { tunnelId: managed.tunnelid, error: e });
1259
+ }
1260
+ }
1172
1261
  /**
1173
1262
  * Get tunnel instance by either configId or tunnelId
1174
1263
  * @param configId - The configuration ID of the tunnel
@@ -1243,6 +1332,9 @@ var TunnelManager = class _TunnelManager {
1243
1332
  additionalForwarding,
1244
1333
  tunnelName
1245
1334
  });
1335
+ if (existingTunnel.createdAt) {
1336
+ newTunnel.createdAt = existingTunnel.createdAt;
1337
+ }
1246
1338
  this.startTunnel(newTunnel.tunnelid);
1247
1339
  } catch (error) {
1248
1340
  logger.error("Failed to restart tunnel", {
@@ -1387,13 +1479,26 @@ var TunnelManager = class _TunnelManager {
1387
1479
  const stats = this.tunnelStats.get(tunnelId);
1388
1480
  return stats || null;
1389
1481
  }
1482
+ getLatestTunnelStats(tunnelId) {
1483
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
1484
+ if (!managed) {
1485
+ return null;
1486
+ }
1487
+ const stats = this.tunnelStats.get(tunnelId);
1488
+ if (stats && stats.length > 0) {
1489
+ return stats[stats.length - 1];
1490
+ }
1491
+ return null;
1492
+ }
1390
1493
  /**
1391
1494
  * Registers a listener function to receive tunnel statistics updates.
1392
1495
  * The listener will be called whenever any tunnel's stats are updated.
1393
1496
  *
1394
1497
  * @param tunnelId - The tunnel ID to listen to stats for
1395
1498
  * @param listener - Function that receives tunnelId and stats when updates occur
1396
- * @returns A unique listener ID that can be used to deregister the listener
1499
+ * @returns A unique listener ID that can be used to deregister the listener and tunnelId
1500
+ *
1501
+ * @throws {Error} When the specified tunnelId does not exist
1397
1502
  */
1398
1503
  async registerStatsListener(tunnelId, listener) {
1399
1504
  const managed = this.tunnelsByTunnelId.get(tunnelId);
@@ -1407,7 +1512,7 @@ var TunnelManager = class _TunnelManager {
1407
1512
  const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
1408
1513
  tunnelListeners.set(listenerId, listener);
1409
1514
  logger.info("Stats listener registered for tunnel", { tunnelId, listenerId });
1410
- return listenerId;
1515
+ return [listenerId, tunnelId];
1411
1516
  }
1412
1517
  async registerErrorListener(tunnelId, listener) {
1413
1518
  const managed = this.tunnelsByTunnelId.get(tunnelId);
@@ -1450,6 +1555,20 @@ var TunnelManager = class _TunnelManager {
1450
1555
  tunnelWorkerErrorListner?.set(listenerId, listener);
1451
1556
  logger.info("TunnelWorker error listener registered for tunnel", { tunnelId, listenerId });
1452
1557
  }
1558
+ async registerStartListener(tunnelId, listener) {
1559
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
1560
+ if (!managed) {
1561
+ throw new Error(`Tunnel "${tunnelId}" not found`);
1562
+ }
1563
+ if (!this.tunnelStartListeners.has(tunnelId)) {
1564
+ this.tunnelStartListeners.set(tunnelId, /* @__PURE__ */ new Map());
1565
+ }
1566
+ const listenerId = await getUuid();
1567
+ const listeners = this.tunnelStartListeners.get(tunnelId);
1568
+ listeners.set(listenerId, listener);
1569
+ logger.info("Start listener registered for tunnel", { tunnelId, listenerId });
1570
+ return listenerId;
1571
+ }
1453
1572
  /**
1454
1573
  * Removes a previously registered stats listener.
1455
1574
  *
@@ -1556,9 +1675,9 @@ var TunnelManager = class _TunnelManager {
1556
1675
  }
1557
1676
  setupErrorCallback(tunnelId, managed) {
1558
1677
  try {
1559
- const callback = (errorNo, errorMsg, recoverable) => {
1678
+ const callback = ({ errorNo, error, recoverable }) => {
1560
1679
  try {
1561
- const msg = typeof errorMsg === "string" ? errorMsg : String(errorMsg);
1680
+ const msg = typeof error === "string" ? error : String(error);
1562
1681
  const isFatal = true;
1563
1682
  logger.debug("Tunnel reported error", { tunnelId, errorNo, errorMsg: msg, recoverable });
1564
1683
  this.notifyErrorListeners(tunnelId, msg, isFatal);
@@ -1574,9 +1693,25 @@ var TunnelManager = class _TunnelManager {
1574
1693
  }
1575
1694
  setupDisconnectCallback(tunnelId, managed) {
1576
1695
  try {
1577
- const callback = (error, messages) => {
1696
+ const callback = ({ error, messages }) => {
1578
1697
  try {
1579
1698
  logger.debug("Tunnel disconnected", { tunnelId, error, messages });
1699
+ const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1700
+ if (managedTunnel) {
1701
+ managedTunnel.isStopped = true;
1702
+ managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
1703
+ }
1704
+ if (managedTunnel && managedTunnel.autoReconnect) {
1705
+ logger.info("Auto-reconnecting tunnel", { tunnelId });
1706
+ setTimeout(async () => {
1707
+ try {
1708
+ await this.restartTunnel(tunnelId);
1709
+ logger.info("Tunnel auto-reconnected successfully", { tunnelId });
1710
+ } catch (e) {
1711
+ logger.error("Failed to auto-reconnect tunnel", { tunnelId, e });
1712
+ }
1713
+ }, 1e4);
1714
+ }
1580
1715
  const listeners = this.tunnelDisconnectListeners.get(tunnelId);
1581
1716
  if (!listeners) return;
1582
1717
  for (const [id, listener] of listeners) {
@@ -1738,6 +1873,7 @@ var cliOptions = {
1738
1873
  v: { type: "boolean", description: "Print logs to stdout for Cli. Overrides PINGGY_LOG_STDOUT environment variable" },
1739
1874
  vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
1740
1875
  vvv: { type: "boolean", description: "Enable all logs from Cli, SDK and internal components." },
1876
+ autoreconnect: { type: "boolean", short: "a", description: "Automatically reconnect tunnel on failure." },
1741
1877
  // Save and load config
1742
1878
  saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
1743
1879
  conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
@@ -1812,7 +1948,8 @@ var defaultOptions = {
1812
1948
  httpsOnly: false,
1813
1949
  originalRequestUrl: false,
1814
1950
  allowPreflight: false,
1815
- reverseProxy: false
1951
+ reverseProxy: false,
1952
+ autoReconnect: false
1816
1953
  };
1817
1954
 
1818
1955
  // src/cli/extendedOptions.ts
@@ -2235,7 +2372,8 @@ async function buildFinalConfig(values, positionals) {
2235
2372
  serverAddress: server || defaultOptions.serverAddress,
2236
2373
  tunnelType: initialTunnel ? [initialTunnel] : defaultOptions.tunnelType,
2237
2374
  NoTUI: values.notui || false,
2238
- qrCode: qrCode || false
2375
+ qrCode: qrCode || false,
2376
+ autoReconnect: values.autoreconnect || false
2239
2377
  };
2240
2378
  parseType(finalConfig, values, type);
2241
2379
  parseToken(finalConfig, token || values.token);
@@ -2335,15 +2473,23 @@ function newStatus(tunnelState, errorCode, errorMsg) {
2335
2473
  };
2336
2474
  }
2337
2475
  function newStats() {
2338
- return [{
2476
+ return {
2339
2477
  numLiveConnections: 0,
2340
2478
  numTotalConnections: 0,
2341
2479
  numTotalReqBytes: 0,
2342
2480
  numTotalResBytes: 0,
2343
2481
  numTotalTxBytes: 0,
2344
2482
  elapsedTime: 0
2345
- }];
2483
+ };
2346
2484
  }
2485
+ var RemoteManagementStatus = {
2486
+ Connecting: "CONNECTING",
2487
+ Disconnecting: "DISCONNECTING",
2488
+ Reconnecting: "RECONNECTING",
2489
+ Running: "RUNNING",
2490
+ NotRunning: "NOT_RUNNING",
2491
+ Error: "ERROR"
2492
+ };
2347
2493
 
2348
2494
  // src/remote_management/handler.ts
2349
2495
  init_cjs_shims();
@@ -2363,7 +2509,10 @@ var AdditionalForwardingSchema = import_zod.z.object({
2363
2509
  localPort: import_zod.z.number()
2364
2510
  });
2365
2511
  var TunnelConfigSchema = import_zod.z.object({
2366
- allowPreflight: import_zod.z.boolean(),
2512
+ allowPreflight: import_zod.z.boolean().optional(),
2513
+ // primary key
2514
+ allowpreflight: import_zod.z.boolean().optional(),
2515
+ // legacy key
2367
2516
  autoreconnect: import_zod.z.boolean(),
2368
2517
  basicauth: import_zod.z.array(import_zod.z.object({ username: import_zod.z.string(), password: import_zod.z.string() })).nullable(),
2369
2518
  bearerauth: import_zod.z.string().nullable(),
@@ -2387,12 +2536,30 @@ var TunnelConfigSchema = import_zod.z.object({
2387
2536
  statusCheckInterval: import_zod.z.number(),
2388
2537
  token: import_zod.z.string(),
2389
2538
  tunnelTimeout: import_zod.z.number(),
2390
- type: import_zod.z.enum([import_pinggy5.TunnelType.Http, import_pinggy5.TunnelType.Tcp, import_pinggy5.TunnelType.Udp, import_pinggy5.TunnelType.Tls, import_pinggy5.TunnelType.TlsTcp]),
2539
+ type: import_zod.z.enum([
2540
+ import_pinggy5.TunnelType.Http,
2541
+ import_pinggy5.TunnelType.Tcp,
2542
+ import_pinggy5.TunnelType.Udp,
2543
+ import_pinggy5.TunnelType.Tls,
2544
+ import_pinggy5.TunnelType.TlsTcp
2545
+ ]),
2391
2546
  webdebuggerport: import_zod.z.number(),
2392
2547
  xff: import_zod.z.string(),
2393
2548
  additionalForwarding: import_zod.z.array(AdditionalForwardingSchema).optional(),
2394
2549
  serve: import_zod.z.string().optional()
2395
- });
2550
+ }).superRefine((data, ctx) => {
2551
+ if (data.allowPreflight === void 0 && data.allowpreflight === void 0) {
2552
+ ctx.addIssue({
2553
+ code: "custom",
2554
+ message: "Either allowPreflight or allowpreflight is required",
2555
+ path: ["allowPreflight"]
2556
+ });
2557
+ }
2558
+ }).transform((data) => ({
2559
+ ...data,
2560
+ allowPreflight: data.allowPreflight ?? data.allowpreflight,
2561
+ allowpreflight: data.allowPreflight ?? data.allowpreflight
2562
+ }));
2396
2563
  var StartSchema = import_zod.z.object({
2397
2564
  tunnelID: import_zod.z.string().uuid().nullable().optional(),
2398
2565
  tunnelConfig: TunnelConfigSchema
@@ -2422,6 +2589,7 @@ function tunnelConfigToPinggyOptions(config) {
2422
2589
  allowPreflight: config.allowPreflight,
2423
2590
  reverseProxy: config.noReverseProxy,
2424
2591
  force: config.force,
2592
+ autoReconnect: config.autoreconnect,
2425
2593
  optional: {
2426
2594
  sniServerName: config.localservertlssni || ""
2427
2595
  }
@@ -2435,7 +2603,8 @@ function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls,
2435
2603
  const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
2436
2604
  return {
2437
2605
  allowPreflight: opts.allowPreflight ?? false,
2438
- autoreconnect: true,
2606
+ allowpreflight: opts.allowPreflight ?? false,
2607
+ autoreconnect: opts.autoReconnect ?? false,
2439
2608
  basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
2440
2609
  bearerauth: parsedTokens.length ? parsedTokens.join(",") : null,
2441
2610
  configid,
@@ -2472,11 +2641,24 @@ var TunnelOperations = class {
2472
2641
  constructor() {
2473
2642
  this.tunnelManager = TunnelManager.getInstance();
2474
2643
  }
2644
+ buildStatus(tunnelId, state, errorCode) {
2645
+ const status = newStatus(state, errorCode, "");
2646
+ try {
2647
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelId);
2648
+ if (managed) {
2649
+ status.createdtimestamp = managed.createdAt || "";
2650
+ status.starttimestamp = managed.startedAt || "";
2651
+ status.endtimestamp = managed.stoppedAt || "";
2652
+ }
2653
+ } catch (e) {
2654
+ }
2655
+ return status;
2656
+ }
2475
2657
  // --- Helper to construct TunnelResponse ---
2476
2658
  async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, additionalForwarding, serve) {
2477
2659
  const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
2478
2660
  this.tunnelManager.getTunnelStatus(tunnelid),
2479
- this.tunnelManager.getTunnelStats(tunnelid),
2661
+ this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
2480
2662
  this.tunnelManager.getLocalserverTlsInfo(tunnelid),
2481
2663
  this.tunnelManager.getTunnelGreetMessage(tunnelid),
2482
2664
  this.tunnelManager.getTunnelUrls(tunnelid)
@@ -2485,7 +2667,7 @@ var TunnelOperations = class {
2485
2667
  tunnelid,
2486
2668
  remoteurls,
2487
2669
  tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg, additionalForwarding),
2488
- status: newStatus(status, "" /* NoError */, ""),
2670
+ status: this.buildStatus(tunnelid, status, "" /* NoError */),
2489
2671
  stats
2490
2672
  };
2491
2673
  }
@@ -2540,8 +2722,7 @@ var TunnelOperations = class {
2540
2722
  }
2541
2723
  return Promise.all(
2542
2724
  tunnels.map(async (t) => {
2543
- const rawStats = this.tunnelManager.getTunnelStats(t.tunnelid);
2544
- const stats = rawStats ?? newStats();
2725
+ const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
2545
2726
  const [status, tlsInfo, greetMsg] = await Promise.all([
2546
2727
  this.tunnelManager.getTunnelStatus(t.tunnelid),
2547
2728
  this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
@@ -2552,8 +2733,8 @@ var TunnelOperations = class {
2552
2733
  return {
2553
2734
  tunnelid: t.tunnelid,
2554
2735
  remoteurls: t.remoteurls,
2555
- status: newStatus(status, "" /* NoError */, ""),
2556
- stats,
2736
+ status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
2737
+ stats: rawStats,
2557
2738
  tunnelconfig: tunnelConfig
2558
2739
  };
2559
2740
  })
@@ -2601,13 +2782,30 @@ var TunnelOperations = class {
2601
2782
  try {
2602
2783
  const stats = this.tunnelManager.getTunnelStats(tunnelid);
2603
2784
  if (!stats) {
2604
- return newStats();
2785
+ return [newStats()];
2605
2786
  }
2606
2787
  return stats;
2607
2788
  } catch (err) {
2608
2789
  return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats");
2609
2790
  }
2610
2791
  }
2792
+ handleRegisterDisconnectListener(tunnelid, listener) {
2793
+ this.tunnelManager.registerDisconnectListener(tunnelid, listener);
2794
+ }
2795
+ handleRemoveStoppedTunnelByConfigId(configId) {
2796
+ try {
2797
+ return this.tunnelManager.removeStoppedTunnelByConfigId(configId);
2798
+ } catch (err) {
2799
+ return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by configId");
2800
+ }
2801
+ }
2802
+ handleRemoveStoppedTunnelByTunnelId(tunnelId) {
2803
+ try {
2804
+ return this.tunnelManager.removeStoppedTunnelByTunnelId(tunnelId);
2805
+ } catch (err) {
2806
+ return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by tunnelId");
2807
+ }
2808
+ }
2611
2809
  };
2612
2810
 
2613
2811
  // src/remote_management/websocket_handlers.ts
@@ -2640,11 +2838,13 @@ var WebSocketCommandHandler = class {
2640
2838
  }
2641
2839
  async handleStartReq(req, raw) {
2642
2840
  const dc = StartSchema.parse(raw);
2841
+ printer_default.info("Starting tunnel with config name: " + dc.tunnelConfig.configname);
2643
2842
  const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
2644
2843
  return this.wrapResponse(result, req);
2645
2844
  }
2646
2845
  async handleStopReq(req, raw) {
2647
2846
  const dc = StopSchema.parse(raw);
2847
+ printer_default.info("Stopping tunnel with ID: " + dc.tunnelID);
2648
2848
  const result = await this.tunnelHandler.handleStop(dc.tunnelID);
2649
2849
  return this.wrapResponse(result, req);
2650
2850
  }
@@ -2674,7 +2874,17 @@ var WebSocketCommandHandler = class {
2674
2874
  errResp.requestid = req.requestid;
2675
2875
  return errResp;
2676
2876
  }
2677
- const respObj = NewResponseObject(result);
2877
+ const finalResult = JSON.parse(JSON.stringify(result));
2878
+ if (Array.isArray(finalResult)) {
2879
+ finalResult.forEach((item) => {
2880
+ if (item?.tunnelconfig) {
2881
+ delete item.tunnelconfig.allowPreflight;
2882
+ }
2883
+ });
2884
+ } else if (finalResult?.tunnelconfig) {
2885
+ delete finalResult.tunnelconfig.allowPreflight;
2886
+ }
2887
+ const respObj = NewResponseObject(finalResult);
2678
2888
  respObj.command = req.command;
2679
2889
  respObj.requestid = req.requestid;
2680
2890
  return respObj;
@@ -2747,6 +2957,12 @@ function handleConnectionStatusMessage(firstMessage) {
2747
2957
  // src/remote_management/remoteManagement.ts
2748
2958
  var RECONNECT_SLEEP_MS = 5e3;
2749
2959
  var PING_INTERVAL_MS = 3e4;
2960
+ var _remoteManagementState = {
2961
+ status: "NOT_RUNNING",
2962
+ errorMessage: ""
2963
+ };
2964
+ var _stopRequested = false;
2965
+ var currentWs = null;
2750
2966
  function buildRemoteManagementWsUrl(manage) {
2751
2967
  let baseUrl = (manage || "dashboard.pinggy.io").trim();
2752
2968
  if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
@@ -2780,95 +2996,133 @@ async function parseRemoteManagement(values) {
2780
2996
  }
2781
2997
  }
2782
2998
  async function initiateRemoteManagement(token, manage) {
2999
+ await printer_default.ensureDeps();
3000
+ await loadChalk();
2783
3001
  if (!token || token.trim().length === 0) {
2784
3002
  throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
2785
3003
  }
2786
3004
  const wsUrl = buildRemoteManagementWsUrl(manage);
2787
3005
  const wsHost = extractHostname(wsUrl);
2788
3006
  logger.info("Remote management mode enabled.");
2789
- let stopRequested = false;
3007
+ _stopRequested = false;
2790
3008
  const sigintHandler = () => {
2791
- stopRequested = true;
3009
+ _stopRequested = true;
2792
3010
  };
2793
3011
  process.once("SIGINT", sigintHandler);
2794
- let firstTry = true;
2795
- while (!stopRequested) {
2796
- if (firstTry) {
2797
- firstTry = false;
2798
- printer_default.print(`Connecting to ${wsHost}`);
2799
- logger.info("Connecting to remote management", { wsUrl });
2800
- } else {
2801
- printer_default.warn(`Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds.`);
2802
- logger.info("Reconnecting after sleep", { seconds: RECONNECT_SLEEP_MS / 1e3 });
2803
- await sleep(RECONNECT_SLEEP_MS);
2804
- if (stopRequested) break;
2805
- printer_default.print(`Connecting to ${wsHost}`);
2806
- logger.info("Connecting to remote management", { wsUrl });
3012
+ const logConnecting = () => {
3013
+ printer_default.print(`Connecting to ${wsHost}`);
3014
+ logger.info("Connecting to remote management", { wsUrl });
3015
+ };
3016
+ while (!_stopRequested) {
3017
+ logConnecting();
3018
+ setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
3019
+ try {
3020
+ await handleWebSocketConnection(wsUrl, wsHost, token);
3021
+ } catch (error) {
3022
+ logger.warn("Remote management connection error", { error: String(error) });
2807
3023
  }
3024
+ if (_stopRequested) break;
3025
+ printer_default.warn(`Remote management disconnected. Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds...`);
3026
+ logger.info("Reconnecting to remote management after disconnect");
3027
+ await sleep(RECONNECT_SLEEP_MS);
3028
+ }
3029
+ process.removeListener("SIGINT", sigintHandler);
3030
+ logger.info("Remote management stopped.");
3031
+ return getRemoteManagementState();
3032
+ }
3033
+ async function handleWebSocketConnection(wsUrl, wsHost, token) {
3034
+ return new Promise((resolve) => {
2808
3035
  const ws = new import_ws.default(wsUrl, {
2809
3036
  headers: { Authorization: `Bearer ${token}` }
2810
3037
  });
2811
- await new Promise((resolve) => {
2812
- let heartbeat;
2813
- const startHeartbeat = () => {
2814
- heartbeat = setInterval(() => {
2815
- if (ws.readyState !== import_ws.default.OPEN) return;
2816
- ws.ping();
2817
- }, PING_INTERVAL_MS);
2818
- };
2819
- ws.on("open", () => {
2820
- printer_default.success(`Connected to ${wsHost}`);
2821
- startHeartbeat();
2822
- });
2823
- ws.on("ping", () => ws.pong());
2824
- let firstMessage = true;
2825
- ws.on("message", async (data) => {
2826
- try {
2827
- if (firstMessage) {
2828
- firstMessage = false;
2829
- const ok = handleConnectionStatusMessage(data);
2830
- if (!ok) {
2831
- ws.close();
2832
- }
2833
- return;
2834
- }
2835
- const text = data.toString("utf8");
2836
- const req = JSON.parse(text);
2837
- const webSocketHandler = new WebSocketCommandHandler();
2838
- await webSocketHandler.handle(ws, req);
2839
- } catch (e) {
2840
- logger.warn("Failed handling websocket message", { error: String(e) });
2841
- }
2842
- });
2843
- ws.on("unexpected-response", (_req, res) => {
2844
- if (res.statusCode === 401) {
2845
- printer_default.error("Unauthorized. Please enter a valid token.");
2846
- logger.error("Unauthorized (401) on remote management connect");
2847
- stopRequested = true;
2848
- ws.close();
2849
- } else {
2850
- printer_default.warn(`Unexpected HTTP response ${res.statusCode} from server. Retrying...`);
2851
- logger.warn("Unexpected HTTP response on WebSocket connect", { statusCode: res.statusCode });
2852
- ws.close();
3038
+ currentWs = ws;
3039
+ let heartbeat = null;
3040
+ let firstMessage = true;
3041
+ const cleanup = () => {
3042
+ if (heartbeat) clearInterval(heartbeat);
3043
+ currentWs = null;
3044
+ resolve();
3045
+ };
3046
+ ws.once("open", () => {
3047
+ printer_default.success(`Connected to ${wsHost}`);
3048
+ heartbeat = setInterval(() => {
3049
+ if (ws.readyState === import_ws.default.OPEN) ws.ping();
3050
+ }, PING_INTERVAL_MS);
3051
+ });
3052
+ ws.on("ping", () => ws.pong());
3053
+ ws.on("message", async (data) => {
3054
+ try {
3055
+ if (firstMessage) {
3056
+ firstMessage = false;
3057
+ const ok = handleConnectionStatusMessage(data);
3058
+ if (!ok) ws.close();
3059
+ return;
2853
3060
  }
2854
- });
2855
- ws.on("close", (code, reason) => {
2856
- logger.info("WebSocket closed", { code, reason: reason.toString() });
2857
- printer_default.warn(`Disconnected from remote management (code: ${code}).Retrying...`);
2858
- clearInterval(heartbeat);
2859
- resolve();
2860
- });
2861
- ws.on("error", (err) => {
2862
- printer_default.error(err);
2863
- logger.warn("WebSocket error", { error: err.message });
2864
- clearInterval(heartbeat);
2865
- resolve();
2866
- });
3061
+ setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
3062
+ const req = JSON.parse(data.toString("utf8"));
3063
+ await new WebSocketCommandHandler().handle(ws, req);
3064
+ } catch (e) {
3065
+ logger.warn("Failed handling websocket message", { error: String(e) });
3066
+ }
3067
+ });
3068
+ ws.on("unexpected-response", (_, res) => {
3069
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
3070
+ if (res.statusCode === 401) {
3071
+ printer_default.error("Unauthorized. Please enter a valid token.");
3072
+ logger.error("Unauthorized (401) on remote management connect");
3073
+ } else {
3074
+ printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
3075
+ logger.warn("Unexpected HTTP response", { statusCode: res.statusCode });
3076
+ }
3077
+ ws.close();
3078
+ });
3079
+ ws.on("close", (code, reason) => {
3080
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
3081
+ logger.info("WebSocket closed", { code, reason: reason.toString() });
3082
+ printer_default.warn(`Disconnected (code: ${code}). Retrying...`);
3083
+ cleanup();
3084
+ });
3085
+ ws.on("error", (err) => {
3086
+ setRemoteManagementState({ status: RemoteManagementStatus.Error, errorMessage: err.message });
3087
+ logger.warn("WebSocket error", { error: err.message });
3088
+ printer_default.error(err);
3089
+ cleanup();
2867
3090
  });
2868
- if (stopRequested) break;
3091
+ });
3092
+ }
3093
+ async function closeRemoteManagement(timeoutMs = 1e4) {
3094
+ _stopRequested = true;
3095
+ try {
3096
+ if (currentWs) {
3097
+ try {
3098
+ setRemoteManagementState({ status: RemoteManagementStatus.Disconnecting, errorMessage: "" });
3099
+ currentWs.close();
3100
+ } catch (e) {
3101
+ logger.warn("Error while closing current remote management websocket", { error: String(e) });
3102
+ }
3103
+ }
3104
+ const start = Date.now();
3105
+ while (_remoteManagementState.status === "RUNNING") {
3106
+ if (Date.now() - start > timeoutMs) {
3107
+ logger.warn("Timed out waiting for remote management to stop");
3108
+ break;
3109
+ }
3110
+ await sleep(200);
3111
+ }
3112
+ } finally {
3113
+ currentWs = null;
3114
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
3115
+ return getRemoteManagementState();
2869
3116
  }
2870
- process.removeListener("SIGINT", sigintHandler);
2871
- logger.info("Remote management stopped.");
3117
+ }
3118
+ function getRemoteManagementState() {
3119
+ return _remoteManagementState;
3120
+ }
3121
+ function setRemoteManagementState(state, errorMessage) {
3122
+ _remoteManagementState = {
3123
+ status: state.status,
3124
+ errorMessage: errorMessage || ""
3125
+ };
2872
3126
  }
2873
3127
 
2874
3128
  // src/utils/parseArgs.ts
@@ -2962,6 +3216,57 @@ var TunnelData = {
2962
3216
  var activeTui = null;
2963
3217
  var disconnectState = null;
2964
3218
  var updateDisconnectState = null;
3219
+ async function launchTui(finalConfig, urls, greet) {
3220
+ try {
3221
+ const { withFullScreen } = await import("fullscreen-ink");
3222
+ const { default: TunnelTui2 } = await Promise.resolve().then(() => (init_tui(), tui_exports));
3223
+ const React3 = await import("react");
3224
+ const isTTYEnabled = process.stdin.isTTY;
3225
+ const TunnelTuiWrapper = ({ finalConfig: finalConfig2, urls: urls2, greet: greet2 }) => {
3226
+ const [disconnectInfo, setDisconnectInfo] = React3.useState(null);
3227
+ React3.useEffect(() => {
3228
+ updateDisconnectState = setDisconnectInfo;
3229
+ return () => {
3230
+ updateDisconnectState = null;
3231
+ };
3232
+ }, []);
3233
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3234
+ TunnelTui2,
3235
+ {
3236
+ urls: urls2 ?? [],
3237
+ greet: greet2 ?? "",
3238
+ tunnelConfig: finalConfig2,
3239
+ disconnectInfo
3240
+ }
3241
+ );
3242
+ };
3243
+ const tui = withFullScreen(
3244
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3245
+ TunnelTuiWrapper,
3246
+ {
3247
+ finalConfig,
3248
+ urls,
3249
+ greet
3250
+ }
3251
+ )
3252
+ );
3253
+ activeTui = tui;
3254
+ if (isTTYEnabled) {
3255
+ try {
3256
+ await tui.start();
3257
+ await tui.waitUntilExit();
3258
+ } catch (e) {
3259
+ logger.warn("TUI error", e);
3260
+ } finally {
3261
+ activeTui = null;
3262
+ }
3263
+ } else {
3264
+ printer_default.warn("Unable to initiate the TUI: your terminal does not support the required input mode.");
3265
+ }
3266
+ } catch (e) {
3267
+ logger.warn("Failed to (re-)initiate TUI", e);
3268
+ }
3269
+ }
2965
3270
  async function startCli(finalConfig, manager) {
2966
3271
  await printer_default.ensureDeps();
2967
3272
  const chalk = await loadChalk();
@@ -3017,64 +3322,61 @@ async function startCli(finalConfig, manager) {
3017
3322
  logger.warn("Failed to wait for TUI exit", e);
3018
3323
  } finally {
3019
3324
  activeTui = null;
3020
- messages.forEach(function(m) {
3325
+ printer_default.warn(`Error in tunnel:`);
3326
+ messages?.forEach(function(m) {
3021
3327
  printer_default.warn(m);
3022
3328
  });
3023
- process.exit(0);
3329
+ if (!finalConfig.autoReconnect) {
3330
+ process.exit(0);
3331
+ }
3024
3332
  }
3025
3333
  } else {
3026
- messages.forEach(function(m) {
3334
+ messages?.forEach(function(m) {
3027
3335
  printer_default.warn(m);
3028
3336
  });
3029
- process.exit(0);
3337
+ if (!finalConfig.autoReconnect) {
3338
+ process.exit(0);
3339
+ }
3340
+ }
3341
+ if (finalConfig.autoReconnect) {
3342
+ printer_default.startSpinner("Reconnecting to Pinggy");
3030
3343
  }
3031
3344
  });
3032
- if (!finalConfig.NoTUI) {
3033
- const { withFullScreen } = await import("fullscreen-ink");
3034
- const { default: TunnelTui2 } = await Promise.resolve().then(() => (init_tui(), tui_exports));
3035
- const React3 = await import("react");
3036
- const isTTYEnabled = process.stdin.isTTY;
3037
- const TunnelTuiWrapper = ({ finalConfig: finalConfig2, urls, greet }) => {
3038
- const [disconnectInfo, setDisconnectInfo] = React3.useState(null);
3039
- React3.useEffect(() => {
3040
- updateDisconnectState = setDisconnectInfo;
3041
- return () => {
3042
- updateDisconnectState = null;
3043
- };
3044
- }, []);
3045
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3046
- TunnelTui2,
3047
- {
3048
- urls: urls ?? [],
3049
- greet: greet ?? "",
3050
- tunnelConfig: finalConfig2,
3051
- disconnectInfo
3052
- }
3053
- );
3054
- };
3055
- const tui = withFullScreen(
3056
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3057
- TunnelTuiWrapper,
3058
- {
3059
- finalConfig,
3060
- urls: TunnelData.urls,
3061
- greet: TunnelData.greet
3062
- }
3063
- )
3064
- );
3065
- activeTui = tui;
3066
- if (isTTYEnabled) {
3345
+ try {
3346
+ await manager2.registerStartListener(tunnel.tunnelid, async (tunnelId, urls) => {
3067
3347
  try {
3068
- await tui.start();
3069
- await tui.waitUntilExit();
3348
+ printer_default.stopSpinnerSuccess("Reconnected to Pinggy");
3070
3349
  } catch (e) {
3071
- logger.warn("TUI error", e);
3072
- } finally {
3073
- activeTui = null;
3074
3350
  }
3075
- } else {
3076
- printer_default.warn("Unable to initiate the TUI: your terminal does not support the required input mode.");
3077
- }
3351
+ printer_default.success(chalk.bold("Tunnel re-established!"));
3352
+ printer_default.print(chalk.gray("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3353
+ TunnelData.urls = urls;
3354
+ TunnelData.greet = await manager2.getTunnelGreetMessage(tunnel.tunnelid);
3355
+ printer_default.info(chalk.cyanBright("Remote URLs:"));
3356
+ (TunnelData.urls ?? []).forEach(
3357
+ (url) => printer_default.print(" " + chalk.magentaBright(url))
3358
+ );
3359
+ printer_default.print(chalk.gray("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3360
+ if (TunnelData.greet?.includes("not authenticated")) {
3361
+ printer_default.warn(chalk.yellowBright(TunnelData.greet));
3362
+ } else if (TunnelData.greet?.includes("authenticated as")) {
3363
+ const emailMatch = /authenticated as (.+)/.exec(TunnelData.greet);
3364
+ if (emailMatch) {
3365
+ const email = emailMatch[1];
3366
+ printer_default.info(chalk.cyanBright("Authenticated as: " + email));
3367
+ }
3368
+ }
3369
+ printer_default.print(chalk.gray("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3370
+ printer_default.print(chalk.gray("\nPress Ctrl+C to stop the tunnel.\n"));
3371
+ if (!finalConfig.NoTUI) {
3372
+ await launchTui(finalConfig, TunnelData.urls, TunnelData.greet);
3373
+ }
3374
+ });
3375
+ } catch (e) {
3376
+ logger.debug("Failed to register start listener", e);
3377
+ }
3378
+ if (!finalConfig.NoTUI) {
3379
+ await launchTui(finalConfig, TunnelData.urls, TunnelData.greet);
3078
3380
  }
3079
3381
  } catch (err) {
3080
3382
  printer_default.stopSpinnerFail("Failed to connect");
@@ -3090,6 +3392,8 @@ var import_fs3 = require("fs");
3090
3392
  init_logger();
3091
3393
  async function main() {
3092
3394
  try {
3395
+ await printer_default.ensureDeps();
3396
+ await loadChalk();
3093
3397
  const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
3094
3398
  configureLogger(values);
3095
3399
  const manager = TunnelManager.getInstance();
@@ -3137,5 +3441,8 @@ if (entryFile && entryFile === currentFile) {
3137
3441
  0 && (module.exports = {
3138
3442
  TunnelManager,
3139
3443
  TunnelOperations,
3140
- enablePackageLogging
3444
+ closeRemoteManagement,
3445
+ enablePackageLogging,
3446
+ getRemoteManagementState,
3447
+ initiateRemoteManagement
3141
3448
  });