pinggy 0.1.8 → 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);
@@ -1040,11 +1059,11 @@ var TunnelManager = class _TunnelManager {
1040
1059
  if (Array.isArray(managed.additionalForwarding) && managed.additionalForwarding.length > 0) {
1041
1060
  for (const f of managed.additionalForwarding) {
1042
1061
  try {
1043
- if (!f || typeof f.remotePort !== "number" || !f.localDomain || typeof f.localPort !== "number") {
1062
+ if (!f || typeof f.remoteDomain !== "string" || !f.localDomain || typeof f.localPort !== "number") {
1044
1063
  logger.warn(`Skipping invalid additional forwarding rule: ${JSON.stringify(f)}`);
1045
1064
  continue;
1046
1065
  }
1047
- const hostname = f.remoteDomain && f.remoteDomain.length > 0 ? `${f.remoteDomain}:${f.remotePort}` : `${f.remotePort}`;
1066
+ const hostname = f.remoteDomain;
1048
1067
  const target = `${f.localDomain}:${f.localPort}`;
1049
1068
  await managed.instance.tunnelRequestAdditionalForwarding(hostname, target);
1050
1069
  logger.info("Applied additional forwarding", { tunnelId, hostname, target });
@@ -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) {
@@ -1125,7 +1160,9 @@ var TunnelManager = class _TunnelManager {
1125
1160
  configid: tunnel.configid,
1126
1161
  tunnelName: tunnel.tunnelName,
1127
1162
  tunnelConfig: tunnel.tunnelConfig,
1128
- remoteurls: !tunnel.isStopped ? await this.getTunnelUrls(tunnel.tunnelid) : []
1163
+ remoteurls: !tunnel.isStopped ? await this.getTunnelUrls(tunnel.tunnelid) : [],
1164
+ additionalForwarding: tunnel.additionalForwarding,
1165
+ serve: tunnel.serve
1129
1166
  };
1130
1167
  }));
1131
1168
  return tunnelList;
@@ -1167,6 +1204,60 @@ var TunnelManager = class _TunnelManager {
1167
1204
  this.tunnelStatsListeners.clear();
1168
1205
  logger.info("All tunnels stopped and cleared");
1169
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
+ }
1170
1261
  /**
1171
1262
  * Get tunnel instance by either configId or tunnelId
1172
1263
  * @param configId - The configuration ID of the tunnel
@@ -1241,6 +1332,9 @@ var TunnelManager = class _TunnelManager {
1241
1332
  additionalForwarding,
1242
1333
  tunnelName
1243
1334
  });
1335
+ if (existingTunnel.createdAt) {
1336
+ newTunnel.createdAt = existingTunnel.createdAt;
1337
+ }
1244
1338
  this.startTunnel(newTunnel.tunnelid);
1245
1339
  } catch (error) {
1246
1340
  logger.error("Failed to restart tunnel", {
@@ -1279,36 +1373,41 @@ var TunnelManager = class _TunnelManager {
1279
1373
  if (!existingTunnel) {
1280
1374
  throw new Error(`Tunnel with config id "${configid}" not found`);
1281
1375
  }
1282
- const wasRunning = await existingTunnel.instance.getStatus() === "live";
1376
+ const isStopped = existingTunnel.isStopped;
1283
1377
  const currentTunnelConfig = existingTunnel.tunnelConfig;
1284
1378
  const currentTunnelId = existingTunnel.tunnelid;
1285
1379
  const currentTunnelConfigId = existingTunnel.configid;
1286
1380
  const currentAdditionalForwarding = existingTunnel.additionalForwarding;
1287
1381
  const currentTunnelName = existingTunnel.tunnelName;
1382
+ const currentServe = existingTunnel.serve;
1288
1383
  try {
1289
- if (wasRunning) {
1384
+ if (!isStopped) {
1290
1385
  existingTunnel.instance.stop();
1291
1386
  }
1292
1387
  this.tunnelsByTunnelId.delete(currentTunnelId);
1293
1388
  this.tunnelsByConfigId.delete(currentTunnelConfigId);
1294
1389
  const mergedConfig = {
1295
- ...currentTunnelConfig,
1296
1390
  ...newConfig,
1297
1391
  configid,
1298
1392
  tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
1299
- additionalForwarding: additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding
1393
+ additionalForwarding: additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding,
1394
+ serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe
1300
1395
  };
1301
1396
  const newTunnel = await this.createTunnel(mergedConfig);
1302
- if (wasRunning) {
1397
+ if (!isStopped) {
1303
1398
  this.startTunnel(newTunnel.tunnelid);
1304
1399
  }
1305
1400
  logger.info("Tunnel configuration updated", {
1306
1401
  tunnelId: newTunnel.tunnelid,
1307
1402
  configId: newTunnel.configid,
1308
- wasRunning
1403
+ isStopped
1309
1404
  });
1310
1405
  return newTunnel;
1311
1406
  } catch (error) {
1407
+ logger.error("Error updating tunnel configuration", {
1408
+ configId: configid,
1409
+ error: error instanceof Error ? error.message : String(error)
1410
+ });
1312
1411
  try {
1313
1412
  const originalTunnel = await this.createTunnel({
1314
1413
  ...currentTunnelConfig,
@@ -1317,7 +1416,7 @@ var TunnelManager = class _TunnelManager {
1317
1416
  tunnelName: currentTunnelName,
1318
1417
  additionalForwarding: currentAdditionalForwarding
1319
1418
  });
1320
- if (wasRunning) {
1419
+ if (!isStopped) {
1321
1420
  await this.startTunnel(originalTunnel.tunnelid);
1322
1421
  }
1323
1422
  logger.warn("Restored original tunnel configuration after update failure", {
@@ -1358,7 +1457,6 @@ var TunnelManager = class _TunnelManager {
1358
1457
  }
1359
1458
  try {
1360
1459
  if (managed.isStopped) {
1361
- logger.debug(`Tunnel "${tunnelId}" is stopped. No greet message available.`);
1362
1460
  return null;
1363
1461
  }
1364
1462
  const messages = await managed.instance.getGreetMessage();
@@ -1381,13 +1479,26 @@ var TunnelManager = class _TunnelManager {
1381
1479
  const stats = this.tunnelStats.get(tunnelId);
1382
1480
  return stats || null;
1383
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
+ }
1384
1493
  /**
1385
1494
  * Registers a listener function to receive tunnel statistics updates.
1386
1495
  * The listener will be called whenever any tunnel's stats are updated.
1387
1496
  *
1388
1497
  * @param tunnelId - The tunnel ID to listen to stats for
1389
1498
  * @param listener - Function that receives tunnelId and stats when updates occur
1390
- * @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
1391
1502
  */
1392
1503
  async registerStatsListener(tunnelId, listener) {
1393
1504
  const managed = this.tunnelsByTunnelId.get(tunnelId);
@@ -1401,7 +1512,7 @@ var TunnelManager = class _TunnelManager {
1401
1512
  const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
1402
1513
  tunnelListeners.set(listenerId, listener);
1403
1514
  logger.info("Stats listener registered for tunnel", { tunnelId, listenerId });
1404
- return listenerId;
1515
+ return [listenerId, tunnelId];
1405
1516
  }
1406
1517
  async registerErrorListener(tunnelId, listener) {
1407
1518
  const managed = this.tunnelsByTunnelId.get(tunnelId);
@@ -1444,6 +1555,20 @@ var TunnelManager = class _TunnelManager {
1444
1555
  tunnelWorkerErrorListner?.set(listenerId, listener);
1445
1556
  logger.info("TunnelWorker error listener registered for tunnel", { tunnelId, listenerId });
1446
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
+ }
1447
1572
  /**
1448
1573
  * Removes a previously registered stats listener.
1449
1574
  *
@@ -1506,7 +1631,6 @@ var TunnelManager = class _TunnelManager {
1506
1631
  }
1507
1632
  try {
1508
1633
  if (managed.isStopped) {
1509
- logger.debug(`Tunnel "${tunnelId}" is stopped. Cannot fetch local server TLS info`);
1510
1634
  return false;
1511
1635
  }
1512
1636
  const tlsInfo = await managed.instance.getLocalServerTls();
@@ -1551,9 +1675,9 @@ var TunnelManager = class _TunnelManager {
1551
1675
  }
1552
1676
  setupErrorCallback(tunnelId, managed) {
1553
1677
  try {
1554
- const callback = (errorNo, errorMsg, recoverable) => {
1678
+ const callback = ({ errorNo, error, recoverable }) => {
1555
1679
  try {
1556
- const msg = typeof errorMsg === "string" ? errorMsg : String(errorMsg);
1680
+ const msg = typeof error === "string" ? error : String(error);
1557
1681
  const isFatal = true;
1558
1682
  logger.debug("Tunnel reported error", { tunnelId, errorNo, errorMsg: msg, recoverable });
1559
1683
  this.notifyErrorListeners(tunnelId, msg, isFatal);
@@ -1569,9 +1693,25 @@ var TunnelManager = class _TunnelManager {
1569
1693
  }
1570
1694
  setupDisconnectCallback(tunnelId, managed) {
1571
1695
  try {
1572
- const callback = (error, messages) => {
1696
+ const callback = ({ error, messages }) => {
1573
1697
  try {
1574
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
+ }
1575
1715
  const listeners = this.tunnelDisconnectListeners.get(tunnelId);
1576
1716
  if (!listeners) return;
1577
1717
  for (const [id, listener] of listeners) {
@@ -1628,7 +1768,7 @@ var TunnelManager = class _TunnelManager {
1628
1768
  if (tunnelListeners) {
1629
1769
  for (const [listenerId, listener] of tunnelListeners) {
1630
1770
  try {
1631
- listener(tunnelId, updatedStats);
1771
+ listener(tunnelId, normalizedStats);
1632
1772
  } catch (error) {
1633
1773
  logger.warn("Error in stats listener callback", { listenerId, tunnelId, error });
1634
1774
  }
@@ -1667,7 +1807,9 @@ var TunnelManager = class _TunnelManager {
1667
1807
  }
1668
1808
  startStaticFileServer(managed) {
1669
1809
  try {
1670
- const fileServerWorkerPath = import_node_path.default.resolve(__dirname, "../workers/file_serve_worker.js");
1810
+ const __filename3 = (0, import_node_url.fileURLToPath)(importMetaUrl);
1811
+ const __dirname2 = import_node_path.default.dirname(__filename3);
1812
+ const fileServerWorkerPath = import_node_path.default.join(__dirname2, "workers", "file_serve_worker.js");
1671
1813
  const staticServerWorker = new import_node_worker_threads.Worker(fileServerWorkerPath, {
1672
1814
  workerData: {
1673
1815
  dir: managed.serve,
@@ -1731,6 +1873,7 @@ var cliOptions = {
1731
1873
  v: { type: "boolean", description: "Print logs to stdout for Cli. Overrides PINGGY_LOG_STDOUT environment variable" },
1732
1874
  vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
1733
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." },
1734
1877
  // Save and load config
1735
1878
  saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
1736
1879
  conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
@@ -1805,7 +1948,8 @@ var defaultOptions = {
1805
1948
  httpsOnly: false,
1806
1949
  originalRequestUrl: false,
1807
1950
  allowPreflight: false,
1808
- reverseProxy: false
1951
+ reverseProxy: false,
1952
+ autoReconnect: false
1809
1953
  };
1810
1954
 
1811
1955
  // src/cli/extendedOptions.ts
@@ -2228,7 +2372,8 @@ async function buildFinalConfig(values, positionals) {
2228
2372
  serverAddress: server || defaultOptions.serverAddress,
2229
2373
  tunnelType: initialTunnel ? [initialTunnel] : defaultOptions.tunnelType,
2230
2374
  NoTUI: values.notui || false,
2231
- qrCode: qrCode || false
2375
+ qrCode: qrCode || false,
2376
+ autoReconnect: values.autoreconnect || false
2232
2377
  };
2233
2378
  parseType(finalConfig, values, type);
2234
2379
  parseToken(finalConfig, token || values.token);
@@ -2312,17 +2457,18 @@ function newStatus(tunnelState, errorCode, errorMsg) {
2312
2457
  if (tunnelState === "live" /* Live */) {
2313
2458
  assignedState = "running" /* Running */;
2314
2459
  } else if (tunnelState === "idle" /* New */) {
2315
- assignedState = "starting" /* Starting */;
2460
+ assignedState = "idle" /* New */;
2316
2461
  } else if (tunnelState === "closed" /* Closed */) {
2317
2462
  assignedState = "exited" /* Exited */;
2318
2463
  }
2464
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2319
2465
  return {
2320
2466
  state: assignedState,
2321
2467
  errorcode: errorCode,
2322
2468
  errormsg: errorMsg,
2323
- createdtimestamp: /* @__PURE__ */ new Date(),
2324
- starttimestamp: /* @__PURE__ */ new Date(),
2325
- endtimestamp: /* @__PURE__ */ new Date(),
2469
+ createdtimestamp: now,
2470
+ starttimestamp: now,
2471
+ endtimestamp: now,
2326
2472
  warnings: []
2327
2473
  };
2328
2474
  }
@@ -2336,6 +2482,14 @@ function newStats() {
2336
2482
  elapsedTime: 0
2337
2483
  };
2338
2484
  }
2485
+ var RemoteManagementStatus = {
2486
+ Connecting: "CONNECTING",
2487
+ Disconnecting: "DISCONNECTING",
2488
+ Reconnecting: "RECONNECTING",
2489
+ Running: "RUNNING",
2490
+ NotRunning: "NOT_RUNNING",
2491
+ Error: "ERROR"
2492
+ };
2339
2493
 
2340
2494
  // src/remote_management/handler.ts
2341
2495
  init_cjs_shims();
@@ -2349,8 +2503,16 @@ var HeaderModificationSchema = import_zod.z.object({
2349
2503
  value: import_zod.z.array(import_zod.z.string()).optional(),
2350
2504
  type: import_zod.z.enum(["add", "remove", "update"])
2351
2505
  });
2506
+ var AdditionalForwardingSchema = import_zod.z.object({
2507
+ remoteDomain: import_zod.z.string().optional(),
2508
+ localDomain: import_zod.z.string(),
2509
+ localPort: import_zod.z.number()
2510
+ });
2352
2511
  var TunnelConfigSchema = import_zod.z.object({
2353
- 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
2354
2516
  autoreconnect: import_zod.z.boolean(),
2355
2517
  basicauth: import_zod.z.array(import_zod.z.object({ username: import_zod.z.string(), password: import_zod.z.string() })).nullable(),
2356
2518
  bearerauth: import_zod.z.string().nullable(),
@@ -2374,10 +2536,30 @@ var TunnelConfigSchema = import_zod.z.object({
2374
2536
  statusCheckInterval: import_zod.z.number(),
2375
2537
  token: import_zod.z.string(),
2376
2538
  tunnelTimeout: import_zod.z.number(),
2377
- 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
+ ]),
2378
2546
  webdebuggerport: import_zod.z.number(),
2379
- xff: import_zod.z.string()
2380
- });
2547
+ xff: import_zod.z.string(),
2548
+ additionalForwarding: import_zod.z.array(AdditionalForwardingSchema).optional(),
2549
+ serve: import_zod.z.string().optional()
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
+ }));
2381
2563
  var StartSchema = import_zod.z.object({
2382
2564
  tunnelID: import_zod.z.string().uuid().nullable().optional(),
2383
2565
  tunnelConfig: TunnelConfigSchema
@@ -2407,32 +2589,36 @@ function tunnelConfigToPinggyOptions(config) {
2407
2589
  allowPreflight: config.allowPreflight,
2408
2590
  reverseProxy: config.noReverseProxy,
2409
2591
  force: config.force,
2592
+ autoReconnect: config.autoreconnect,
2410
2593
  optional: {
2411
2594
  sniServerName: config.localservertlssni || ""
2412
2595
  }
2413
2596
  };
2414
2597
  }
2415
- function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg) {
2598
+ function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg, additionalForwarding, serve) {
2416
2599
  const forwarding = Array.isArray(opts.forwarding) ? String(opts.forwarding[0].address).replace("//", "").replace(/\/$/, "") : String(opts.forwarding).replace("//", "").replace(/\/$/, "");
2600
+ const parsedForwardedHost = forwarding.split(":").length == 3 ? forwarding.split(":")[1] : forwarding.split(":")[0];
2601
+ const parsedLocalPort = forwarding.split(":").length == 3 ? parseInt(forwarding.split(":")[2], 10) : parseInt(forwarding.split(":")[1], 10);
2417
2602
  const tunnelType = Array.isArray(opts.tunnelType) ? opts.tunnelType[0] : opts.tunnelType ?? "http";
2418
2603
  const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
2419
2604
  return {
2420
2605
  allowPreflight: opts.allowPreflight ?? false,
2421
- autoreconnect: true,
2606
+ allowpreflight: opts.allowPreflight ?? false,
2607
+ autoreconnect: opts.autoReconnect ?? false,
2422
2608
  basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
2423
2609
  bearerauth: parsedTokens.length ? parsedTokens.join(",") : null,
2424
2610
  configid,
2425
2611
  configname: configName,
2426
2612
  greetmsg: greetMsg || "",
2427
2613
  force: opts.force ?? false,
2428
- forwardedhost: forwarding?.split(":")[1] || "localhost",
2614
+ forwardedhost: parsedForwardedHost || "localhost",
2429
2615
  fullRequestUrl: opts.originalRequestUrl ?? false,
2430
2616
  headermodification: opts.headerModification || [],
2431
2617
  //structured list
2432
2618
  httpsOnly: opts.httpsOnly ?? false,
2433
2619
  internalwebdebuggerport: 0,
2434
2620
  ipwhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : null,
2435
- localport: parseInt(forwarding?.split(":")[2] || "0", 10),
2621
+ localport: parsedLocalPort || 0,
2436
2622
  localservertlssni: null,
2437
2623
  regioncode: "",
2438
2624
  noReverseProxy: opts.reverseProxy ?? false,
@@ -2444,7 +2630,9 @@ function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls,
2444
2630
  type: tunnelType,
2445
2631
  webdebuggerport: Number(opts.webDebugger?.split(":")[0]) || 0,
2446
2632
  xff: opts.xForwardedFor ? "1" : "",
2447
- localsservertls: localserverTls || false
2633
+ localsservertls: localserverTls || false,
2634
+ additionalForwarding: additionalForwarding || [],
2635
+ serve: serve || ""
2448
2636
  };
2449
2637
  }
2450
2638
 
@@ -2453,11 +2641,24 @@ var TunnelOperations = class {
2453
2641
  constructor() {
2454
2642
  this.tunnelManager = TunnelManager.getInstance();
2455
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
+ }
2456
2657
  // --- Helper to construct TunnelResponse ---
2457
- async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName) {
2658
+ async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, additionalForwarding, serve) {
2458
2659
  const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
2459
2660
  this.tunnelManager.getTunnelStatus(tunnelid),
2460
- this.tunnelManager.getTunnelStats(tunnelid),
2661
+ this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
2461
2662
  this.tunnelManager.getLocalserverTlsInfo(tunnelid),
2462
2663
  this.tunnelManager.getTunnelGreetMessage(tunnelid),
2463
2664
  this.tunnelManager.getTunnelUrls(tunnelid)
@@ -2465,8 +2666,8 @@ var TunnelOperations = class {
2465
2666
  return {
2466
2667
  tunnelid,
2467
2668
  remoteurls,
2468
- tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg),
2469
- status: newStatus(status, "" /* NoError */, ""),
2669
+ tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg, additionalForwarding),
2670
+ status: this.buildStatus(tunnelid, status, "" /* NoError */),
2470
2671
  stats
2471
2672
  };
2472
2673
  }
@@ -2480,14 +2681,17 @@ var TunnelOperations = class {
2480
2681
  async handleStart(config) {
2481
2682
  try {
2482
2683
  const opts = tunnelConfigToPinggyOptions(config);
2483
- const { tunnelid, instance, tunnelName } = await this.tunnelManager.createTunnel({
2684
+ const additionalForwardingParsed = config.additionalForwarding || [];
2685
+ const { tunnelid, instance, tunnelName, additionalForwarding, serve } = await this.tunnelManager.createTunnel({
2484
2686
  ...opts,
2485
2687
  configid: config.configid,
2486
- tunnelName: config.configname
2688
+ tunnelName: config.configname,
2689
+ additionalForwarding: additionalForwardingParsed,
2690
+ serve: config.serve
2487
2691
  });
2488
2692
  this.tunnelManager.startTunnel(tunnelid);
2489
2693
  const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
2490
- const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName);
2694
+ const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, additionalForwarding, serve);
2491
2695
  return resp;
2492
2696
  } catch (err) {
2493
2697
  return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
@@ -2499,11 +2703,13 @@ var TunnelOperations = class {
2499
2703
  const tunnel = await this.tunnelManager.updateConfig({
2500
2704
  ...opts,
2501
2705
  configid: config.configid,
2502
- tunnelName: config.configname
2706
+ tunnelName: config.configname,
2707
+ additionalForwarding: config.additionalForwarding || [],
2708
+ serve: config.serve
2503
2709
  });
2504
2710
  if (!tunnel.instance || !tunnel.tunnelConfig)
2505
2711
  throw new Error("Invalid tunnel state after configuration update");
2506
- return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName);
2712
+ return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.additionalForwarding, tunnel.serve);
2507
2713
  } catch (err) {
2508
2714
  return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
2509
2715
  }
@@ -2516,19 +2722,19 @@ var TunnelOperations = class {
2516
2722
  }
2517
2723
  return Promise.all(
2518
2724
  tunnels.map(async (t) => {
2519
- const rawStats = this.tunnelManager.getTunnelStats(t.tunnelid);
2520
- const stats = rawStats ?? newStats();
2725
+ const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
2521
2726
  const [status, tlsInfo, greetMsg] = await Promise.all([
2522
2727
  this.tunnelManager.getTunnelStatus(t.tunnelid),
2523
2728
  this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
2524
2729
  this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
2525
2730
  ]);
2526
- const tunnelConfig = pinggyOptionsToTunnelConfig(t.tunnelConfig, t.configid, t.tunnelName, tlsInfo, greetMsg);
2731
+ const pinggyOptions = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
2732
+ const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configid, t.tunnelName, tlsInfo, greetMsg, t.additionalForwarding, t.serve);
2527
2733
  return {
2528
2734
  tunnelid: t.tunnelid,
2529
2735
  remoteurls: t.remoteurls,
2530
- status: newStatus(status, "" /* NoError */, ""),
2531
- stats,
2736
+ status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
2737
+ stats: rawStats,
2532
2738
  tunnelconfig: tunnelConfig
2533
2739
  };
2534
2740
  })
@@ -2542,7 +2748,7 @@ var TunnelOperations = class {
2542
2748
  const { configid } = this.tunnelManager.stopTunnel(tunnelid);
2543
2749
  const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2544
2750
  if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2545
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configid, managed.tunnelName);
2751
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2546
2752
  } catch (err) {
2547
2753
  return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
2548
2754
  }
@@ -2551,7 +2757,7 @@ var TunnelOperations = class {
2551
2757
  try {
2552
2758
  const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2553
2759
  if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2554
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName);
2760
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2555
2761
  } catch (err) {
2556
2762
  return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
2557
2763
  }
@@ -2561,7 +2767,7 @@ var TunnelOperations = class {
2561
2767
  await this.tunnelManager.restartTunnel(tunnelid);
2562
2768
  const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2563
2769
  if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2564
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName);
2770
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2565
2771
  } catch (err) {
2566
2772
  return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
2567
2773
  }
@@ -2572,6 +2778,34 @@ var TunnelOperations = class {
2572
2778
  handleUnregisterStatsListener(tunnelid, listnerId) {
2573
2779
  this.tunnelManager.deregisterStatsListener(tunnelid, listnerId);
2574
2780
  }
2781
+ handleGetTunnelStats(tunnelid) {
2782
+ try {
2783
+ const stats = this.tunnelManager.getTunnelStats(tunnelid);
2784
+ if (!stats) {
2785
+ return [newStats()];
2786
+ }
2787
+ return stats;
2788
+ } catch (err) {
2789
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats");
2790
+ }
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
+ }
2575
2809
  };
2576
2810
 
2577
2811
  // src/remote_management/websocket_handlers.ts
@@ -2604,11 +2838,13 @@ var WebSocketCommandHandler = class {
2604
2838
  }
2605
2839
  async handleStartReq(req, raw) {
2606
2840
  const dc = StartSchema.parse(raw);
2841
+ printer_default.info("Starting tunnel with config name: " + dc.tunnelConfig.configname);
2607
2842
  const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
2608
2843
  return this.wrapResponse(result, req);
2609
2844
  }
2610
2845
  async handleStopReq(req, raw) {
2611
2846
  const dc = StopSchema.parse(raw);
2847
+ printer_default.info("Stopping tunnel with ID: " + dc.tunnelID);
2612
2848
  const result = await this.tunnelHandler.handleStop(dc.tunnelID);
2613
2849
  return this.wrapResponse(result, req);
2614
2850
  }
@@ -2638,7 +2874,17 @@ var WebSocketCommandHandler = class {
2638
2874
  errResp.requestid = req.requestid;
2639
2875
  return errResp;
2640
2876
  }
2641
- 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);
2642
2888
  respObj.command = req.command;
2643
2889
  respObj.requestid = req.requestid;
2644
2890
  return respObj;
@@ -2711,6 +2957,12 @@ function handleConnectionStatusMessage(firstMessage) {
2711
2957
  // src/remote_management/remoteManagement.ts
2712
2958
  var RECONNECT_SLEEP_MS = 5e3;
2713
2959
  var PING_INTERVAL_MS = 3e4;
2960
+ var _remoteManagementState = {
2961
+ status: "NOT_RUNNING",
2962
+ errorMessage: ""
2963
+ };
2964
+ var _stopRequested = false;
2965
+ var currentWs = null;
2714
2966
  function buildRemoteManagementWsUrl(manage) {
2715
2967
  let baseUrl = (manage || "dashboard.pinggy.io").trim();
2716
2968
  if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
@@ -2744,95 +2996,133 @@ async function parseRemoteManagement(values) {
2744
2996
  }
2745
2997
  }
2746
2998
  async function initiateRemoteManagement(token, manage) {
2999
+ await printer_default.ensureDeps();
3000
+ await loadChalk();
2747
3001
  if (!token || token.trim().length === 0) {
2748
3002
  throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
2749
3003
  }
2750
3004
  const wsUrl = buildRemoteManagementWsUrl(manage);
2751
3005
  const wsHost = extractHostname(wsUrl);
2752
3006
  logger.info("Remote management mode enabled.");
2753
- let stopRequested = false;
3007
+ _stopRequested = false;
2754
3008
  const sigintHandler = () => {
2755
- stopRequested = true;
3009
+ _stopRequested = true;
2756
3010
  };
2757
3011
  process.once("SIGINT", sigintHandler);
2758
- let firstTry = true;
2759
- while (!stopRequested) {
2760
- if (firstTry) {
2761
- firstTry = false;
2762
- printer_default.print(`Connecting to ${wsHost}`);
2763
- logger.info("Connecting to remote management", { wsUrl });
2764
- } else {
2765
- printer_default.warn(`Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds.`);
2766
- logger.info("Reconnecting after sleep", { seconds: RECONNECT_SLEEP_MS / 1e3 });
2767
- await sleep(RECONNECT_SLEEP_MS);
2768
- if (stopRequested) break;
2769
- printer_default.print(`Connecting to ${wsHost}`);
2770
- 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) });
2771
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) => {
2772
3035
  const ws = new import_ws.default(wsUrl, {
2773
3036
  headers: { Authorization: `Bearer ${token}` }
2774
3037
  });
2775
- await new Promise((resolve) => {
2776
- let heartbeat;
2777
- const startHeartbeat = () => {
2778
- heartbeat = setInterval(() => {
2779
- if (ws.readyState !== import_ws.default.OPEN) return;
2780
- ws.ping();
2781
- }, PING_INTERVAL_MS);
2782
- };
2783
- ws.on("open", () => {
2784
- printer_default.success(`Connected to ${wsHost}`);
2785
- startHeartbeat();
2786
- });
2787
- ws.on("ping", () => ws.pong());
2788
- let firstMessage = true;
2789
- ws.on("message", async (data) => {
2790
- try {
2791
- if (firstMessage) {
2792
- firstMessage = false;
2793
- const ok = handleConnectionStatusMessage(data);
2794
- if (!ok) {
2795
- ws.close();
2796
- }
2797
- return;
2798
- }
2799
- const text = data.toString("utf8");
2800
- const req = JSON.parse(text);
2801
- const webSocketHandler = new WebSocketCommandHandler();
2802
- await webSocketHandler.handle(ws, req);
2803
- } catch (e) {
2804
- logger.warn("Failed handling websocket message", { error: String(e) });
2805
- }
2806
- });
2807
- ws.on("unexpected-response", (_req, res) => {
2808
- if (res.statusCode === 401) {
2809
- printer_default.error("Unauthorized. Please enter a valid token.");
2810
- logger.error("Unauthorized (401) on remote management connect");
2811
- stopRequested = true;
2812
- ws.close();
2813
- } else {
2814
- printer_default.warn(`Unexpected HTTP response ${res.statusCode} from server. Retrying...`);
2815
- logger.warn("Unexpected HTTP response on WebSocket connect", { statusCode: res.statusCode });
2816
- 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;
2817
3060
  }
2818
- });
2819
- ws.on("close", (code, reason) => {
2820
- logger.info("WebSocket closed", { code, reason: reason.toString() });
2821
- printer_default.warn(`Disconnected from remote management (code: ${code}).Retrying...`);
2822
- clearInterval(heartbeat);
2823
- resolve();
2824
- });
2825
- ws.on("error", (err) => {
2826
- printer_default.error(err);
2827
- logger.warn("WebSocket error", { error: err.message });
2828
- clearInterval(heartbeat);
2829
- resolve();
2830
- });
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();
2831
3090
  });
2832
- 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();
2833
3116
  }
2834
- process.removeListener("SIGINT", sigintHandler);
2835
- 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
+ };
2836
3126
  }
2837
3127
 
2838
3128
  // src/utils/parseArgs.ts
@@ -2926,6 +3216,57 @@ var TunnelData = {
2926
3216
  var activeTui = null;
2927
3217
  var disconnectState = null;
2928
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
+ }
2929
3270
  async function startCli(finalConfig, manager) {
2930
3271
  await printer_default.ensureDeps();
2931
3272
  const chalk = await loadChalk();
@@ -2981,64 +3322,61 @@ async function startCli(finalConfig, manager) {
2981
3322
  logger.warn("Failed to wait for TUI exit", e);
2982
3323
  } finally {
2983
3324
  activeTui = null;
2984
- messages.forEach(function(m) {
3325
+ printer_default.warn(`Error in tunnel:`);
3326
+ messages?.forEach(function(m) {
2985
3327
  printer_default.warn(m);
2986
3328
  });
2987
- process.exit(0);
3329
+ if (!finalConfig.autoReconnect) {
3330
+ process.exit(0);
3331
+ }
2988
3332
  }
2989
3333
  } else {
2990
- messages.forEach(function(m) {
3334
+ messages?.forEach(function(m) {
2991
3335
  printer_default.warn(m);
2992
3336
  });
2993
- process.exit(0);
3337
+ if (!finalConfig.autoReconnect) {
3338
+ process.exit(0);
3339
+ }
3340
+ }
3341
+ if (finalConfig.autoReconnect) {
3342
+ printer_default.startSpinner("Reconnecting to Pinggy");
2994
3343
  }
2995
3344
  });
2996
- if (!finalConfig.NoTUI) {
2997
- const { withFullScreen } = await import("fullscreen-ink");
2998
- const { default: TunnelTui2 } = await Promise.resolve().then(() => (init_tui(), tui_exports));
2999
- const React3 = await import("react");
3000
- const isTTYEnabled = process.stdin.isTTY;
3001
- const TunnelTuiWrapper = ({ finalConfig: finalConfig2, urls, greet }) => {
3002
- const [disconnectInfo, setDisconnectInfo] = React3.useState(null);
3003
- React3.useEffect(() => {
3004
- updateDisconnectState = setDisconnectInfo;
3005
- return () => {
3006
- updateDisconnectState = null;
3007
- };
3008
- }, []);
3009
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3010
- TunnelTui2,
3011
- {
3012
- urls: urls ?? [],
3013
- greet: greet ?? "",
3014
- tunnelConfig: finalConfig2,
3015
- disconnectInfo
3016
- }
3017
- );
3018
- };
3019
- const tui = withFullScreen(
3020
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3021
- TunnelTuiWrapper,
3022
- {
3023
- finalConfig,
3024
- urls: TunnelData.urls,
3025
- greet: TunnelData.greet
3026
- }
3027
- )
3028
- );
3029
- activeTui = tui;
3030
- if (isTTYEnabled) {
3345
+ try {
3346
+ await manager2.registerStartListener(tunnel.tunnelid, async (tunnelId, urls) => {
3031
3347
  try {
3032
- await tui.start();
3033
- await tui.waitUntilExit();
3348
+ printer_default.stopSpinnerSuccess("Reconnected to Pinggy");
3034
3349
  } catch (e) {
3035
- logger.warn("TUI error", e);
3036
- } finally {
3037
- activeTui = null;
3038
3350
  }
3039
- } else {
3040
- printer_default.warn("Unable to initiate the TUI: your terminal does not support the required input mode.");
3041
- }
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);
3042
3380
  }
3043
3381
  } catch (err) {
3044
3382
  printer_default.stopSpinnerFail("Failed to connect");
@@ -3054,6 +3392,8 @@ var import_fs3 = require("fs");
3054
3392
  init_logger();
3055
3393
  async function main() {
3056
3394
  try {
3395
+ await printer_default.ensureDeps();
3396
+ await loadChalk();
3057
3397
  const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
3058
3398
  configureLogger(values);
3059
3399
  const manager = TunnelManager.getInstance();
@@ -3101,5 +3441,8 @@ if (entryFile && entryFile === currentFile) {
3101
3441
  0 && (module.exports = {
3102
3442
  TunnelManager,
3103
3443
  TunnelOperations,
3104
- enablePackageLogging
3444
+ closeRemoteManagement,
3445
+ enablePackageLogging,
3446
+ getRemoteManagementState,
3447
+ initiateRemoteManagement
3105
3448
  });