pinggy 0.3.5 → 0.3.7

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.
Files changed (68) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-65R2GMKQ.js +2101 -0
  3. package/dist/index.cjs +1798 -1349
  4. package/dist/index.d.cts +616 -0
  5. package/dist/index.d.ts +616 -0
  6. package/dist/index.js +24 -2
  7. package/dist/{main-K44C44NW.js → main-2QDG7PWL.js} +166 -1705
  8. package/package.json +2 -3
  9. package/.github/workflows/npm-publish-github-packages.yml +0 -34
  10. package/.github/workflows/publish-binaries.yml +0 -223
  11. package/Makefile +0 -4
  12. package/caxa_build.js +0 -24
  13. package/dist/chunk-T5ESYDJY.js +0 -121
  14. package/ent.plist +0 -14
  15. package/jest.config.js +0 -19
  16. package/src/_tests_/build_config.test.ts +0 -91
  17. package/src/cli/buildConfig.ts +0 -535
  18. package/src/cli/defaults.ts +0 -20
  19. package/src/cli/extendedOptions.ts +0 -153
  20. package/src/cli/help.ts +0 -43
  21. package/src/cli/options.ts +0 -50
  22. package/src/cli/starCli.ts +0 -229
  23. package/src/index.ts +0 -31
  24. package/src/logger.ts +0 -138
  25. package/src/main.ts +0 -87
  26. package/src/remote_management/handler.ts +0 -244
  27. package/src/remote_management/remoteManagement.ts +0 -226
  28. package/src/remote_management/remote_schema.ts +0 -176
  29. package/src/remote_management/websocket_handlers.ts +0 -180
  30. package/src/tui/blessed/TunnelTui.ts +0 -340
  31. package/src/tui/blessed/components/DisplayUpdaters.ts +0 -189
  32. package/src/tui/blessed/components/KeyBindings.ts +0 -236
  33. package/src/tui/blessed/components/Modals.ts +0 -302
  34. package/src/tui/blessed/components/UIComponents.ts +0 -306
  35. package/src/tui/blessed/components/index.ts +0 -4
  36. package/src/tui/blessed/config.ts +0 -53
  37. package/src/tui/blessed/headerFetcher.ts +0 -42
  38. package/src/tui/blessed/index.ts +0 -2
  39. package/src/tui/blessed/qrCodeGenerator.ts +0 -20
  40. package/src/tui/blessed/webDebuggerConnection.ts +0 -128
  41. package/src/tui/ink/asciArt.ts +0 -7
  42. package/src/tui/ink/hooks/useQrCodes.ts +0 -27
  43. package/src/tui/ink/hooks/useReqResHeaders.ts +0 -27
  44. package/src/tui/ink/hooks/useTerminalSize.ts +0 -26
  45. package/src/tui/ink/hooks/useTerminalStats.ts +0 -24
  46. package/src/tui/ink/hooks/useWebDebugger.ts +0 -98
  47. package/src/tui/ink/index.tsx +0 -243
  48. package/src/tui/ink/layout/Borders.tsx +0 -15
  49. package/src/tui/ink/layout/Container.tsx +0 -15
  50. package/src/tui/ink/sections/DebuggerDetailModal.tsx +0 -53
  51. package/src/tui/ink/sections/KeyBindings.tsx +0 -58
  52. package/src/tui/ink/sections/QrCodeSection.tsx +0 -28
  53. package/src/tui/ink/sections/StatsSection.tsx +0 -20
  54. package/src/tui/ink/sections/URLsSection.tsx +0 -53
  55. package/src/tui/ink/utils/utils.ts +0 -35
  56. package/src/tui/spinner/spinner.ts +0 -64
  57. package/src/tunnel_manager/TunnelManager.ts +0 -1212
  58. package/src/types.ts +0 -255
  59. package/src/utils/FileServer.ts +0 -112
  60. package/src/utils/detect_vc_redist_on_windows.ts +0 -111
  61. package/src/utils/getFreePort.ts +0 -41
  62. package/src/utils/htmlTemplates.ts +0 -146
  63. package/src/utils/parseArgs.ts +0 -79
  64. package/src/utils/printer.ts +0 -81
  65. package/src/utils/util.ts +0 -18
  66. package/src/workers/file_serve_worker.ts +0 -33
  67. package/tsconfig.json +0 -17
  68. package/tsup.config.ts +0 -12
package/dist/index.cjs CHANGED
@@ -29,6 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
29
29
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
30
  mod
31
31
  ));
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
33
 
33
34
  // node_modules/tsup/assets/cjs_shims.js
34
35
  var getImportMetaUrl, importMetaUrl;
@@ -321,6 +322,10 @@ var init_TunnelManager = __esm({
321
322
  this.tunnelDisconnectListeners = /* @__PURE__ */ new Map();
322
323
  this.tunnelWorkerErrorListeners = /* @__PURE__ */ new Map();
323
324
  this.tunnelStartListeners = /* @__PURE__ */ new Map();
325
+ this.tunnelWillReconnectListeners = /* @__PURE__ */ new Map();
326
+ this.tunnelReconnectingListeners = /* @__PURE__ */ new Map();
327
+ this.tunnelReconnectionCompletedListeners = /* @__PURE__ */ new Map();
328
+ this.tunnelReconnectionFailedListeners = /* @__PURE__ */ new Map();
324
329
  }
325
330
  static getInstance() {
326
331
  if (!_TunnelManager.instance) {
@@ -403,6 +408,10 @@ var init_TunnelManager = __esm({
403
408
  this.setupStatsCallback(params.tunnelid, managed);
404
409
  this.setupErrorCallback(params.tunnelid, managed);
405
410
  this.setupDisconnectCallback(params.tunnelid, managed);
411
+ this.setupWillReconnectCallback(params.tunnelid, managed);
412
+ this.setupReconnectingCallback(params.tunnelid, managed);
413
+ this.setupReconnectionCompletedCallback(params.tunnelid, managed);
414
+ this.setupReconnectionFailedCallback(params.tunnelid, managed);
406
415
  this.setUpTunnelWorkerErrorCallback(params.tunnelid, managed);
407
416
  this.tunnelsByTunnelId.set(params.tunnelid, managed);
408
417
  this.tunnelsByConfigId.set(params.configid, managed);
@@ -505,6 +514,10 @@ var init_TunnelManager = __esm({
505
514
  this.tunnelDisconnectListeners.delete(tunnelId);
506
515
  this.tunnelWorkerErrorListeners.delete(tunnelId);
507
516
  this.tunnelStartListeners.delete(tunnelId);
517
+ this.tunnelWillReconnectListeners.delete(tunnelId);
518
+ this.tunnelReconnectingListeners.delete(tunnelId);
519
+ this.tunnelReconnectionCompletedListeners.delete(tunnelId);
520
+ this.tunnelReconnectionFailedListeners.delete(tunnelId);
508
521
  managed.serveWorker = null;
509
522
  managed.warnings = managed.warnings ?? [];
510
523
  managed.isStopped = true;
@@ -638,6 +651,10 @@ var init_TunnelManager = __esm({
638
651
  this.tunnelDisconnectListeners.delete(managed.tunnelid);
639
652
  this.tunnelWorkerErrorListeners.delete(managed.tunnelid);
640
653
  this.tunnelStartListeners.delete(managed.tunnelid);
654
+ this.tunnelWillReconnectListeners.delete(managed.tunnelid);
655
+ this.tunnelReconnectingListeners.delete(managed.tunnelid);
656
+ this.tunnelReconnectionCompletedListeners.delete(managed.tunnelid);
657
+ this.tunnelReconnectionFailedListeners.delete(managed.tunnelid);
641
658
  this.tunnelsByTunnelId.delete(managed.tunnelid);
642
659
  this.tunnelsByConfigId.delete(managed.configid);
643
660
  } catch (e) {
@@ -718,6 +735,10 @@ var init_TunnelManager = __esm({
718
735
  this.tunnelDisconnectListeners.delete(tunnelid);
719
736
  this.tunnelWorkerErrorListeners.delete(tunnelid);
720
737
  this.tunnelStartListeners.delete(tunnelid);
738
+ this.tunnelWillReconnectListeners.delete(tunnelid);
739
+ this.tunnelReconnectingListeners.delete(tunnelid);
740
+ this.tunnelReconnectionCompletedListeners.delete(tunnelid);
741
+ this.tunnelReconnectionFailedListeners.delete(tunnelid);
721
742
  const newTunnel = await this._createTunnelWithProcessedConfig({
722
743
  configid: currentConfigId,
723
744
  tunnelid,
@@ -974,6 +995,58 @@ var init_TunnelManager = __esm({
974
995
  logger.info("Start listener registered for tunnel", { tunnelId, listenerId });
975
996
  return listenerId;
976
997
  }
998
+ async registerWillReconnectListener(tunnelId, listener) {
999
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
1000
+ if (!managed) {
1001
+ throw new Error(`Tunnel "${tunnelId}" not found`);
1002
+ }
1003
+ if (!this.tunnelWillReconnectListeners.has(tunnelId)) {
1004
+ this.tunnelWillReconnectListeners.set(tunnelId, /* @__PURE__ */ new Map());
1005
+ }
1006
+ const listenerId = getRandomId();
1007
+ this.tunnelWillReconnectListeners.get(tunnelId).set(listenerId, listener);
1008
+ logger.info("WillReconnect listener registered for tunnel", { tunnelId, listenerId });
1009
+ return listenerId;
1010
+ }
1011
+ async registerReconnectingListener(tunnelId, listener) {
1012
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
1013
+ if (!managed) {
1014
+ throw new Error(`Tunnel "${tunnelId}" not found`);
1015
+ }
1016
+ if (!this.tunnelReconnectingListeners.has(tunnelId)) {
1017
+ this.tunnelReconnectingListeners.set(tunnelId, /* @__PURE__ */ new Map());
1018
+ }
1019
+ const listenerId = getRandomId();
1020
+ this.tunnelReconnectingListeners.get(tunnelId).set(listenerId, listener);
1021
+ logger.info("Reconnecting listener registered for tunnel", { tunnelId, listenerId });
1022
+ return listenerId;
1023
+ }
1024
+ async registerReconnectionCompletedListener(tunnelId, listener) {
1025
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
1026
+ if (!managed) {
1027
+ throw new Error(`Tunnel "${tunnelId}" not found`);
1028
+ }
1029
+ if (!this.tunnelReconnectionCompletedListeners.has(tunnelId)) {
1030
+ this.tunnelReconnectionCompletedListeners.set(tunnelId, /* @__PURE__ */ new Map());
1031
+ }
1032
+ const listenerId = getRandomId();
1033
+ this.tunnelReconnectionCompletedListeners.get(tunnelId).set(listenerId, listener);
1034
+ logger.info("ReconnectionCompleted listener registered for tunnel", { tunnelId, listenerId });
1035
+ return listenerId;
1036
+ }
1037
+ async registerReconnectionFailedListener(tunnelId, listener) {
1038
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
1039
+ if (!managed) {
1040
+ throw new Error(`Tunnel "${tunnelId}" not found`);
1041
+ }
1042
+ if (!this.tunnelReconnectionFailedListeners.has(tunnelId)) {
1043
+ this.tunnelReconnectionFailedListeners.set(tunnelId, /* @__PURE__ */ new Map());
1044
+ }
1045
+ const listenerId = getRandomId();
1046
+ this.tunnelReconnectionFailedListeners.get(tunnelId).set(listenerId, listener);
1047
+ logger.info("ReconnectionFailed listener registered for tunnel", { tunnelId, listenerId });
1048
+ return listenerId;
1049
+ }
977
1050
  /**
978
1051
  * Removes a previously registered stats listener.
979
1052
  *
@@ -1028,6 +1101,72 @@ var init_TunnelManager = __esm({
1028
1101
  logger.warn("Attempted to deregister non-existent disconnect listener", { tunnelId, listenerId });
1029
1102
  }
1030
1103
  }
1104
+ deregisterWillReconnectListener(tunnelId, listenerId) {
1105
+ const listeners = this.tunnelWillReconnectListeners.get(tunnelId);
1106
+ if (!listeners) {
1107
+ logger.warn("No will-reconnect listeners found for tunnel", { tunnelId });
1108
+ return;
1109
+ }
1110
+ ;
1111
+ const removed = listeners.delete(listenerId);
1112
+ if (removed) {
1113
+ logger.info("WillReconnect listener deregistered", { tunnelId, listenerId });
1114
+ if (listeners.size === 0) {
1115
+ this.tunnelWillReconnectListeners.delete(tunnelId);
1116
+ }
1117
+ } else {
1118
+ logger.warn("Attempted to deregister non-existent will-reconnect listener", { tunnelId, listenerId });
1119
+ }
1120
+ }
1121
+ deregisterReconnectingListener(tunnelId, listenerId) {
1122
+ const listeners = this.tunnelReconnectingListeners.get(tunnelId);
1123
+ if (!listeners) {
1124
+ logger.warn("No reconnecting listeners found for tunnel", { tunnelId });
1125
+ return;
1126
+ }
1127
+ ;
1128
+ const removed = listeners.delete(listenerId);
1129
+ if (removed) {
1130
+ logger.info("Reconnecting listener deregistered", { tunnelId, listenerId });
1131
+ if (listeners.size === 0) {
1132
+ this.tunnelReconnectingListeners.delete(tunnelId);
1133
+ }
1134
+ } else {
1135
+ logger.warn("Attempted to deregister non-existent reconnecting listener", { tunnelId, listenerId });
1136
+ }
1137
+ }
1138
+ deregisterReconnectionCompletedListener(tunnelId, listenerId) {
1139
+ const listeners = this.tunnelReconnectionCompletedListeners.get(tunnelId);
1140
+ if (!listeners) {
1141
+ logger.warn("No reconnection completed listeners found for tunnel", { tunnelId });
1142
+ return;
1143
+ }
1144
+ const removed = listeners.delete(listenerId);
1145
+ if (removed) {
1146
+ logger.info("Reconnection completed listener deregistered", { tunnelId, listenerId });
1147
+ if (listeners.size === 0) {
1148
+ this.tunnelReconnectionCompletedListeners.delete(tunnelId);
1149
+ }
1150
+ } else {
1151
+ logger.warn("Attempted to deregister non-existent reconnection completed listener", { tunnelId, listenerId });
1152
+ }
1153
+ }
1154
+ deregisterReconnectionFailedListener(tunnelId, listenerId) {
1155
+ const listeners = this.tunnelReconnectionFailedListeners.get(tunnelId);
1156
+ if (!listeners) {
1157
+ logger.warn("No reconnection failed listeners found for tunnel", { tunnelId });
1158
+ return;
1159
+ }
1160
+ const removed = listeners.delete(listenerId);
1161
+ if (removed) {
1162
+ logger.info("Reconnection failed listener deregistered", { tunnelId, listenerId });
1163
+ if (listeners.size === 0) {
1164
+ this.tunnelReconnectionFailedListeners.delete(tunnelId);
1165
+ }
1166
+ } else {
1167
+ logger.warn("Attempted to deregister non-existent reconnection failed listener", { tunnelId, listenerId });
1168
+ }
1169
+ }
1031
1170
  async getLocalserverTlsInfo(tunnelId) {
1032
1171
  const managed = this.tunnelsByTunnelId.get(tunnelId);
1033
1172
  if (!managed) {
@@ -1106,17 +1245,6 @@ var init_TunnelManager = __esm({
1106
1245
  managedTunnel.isStopped = true;
1107
1246
  managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
1108
1247
  }
1109
- if (managedTunnel && managedTunnel.autoReconnect) {
1110
- logger.info("Auto-reconnecting tunnel", { tunnelId });
1111
- setTimeout(async () => {
1112
- try {
1113
- await this.restartTunnel(tunnelId);
1114
- logger.info("Tunnel auto-reconnected successfully", { tunnelId });
1115
- } catch (e) {
1116
- logger.error("Failed to auto-reconnect tunnel", { tunnelId, e });
1117
- }
1118
- }, 1e4);
1119
- }
1120
1248
  const listeners = this.tunnelDisconnectListeners.get(tunnelId);
1121
1249
  if (!listeners) return;
1122
1250
  for (const [id, listener] of listeners) {
@@ -1136,6 +1264,140 @@ var init_TunnelManager = __esm({
1136
1264
  logger.warn("Failed to set up disconnect callback", { tunnelId, error });
1137
1265
  }
1138
1266
  }
1267
+ /**
1268
+ * Called when the tunnel disconnects and the SDK is about to start reconnecting.
1269
+ * Notifies registered will-reconnect listeners.
1270
+ */
1271
+ setupWillReconnectCallback(tunnelId, managed) {
1272
+ try {
1273
+ const callback = ({ error, messages }) => {
1274
+ try {
1275
+ logger.info("Tunnel will reconnect", { tunnelId, error, messages });
1276
+ const listeners = this.tunnelWillReconnectListeners.get(tunnelId);
1277
+ if (!listeners) return;
1278
+ for (const [id, listener] of listeners) {
1279
+ try {
1280
+ listener(tunnelId, error, messages);
1281
+ } catch (err) {
1282
+ logger.debug("Error in will-reconnect-listener callback", { listenerId: id, tunnelId, err });
1283
+ }
1284
+ }
1285
+ } catch (e) {
1286
+ logger.warn("Error handling will-reconnect callback", { tunnelId, e });
1287
+ }
1288
+ };
1289
+ managed.instance.setWillReconnectCallback(callback);
1290
+ logger.debug("WillReconnect callback set up for tunnel", { tunnelId });
1291
+ } catch (error) {
1292
+ logger.warn("Failed to set up will-reconnect callback", { tunnelId, error });
1293
+ }
1294
+ }
1295
+ /**
1296
+ * Called for each reconnection attempt with the current retry count.
1297
+ * Notifies registered reconnecting listeners.
1298
+ */
1299
+ setupReconnectingCallback(tunnelId, managed) {
1300
+ try {
1301
+ const callback = ({ retryCnt }) => {
1302
+ try {
1303
+ logger.info("Tunnel reconnecting", { tunnelId, retryCnt });
1304
+ const listeners = this.tunnelReconnectingListeners.get(tunnelId);
1305
+ if (!listeners) return;
1306
+ for (const [id, listener] of listeners) {
1307
+ try {
1308
+ listener(tunnelId, retryCnt);
1309
+ } catch (err) {
1310
+ logger.debug("Error in reconnecting-listener callback", { listenerId: id, tunnelId, err });
1311
+ }
1312
+ }
1313
+ } catch (e) {
1314
+ logger.warn("Error handling reconnecting callback", { tunnelId, e });
1315
+ }
1316
+ };
1317
+ managed.instance.setReconnectingCallback(callback);
1318
+ logger.debug("Reconnecting callback set up for tunnel", { tunnelId });
1319
+ } catch (error) {
1320
+ logger.warn("Failed to set up reconnecting callback", { tunnelId, error });
1321
+ }
1322
+ }
1323
+ /**
1324
+ * Called when reconnection succeeds. Updates tunnel state back to active,
1325
+ * and notifies registered reconnection-completed and start listeners with new URLs.
1326
+ */
1327
+ setupReconnectionCompletedCallback(tunnelId, managed) {
1328
+ try {
1329
+ const callback = ({ urls }) => {
1330
+ try {
1331
+ logger.info("Tunnel reconnection completed", { tunnelId, urls });
1332
+ const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1333
+ if (managedTunnel) {
1334
+ managedTunnel.isStopped = false;
1335
+ managedTunnel.startedAt = (/* @__PURE__ */ new Date()).toISOString();
1336
+ managedTunnel.stoppedAt = null;
1337
+ }
1338
+ const listeners = this.tunnelReconnectionCompletedListeners.get(tunnelId);
1339
+ if (listeners) {
1340
+ for (const [id, listener] of listeners) {
1341
+ try {
1342
+ listener(tunnelId, urls);
1343
+ } catch (err) {
1344
+ logger.debug("Error in reconnection-completed-listener callback", { listenerId: id, tunnelId, err });
1345
+ }
1346
+ }
1347
+ }
1348
+ const startListeners = this.tunnelStartListeners.get(tunnelId);
1349
+ if (startListeners) {
1350
+ for (const [id, listener] of startListeners) {
1351
+ try {
1352
+ listener(tunnelId, urls);
1353
+ } catch (err) {
1354
+ logger.debug("Error in start-listener callback on reconnection", { listenerId: id, tunnelId, err });
1355
+ }
1356
+ }
1357
+ }
1358
+ } catch (e) {
1359
+ logger.warn("Error handling reconnection-completed callback", { tunnelId, e });
1360
+ }
1361
+ };
1362
+ managed.instance.setReconnectionCompletedCallback(callback);
1363
+ logger.debug("ReconnectionCompleted callback set up for tunnel", { tunnelId });
1364
+ } catch (error) {
1365
+ logger.warn("Failed to set up reconnection-completed callback", { tunnelId, error });
1366
+ }
1367
+ }
1368
+ /**
1369
+ * Called when all reconnection attempts are exhausted.
1370
+ * Marks the tunnel as stopped and notifies registered reconnection-failed listeners.
1371
+ */
1372
+ setupReconnectionFailedCallback(tunnelId, managed) {
1373
+ try {
1374
+ const callback = ({ retryCnt }) => {
1375
+ try {
1376
+ logger.error("Tunnel reconnection failed", { tunnelId, retryCnt });
1377
+ const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1378
+ if (managedTunnel) {
1379
+ managedTunnel.isStopped = true;
1380
+ managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
1381
+ }
1382
+ const listeners = this.tunnelReconnectionFailedListeners.get(tunnelId);
1383
+ if (!listeners) return;
1384
+ for (const [id, listener] of listeners) {
1385
+ try {
1386
+ listener(tunnelId, retryCnt);
1387
+ } catch (err) {
1388
+ logger.debug("Error in reconnection-failed-listener callback", { listenerId: id, tunnelId, err });
1389
+ }
1390
+ }
1391
+ } catch (e) {
1392
+ logger.warn("Error handling reconnection-failed callback", { tunnelId, e });
1393
+ }
1394
+ };
1395
+ managed.instance.setReconnectionFailedCallback(callback);
1396
+ logger.debug("ReconnectionFailed callback set up for tunnel", { tunnelId });
1397
+ } catch (error) {
1398
+ logger.warn("Failed to set up reconnection-failed callback", { tunnelId, error });
1399
+ }
1400
+ }
1139
1401
  setUpTunnelWorkerErrorCallback(tunnelId, managed) {
1140
1402
  try {
1141
1403
  const callback = (error) => {
@@ -1251,1439 +1513,1461 @@ var init_TunnelManager = __esm({
1251
1513
  }
1252
1514
  });
1253
1515
 
1254
- // src/cli/options.ts
1255
- var cliOptions;
1256
- var init_options = __esm({
1257
- "src/cli/options.ts"() {
1258
- "use strict";
1259
- init_cjs_shims();
1260
- cliOptions = {
1261
- // SSH-like options
1262
- R: { type: "string", multiple: true, description: "Local port. Eg. -R0:localhost:3000 will forward tunnel connections to local port 3000." },
1263
- L: { type: "string", multiple: true, description: "Web Debugger address. Eg. -L4300:localhost:4300 will start web debugger on port 4300." },
1264
- o: { type: "string", multiple: true, description: "Options", hidden: true },
1265
- "server-port": { type: "string", short: "p", description: "Pinggy server port. Default: 443" },
1266
- v4: { type: "boolean", short: "4", description: "IPv4 only", hidden: true },
1267
- v6: { type: "boolean", short: "6", description: "IPv6 only", hidden: true },
1268
- // These options appear in the ssh command, but we ignore it in CLI
1269
- t: { type: "boolean", description: "hidden", hidden: true },
1270
- T: { type: "boolean", description: "hidden", hidden: true },
1271
- n: { type: "boolean", description: "hidden", hidden: true },
1272
- N: { type: "boolean", description: "hidden", hidden: true },
1273
- // Better options
1274
- type: { type: "string", description: "Type of the connection. Eg. --type tcp" },
1275
- localport: { type: "string", short: "l", description: "Takes input as [protocol:][host:]port. Eg. --localport https://localhost:8000 OR -l 3000" },
1276
- debugger: { type: "string", short: "d", description: "Port for web debugger. Eg. --debugger 4300 OR -d 4300" },
1277
- token: { type: "string", description: "Token for authentication. Eg. --token TOKEN_VALUE" },
1278
- // Logging options (CLI overrides env)
1279
- loglevel: { type: "string", description: "Logging level: ERROR, INFO, DEBUG. Overrides PINGGY_LOG_LEVEL environment variable" },
1280
- logfile: { type: "string", description: "Path to log file. Overrides PINGGY_LOG_FILE environment variable" },
1281
- v: { type: "boolean", description: "Print logs to stdout for Cli. Overrides PINGGY_LOG_STDOUT environment variable" },
1282
- vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
1283
- vvv: { type: "boolean", description: "Enable all logs from Cli, SDK and internal components." },
1284
- autoreconnect: { type: "string", short: "a", description: "Automatically reconnect tunnel on failure. Use -a (defaults to true), -a true, or -a false." },
1285
- // Save and load config
1286
- saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
1287
- conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
1288
- // File server
1289
- serve: { type: "string", description: "Start a webserver to serve files from the specified path. Eg --serve /path/to/files" },
1290
- // Remote Control
1291
- "remote-management": { type: "string", description: "Enable remote management of tunnels with token. Eg. --remote-management API_KEY" },
1292
- manage: { type: "string", description: "Provide a server address to manage tunnels. Eg --manage dashboard.pinggy.io" },
1293
- notui: { type: "boolean", description: "Disable TUI in remote management mode" },
1294
- // Misc
1295
- version: { type: "boolean", description: "Print version" },
1296
- // Help
1297
- help: { type: "boolean", short: "h", description: "Show this help message" }
1298
- };
1299
- }
1300
- });
1301
-
1302
- // src/cli/help.ts
1303
- function printHelpMessage() {
1304
- console.log("\nPinggy CLI Tool - Create secure tunnels to your localhost.");
1305
- console.log("\nUsage:");
1306
- console.log(" pinggy [options] -l <port>\n");
1307
- console.log("Options:");
1308
- for (const [key, value] of Object.entries(cliOptions)) {
1309
- if (value.hidden) continue;
1310
- const short = "short" in value && value.short ? `-${value.short}, ` : " ";
1311
- const optType = value.type === "boolean" ? "" : "<value>";
1312
- console.log(` ${short}--${key.padEnd(17)} ${optType.padEnd(8)} ${value.description}`);
1313
- }
1314
- console.log("\nExtended options :");
1315
- console.log(" x:https Enforce HTTPS only (redirect HTTP to HTTPS)");
1316
- console.log(" x:noreverseproxy Disable built-in reverse-proxy header injection");
1317
- console.log(" x:localservertls:host Connect to local HTTPS server with SNI");
1318
- console.log(" x:passpreflight Pass CORS preflight requests unchanged");
1319
- console.log(" a:Key:Val Add header");
1320
- console.log(" u:Key:Val Update header");
1321
- console.log(" r:Key Remove header");
1322
- console.log(" b:user:pass Basic auth");
1323
- console.log(" k:BEARER Bearer token");
1324
- console.log(" w:192.168.1.0/24 IP whitelist (CIDR)");
1325
- console.log("\nExamples (User-friendly):");
1326
- console.log(" pinggy -l 3000 # HTTP(S) tunnel to localhost port 3000");
1327
- console.log(" pinggy --type tcp -l 22 # TCP tunnel for SSH (port 22)");
1328
- console.log(" pinggy -l 8080 -d 4300 # HTTP tunnel to port 8080 with debugger running at localhost:4300");
1329
- console.log(" pinggy --token mytoken -l 3000 # Authenticated tunnel");
1330
- console.log(" pinggy x:https x:xff -l https://localhost:8443 # HTTPS-only + XFF");
1331
- console.log(" pinggy w:192.168.1.0/24 -l 8080 # IP whitelist restriction");
1332
- console.log("\nExamples (SSH-style):");
1333
- console.log(" pinggy -R0:localhost:3000 # Basic HTTP tunnel");
1334
- console.log(" pinggy --type tcp -R0:localhost:22 # TCP tunnel for SSH");
1335
- console.log(" pinggy -R0:localhost:8080 -L4300:localhost:4300 # HTTP tunnel with debugger");
1336
- console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region\n");
1516
+ // src/types.ts
1517
+ function isErrorResponse(obj) {
1518
+ return typeof obj === "object" && obj !== null && "code" in obj && "message" in obj && typeof obj.message === "string" && Object.values(ErrorCode).includes(obj.code);
1337
1519
  }
1338
- var init_help = __esm({
1339
- "src/cli/help.ts"() {
1340
- "use strict";
1341
- init_cjs_shims();
1342
- init_options();
1343
- }
1344
- });
1345
-
1346
- // src/cli/defaults.ts
1347
- var defaultOptions;
1348
- var init_defaults = __esm({
1349
- "src/cli/defaults.ts"() {
1350
- "use strict";
1351
- init_cjs_shims();
1352
- defaultOptions = {
1353
- token: void 0,
1354
- // No default token
1355
- serverAddress: "a.pinggy.io",
1356
- forwarding: "localhost:8000",
1357
- webDebugger: "",
1358
- ipWhitelist: [],
1359
- basicAuth: [],
1360
- bearerTokenAuth: [],
1361
- headerModification: [],
1362
- force: false,
1363
- xForwardedFor: false,
1364
- httpsOnly: false,
1365
- originalRequestUrl: false,
1366
- allowPreflight: false,
1367
- reverseProxy: false,
1368
- autoReconnect: true
1369
- };
1370
- }
1371
- });
1372
-
1373
- // src/cli/extendedOptions.ts
1374
- function parseExtendedOptions(options, config) {
1375
- if (!options) return;
1376
- for (const opt of options) {
1377
- const [key, value] = opt.replace(/^"|"$/g, "").split(/:(.+)/).filter(Boolean);
1378
- switch (key) {
1379
- case "x":
1380
- switch (value) {
1381
- case "https":
1382
- case "httpsonly":
1383
- config.httpsOnly = true;
1384
- break;
1385
- case "passpreflight":
1386
- case "allowpreflight":
1387
- config.allowPreflight = true;
1388
- break;
1389
- case "reverseproxy":
1390
- config.reverseProxy = false;
1391
- break;
1392
- case "xff":
1393
- config.xForwardedFor = true;
1394
- break;
1395
- case "fullurl":
1396
- case "fullrequesturl":
1397
- config.originalRequestUrl = true;
1398
- break;
1399
- default:
1400
- printer_default.warn(`Unknown extended option "${key}"`);
1401
- logger.warn(`Warning: Unknown extended option "${key}"`);
1402
- break;
1403
- }
1404
- break;
1405
- case "w":
1406
- if (value) {
1407
- const ips = value.split(",").map((ip) => ip.trim()).filter(Boolean);
1408
- const invalidIps = ips.filter((ip) => !(isValidIpV4Cidr(ip) || isValidIpV6Cidr(ip)));
1409
- if (invalidIps.length > 0) {
1410
- printer_default.warn(`Invalid IP/CIDR(s) in whitelist: ${invalidIps.join(", ")}`);
1411
- logger.warn(`Warning: Invalid IP/CIDR(s) in whitelist: ${invalidIps.join(", ")}`);
1412
- }
1413
- if (!(invalidIps.length > 0)) {
1414
- config.ipWhitelist = ips;
1415
- }
1416
- } else {
1417
- printer_default.warn(`Extended option "${opt}" for 'w' requires IP(s)`);
1418
- logger.warn(`Warning: Extended option "${opt}" for 'w' requires IP(s)`);
1419
- }
1420
- break;
1421
- case "k":
1422
- if (!config.bearerTokenAuth) config.bearerTokenAuth = [];
1423
- if (value) {
1424
- config.bearerTokenAuth.push(value);
1425
- } else {
1426
- printer_default.warn(`Extended option "${opt}" for 'k' requires a value`);
1427
- logger.warn(`Warning: Extended option "${opt}" for 'k' requires a value`);
1428
- }
1429
- break;
1430
- case "b":
1431
- if (value && value.includes(":")) {
1432
- const [username, password] = value.split(/:(.+)/);
1433
- if (!config.basicAuth) config.basicAuth = [];
1434
- config.basicAuth.push({ username, password });
1435
- } else {
1436
- printer_default.warn(`Extended option "${opt}" for 'b' requires value in format username:password`);
1437
- logger.warn(`Warning: Extended option "${opt}" for 'b' requires value in format username:password`);
1438
- }
1439
- break;
1440
- case "a":
1441
- if (value && value.includes(":")) {
1442
- const [key2, val] = value.split(/:(.+)/);
1443
- if (!config.headerModification) config.headerModification = [];
1444
- config.headerModification.push({ type: "add", key: key2, value: [val] });
1445
- } else {
1446
- printer_default.warn(`Extended option "${opt}" for 'a' requires key:value`);
1447
- logger.warn(`Warning: Extended option "${opt}" for 'a' requires key:value`);
1448
- }
1449
- break;
1450
- case "u":
1451
- if (value && value.includes(":")) {
1452
- const [key2, val] = value.split(/:(.+)/);
1453
- if (!config.headerModification) config.headerModification = [];
1454
- config.headerModification.push({ type: "update", key: key2, value: [val] });
1455
- } else {
1456
- printer_default.warn(`Extended option "${opt}" for 'u' requires key:value`);
1457
- logger.warn(`Warning: Extended option "${opt}" for 'u' requires key:value`);
1458
- }
1459
- break;
1460
- case "r":
1461
- if (value) {
1462
- if (!config.headerModification) config.headerModification = [];
1463
- config.headerModification.push({ type: "remove", key: value });
1464
- } else {
1465
- printer_default.warn(`Extended option "${opt}" for 'r' requires a key`);
1466
- }
1467
- break;
1468
- default:
1469
- printer_default.warn(`Unknown extended option "${key}"`);
1470
- break;
1471
- }
1520
+ function newErrorResponse(codeOrError, message) {
1521
+ if (typeof codeOrError === "object") {
1522
+ return codeOrError;
1472
1523
  }
1524
+ return {
1525
+ code: codeOrError,
1526
+ message
1527
+ };
1473
1528
  }
1474
- function isValidIpV4Cidr(input) {
1475
- if (input.includes("/")) {
1476
- const [ip, mask] = input.split("/");
1477
- if (!ip || !mask) return false;
1478
- const isIp4 = (0, import_net.isIP)(ip) === 4;
1479
- const maskNum = parseInt(mask, 10);
1480
- const isMaskValid = !isNaN(maskNum) && maskNum >= 0 && maskNum <= 32;
1481
- return isIp4 && isMaskValid;
1482
- }
1483
- return false;
1529
+ function NewResponseObject(data) {
1530
+ const encoder = new TextEncoder();
1531
+ const bytes = encoder.encode(JSON.stringify(data));
1532
+ return {
1533
+ response: bytes,
1534
+ requestid: "",
1535
+ command: "",
1536
+ error: false,
1537
+ errorresponse: {}
1538
+ };
1484
1539
  }
1485
- function isValidIpV6Cidr(input) {
1486
- if (input.includes("/")) {
1487
- const [rawIp, mask] = input.split("/");
1488
- if (!rawIp || !mask) return false;
1489
- const ip = rawIp.split("%")[0].replace(/^\[|\]$/g, "");
1490
- const isIp6 = (0, import_net.isIP)(ip) === 6;
1491
- const maskNum = parseInt(mask, 10);
1492
- const isMaskValid = !isNaN(maskNum) && maskNum >= 0 && maskNum <= 128;
1493
- return isIp6 && isMaskValid;
1540
+ function NewErrorResponseObject(errorResponse) {
1541
+ return {
1542
+ response: new Uint8Array(),
1543
+ requestid: "",
1544
+ command: "",
1545
+ error: true,
1546
+ errorresponse: errorResponse
1547
+ };
1548
+ }
1549
+ function newStatus(tunnelState, errorCode, errorMsg) {
1550
+ let assignedState = tunnelState;
1551
+ if (tunnelState === "live" /* Live */) {
1552
+ assignedState = "running" /* Running */;
1553
+ } else if (tunnelState === "idle" /* New */) {
1554
+ assignedState = "idle" /* New */;
1555
+ } else if (tunnelState === "closed" /* Closed */) {
1556
+ assignedState = "exited" /* Exited */;
1494
1557
  }
1495
- return false;
1558
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1559
+ return {
1560
+ state: assignedState,
1561
+ errorcode: errorCode,
1562
+ errormsg: errorMsg,
1563
+ createdtimestamp: now,
1564
+ starttimestamp: now,
1565
+ endtimestamp: now,
1566
+ warnings: []
1567
+ };
1496
1568
  }
1497
- var import_net;
1498
- var init_extendedOptions = __esm({
1499
- "src/cli/extendedOptions.ts"() {
1569
+ function newStats() {
1570
+ return {
1571
+ numLiveConnections: 0,
1572
+ numTotalConnections: 0,
1573
+ numTotalReqBytes: 0,
1574
+ numTotalResBytes: 0,
1575
+ numTotalTxBytes: 0,
1576
+ elapsedTime: 0
1577
+ };
1578
+ }
1579
+ var TunnelStateType, TunnelErrorCodeType, TunnelWarningCode, ErrorCode, RemoteManagementStatus;
1580
+ var init_types = __esm({
1581
+ "src/types.ts"() {
1500
1582
  "use strict";
1501
1583
  init_cjs_shims();
1502
- import_net = require("net");
1503
- init_logger();
1504
- init_printer();
1584
+ TunnelStateType = /* @__PURE__ */ ((TunnelStateType2) => {
1585
+ TunnelStateType2["New"] = "idle";
1586
+ TunnelStateType2["Starting"] = "starting";
1587
+ TunnelStateType2["Running"] = "running";
1588
+ TunnelStateType2["Live"] = "live";
1589
+ TunnelStateType2["Closed"] = "closed";
1590
+ TunnelStateType2["Exited"] = "exited";
1591
+ return TunnelStateType2;
1592
+ })(TunnelStateType || {});
1593
+ TunnelErrorCodeType = /* @__PURE__ */ ((TunnelErrorCodeType2) => {
1594
+ TunnelErrorCodeType2["NonResponsive"] = "non_responsive";
1595
+ TunnelErrorCodeType2["FailedToConnect"] = "failed_to_connect";
1596
+ TunnelErrorCodeType2["ErrorInAdditionalForwarding"] = "additional_forwarding_error";
1597
+ TunnelErrorCodeType2["WebdebuggerError"] = "webdebugger_error";
1598
+ TunnelErrorCodeType2["NoError"] = "";
1599
+ return TunnelErrorCodeType2;
1600
+ })(TunnelErrorCodeType || {});
1601
+ TunnelWarningCode = /* @__PURE__ */ ((TunnelWarningCode2) => {
1602
+ TunnelWarningCode2["InvalidTunnelServePath"] = "INVALID_TUNNEL_SERVE_PATH";
1603
+ TunnelWarningCode2["UnknownWarning"] = "UNKNOWN_WARNING";
1604
+ return TunnelWarningCode2;
1605
+ })(TunnelWarningCode || {});
1606
+ ErrorCode = {
1607
+ InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
1608
+ InvalidRequestBodyError: "COULD_NOT_READ_BODY",
1609
+ InternalServerError: "INTERNAL_SERVER_ERROR",
1610
+ InvalidBodyFormatError: "INVALID_DATA_FORMAT",
1611
+ ErrorStartingTunnel: "ERROR_STARTING_TUNNEL",
1612
+ TunnelNotFound: "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND",
1613
+ TunnelAlreadyRunningError: "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING",
1614
+ WebsocketUpgradeFailError: "WEBSOCKET_UPGRADE_FAILED",
1615
+ RemoteManagementAlreadyRunning: "REMOTE_MANAGEMENT_ALREADY_RUNNING",
1616
+ RemoteManagementNotRunning: "REMOTE_MANAGEMENT_NOT_RUNNING",
1617
+ RemoteManagementDeserializationFailed: "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED"
1618
+ };
1619
+ RemoteManagementStatus = {
1620
+ Connecting: "CONNECTING",
1621
+ Disconnecting: "DISCONNECTING",
1622
+ Reconnecting: "RECONNECTING",
1623
+ Running: "RUNNING",
1624
+ NotRunning: "NOT_RUNNING",
1625
+ Error: "ERROR"
1626
+ };
1505
1627
  }
1506
1628
  });
1507
1629
 
1508
- // src/cli/buildConfig.ts
1509
- function isKeyword(str) {
1510
- return KEYWORDS.has(str.toLowerCase());
1630
+ // src/remote_management/remote_schema.ts
1631
+ function tunnelConfigToPinggyOptions(config) {
1632
+ return {
1633
+ token: config.token || "",
1634
+ serverAddress: config.serveraddress || "free.pinggy.io",
1635
+ forwarding: `${config.forwardedhost || "localhost"}:${config.localport}`,
1636
+ webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
1637
+ ipWhitelist: config.ipwhitelist || [],
1638
+ basicAuth: config.basicauth ? config.basicauth : [],
1639
+ bearerTokenAuth: config.bearerauth ? [config.bearerauth] : [],
1640
+ headerModification: config.headermodification,
1641
+ xForwardedFor: !!config.xff,
1642
+ httpsOnly: config.httpsOnly,
1643
+ originalRequestUrl: config.fullRequestUrl,
1644
+ allowPreflight: config.allowPreflight,
1645
+ reverseProxy: config.noReverseProxy,
1646
+ force: config.force,
1647
+ autoReconnect: config.autoreconnect,
1648
+ optional: {
1649
+ sniServerName: config.localservertlssni || ""
1650
+ }
1651
+ };
1511
1652
  }
1512
- function parseUserAndDomain(str) {
1513
- let token;
1514
- let type;
1515
- let server;
1516
- let qrCode;
1517
- let forceFlag;
1518
- if (!str) return { token, type, server, qrCode, forceFlag };
1519
- if (str.includes("@")) {
1520
- const [user, domain] = str.split("@", 2);
1521
- if (domainRegex.test(domain)) {
1522
- let processKeyword2 = function(keyword) {
1523
- if ([import_pinggy3.TunnelType.Http, import_pinggy3.TunnelType.Tcp, import_pinggy3.TunnelType.Tls, import_pinggy3.TunnelType.Udp, import_pinggy3.TunnelType.TlsTcp].includes(keyword)) {
1524
- type = keyword;
1525
- } else if (keyword === "force") {
1526
- forceFlag = true;
1527
- } else if (keyword === "qr") {
1528
- qrCode = true;
1529
- }
1530
- };
1531
- var processKeyword = processKeyword2;
1532
- server = domain;
1533
- const parts = user.split("+");
1534
- if (parts.length === 0) {
1535
- return { token, type, server, qrCode, forceFlag };
1653
+ function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg, additionalForwarding, serve) {
1654
+ const forwarding = Array.isArray(opts.forwarding) ? String(opts.forwarding[0].address).replace("//", "").replace(/\/$/, "") : String(opts.forwarding).replace("//", "").replace(/\/$/, "");
1655
+ const parsedForwardedHost = forwarding.split(":").length == 3 ? forwarding.split(":")[1] : forwarding.split(":")[0];
1656
+ const parsedLocalPort = forwarding.split(":").length == 3 ? parseInt(forwarding.split(":")[2], 10) : parseInt(forwarding.split(":")[1], 10);
1657
+ const tunnelType = (Array.isArray(opts.forwarding) ? opts.forwarding[0]?.type : void 0) ?? import_pinggy3.TunnelType.Http;
1658
+ const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
1659
+ return {
1660
+ allowPreflight: opts.allowPreflight ?? false,
1661
+ allowpreflight: opts.allowPreflight ?? false,
1662
+ autoreconnect: opts.autoReconnect ?? false,
1663
+ basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
1664
+ bearerauth: parsedTokens.length ? parsedTokens.join(",") : null,
1665
+ configid,
1666
+ configname: configName,
1667
+ greetmsg: greetMsg || "",
1668
+ force: opts.force ?? false,
1669
+ forwardedhost: parsedForwardedHost || "localhost",
1670
+ fullRequestUrl: opts.originalRequestUrl ?? false,
1671
+ headermodification: opts.headerModification || [],
1672
+ //structured list
1673
+ httpsOnly: opts.httpsOnly ?? false,
1674
+ internalwebdebuggerport: 0,
1675
+ ipwhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : null,
1676
+ localport: parsedLocalPort || 0,
1677
+ localservertlssni: null,
1678
+ regioncode: "",
1679
+ noReverseProxy: opts.reverseProxy ?? false,
1680
+ serveraddress: opts.serverAddress || "free.pinggy.io",
1681
+ serverport: 0,
1682
+ statusCheckInterval: 0,
1683
+ token: opts.token || "",
1684
+ tunnelTimeout: 0,
1685
+ type: tunnelType,
1686
+ webdebuggerport: Number(opts.webDebugger?.split(":")[0]) || 0,
1687
+ xff: opts.xForwardedFor ? "1" : "",
1688
+ localsservertls: localserverTls || false,
1689
+ additionalForwarding: additionalForwarding || [],
1690
+ serve: serve || ""
1691
+ };
1692
+ }
1693
+ var import_pinggy3, import_zod, HeaderModificationSchema, AdditionalForwardingSchema, TunnelConfigSchema, StartSchema, StopSchema, GetSchema, RestartSchema, UpdateConfigSchema;
1694
+ var init_remote_schema = __esm({
1695
+ "src/remote_management/remote_schema.ts"() {
1696
+ "use strict";
1697
+ init_cjs_shims();
1698
+ import_pinggy3 = require("@pinggy/pinggy");
1699
+ import_zod = require("zod");
1700
+ HeaderModificationSchema = import_zod.z.object({
1701
+ key: import_zod.z.string(),
1702
+ value: import_zod.z.array(import_zod.z.string()).optional(),
1703
+ type: import_zod.z.enum(["add", "remove", "update"])
1704
+ });
1705
+ AdditionalForwardingSchema = import_zod.z.object({
1706
+ remoteDomain: import_zod.z.string().optional(),
1707
+ remotePort: import_zod.z.number().optional(),
1708
+ localDomain: import_zod.z.string(),
1709
+ localPort: import_zod.z.number()
1710
+ });
1711
+ TunnelConfigSchema = import_zod.z.object({
1712
+ allowPreflight: import_zod.z.boolean().optional(),
1713
+ // primary key
1714
+ allowpreflight: import_zod.z.boolean().optional(),
1715
+ // legacy key
1716
+ autoreconnect: import_zod.z.boolean(),
1717
+ basicauth: import_zod.z.array(import_zod.z.object({ username: import_zod.z.string(), password: import_zod.z.string() })).nullable(),
1718
+ bearerauth: import_zod.z.string().nullable(),
1719
+ configid: import_zod.z.string(),
1720
+ configname: import_zod.z.string(),
1721
+ greetmsg: import_zod.z.string().optional(),
1722
+ force: import_zod.z.boolean(),
1723
+ forwardedhost: import_zod.z.string(),
1724
+ fullRequestUrl: import_zod.z.boolean(),
1725
+ headermodification: import_zod.z.array(HeaderModificationSchema),
1726
+ httpsOnly: import_zod.z.boolean(),
1727
+ internalwebdebuggerport: import_zod.z.number(),
1728
+ ipwhitelist: import_zod.z.array(import_zod.z.string()).nullable(),
1729
+ localport: import_zod.z.number(),
1730
+ localsservertls: import_zod.z.union([import_zod.z.boolean(), import_zod.z.string()]),
1731
+ localservertlssni: import_zod.z.string().nullable(),
1732
+ regioncode: import_zod.z.string(),
1733
+ noReverseProxy: import_zod.z.boolean(),
1734
+ serveraddress: import_zod.z.string(),
1735
+ serverport: import_zod.z.number(),
1736
+ statusCheckInterval: import_zod.z.number(),
1737
+ token: import_zod.z.string(),
1738
+ tunnelTimeout: import_zod.z.number(),
1739
+ type: import_zod.z.enum([
1740
+ import_pinggy3.TunnelType.Http,
1741
+ import_pinggy3.TunnelType.Tcp,
1742
+ import_pinggy3.TunnelType.Udp,
1743
+ import_pinggy3.TunnelType.Tls,
1744
+ import_pinggy3.TunnelType.TlsTcp
1745
+ ]),
1746
+ webdebuggerport: import_zod.z.number(),
1747
+ xff: import_zod.z.string(),
1748
+ additionalForwarding: import_zod.z.array(AdditionalForwardingSchema).optional(),
1749
+ serve: import_zod.z.string().optional()
1750
+ }).superRefine((data, ctx) => {
1751
+ if (data.allowPreflight === void 0 && data.allowpreflight === void 0) {
1752
+ ctx.addIssue({
1753
+ code: "custom",
1754
+ message: "Either allowPreflight or allowpreflight is required",
1755
+ path: ["allowPreflight"]
1756
+ });
1536
1757
  }
1537
- const firstPart = parts[0];
1538
- if (!isKeyword(firstPart)) {
1539
- token = firstPart;
1540
- for (let i = 1; i < parts.length; i++) {
1541
- const part = parts[i].toLowerCase();
1542
- if (!isKeyword(part)) {
1543
- throw new Error(`Invalid user format: unexpected token '${part}' when keywords are expected.`);
1758
+ }).transform((data) => ({
1759
+ ...data,
1760
+ allowPreflight: data.allowPreflight ?? data.allowpreflight,
1761
+ allowpreflight: data.allowPreflight ?? data.allowpreflight
1762
+ }));
1763
+ StartSchema = import_zod.z.object({
1764
+ tunnelID: import_zod.z.string().nullable().optional(),
1765
+ tunnelConfig: TunnelConfigSchema
1766
+ });
1767
+ StopSchema = import_zod.z.object({
1768
+ tunnelID: import_zod.z.string().min(1)
1769
+ });
1770
+ GetSchema = StopSchema;
1771
+ RestartSchema = StopSchema;
1772
+ UpdateConfigSchema = import_zod.z.object({
1773
+ tunnelConfig: TunnelConfigSchema
1774
+ });
1775
+ }
1776
+ });
1777
+
1778
+ // src/remote_management/handler.ts
1779
+ var import_pinggy4, TunnelOperations;
1780
+ var init_handler = __esm({
1781
+ "src/remote_management/handler.ts"() {
1782
+ "use strict";
1783
+ init_cjs_shims();
1784
+ init_types();
1785
+ init_TunnelManager();
1786
+ init_remote_schema();
1787
+ import_pinggy4 = require("@pinggy/pinggy");
1788
+ TunnelOperations = class {
1789
+ constructor() {
1790
+ this.tunnelManager = TunnelManager.getInstance();
1791
+ }
1792
+ buildStatus(tunnelId, state, errorCode) {
1793
+ const status = newStatus(state, errorCode, "");
1794
+ try {
1795
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelId);
1796
+ if (managed) {
1797
+ status.createdtimestamp = managed.createdAt || "";
1798
+ status.starttimestamp = managed.startedAt || "";
1799
+ status.endtimestamp = managed.stoppedAt || "";
1544
1800
  }
1545
- processKeyword2(part);
1801
+ } catch (e) {
1546
1802
  }
1547
- } else {
1548
- for (const part of parts) {
1549
- const lowerPart = part.toLowerCase();
1550
- if (!isKeyword(lowerPart)) {
1551
- throw new Error(`Invalid user format: unexpected token '${lowerPart}' when keywords are expected.`);
1803
+ return status;
1804
+ }
1805
+ // --- Helper to construct TunnelResponse ---
1806
+ async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, additionalForwarding, serve) {
1807
+ const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
1808
+ this.tunnelManager.getTunnelStatus(tunnelid),
1809
+ this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
1810
+ this.tunnelManager.getLocalserverTlsInfo(tunnelid),
1811
+ this.tunnelManager.getTunnelGreetMessage(tunnelid),
1812
+ this.tunnelManager.getTunnelUrls(tunnelid)
1813
+ ]);
1814
+ return {
1815
+ tunnelid,
1816
+ remoteurls,
1817
+ tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg, additionalForwarding),
1818
+ status: this.buildStatus(tunnelid, status, "" /* NoError */),
1819
+ stats
1820
+ };
1821
+ }
1822
+ error(code, err, fallback) {
1823
+ return newErrorResponse({
1824
+ code,
1825
+ message: err instanceof Error ? err.message : fallback
1826
+ });
1827
+ }
1828
+ // --- Operations ---
1829
+ async handleStart(config) {
1830
+ try {
1831
+ const opts = tunnelConfigToPinggyOptions(config);
1832
+ const additionalForwardingParsed = config.additionalForwarding || [];
1833
+ const { tunnelid, instance, tunnelName, additionalForwarding, serve } = await this.tunnelManager.createTunnel({
1834
+ ...opts,
1835
+ tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [import_pinggy4.TunnelType.Http],
1836
+ // Temporary fix in future we will not use this field.
1837
+ configid: config.configid,
1838
+ tunnelName: config.configname,
1839
+ additionalForwarding: additionalForwardingParsed,
1840
+ serve: config.serve
1841
+ });
1842
+ this.tunnelManager.startTunnel(tunnelid);
1843
+ const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
1844
+ const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, additionalForwarding, serve);
1845
+ return resp;
1846
+ } catch (err) {
1847
+ return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
1848
+ }
1849
+ }
1850
+ async handleUpdateConfig(config) {
1851
+ try {
1852
+ const opts = tunnelConfigToPinggyOptions(config);
1853
+ const tunnel = await this.tunnelManager.updateConfig({
1854
+ ...opts,
1855
+ tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [import_pinggy4.TunnelType.Http],
1856
+ // // Temporary fix in future we will not use this field.
1857
+ configid: config.configid,
1858
+ tunnelName: config.configname,
1859
+ additionalForwarding: config.additionalForwarding || [],
1860
+ serve: config.serve
1861
+ });
1862
+ if (!tunnel.instance || !tunnel.tunnelConfig)
1863
+ throw new Error("Invalid tunnel state after configuration update");
1864
+ return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.additionalForwarding, tunnel.serve);
1865
+ } catch (err) {
1866
+ return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
1867
+ }
1868
+ }
1869
+ async handleList() {
1870
+ try {
1871
+ const tunnels = await this.tunnelManager.getAllTunnels();
1872
+ if (tunnels.length === 0) {
1873
+ return [];
1552
1874
  }
1553
- processKeyword2(lowerPart);
1875
+ return Promise.all(
1876
+ tunnels.map(async (t) => {
1877
+ const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
1878
+ const [status, tlsInfo, greetMsg] = await Promise.all([
1879
+ this.tunnelManager.getTunnelStatus(t.tunnelid),
1880
+ this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
1881
+ this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
1882
+ ]);
1883
+ const pinggyOptions = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
1884
+ const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configid, t.tunnelName, tlsInfo, greetMsg, t.additionalForwarding, t.serve);
1885
+ return {
1886
+ tunnelid: t.tunnelid,
1887
+ remoteurls: t.remoteurls,
1888
+ status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
1889
+ stats: rawStats,
1890
+ tunnelconfig: tunnelConfig
1891
+ };
1892
+ })
1893
+ );
1894
+ } catch (err) {
1895
+ return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
1554
1896
  }
1555
1897
  }
1556
- }
1557
- } else if (domainRegex.test(str)) {
1558
- server = str;
1559
- }
1560
- return { token, type, server, qrCode, forceFlag };
1561
- }
1562
- function parseUsers(positionalArgs, explicitToken) {
1563
- let token;
1564
- let server;
1565
- let type;
1566
- let forceFlag = false;
1567
- let qrCode = false;
1568
- let remaining = [...positionalArgs];
1569
- if (typeof explicitToken === "string") {
1570
- const parsed = parseUserAndDomain(explicitToken);
1571
- if (parsed.server) server = parsed.server;
1572
- if (parsed.type) type = parsed.type;
1573
- if (parsed.token) token = parsed.token;
1574
- if (parsed.forceFlag) forceFlag = true;
1575
- if (parsed.qrCode) qrCode = true;
1576
- }
1577
- if (remaining.length > 0) {
1578
- const first = remaining[0];
1579
- const parsed = parseUserAndDomain(first);
1580
- if (parsed.server) {
1581
- server = parsed.server;
1582
- if (parsed.type) type = parsed.type;
1583
- if (parsed.token) token = parsed.token;
1584
- if (parsed.forceFlag) forceFlag = true;
1585
- if (parsed.qrCode) qrCode = true;
1586
- remaining = remaining.slice(1);
1587
- }
1588
- }
1589
- return { token, server, type, forceFlag, qrCode, remaining };
1590
- }
1591
- function parseType(finalConfig, values, inferredType) {
1592
- const t = inferredType || values.type || finalConfig.tunnelType;
1593
- if (t === import_pinggy3.TunnelType.Http || t === import_pinggy3.TunnelType.Tcp || t === import_pinggy3.TunnelType.Tls || t === import_pinggy3.TunnelType.Udp || t === import_pinggy3.TunnelType.TlsTcp) {
1594
- finalConfig.tunnelType = [t];
1595
- }
1596
- }
1597
- function parseLocalPort(finalConfig, values) {
1598
- if (typeof values.localport !== "string") return null;
1599
- let lp = values.localport.trim();
1600
- let isHttps = false;
1601
- if (lp.startsWith("https://")) {
1602
- isHttps = true;
1603
- lp = lp.replace(/^https:\/\//, "");
1604
- } else if (lp.startsWith("http://")) {
1605
- lp = lp.replace(/^http:\/\//, "");
1606
- }
1607
- const parts = lp.split(":");
1608
- if (parts.length === 1) {
1609
- const port = parseInt(parts[0], 10);
1610
- if (!Number.isNaN(port) && isValidPort(port)) {
1611
- finalConfig.forwarding = `localhost:${port}`;
1612
- } else {
1613
- return new Error("Invalid local port");
1614
- }
1615
- } else if (parts.length === 2) {
1616
- const host = parts[0] || "localhost";
1617
- const port = parseInt(parts[1], 10);
1618
- if (!Number.isNaN(port) && isValidPort(port)) {
1619
- finalConfig.forwarding = `${host}:${port}`;
1620
- } else {
1621
- return new Error("Invalid local port. Please use -h option for help.");
1622
- }
1623
- } else {
1624
- return new Error("Invalid --localport format. Please use -h option for help.");
1625
- }
1626
- return null;
1627
- }
1628
- function removeIPv6Brackets(ip) {
1629
- if (ip.startsWith("[") && ip.endsWith("]")) {
1630
- return ip.slice(1, -1);
1631
- }
1632
- return ip;
1633
- }
1634
- function ipv6SafeSplitColon(s) {
1635
- const result = [];
1636
- let buf = "";
1637
- const stack = [];
1638
- for (let i = 0; i < s.length; i++) {
1639
- const c = s[i];
1640
- if (c === "[") {
1641
- stack.push(c);
1642
- } else if (c === "]" && stack.length > 0) {
1643
- stack.pop();
1644
- }
1645
- if (c === ":" && stack.length === 0) {
1646
- result.push(buf);
1647
- buf = "";
1648
- } else {
1649
- buf += c;
1650
- }
1651
- }
1652
- result.push(buf);
1653
- return result;
1654
- }
1655
- function parseDefaultForwarding(forwarding) {
1656
- const parts = ipv6SafeSplitColon(forwarding);
1657
- if (parts.length === 3) {
1658
- const remotePort = parseInt(parts[0], 10);
1659
- const localDomain = removeIPv6Brackets(parts[1] || "localhost");
1660
- const localPort = parseInt(parts[2], 10);
1661
- return { remotePort, localDomain, localPort };
1662
- }
1663
- if (parts.length === 4) {
1664
- const remoteDomain = removeIPv6Brackets(parts[0]);
1665
- const remotePort = parseInt(parts[1], 10);
1666
- const localDomain = removeIPv6Brackets(parts[2] || "localhost");
1667
- const localPort = parseInt(parts[3], 10);
1668
- return { remoteDomain, remotePort, localDomain, localPort };
1669
- }
1670
- return new Error("forwarding address incorrect");
1671
- }
1672
- function parseAdditionalForwarding(forwarding) {
1673
- const toPort = (v) => {
1674
- if (!v) return null;
1675
- const n = parseInt(v, 10);
1676
- return Number.isNaN(n) ? null : n;
1677
- };
1678
- const parsed = ipv6SafeSplitColon(forwarding);
1679
- if (parsed.length !== 4) {
1680
- return new Error(
1681
- "forwarding must be in format: [schema//]hostname[/port][@forwardingId]:<placeholder>:<forwardingAddress>:<forwardingPort>"
1682
- );
1683
- }
1684
- const firstPart = parsed[0];
1685
- const [hostPart] = firstPart.split("@");
1686
- let protocol = "http";
1687
- let remoteDomainRaw;
1688
- let remotePort = 0;
1689
- if (hostPart.includes("//")) {
1690
- const [schema, rest] = hostPart.split("//");
1691
- if (!schema || !VALID_PROTOCOLS.includes(schema)) {
1692
- return new Error(`invalid protocol: ${schema}`);
1693
- }
1694
- protocol = schema;
1695
- const domainAndPort = rest.split("/");
1696
- if (domainAndPort.length > 2) {
1697
- return new Error("invalid forwarding address format");
1698
- }
1699
- remoteDomainRaw = domainAndPort[0];
1700
- if (!remoteDomainRaw || !domainRegex.test(remoteDomainRaw)) {
1701
- return new Error("invalid remote domain");
1702
- }
1703
- const parsedRemotePort = toPort(domainAndPort[1]);
1704
- if (protocol === "http") {
1705
- remotePort = 0;
1706
- } else {
1707
- if (parsedRemotePort === null || !isValidPort(parsedRemotePort)) {
1708
- return new Error(
1709
- `${protocol} forwarding requires port in format ${protocol}//domain/remotePort`
1710
- );
1898
+ async handleStop(tunnelid) {
1899
+ try {
1900
+ const { configid } = this.tunnelManager.stopTunnel(tunnelid);
1901
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1902
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
1903
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
1904
+ } catch (err) {
1905
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
1906
+ }
1711
1907
  }
1712
- remotePort = parsedRemotePort;
1713
- }
1714
- } else {
1715
- remoteDomainRaw = hostPart;
1716
- if (!domainRegex.test(remoteDomainRaw)) {
1717
- return new Error("invalid remote domain");
1718
- }
1719
- protocol = "http";
1720
- remotePort = 0;
1721
- }
1722
- const localDomain = removeIPv6Brackets(parsed[2] || "localhost");
1723
- const localPort = toPort(parsed[3]);
1724
- if (localPort === null || !isValidPort(localPort)) {
1725
- return new Error("forwarding address incorrect: invalid local port");
1726
- }
1727
- return {
1728
- protocol,
1729
- remoteDomain: remoteDomainRaw,
1730
- remotePort,
1731
- localDomain,
1732
- localPort
1733
- };
1734
- }
1735
- function parseReverseTunnelAddr(finalConfig, values) {
1736
- const reverseTunnel = values.R;
1737
- if ((!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) && !values.localport && !finalConfig.forwarding) {
1738
- return new Error("local port not specified. Please use '-h' option for help.");
1739
- }
1740
- if (!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) {
1741
- return null;
1742
- }
1743
- for (const forwarding of reverseTunnel) {
1744
- const slicedForwarding = ipv6SafeSplitColon(forwarding);
1745
- if (slicedForwarding.length === 3) {
1746
- const parsed = parseDefaultForwarding(forwarding);
1747
- if (parsed instanceof Error) return parsed;
1748
- finalConfig.forwarding = `${parsed.localDomain}:${parsed.localPort}`;
1749
- } else if (slicedForwarding.length === 4) {
1750
- finalConfig.additionalForwarding ?? (finalConfig.additionalForwarding = []);
1751
- const parsed = parseAdditionalForwarding(forwarding);
1752
- if (parsed instanceof Error) return parsed;
1753
- finalConfig.additionalForwarding.push(parsed);
1754
- } else {
1755
- return new Error(
1756
- "Incorrect command line arguments: reverse tunnel address incorrect. Please use '-h' option for help."
1757
- );
1758
- }
1759
- }
1760
- return null;
1761
- }
1762
- function parseLocalTunnelAddr(finalConfig, values) {
1763
- if (!Array.isArray(values.L) || values.L.length === 0) return null;
1764
- const firstL = values.L[0];
1765
- const parts = firstL.split(":");
1766
- if (parts.length === 3) {
1767
- const lp = parseInt(parts[0], 10);
1768
- if (!Number.isNaN(lp) && isValidPort(lp)) {
1769
- finalConfig.webDebugger = `localhost:${lp}`;
1770
- } else {
1771
- return new Error(`Invalid debugger port ${lp}`);
1772
- }
1773
- } else {
1774
- return new Error("Incorrect command line arguments: web debugger address incorrect. Please use '-h' option for help.");
1775
- }
1776
- }
1777
- function parseDebugger(finalConfig, values) {
1778
- let dbg = values.debugger;
1779
- if (typeof dbg !== "string") return;
1780
- dbg = dbg.startsWith(":") ? dbg.slice(1) : dbg;
1781
- const d = parseInt(dbg, 10);
1782
- if (!Number.isNaN(d) && isValidPort(d)) {
1783
- finalConfig.webDebugger = `localhost:${d}`;
1784
- } else {
1785
- logger.error("Invalid debugger port:", dbg);
1786
- return new Error(`Invalid debugger port ${dbg}. Please use '-h' option for help.`);
1787
- }
1788
- }
1789
- function parseToken(finalConfig, explicitToken) {
1790
- if (typeof explicitToken === "string" && explicitToken) {
1791
- finalConfig.token = explicitToken;
1792
- }
1793
- }
1794
- function parseArgs(finalConfig, remainingPositionals) {
1795
- parseExtendedOptions(remainingPositionals, finalConfig);
1796
- }
1797
- function storeJson(config, saveconf) {
1798
- if (saveconf) {
1799
- const path5 = saveconf;
1800
- try {
1801
- import_fs3.default.writeFileSync(path5, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
1802
- logger.info(`Configuration saved to ${path5}`);
1803
- } catch (err) {
1804
- const msg = err instanceof Error ? err.message : String(err);
1805
- logger.error("Error loading configuration:", msg);
1806
- }
1807
- }
1808
- }
1809
- function loadJsonConfig(config) {
1810
- const configpath = config["conf"];
1811
- if (typeof configpath === "string" && configpath.trim().length > 0) {
1812
- const filepath = import_path3.default.resolve(configpath);
1813
- try {
1814
- const data = import_fs3.default.readFileSync(filepath, { encoding: "utf-8" });
1815
- const json = JSON.parse(data);
1816
- return json;
1817
- } catch (err) {
1818
- logger.error("Error loading configuration:", err);
1819
- }
1820
- }
1821
- return null;
1822
- }
1823
- function isSaveConfOption(values) {
1824
- const saveconf = values["saveconf"];
1825
- if (typeof saveconf === "string" && saveconf.trim().length > 0) {
1826
- return saveconf;
1908
+ async handleGet(tunnelid) {
1909
+ try {
1910
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1911
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
1912
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
1913
+ } catch (err) {
1914
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
1915
+ }
1916
+ }
1917
+ async handleRestart(tunnelid) {
1918
+ try {
1919
+ await this.tunnelManager.restartTunnel(tunnelid);
1920
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1921
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
1922
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
1923
+ } catch (err) {
1924
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
1925
+ }
1926
+ }
1927
+ handleRegisterStatsListener(tunnelid, listener) {
1928
+ this.tunnelManager.registerStatsListener(tunnelid, listener);
1929
+ }
1930
+ handleUnregisterStatsListener(tunnelid, listnerId) {
1931
+ this.tunnelManager.deregisterStatsListener(tunnelid, listnerId);
1932
+ }
1933
+ handleGetTunnelStats(tunnelid) {
1934
+ try {
1935
+ const stats = this.tunnelManager.getTunnelStats(tunnelid);
1936
+ if (!stats) {
1937
+ return [newStats()];
1938
+ }
1939
+ return stats;
1940
+ } catch (err) {
1941
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats");
1942
+ }
1943
+ }
1944
+ handleRegisterDisconnectListener(tunnelid, listener) {
1945
+ this.tunnelManager.registerDisconnectListener(tunnelid, listener);
1946
+ }
1947
+ handleRemoveStoppedTunnelByConfigId(configId) {
1948
+ try {
1949
+ return this.tunnelManager.removeStoppedTunnelByConfigId(configId);
1950
+ } catch (err) {
1951
+ return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by configId");
1952
+ }
1953
+ }
1954
+ handleRemoveStoppedTunnelByTunnelId(tunnelId) {
1955
+ try {
1956
+ return this.tunnelManager.removeStoppedTunnelByTunnelId(tunnelId);
1957
+ } catch (err) {
1958
+ return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by tunnelId");
1959
+ }
1960
+ }
1961
+ };
1827
1962
  }
1828
- return null;
1829
- }
1830
- function parseServe(finalConfig, values) {
1831
- const sv = values.serve;
1832
- if (typeof sv !== "string" || sv.trim().length === 0) return null;
1833
- finalConfig.serve = sv;
1834
- return null;
1835
- }
1836
- function parseAutoReconnect(finalConfig, values) {
1837
- const autoReconnectValue = values.autoreconnect;
1838
- if (typeof autoReconnectValue === "string") {
1839
- const trimmed = autoReconnectValue.trim().toLowerCase();
1840
- if (trimmed === "true" || trimmed === "") {
1841
- finalConfig.autoReconnect = true;
1842
- } else if (trimmed === "false") {
1843
- finalConfig.autoReconnect = false;
1844
- } else {
1845
- return new Error(`Invalid autoreconnect value: ${autoReconnectValue}. Use true or false.`);
1963
+ });
1964
+
1965
+ // src/remote_management/websocket_handlers.ts
1966
+ function handleConnectionStatusMessage(firstMessage) {
1967
+ try {
1968
+ const text = typeof firstMessage === "string" ? firstMessage : firstMessage.toString();
1969
+ const cs = JSON.parse(text);
1970
+ if (!cs.success) {
1971
+ const msg = cs.error_msg || "Connection failed";
1972
+ printer_default.warn(`Connection failed: ${msg}`);
1973
+ logger.warn("Remote management connection failed", { error_code: cs.error_code, error_msg: msg });
1974
+ return false;
1846
1975
  }
1976
+ return true;
1977
+ } catch (e) {
1978
+ logger.warn("Failed to parse connection status message", { error: String(e) });
1979
+ return true;
1847
1980
  }
1848
- return null;
1849
- }
1850
- async function buildFinalConfig(values, positionals) {
1851
- let token;
1852
- let server;
1853
- let type;
1854
- let forceFlag = false;
1855
- let qrCode = false;
1856
- let finalConfig = new Object();
1857
- let saveconf = isSaveConfOption(values);
1858
- const configFromFile = loadJsonConfig(values);
1859
- const userParse = parseUsers(positionals, values.token);
1860
- token = userParse.token;
1861
- server = userParse.server;
1862
- type = userParse.type;
1863
- forceFlag = userParse.forceFlag;
1864
- qrCode = userParse.qrCode;
1865
- const remainingPositionals = userParse.remaining;
1866
- const initialTunnel = type || values.type;
1867
- finalConfig = {
1868
- ...defaultOptions,
1869
- ...configFromFile || {},
1870
- // Apply loaded config on top of defaults
1871
- configid: getRandomId(),
1872
- token: token || (configFromFile?.token || (typeof values.token === "string" ? values.token : "")),
1873
- serverAddress: server || (configFromFile?.serverAddress || defaultOptions.serverAddress),
1874
- tunnelType: initialTunnel ? [initialTunnel] : configFromFile?.tunnelType || [import_pinggy3.TunnelType.Http],
1875
- NoTUI: values.notui || (configFromFile?.NoTUI || false),
1876
- qrCode: qrCode || (configFromFile?.qrCode || false),
1877
- autoReconnect: configFromFile?.autoReconnect ? configFromFile.autoReconnect : defaultOptions.autoReconnect
1878
- };
1879
- parseType(finalConfig, values, type);
1880
- parseToken(finalConfig, token || values.token);
1881
- const dbgErr = parseDebugger(finalConfig, values);
1882
- if (dbgErr instanceof Error) throw dbgErr;
1883
- const lpErr = parseLocalPort(finalConfig, values);
1884
- if (lpErr instanceof Error) throw lpErr;
1885
- const rErr = parseReverseTunnelAddr(finalConfig, values);
1886
- if (rErr instanceof Error) throw rErr;
1887
- const lErr = parseLocalTunnelAddr(finalConfig, values);
1888
- if (lErr instanceof Error) throw lErr;
1889
- const serveErr = parseServe(finalConfig, values);
1890
- if (serveErr instanceof Error) throw serveErr;
1891
- const autoReconnectErr = parseAutoReconnect(finalConfig, values);
1892
- if (autoReconnectErr instanceof Error) throw autoReconnectErr;
1893
- if (forceFlag) finalConfig.force = true;
1894
- parseArgs(finalConfig, remainingPositionals);
1895
- storeJson(finalConfig, saveconf);
1896
- return finalConfig;
1897
1981
  }
1898
- var import_pinggy3, import_fs3, import_path3, domainRegex, KEYWORDS, VALID_PROTOCOLS;
1899
- var init_buildConfig = __esm({
1900
- "src/cli/buildConfig.ts"() {
1982
+ var import_zod2, WebSocketCommandHandler;
1983
+ var init_websocket_handlers = __esm({
1984
+ "src/remote_management/websocket_handlers.ts"() {
1901
1985
  "use strict";
1902
1986
  init_cjs_shims();
1903
- init_defaults();
1904
- init_extendedOptions();
1905
1987
  init_logger();
1906
- init_util();
1907
- import_pinggy3 = require("@pinggy/pinggy");
1908
- import_fs3 = __toESM(require("fs"), 1);
1909
- import_path3 = __toESM(require("path"), 1);
1910
- domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
1911
- KEYWORDS = /* @__PURE__ */ new Set([
1912
- import_pinggy3.TunnelType.Http,
1913
- import_pinggy3.TunnelType.Tcp,
1914
- import_pinggy3.TunnelType.Tls,
1915
- import_pinggy3.TunnelType.Udp,
1916
- import_pinggy3.TunnelType.TlsTcp,
1917
- "force",
1918
- "qr"
1919
- ]);
1920
- VALID_PROTOCOLS = ["http", "tcp", "udp", "tls"];
1988
+ init_types();
1989
+ init_handler();
1990
+ init_remote_schema();
1991
+ import_zod2 = __toESM(require("zod"), 1);
1992
+ init_printer();
1993
+ WebSocketCommandHandler = class {
1994
+ constructor() {
1995
+ this.tunnelHandler = new TunnelOperations();
1996
+ }
1997
+ safeParse(text) {
1998
+ if (!text) return void 0;
1999
+ try {
2000
+ return JSON.parse(text);
2001
+ } catch (e) {
2002
+ logger.warn("Invalid JSON payload", { error: String(e), text });
2003
+ return void 0;
2004
+ }
2005
+ }
2006
+ sendResponse(ws, resp) {
2007
+ const payload = {
2008
+ ...resp,
2009
+ response: Buffer.from(resp.response || []).toString("base64")
2010
+ };
2011
+ ws.send(JSON.stringify(payload));
2012
+ }
2013
+ sendError(ws, req, message, code = ErrorCode.InternalServerError) {
2014
+ const resp = NewErrorResponseObject({ code, message });
2015
+ resp.command = req.command || "";
2016
+ resp.requestid = req.requestid || "";
2017
+ this.sendResponse(ws, resp);
2018
+ }
2019
+ async handleStartReq(req, raw) {
2020
+ const dc = StartSchema.parse(raw);
2021
+ printer_default.info("Starting tunnel with config name: " + dc.tunnelConfig.configname);
2022
+ const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
2023
+ return this.wrapResponse(result, req);
2024
+ }
2025
+ async handleStopReq(req, raw) {
2026
+ const dc = StopSchema.parse(raw);
2027
+ printer_default.info("Stopping tunnel with ID: " + dc.tunnelID);
2028
+ const result = await this.tunnelHandler.handleStop(dc.tunnelID);
2029
+ return this.wrapResponse(result, req);
2030
+ }
2031
+ async handleGetReq(req, raw) {
2032
+ const dc = GetSchema.parse(raw);
2033
+ const result = await this.tunnelHandler.handleGet(dc.tunnelID);
2034
+ return this.wrapResponse(result, req);
2035
+ }
2036
+ async handleRestartReq(req, raw) {
2037
+ const dc = RestartSchema.parse(raw);
2038
+ const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
2039
+ return this.wrapResponse(result, req);
2040
+ }
2041
+ async handleUpdateConfigReq(req, raw) {
2042
+ const dc = UpdateConfigSchema.parse(raw);
2043
+ const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
2044
+ return this.wrapResponse(result, req);
2045
+ }
2046
+ async handleListReq(req) {
2047
+ const result = await this.tunnelHandler.handleList();
2048
+ return this.wrapResponse(result, req);
2049
+ }
2050
+ wrapResponse(result, req) {
2051
+ if (isErrorResponse(result)) {
2052
+ const errResp = NewErrorResponseObject(result);
2053
+ errResp.command = req.command;
2054
+ errResp.requestid = req.requestid;
2055
+ return errResp;
2056
+ }
2057
+ const finalResult = JSON.parse(JSON.stringify(result));
2058
+ if (Array.isArray(finalResult)) {
2059
+ finalResult.forEach((item) => {
2060
+ if (item?.tunnelconfig) {
2061
+ delete item.tunnelconfig.allowPreflight;
2062
+ }
2063
+ });
2064
+ } else if (finalResult?.tunnelconfig) {
2065
+ delete finalResult.tunnelconfig.allowPreflight;
2066
+ }
2067
+ const respObj = NewResponseObject(finalResult);
2068
+ respObj.command = req.command;
2069
+ respObj.requestid = req.requestid;
2070
+ return respObj;
2071
+ }
2072
+ async handle(ws, req) {
2073
+ const cmd = (req.command || "").toLowerCase();
2074
+ const raw = this.safeParse(req.data);
2075
+ try {
2076
+ let response;
2077
+ switch (cmd) {
2078
+ case "start": {
2079
+ response = await this.handleStartReq(req, raw);
2080
+ break;
2081
+ }
2082
+ case "stop": {
2083
+ response = await this.handleStopReq(req, raw);
2084
+ break;
2085
+ }
2086
+ case "get": {
2087
+ response = await this.handleGetReq(req, raw);
2088
+ break;
2089
+ }
2090
+ case "restart": {
2091
+ response = await this.handleRestartReq(req, raw);
2092
+ break;
2093
+ }
2094
+ case "updateconfig": {
2095
+ response = await this.handleUpdateConfigReq(req, raw);
2096
+ break;
2097
+ }
2098
+ case "list": {
2099
+ response = await this.handleListReq(req);
2100
+ break;
2101
+ }
2102
+ default:
2103
+ if (typeof req.command === "string") {
2104
+ logger.warn("Unknown command", { command: req.command });
2105
+ }
2106
+ return this.sendError(ws, req, "Invalid command");
2107
+ }
2108
+ logger.debug("Sending response", { command: response.command, requestid: response.requestid });
2109
+ this.sendResponse(ws, response);
2110
+ } catch (e) {
2111
+ if (e instanceof import_zod2.default.ZodError) {
2112
+ logger.warn("Validation failed", { cmd, issues: e.issues });
2113
+ return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
2114
+ }
2115
+ logger.error("Error handling command", { cmd, error: String(e) });
2116
+ return this.sendError(ws, req, e?.message || "Internal error");
2117
+ }
2118
+ }
2119
+ };
1921
2120
  }
1922
2121
  });
1923
2122
 
1924
- // src/types.ts
1925
- function isErrorResponse(obj) {
1926
- return typeof obj === "object" && obj !== null && "code" in obj && "message" in obj && typeof obj.message === "string" && Object.values(ErrorCode).includes(obj.code);
1927
- }
1928
- function newErrorResponse(codeOrError, message) {
1929
- if (typeof codeOrError === "object") {
1930
- return codeOrError;
2123
+ // src/remote_management/remoteManagement.ts
2124
+ function buildRemoteManagementWsUrl(manage) {
2125
+ let baseUrl = (manage || "dashboard.pinggy.io").trim();
2126
+ if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
2127
+ baseUrl = "wss://" + baseUrl;
1931
2128
  }
1932
- return {
1933
- code: codeOrError,
1934
- message
1935
- };
2129
+ const trimmed = baseUrl.replace(/\/$/, "");
2130
+ return `${trimmed}/backend/api/v1/remote-management/connect`;
1936
2131
  }
1937
- function NewResponseObject(data) {
1938
- const encoder = new TextEncoder();
1939
- const bytes = encoder.encode(JSON.stringify(data));
1940
- return {
1941
- response: bytes,
1942
- requestid: "",
1943
- command: "",
1944
- error: false,
1945
- errorresponse: {}
1946
- };
2132
+ function extractHostname(u) {
2133
+ try {
2134
+ const url = new URL(u);
2135
+ return url.host;
2136
+ } catch {
2137
+ return u;
2138
+ }
1947
2139
  }
1948
- function NewErrorResponseObject(errorResponse) {
1949
- return {
1950
- response: new Uint8Array(),
1951
- requestid: "",
1952
- command: "",
1953
- error: true,
1954
- errorresponse: errorResponse
1955
- };
2140
+ function sleep(ms) {
2141
+ return new Promise((res) => setTimeout(res, ms));
1956
2142
  }
1957
- function newStatus(tunnelState, errorCode, errorMsg) {
1958
- let assignedState = tunnelState;
1959
- if (tunnelState === "live" /* Live */) {
1960
- assignedState = "running" /* Running */;
1961
- } else if (tunnelState === "idle" /* New */) {
1962
- assignedState = "idle" /* New */;
1963
- } else if (tunnelState === "closed" /* Closed */) {
1964
- assignedState = "exited" /* Exited */;
2143
+ async function parseRemoteManagement(values) {
2144
+ const rmToken = values["remote-management"];
2145
+ if (typeof rmToken === "string" && rmToken.trim().length > 0) {
2146
+ const manageHost = values["manage"];
2147
+ try {
2148
+ await initiateRemoteManagement(rmToken, manageHost);
2149
+ return { ok: true };
2150
+ } catch (e) {
2151
+ logger.error("Failed to initiate remote management:", e);
2152
+ return { ok: false, error: e };
2153
+ }
1965
2154
  }
1966
- const now = (/* @__PURE__ */ new Date()).toISOString();
1967
- return {
1968
- state: assignedState,
1969
- errorcode: errorCode,
1970
- errormsg: errorMsg,
1971
- createdtimestamp: now,
1972
- starttimestamp: now,
1973
- endtimestamp: now,
1974
- warnings: []
1975
- };
1976
- }
1977
- function newStats() {
1978
- return {
1979
- numLiveConnections: 0,
1980
- numTotalConnections: 0,
1981
- numTotalReqBytes: 0,
1982
- numTotalResBytes: 0,
1983
- numTotalTxBytes: 0,
1984
- elapsedTime: 0
1985
- };
1986
2155
  }
1987
- var ErrorCode, RemoteManagementStatus;
1988
- var init_types = __esm({
1989
- "src/types.ts"() {
1990
- "use strict";
1991
- init_cjs_shims();
1992
- ErrorCode = {
1993
- InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
1994
- InvalidRequestBodyError: "COULD_NOT_READ_BODY",
1995
- InternalServerError: "INTERNAL_SERVER_ERROR",
1996
- InvalidBodyFormatError: "INVALID_DATA_FORMAT",
1997
- ErrorStartingTunnel: "ERROR_STARTING_TUNNEL",
1998
- TunnelNotFound: "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND",
1999
- TunnelAlreadyRunningError: "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING",
2000
- WebsocketUpgradeFailError: "WEBSOCKET_UPGRADE_FAILED",
2001
- RemoteManagementAlreadyRunning: "REMOTE_MANAGEMENT_ALREADY_RUNNING",
2002
- RemoteManagementNotRunning: "REMOTE_MANAGEMENT_NOT_RUNNING",
2003
- RemoteManagementDeserializationFailed: "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED"
2004
- };
2005
- RemoteManagementStatus = {
2006
- Connecting: "CONNECTING",
2007
- Disconnecting: "DISCONNECTING",
2008
- Reconnecting: "RECONNECTING",
2009
- Running: "RUNNING",
2010
- NotRunning: "NOT_RUNNING",
2011
- Error: "ERROR"
2012
- };
2156
+ async function initiateRemoteManagement(token, manage) {
2157
+ if (!token || token.trim().length === 0) {
2158
+ throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
2013
2159
  }
2014
- });
2015
-
2016
- // src/remote_management/remote_schema.ts
2017
- function tunnelConfigToPinggyOptions(config) {
2018
- return {
2019
- token: config.token || "",
2020
- serverAddress: config.serveraddress || "free.pinggy.io",
2021
- forwarding: `${config.forwardedhost || "localhost"}:${config.localport}`,
2022
- webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
2023
- ipWhitelist: config.ipwhitelist || [],
2024
- basicAuth: config.basicauth ? config.basicauth : [],
2025
- bearerTokenAuth: config.bearerauth ? [config.bearerauth] : [],
2026
- headerModification: config.headermodification,
2027
- xForwardedFor: !!config.xff,
2028
- httpsOnly: config.httpsOnly,
2029
- originalRequestUrl: config.fullRequestUrl,
2030
- allowPreflight: config.allowPreflight,
2031
- reverseProxy: config.noReverseProxy,
2032
- force: config.force,
2033
- autoReconnect: config.autoreconnect,
2034
- optional: {
2035
- sniServerName: config.localservertlssni || ""
2036
- }
2160
+ const wsUrl = buildRemoteManagementWsUrl(manage);
2161
+ const wsHost = extractHostname(wsUrl);
2162
+ logger.info("Remote management mode enabled.");
2163
+ _stopRequested = false;
2164
+ const sigintHandler = () => {
2165
+ _stopRequested = true;
2037
2166
  };
2038
- }
2039
- function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg, additionalForwarding, serve) {
2040
- const forwarding = Array.isArray(opts.forwarding) ? String(opts.forwarding[0].address).replace("//", "").replace(/\/$/, "") : String(opts.forwarding).replace("//", "").replace(/\/$/, "");
2041
- const parsedForwardedHost = forwarding.split(":").length == 3 ? forwarding.split(":")[1] : forwarding.split(":")[0];
2042
- const parsedLocalPort = forwarding.split(":").length == 3 ? parseInt(forwarding.split(":")[2], 10) : parseInt(forwarding.split(":")[1], 10);
2043
- const tunnelType = (Array.isArray(opts.forwarding) ? opts.forwarding[0]?.type : void 0) ?? import_pinggy4.TunnelType.Http;
2044
- const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
2045
- return {
2046
- allowPreflight: opts.allowPreflight ?? false,
2047
- allowpreflight: opts.allowPreflight ?? false,
2048
- autoreconnect: opts.autoReconnect ?? false,
2049
- basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
2050
- bearerauth: parsedTokens.length ? parsedTokens.join(",") : null,
2051
- configid,
2052
- configname: configName,
2053
- greetmsg: greetMsg || "",
2054
- force: opts.force ?? false,
2055
- forwardedhost: parsedForwardedHost || "localhost",
2056
- fullRequestUrl: opts.originalRequestUrl ?? false,
2057
- headermodification: opts.headerModification || [],
2058
- //structured list
2059
- httpsOnly: opts.httpsOnly ?? false,
2060
- internalwebdebuggerport: 0,
2061
- ipwhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : null,
2062
- localport: parsedLocalPort || 0,
2063
- localservertlssni: null,
2064
- regioncode: "",
2065
- noReverseProxy: opts.reverseProxy ?? false,
2066
- serveraddress: opts.serverAddress || "free.pinggy.io",
2067
- serverport: 0,
2068
- statusCheckInterval: 0,
2069
- token: opts.token || "",
2070
- tunnelTimeout: 0,
2071
- type: tunnelType,
2072
- webdebuggerport: Number(opts.webDebugger?.split(":")[0]) || 0,
2073
- xff: opts.xForwardedFor ? "1" : "",
2074
- localsservertls: localserverTls || false,
2075
- additionalForwarding: additionalForwarding || [],
2076
- serve: serve || ""
2167
+ process.once("SIGINT", sigintHandler);
2168
+ const logConnecting = () => {
2169
+ printer_default.print(`Connecting to ${wsHost}`);
2170
+ logger.info("Connecting to remote management", { wsUrl });
2077
2171
  };
2172
+ while (!_stopRequested) {
2173
+ logConnecting();
2174
+ setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
2175
+ try {
2176
+ await handleWebSocketConnection(wsUrl, wsHost, token);
2177
+ } catch (error) {
2178
+ logger.warn("Remote management connection error", { error: String(error) });
2179
+ }
2180
+ if (_stopRequested) break;
2181
+ printer_default.warn(`Remote management disconnected. Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds...`);
2182
+ logger.info("Reconnecting to remote management after disconnect");
2183
+ await sleep(RECONNECT_SLEEP_MS);
2184
+ }
2185
+ process.removeListener("SIGINT", sigintHandler);
2186
+ logger.info("Remote management stopped.");
2187
+ return getRemoteManagementState();
2078
2188
  }
2079
- var import_pinggy4, import_zod, HeaderModificationSchema, AdditionalForwardingSchema, TunnelConfigSchema, StartSchema, StopSchema, GetSchema, RestartSchema, UpdateConfigSchema;
2080
- var init_remote_schema = __esm({
2081
- "src/remote_management/remote_schema.ts"() {
2082
- "use strict";
2083
- init_cjs_shims();
2084
- import_pinggy4 = require("@pinggy/pinggy");
2085
- import_zod = require("zod");
2086
- HeaderModificationSchema = import_zod.z.object({
2087
- key: import_zod.z.string(),
2088
- value: import_zod.z.array(import_zod.z.string()).optional(),
2089
- type: import_zod.z.enum(["add", "remove", "update"])
2090
- });
2091
- AdditionalForwardingSchema = import_zod.z.object({
2092
- remoteDomain: import_zod.z.string().optional(),
2093
- remotePort: import_zod.z.number().optional(),
2094
- localDomain: import_zod.z.string(),
2095
- localPort: import_zod.z.number()
2189
+ async function handleWebSocketConnection(wsUrl, wsHost, token) {
2190
+ return new Promise((resolve) => {
2191
+ const ws = new import_ws.default(wsUrl, {
2192
+ headers: { Authorization: `Bearer ${token}` }
2096
2193
  });
2097
- TunnelConfigSchema = import_zod.z.object({
2098
- allowPreflight: import_zod.z.boolean().optional(),
2099
- // primary key
2100
- allowpreflight: import_zod.z.boolean().optional(),
2101
- // legacy key
2102
- autoreconnect: import_zod.z.boolean(),
2103
- basicauth: import_zod.z.array(import_zod.z.object({ username: import_zod.z.string(), password: import_zod.z.string() })).nullable(),
2104
- bearerauth: import_zod.z.string().nullable(),
2105
- configid: import_zod.z.string(),
2106
- configname: import_zod.z.string(),
2107
- greetmsg: import_zod.z.string().optional(),
2108
- force: import_zod.z.boolean(),
2109
- forwardedhost: import_zod.z.string(),
2110
- fullRequestUrl: import_zod.z.boolean(),
2111
- headermodification: import_zod.z.array(HeaderModificationSchema),
2112
- httpsOnly: import_zod.z.boolean(),
2113
- internalwebdebuggerport: import_zod.z.number(),
2114
- ipwhitelist: import_zod.z.array(import_zod.z.string()).nullable(),
2115
- localport: import_zod.z.number(),
2116
- localsservertls: import_zod.z.union([import_zod.z.boolean(), import_zod.z.string()]),
2117
- localservertlssni: import_zod.z.string().nullable(),
2118
- regioncode: import_zod.z.string(),
2119
- noReverseProxy: import_zod.z.boolean(),
2120
- serveraddress: import_zod.z.string(),
2121
- serverport: import_zod.z.number(),
2122
- statusCheckInterval: import_zod.z.number(),
2123
- token: import_zod.z.string(),
2124
- tunnelTimeout: import_zod.z.number(),
2125
- type: import_zod.z.enum([
2126
- import_pinggy4.TunnelType.Http,
2127
- import_pinggy4.TunnelType.Tcp,
2128
- import_pinggy4.TunnelType.Udp,
2129
- import_pinggy4.TunnelType.Tls,
2130
- import_pinggy4.TunnelType.TlsTcp
2131
- ]),
2132
- webdebuggerport: import_zod.z.number(),
2133
- xff: import_zod.z.string(),
2134
- additionalForwarding: import_zod.z.array(AdditionalForwardingSchema).optional(),
2135
- serve: import_zod.z.string().optional()
2136
- }).superRefine((data, ctx) => {
2137
- if (data.allowPreflight === void 0 && data.allowpreflight === void 0) {
2138
- ctx.addIssue({
2139
- code: "custom",
2140
- message: "Either allowPreflight or allowpreflight is required",
2141
- path: ["allowPreflight"]
2142
- });
2194
+ currentWs = ws;
2195
+ let heartbeat = null;
2196
+ let firstMessage = true;
2197
+ const cleanup = () => {
2198
+ if (heartbeat) clearInterval(heartbeat);
2199
+ currentWs = null;
2200
+ resolve();
2201
+ };
2202
+ ws.once("open", () => {
2203
+ printer_default.success(`Connected to ${wsHost}`);
2204
+ heartbeat = setInterval(() => {
2205
+ if (ws.readyState === import_ws.default.OPEN) ws.ping();
2206
+ }, PING_INTERVAL_MS);
2207
+ });
2208
+ ws.on("ping", () => ws.pong());
2209
+ ws.on("message", async (data) => {
2210
+ try {
2211
+ if (firstMessage) {
2212
+ firstMessage = false;
2213
+ const ok = handleConnectionStatusMessage(data);
2214
+ if (!ok) ws.close();
2215
+ return;
2216
+ }
2217
+ setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
2218
+ const req = JSON.parse(data.toString("utf8"));
2219
+ await new WebSocketCommandHandler().handle(ws, req);
2220
+ } catch (e) {
2221
+ logger.warn("Failed handling websocket message", { error: String(e) });
2143
2222
  }
2144
- }).transform((data) => ({
2145
- ...data,
2146
- allowPreflight: data.allowPreflight ?? data.allowpreflight,
2147
- allowpreflight: data.allowPreflight ?? data.allowpreflight
2148
- }));
2149
- StartSchema = import_zod.z.object({
2150
- tunnelID: import_zod.z.string().nullable().optional(),
2151
- tunnelConfig: TunnelConfigSchema
2152
2223
  });
2153
- StopSchema = import_zod.z.object({
2154
- tunnelID: import_zod.z.string().min(1)
2224
+ ws.on("unexpected-response", (_, res) => {
2225
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
2226
+ if (res.statusCode === 401) {
2227
+ printer_default.error("Unauthorized. Please enter a valid token.");
2228
+ logger.error("Unauthorized (401) on remote management connect");
2229
+ } else {
2230
+ printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
2231
+ logger.warn("Unexpected HTTP response", { statusCode: res.statusCode });
2232
+ }
2233
+ ws.close();
2155
2234
  });
2156
- GetSchema = StopSchema;
2157
- RestartSchema = StopSchema;
2158
- UpdateConfigSchema = import_zod.z.object({
2159
- tunnelConfig: TunnelConfigSchema
2235
+ ws.on("close", (code, reason) => {
2236
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2237
+ logger.info("WebSocket closed", { code, reason: reason.toString() });
2238
+ printer_default.warn(`Disconnected (code: ${code}). Retrying...`);
2239
+ cleanup();
2240
+ });
2241
+ ws.on("error", (err) => {
2242
+ setRemoteManagementState({ status: RemoteManagementStatus.Error, errorMessage: err.message });
2243
+ logger.warn("WebSocket error", { error: err.message });
2244
+ printer_default.error(err);
2245
+ cleanup();
2160
2246
  });
2247
+ });
2248
+ }
2249
+ async function closeRemoteManagement(timeoutMs = 1e4) {
2250
+ _stopRequested = true;
2251
+ try {
2252
+ if (currentWs) {
2253
+ try {
2254
+ setRemoteManagementState({ status: RemoteManagementStatus.Disconnecting, errorMessage: "" });
2255
+ currentWs.close();
2256
+ } catch (e) {
2257
+ logger.warn("Error while closing current remote management websocket", { error: String(e) });
2258
+ }
2259
+ }
2260
+ const start = Date.now();
2261
+ while (_remoteManagementState.status === "RUNNING") {
2262
+ if (Date.now() - start > timeoutMs) {
2263
+ logger.warn("Timed out waiting for remote management to stop");
2264
+ break;
2265
+ }
2266
+ await sleep(200);
2267
+ }
2268
+ } finally {
2269
+ currentWs = null;
2270
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2271
+ return getRemoteManagementState();
2272
+ }
2273
+ }
2274
+ function getRemoteManagementState() {
2275
+ return _remoteManagementState;
2276
+ }
2277
+ function setRemoteManagementState(state, errorMessage) {
2278
+ _remoteManagementState = {
2279
+ status: state.status,
2280
+ errorMessage: errorMessage || ""
2281
+ };
2282
+ }
2283
+ var import_ws, RECONNECT_SLEEP_MS, PING_INTERVAL_MS, _remoteManagementState, _stopRequested, currentWs;
2284
+ var init_remoteManagement = __esm({
2285
+ "src/remote_management/remoteManagement.ts"() {
2286
+ "use strict";
2287
+ init_cjs_shims();
2288
+ import_ws = __toESM(require("ws"), 1);
2289
+ init_logger();
2290
+ init_websocket_handlers();
2291
+ init_printer();
2292
+ init_types();
2293
+ RECONNECT_SLEEP_MS = 5e3;
2294
+ PING_INTERVAL_MS = 3e4;
2295
+ _remoteManagementState = {
2296
+ status: "NOT_RUNNING",
2297
+ errorMessage: ""
2298
+ };
2299
+ _stopRequested = false;
2300
+ currentWs = null;
2161
2301
  }
2162
2302
  });
2163
2303
 
2164
- // src/remote_management/handler.ts
2165
- var import_pinggy5, TunnelOperations;
2166
- var init_handler = __esm({
2167
- "src/remote_management/handler.ts"() {
2304
+ // src/cli/options.ts
2305
+ var cliOptions;
2306
+ var init_options = __esm({
2307
+ "src/cli/options.ts"() {
2168
2308
  "use strict";
2169
2309
  init_cjs_shims();
2170
- init_types();
2171
- init_TunnelManager();
2172
- init_remote_schema();
2173
- import_pinggy5 = require("@pinggy/pinggy");
2174
- TunnelOperations = class {
2175
- constructor() {
2176
- this.tunnelManager = TunnelManager.getInstance();
2177
- }
2178
- buildStatus(tunnelId, state, errorCode) {
2179
- const status = newStatus(state, errorCode, "");
2180
- try {
2181
- const managed = this.tunnelManager.getManagedTunnel("", tunnelId);
2182
- if (managed) {
2183
- status.createdtimestamp = managed.createdAt || "";
2184
- status.starttimestamp = managed.startedAt || "";
2185
- status.endtimestamp = managed.stoppedAt || "";
2186
- }
2187
- } catch (e) {
2188
- }
2189
- return status;
2190
- }
2191
- // --- Helper to construct TunnelResponse ---
2192
- async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, additionalForwarding, serve) {
2193
- const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
2194
- this.tunnelManager.getTunnelStatus(tunnelid),
2195
- this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
2196
- this.tunnelManager.getLocalserverTlsInfo(tunnelid),
2197
- this.tunnelManager.getTunnelGreetMessage(tunnelid),
2198
- this.tunnelManager.getTunnelUrls(tunnelid)
2199
- ]);
2200
- return {
2201
- tunnelid,
2202
- remoteurls,
2203
- tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg, additionalForwarding),
2204
- status: this.buildStatus(tunnelid, status, "" /* NoError */),
2205
- stats
2206
- };
2207
- }
2208
- error(code, err, fallback) {
2209
- return newErrorResponse({
2210
- code,
2211
- message: err instanceof Error ? err.message : fallback
2212
- });
2213
- }
2214
- // --- Operations ---
2215
- async handleStart(config) {
2216
- try {
2217
- const opts = tunnelConfigToPinggyOptions(config);
2218
- const additionalForwardingParsed = config.additionalForwarding || [];
2219
- const { tunnelid, instance, tunnelName, additionalForwarding, serve } = await this.tunnelManager.createTunnel({
2220
- ...opts,
2221
- tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [import_pinggy5.TunnelType.Http],
2222
- // Temporary fix in future we will not use this field.
2223
- configid: config.configid,
2224
- tunnelName: config.configname,
2225
- additionalForwarding: additionalForwardingParsed,
2226
- serve: config.serve
2227
- });
2228
- this.tunnelManager.startTunnel(tunnelid);
2229
- const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
2230
- const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, additionalForwarding, serve);
2231
- return resp;
2232
- } catch (err) {
2233
- return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
2234
- }
2235
- }
2236
- async handleUpdateConfig(config) {
2237
- try {
2238
- const opts = tunnelConfigToPinggyOptions(config);
2239
- const tunnel = await this.tunnelManager.updateConfig({
2240
- ...opts,
2241
- tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [import_pinggy5.TunnelType.Http],
2242
- // // Temporary fix in future we will not use this field.
2243
- configid: config.configid,
2244
- tunnelName: config.configname,
2245
- additionalForwarding: config.additionalForwarding || [],
2246
- serve: config.serve
2247
- });
2248
- if (!tunnel.instance || !tunnel.tunnelConfig)
2249
- throw new Error("Invalid tunnel state after configuration update");
2250
- return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.additionalForwarding, tunnel.serve);
2251
- } catch (err) {
2252
- return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
2310
+ cliOptions = {
2311
+ // SSH-like options
2312
+ R: { type: "string", multiple: true, description: "Local port. Eg. -R0:localhost:3000 will forward tunnel connections to local port 3000." },
2313
+ L: { type: "string", multiple: true, description: "Web Debugger address. Eg. -L4300:localhost:4300 will start web debugger on port 4300." },
2314
+ o: { type: "string", multiple: true, description: "Options", hidden: true },
2315
+ "server-port": { type: "string", short: "p", description: "Pinggy server port. Default: 443" },
2316
+ v4: { type: "boolean", short: "4", description: "IPv4 only", hidden: true },
2317
+ v6: { type: "boolean", short: "6", description: "IPv6 only", hidden: true },
2318
+ // These options appear in the ssh command, but we ignore it in CLI
2319
+ t: { type: "boolean", description: "hidden", hidden: true },
2320
+ T: { type: "boolean", description: "hidden", hidden: true },
2321
+ n: { type: "boolean", description: "hidden", hidden: true },
2322
+ N: { type: "boolean", description: "hidden", hidden: true },
2323
+ // Better options
2324
+ type: { type: "string", description: "Type of the connection. Eg. --type tcp" },
2325
+ localport: { type: "string", short: "l", description: "Takes input as [protocol:][host:]port. Eg. --localport https://localhost:8000 OR -l 3000" },
2326
+ debugger: { type: "string", short: "d", description: "Port for web debugger. Eg. --debugger 4300 OR -d 4300" },
2327
+ token: { type: "string", description: "Token for authentication. Eg. --token TOKEN_VALUE" },
2328
+ // Logging options (CLI overrides env)
2329
+ loglevel: { type: "string", description: "Logging level: ERROR, INFO, DEBUG. Overrides PINGGY_LOG_LEVEL environment variable" },
2330
+ logfile: { type: "string", description: "Path to log file. Overrides PINGGY_LOG_FILE environment variable" },
2331
+ v: { type: "boolean", description: "Print logs to stdout for Cli. Overrides PINGGY_LOG_STDOUT environment variable" },
2332
+ vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
2333
+ vvv: { type: "boolean", description: "Enable all logs from Cli, SDK and internal components." },
2334
+ autoreconnect: { type: "string", short: "a", description: "Automatically reconnect tunnel on failure (enabled by default). Use -a false to disable." },
2335
+ // Save and load config
2336
+ saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
2337
+ conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
2338
+ // File server
2339
+ serve: { type: "string", description: "Start a webserver to serve files from the specified path. Eg --serve /path/to/files" },
2340
+ // Remote Control
2341
+ "remote-management": { type: "string", description: "Enable remote management of tunnels with token. Eg. --remote-management API_KEY" },
2342
+ manage: { type: "string", description: "Provide a server address to manage tunnels. Eg --manage dashboard.pinggy.io" },
2343
+ notui: { type: "boolean", description: "Disable TUI in remote management mode" },
2344
+ // Misc
2345
+ version: { type: "boolean", description: "Print version" },
2346
+ // Help
2347
+ help: { type: "boolean", short: "h", description: "Show this help message" }
2348
+ };
2349
+ }
2350
+ });
2351
+
2352
+ // src/cli/help.ts
2353
+ function printHelpMessage() {
2354
+ console.log("\nPinggy CLI Tool - Create secure tunnels to your localhost.");
2355
+ console.log("\nUsage:");
2356
+ console.log(" pinggy [options] -l <port>\n");
2357
+ console.log("Options:");
2358
+ for (const [key, value] of Object.entries(cliOptions)) {
2359
+ if (value.hidden) continue;
2360
+ const short = "short" in value && value.short ? `-${value.short}, ` : " ";
2361
+ const optType = value.type === "boolean" ? "" : "<value>";
2362
+ console.log(` ${short}--${key.padEnd(17)} ${optType.padEnd(8)} ${value.description}`);
2363
+ }
2364
+ console.log("\nExtended options :");
2365
+ console.log(" x:https Enforce HTTPS only (redirect HTTP to HTTPS)");
2366
+ console.log(" x:noreverseproxy Disable built-in reverse-proxy header injection");
2367
+ console.log(" x:localservertls:host Connect to local HTTPS server with SNI");
2368
+ console.log(" x:passpreflight Pass CORS preflight requests unchanged");
2369
+ console.log(" a:Key:Val Add header");
2370
+ console.log(" u:Key:Val Update header");
2371
+ console.log(" r:Key Remove header");
2372
+ console.log(" b:user:pass Basic auth");
2373
+ console.log(" k:BEARER Bearer token");
2374
+ console.log(" w:192.168.1.0/24 IP whitelist (CIDR)");
2375
+ console.log("\nExamples (User-friendly):");
2376
+ console.log(" pinggy -l 3000 # HTTP(S) tunnel to localhost port 3000");
2377
+ console.log(" pinggy --type tcp -l 22 # TCP tunnel for SSH (port 22)");
2378
+ console.log(" pinggy -l 8080 -d 4300 # HTTP tunnel to port 8080 with debugger running at localhost:4300");
2379
+ console.log(" pinggy --token mytoken -l 3000 # Authenticated tunnel");
2380
+ console.log(" pinggy x:https x:xff -l https://localhost:8443 # HTTPS-only + XFF");
2381
+ console.log(" pinggy w:192.168.1.0/24 -l 8080 # IP whitelist restriction");
2382
+ console.log("\nExamples (SSH-style):");
2383
+ console.log(" pinggy -R0:localhost:3000 # Basic HTTP tunnel");
2384
+ console.log(" pinggy --type tcp -R0:localhost:22 # TCP tunnel for SSH");
2385
+ console.log(" pinggy -R0:localhost:8080 -L4300:localhost:4300 # HTTP tunnel with debugger");
2386
+ console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region\n");
2387
+ }
2388
+ var init_help = __esm({
2389
+ "src/cli/help.ts"() {
2390
+ "use strict";
2391
+ init_cjs_shims();
2392
+ init_options();
2393
+ }
2394
+ });
2395
+
2396
+ // src/cli/defaults.ts
2397
+ var defaultOptions;
2398
+ var init_defaults = __esm({
2399
+ "src/cli/defaults.ts"() {
2400
+ "use strict";
2401
+ init_cjs_shims();
2402
+ defaultOptions = {
2403
+ token: void 0,
2404
+ // No default token
2405
+ serverAddress: "a.pinggy.io",
2406
+ forwarding: "localhost:8000",
2407
+ webDebugger: "",
2408
+ ipWhitelist: [],
2409
+ basicAuth: [],
2410
+ bearerTokenAuth: [],
2411
+ headerModification: [],
2412
+ force: false,
2413
+ xForwardedFor: false,
2414
+ httpsOnly: false,
2415
+ originalRequestUrl: false,
2416
+ allowPreflight: false,
2417
+ reverseProxy: false,
2418
+ autoReconnect: true
2419
+ };
2420
+ }
2421
+ });
2422
+
2423
+ // src/cli/extendedOptions.ts
2424
+ function parseExtendedOptions(options, config) {
2425
+ if (!options) return;
2426
+ for (const opt of options) {
2427
+ const [key, value] = opt.replace(/^"|"$/g, "").split(/:(.+)/).filter(Boolean);
2428
+ switch (key) {
2429
+ case "x":
2430
+ switch (value) {
2431
+ case "https":
2432
+ case "httpsonly":
2433
+ config.httpsOnly = true;
2434
+ break;
2435
+ case "passpreflight":
2436
+ case "allowpreflight":
2437
+ config.allowPreflight = true;
2438
+ break;
2439
+ case "reverseproxy":
2440
+ config.reverseProxy = false;
2441
+ break;
2442
+ case "xff":
2443
+ config.xForwardedFor = true;
2444
+ break;
2445
+ case "fullurl":
2446
+ case "fullrequesturl":
2447
+ config.originalRequestUrl = true;
2448
+ break;
2449
+ default:
2450
+ printer_default.warn(`Unknown extended option "${key}"`);
2451
+ logger.warn(`Warning: Unknown extended option "${key}"`);
2452
+ break;
2253
2453
  }
2254
- }
2255
- async handleList() {
2256
- try {
2257
- const tunnels = await this.tunnelManager.getAllTunnels();
2258
- if (tunnels.length === 0) {
2259
- return [];
2454
+ break;
2455
+ case "w":
2456
+ if (value) {
2457
+ const ips = value.split(",").map((ip) => ip.trim()).filter(Boolean);
2458
+ const invalidIps = ips.filter((ip) => !(isValidIpV4Cidr(ip) || isValidIpV6Cidr(ip)));
2459
+ if (invalidIps.length > 0) {
2460
+ printer_default.warn(`Invalid IP/CIDR(s) in whitelist: ${invalidIps.join(", ")}`);
2461
+ logger.warn(`Warning: Invalid IP/CIDR(s) in whitelist: ${invalidIps.join(", ")}`);
2260
2462
  }
2261
- return Promise.all(
2262
- tunnels.map(async (t) => {
2263
- const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
2264
- const [status, tlsInfo, greetMsg] = await Promise.all([
2265
- this.tunnelManager.getTunnelStatus(t.tunnelid),
2266
- this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
2267
- this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
2268
- ]);
2269
- const pinggyOptions = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
2270
- const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configid, t.tunnelName, tlsInfo, greetMsg, t.additionalForwarding, t.serve);
2271
- return {
2272
- tunnelid: t.tunnelid,
2273
- remoteurls: t.remoteurls,
2274
- status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
2275
- stats: rawStats,
2276
- tunnelconfig: tunnelConfig
2277
- };
2278
- })
2279
- );
2280
- } catch (err) {
2281
- return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
2282
- }
2283
- }
2284
- async handleStop(tunnelid) {
2285
- try {
2286
- const { configid } = this.tunnelManager.stopTunnel(tunnelid);
2287
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2288
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2289
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2290
- } catch (err) {
2291
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
2463
+ if (!(invalidIps.length > 0)) {
2464
+ config.ipWhitelist = ips;
2465
+ }
2466
+ } else {
2467
+ printer_default.warn(`Extended option "${opt}" for 'w' requires IP(s)`);
2468
+ logger.warn(`Warning: Extended option "${opt}" for 'w' requires IP(s)`);
2292
2469
  }
2293
- }
2294
- async handleGet(tunnelid) {
2295
- try {
2296
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2297
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2298
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2299
- } catch (err) {
2300
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
2470
+ break;
2471
+ case "k":
2472
+ if (!config.bearerTokenAuth) config.bearerTokenAuth = [];
2473
+ if (value) {
2474
+ config.bearerTokenAuth.push(value);
2475
+ } else {
2476
+ printer_default.warn(`Extended option "${opt}" for 'k' requires a value`);
2477
+ logger.warn(`Warning: Extended option "${opt}" for 'k' requires a value`);
2301
2478
  }
2302
- }
2303
- async handleRestart(tunnelid) {
2304
- try {
2305
- await this.tunnelManager.restartTunnel(tunnelid);
2306
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2307
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2308
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2309
- } catch (err) {
2310
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
2479
+ break;
2480
+ case "b":
2481
+ if (value && value.includes(":")) {
2482
+ const [username, password] = value.split(/:(.+)/);
2483
+ if (!config.basicAuth) config.basicAuth = [];
2484
+ config.basicAuth.push({ username, password });
2485
+ } else {
2486
+ printer_default.warn(`Extended option "${opt}" for 'b' requires value in format username:password`);
2487
+ logger.warn(`Warning: Extended option "${opt}" for 'b' requires value in format username:password`);
2311
2488
  }
2312
- }
2313
- handleRegisterStatsListener(tunnelid, listener) {
2314
- this.tunnelManager.registerStatsListener(tunnelid, listener);
2315
- }
2316
- handleUnregisterStatsListener(tunnelid, listnerId) {
2317
- this.tunnelManager.deregisterStatsListener(tunnelid, listnerId);
2318
- }
2319
- handleGetTunnelStats(tunnelid) {
2320
- try {
2321
- const stats = this.tunnelManager.getTunnelStats(tunnelid);
2322
- if (!stats) {
2323
- return [newStats()];
2324
- }
2325
- return stats;
2326
- } catch (err) {
2327
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats");
2489
+ break;
2490
+ case "a":
2491
+ if (value && value.includes(":")) {
2492
+ const [key2, val] = value.split(/:(.+)/);
2493
+ if (!config.headerModification) config.headerModification = [];
2494
+ config.headerModification.push({ type: "add", key: key2, value: [val] });
2495
+ } else {
2496
+ printer_default.warn(`Extended option "${opt}" for 'a' requires key:value`);
2497
+ logger.warn(`Warning: Extended option "${opt}" for 'a' requires key:value`);
2328
2498
  }
2329
- }
2330
- handleRegisterDisconnectListener(tunnelid, listener) {
2331
- this.tunnelManager.registerDisconnectListener(tunnelid, listener);
2332
- }
2333
- handleRemoveStoppedTunnelByConfigId(configId) {
2334
- try {
2335
- return this.tunnelManager.removeStoppedTunnelByConfigId(configId);
2336
- } catch (err) {
2337
- return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by configId");
2499
+ break;
2500
+ case "u":
2501
+ if (value && value.includes(":")) {
2502
+ const [key2, val] = value.split(/:(.+)/);
2503
+ if (!config.headerModification) config.headerModification = [];
2504
+ config.headerModification.push({ type: "update", key: key2, value: [val] });
2505
+ } else {
2506
+ printer_default.warn(`Extended option "${opt}" for 'u' requires key:value`);
2507
+ logger.warn(`Warning: Extended option "${opt}" for 'u' requires key:value`);
2338
2508
  }
2339
- }
2340
- handleRemoveStoppedTunnelByTunnelId(tunnelId) {
2341
- try {
2342
- return this.tunnelManager.removeStoppedTunnelByTunnelId(tunnelId);
2343
- } catch (err) {
2344
- return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by tunnelId");
2509
+ break;
2510
+ case "r":
2511
+ if (value) {
2512
+ if (!config.headerModification) config.headerModification = [];
2513
+ config.headerModification.push({ type: "remove", key: value });
2514
+ } else {
2515
+ printer_default.warn(`Extended option "${opt}" for 'r' requires a key`);
2345
2516
  }
2346
- }
2347
- };
2348
- }
2349
- });
2350
-
2351
- // src/remote_management/websocket_handlers.ts
2352
- function handleConnectionStatusMessage(firstMessage) {
2353
- try {
2354
- const text = typeof firstMessage === "string" ? firstMessage : firstMessage.toString();
2355
- const cs = JSON.parse(text);
2356
- if (!cs.success) {
2357
- const msg = cs.error_msg || "Connection failed";
2358
- printer_default.warn(`Connection failed: ${msg}`);
2359
- logger.warn("Remote management connection failed", { error_code: cs.error_code, error_msg: msg });
2360
- return false;
2517
+ break;
2518
+ default:
2519
+ printer_default.warn(`Unknown extended option "${key}"`);
2520
+ break;
2361
2521
  }
2362
- return true;
2363
- } catch (e) {
2364
- logger.warn("Failed to parse connection status message", { error: String(e) });
2365
- return true;
2366
2522
  }
2367
2523
  }
2368
- var import_zod2, WebSocketCommandHandler;
2369
- var init_websocket_handlers = __esm({
2370
- "src/remote_management/websocket_handlers.ts"() {
2524
+ function isValidIpV4Cidr(input) {
2525
+ if (input.includes("/")) {
2526
+ const [ip, mask] = input.split("/");
2527
+ if (!ip || !mask) return false;
2528
+ const isIp4 = (0, import_net.isIP)(ip) === 4;
2529
+ const maskNum = parseInt(mask, 10);
2530
+ const isMaskValid = !isNaN(maskNum) && maskNum >= 0 && maskNum <= 32;
2531
+ return isIp4 && isMaskValid;
2532
+ }
2533
+ return false;
2534
+ }
2535
+ function isValidIpV6Cidr(input) {
2536
+ if (input.includes("/")) {
2537
+ const [rawIp, mask] = input.split("/");
2538
+ if (!rawIp || !mask) return false;
2539
+ const ip = rawIp.split("%")[0].replace(/^\[|\]$/g, "");
2540
+ const isIp6 = (0, import_net.isIP)(ip) === 6;
2541
+ const maskNum = parseInt(mask, 10);
2542
+ const isMaskValid = !isNaN(maskNum) && maskNum >= 0 && maskNum <= 128;
2543
+ return isIp6 && isMaskValid;
2544
+ }
2545
+ return false;
2546
+ }
2547
+ var import_net;
2548
+ var init_extendedOptions = __esm({
2549
+ "src/cli/extendedOptions.ts"() {
2371
2550
  "use strict";
2372
2551
  init_cjs_shims();
2552
+ import_net = require("net");
2373
2553
  init_logger();
2374
- init_types();
2375
- init_handler();
2376
- init_remote_schema();
2377
- import_zod2 = __toESM(require("zod"), 1);
2378
2554
  init_printer();
2379
- WebSocketCommandHandler = class {
2380
- constructor() {
2381
- this.tunnelHandler = new TunnelOperations();
2382
- }
2383
- safeParse(text) {
2384
- if (!text) return void 0;
2385
- try {
2386
- return JSON.parse(text);
2387
- } catch (e) {
2388
- logger.warn("Invalid JSON payload", { error: String(e), text });
2389
- return void 0;
2390
- }
2391
- }
2392
- sendResponse(ws, resp) {
2393
- const payload = {
2394
- ...resp,
2395
- response: Buffer.from(resp.response || []).toString("base64")
2396
- };
2397
- ws.send(JSON.stringify(payload));
2398
- }
2399
- sendError(ws, req, message, code = ErrorCode.InternalServerError) {
2400
- const resp = NewErrorResponseObject({ code, message });
2401
- resp.command = req.command || "";
2402
- resp.requestid = req.requestid || "";
2403
- this.sendResponse(ws, resp);
2404
- }
2405
- async handleStartReq(req, raw) {
2406
- const dc = StartSchema.parse(raw);
2407
- printer_default.info("Starting tunnel with config name: " + dc.tunnelConfig.configname);
2408
- const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
2409
- return this.wrapResponse(result, req);
2410
- }
2411
- async handleStopReq(req, raw) {
2412
- const dc = StopSchema.parse(raw);
2413
- printer_default.info("Stopping tunnel with ID: " + dc.tunnelID);
2414
- const result = await this.tunnelHandler.handleStop(dc.tunnelID);
2415
- return this.wrapResponse(result, req);
2416
- }
2417
- async handleGetReq(req, raw) {
2418
- const dc = GetSchema.parse(raw);
2419
- const result = await this.tunnelHandler.handleGet(dc.tunnelID);
2420
- return this.wrapResponse(result, req);
2421
- }
2422
- async handleRestartReq(req, raw) {
2423
- const dc = RestartSchema.parse(raw);
2424
- const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
2425
- return this.wrapResponse(result, req);
2426
- }
2427
- async handleUpdateConfigReq(req, raw) {
2428
- const dc = UpdateConfigSchema.parse(raw);
2429
- const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
2430
- return this.wrapResponse(result, req);
2431
- }
2432
- async handleListReq(req) {
2433
- const result = await this.tunnelHandler.handleList();
2434
- return this.wrapResponse(result, req);
2435
- }
2436
- wrapResponse(result, req) {
2437
- if (isErrorResponse(result)) {
2438
- const errResp = NewErrorResponseObject(result);
2439
- errResp.command = req.command;
2440
- errResp.requestid = req.requestid;
2441
- return errResp;
2442
- }
2443
- const finalResult = JSON.parse(JSON.stringify(result));
2444
- if (Array.isArray(finalResult)) {
2445
- finalResult.forEach((item) => {
2446
- if (item?.tunnelconfig) {
2447
- delete item.tunnelconfig.allowPreflight;
2448
- }
2449
- });
2450
- } else if (finalResult?.tunnelconfig) {
2451
- delete finalResult.tunnelconfig.allowPreflight;
2555
+ }
2556
+ });
2557
+
2558
+ // src/cli/buildConfig.ts
2559
+ function isKeyword(str) {
2560
+ return KEYWORDS.has(str.toLowerCase());
2561
+ }
2562
+ function parseUserAndDomain(str) {
2563
+ let token;
2564
+ let type;
2565
+ let server;
2566
+ let qrCode;
2567
+ let forceFlag;
2568
+ if (!str) return { token, type, server, qrCode, forceFlag };
2569
+ if (str.includes("@")) {
2570
+ const [user, domain] = str.split("@", 2);
2571
+ if (domainRegex.test(domain)) {
2572
+ let processKeyword2 = function(keyword) {
2573
+ if ([import_pinggy5.TunnelType.Http, import_pinggy5.TunnelType.Tcp, import_pinggy5.TunnelType.Tls, import_pinggy5.TunnelType.Udp, import_pinggy5.TunnelType.TlsTcp].includes(keyword)) {
2574
+ type = keyword;
2575
+ } else if (keyword === "force") {
2576
+ forceFlag = true;
2577
+ } else if (keyword === "qr") {
2578
+ qrCode = true;
2452
2579
  }
2453
- const respObj = NewResponseObject(finalResult);
2454
- respObj.command = req.command;
2455
- respObj.requestid = req.requestid;
2456
- return respObj;
2580
+ };
2581
+ var processKeyword = processKeyword2;
2582
+ server = domain;
2583
+ const parts = user.split("+");
2584
+ if (parts.length === 0) {
2585
+ return { token, type, server, qrCode, forceFlag };
2457
2586
  }
2458
- async handle(ws, req) {
2459
- const cmd = (req.command || "").toLowerCase();
2460
- const raw = this.safeParse(req.data);
2461
- try {
2462
- let response;
2463
- switch (cmd) {
2464
- case "start": {
2465
- response = await this.handleStartReq(req, raw);
2466
- break;
2467
- }
2468
- case "stop": {
2469
- response = await this.handleStopReq(req, raw);
2470
- break;
2471
- }
2472
- case "get": {
2473
- response = await this.handleGetReq(req, raw);
2474
- break;
2475
- }
2476
- case "restart": {
2477
- response = await this.handleRestartReq(req, raw);
2478
- break;
2479
- }
2480
- case "updateconfig": {
2481
- response = await this.handleUpdateConfigReq(req, raw);
2482
- break;
2483
- }
2484
- case "list": {
2485
- response = await this.handleListReq(req);
2486
- break;
2487
- }
2488
- default:
2489
- if (typeof req.command === "string") {
2490
- logger.warn("Unknown command", { command: req.command });
2491
- }
2492
- return this.sendError(ws, req, "Invalid command");
2587
+ const firstPart = parts[0];
2588
+ if (!isKeyword(firstPart)) {
2589
+ token = firstPart;
2590
+ for (let i = 1; i < parts.length; i++) {
2591
+ const part = parts[i].toLowerCase();
2592
+ if (!isKeyword(part)) {
2593
+ throw new Error(`Invalid user format: unexpected token '${part}' when keywords are expected.`);
2493
2594
  }
2494
- logger.debug("Sending response", { command: response.command, requestid: response.requestid });
2495
- this.sendResponse(ws, response);
2496
- } catch (e) {
2497
- if (e instanceof import_zod2.default.ZodError) {
2498
- logger.warn("Validation failed", { cmd, issues: e.issues });
2499
- return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
2595
+ processKeyword2(part);
2596
+ }
2597
+ } else {
2598
+ for (const part of parts) {
2599
+ const lowerPart = part.toLowerCase();
2600
+ if (!isKeyword(lowerPart)) {
2601
+ throw new Error(`Invalid user format: unexpected token '${lowerPart}' when keywords are expected.`);
2500
2602
  }
2501
- logger.error("Error handling command", { cmd, error: String(e) });
2502
- return this.sendError(ws, req, e?.message || "Internal error");
2603
+ processKeyword2(lowerPart);
2503
2604
  }
2504
2605
  }
2505
- };
2606
+ }
2607
+ } else if (domainRegex.test(str)) {
2608
+ server = str;
2506
2609
  }
2507
- });
2508
-
2509
- // src/remote_management/remoteManagement.ts
2510
- function buildRemoteManagementWsUrl(manage) {
2511
- let baseUrl = (manage || "dashboard.pinggy.io").trim();
2512
- if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
2513
- baseUrl = "wss://" + baseUrl;
2610
+ return { token, type, server, qrCode, forceFlag };
2611
+ }
2612
+ function parseUsers(positionalArgs, explicitToken) {
2613
+ let token;
2614
+ let server;
2615
+ let type;
2616
+ let forceFlag = false;
2617
+ let qrCode = false;
2618
+ let remaining = [...positionalArgs];
2619
+ if (typeof explicitToken === "string") {
2620
+ const parsed = parseUserAndDomain(explicitToken);
2621
+ if (parsed.server) server = parsed.server;
2622
+ if (parsed.type) type = parsed.type;
2623
+ if (parsed.token) token = parsed.token;
2624
+ if (parsed.forceFlag) forceFlag = true;
2625
+ if (parsed.qrCode) qrCode = true;
2626
+ }
2627
+ if (remaining.length > 0) {
2628
+ const first = remaining[0];
2629
+ const parsed = parseUserAndDomain(first);
2630
+ if (parsed.server) {
2631
+ server = parsed.server;
2632
+ if (parsed.type) type = parsed.type;
2633
+ if (parsed.token) token = parsed.token;
2634
+ if (parsed.forceFlag) forceFlag = true;
2635
+ if (parsed.qrCode) qrCode = true;
2636
+ remaining = remaining.slice(1);
2637
+ }
2638
+ }
2639
+ return { token, server, type, forceFlag, qrCode, remaining };
2640
+ }
2641
+ function parseType(finalConfig, values, inferredType) {
2642
+ const t = inferredType || values.type || finalConfig.tunnelType;
2643
+ if (t === import_pinggy5.TunnelType.Http || t === import_pinggy5.TunnelType.Tcp || t === import_pinggy5.TunnelType.Tls || t === import_pinggy5.TunnelType.Udp || t === import_pinggy5.TunnelType.TlsTcp) {
2644
+ finalConfig.tunnelType = [t];
2645
+ }
2646
+ }
2647
+ function parseLocalPort(finalConfig, values) {
2648
+ if (typeof values.localport !== "string") return null;
2649
+ let lp = values.localport.trim();
2650
+ let isHttps = false;
2651
+ if (lp.startsWith("https://")) {
2652
+ isHttps = true;
2653
+ lp = lp.replace(/^https:\/\//, "");
2654
+ } else if (lp.startsWith("http://")) {
2655
+ lp = lp.replace(/^http:\/\//, "");
2656
+ }
2657
+ const parts = lp.split(":");
2658
+ if (parts.length === 1) {
2659
+ const port = parseInt(parts[0], 10);
2660
+ if (!Number.isNaN(port) && isValidPort(port)) {
2661
+ finalConfig.forwarding = `localhost:${port}`;
2662
+ } else {
2663
+ return new Error("Invalid local port");
2664
+ }
2665
+ } else if (parts.length === 2) {
2666
+ const host = parts[0] || "localhost";
2667
+ const port = parseInt(parts[1], 10);
2668
+ if (!Number.isNaN(port) && isValidPort(port)) {
2669
+ finalConfig.forwarding = `${host}:${port}`;
2670
+ } else {
2671
+ return new Error("Invalid local port. Please use -h option for help.");
2672
+ }
2673
+ } else {
2674
+ return new Error("Invalid --localport format. Please use -h option for help.");
2514
2675
  }
2515
- const trimmed = baseUrl.replace(/\/$/, "");
2516
- return `${trimmed}/backend/api/v1/remote-management/connect`;
2676
+ return null;
2517
2677
  }
2518
- function extractHostname(u) {
2519
- try {
2520
- const url = new URL(u);
2521
- return url.host;
2522
- } catch {
2523
- return u;
2678
+ function removeIPv6Brackets(ip) {
2679
+ if (ip.startsWith("[") && ip.endsWith("]")) {
2680
+ return ip.slice(1, -1);
2524
2681
  }
2682
+ return ip;
2525
2683
  }
2526
- function sleep(ms) {
2527
- return new Promise((res) => setTimeout(res, ms));
2528
- }
2529
- async function parseRemoteManagement(values) {
2530
- const rmToken = values["remote-management"];
2531
- if (typeof rmToken === "string" && rmToken.trim().length > 0) {
2532
- const manageHost = values["manage"];
2533
- try {
2534
- await initiateRemoteManagement(rmToken, manageHost);
2535
- return { ok: true };
2536
- } catch (e) {
2537
- logger.error("Failed to initiate remote management:", e);
2538
- return { ok: false, error: e };
2684
+ function ipv6SafeSplitColon(s) {
2685
+ const result = [];
2686
+ let buf = "";
2687
+ const stack = [];
2688
+ for (let i = 0; i < s.length; i++) {
2689
+ const c = s[i];
2690
+ if (c === "[") {
2691
+ stack.push(c);
2692
+ } else if (c === "]" && stack.length > 0) {
2693
+ stack.pop();
2694
+ }
2695
+ if (c === ":" && stack.length === 0) {
2696
+ result.push(buf);
2697
+ buf = "";
2698
+ } else {
2699
+ buf += c;
2539
2700
  }
2540
2701
  }
2702
+ result.push(buf);
2703
+ return result;
2541
2704
  }
2542
- async function initiateRemoteManagement(token, manage) {
2543
- if (!token || token.trim().length === 0) {
2544
- throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
2705
+ function parseDefaultForwarding(forwarding) {
2706
+ const parts = ipv6SafeSplitColon(forwarding);
2707
+ if (parts.length === 3) {
2708
+ const remotePort = parseInt(parts[0], 10);
2709
+ const localDomain = removeIPv6Brackets(parts[1] || "localhost");
2710
+ const localPort = parseInt(parts[2], 10);
2711
+ return { remotePort, localDomain, localPort };
2545
2712
  }
2546
- const wsUrl = buildRemoteManagementWsUrl(manage);
2547
- const wsHost = extractHostname(wsUrl);
2548
- logger.info("Remote management mode enabled.");
2549
- _stopRequested = false;
2550
- const sigintHandler = () => {
2551
- _stopRequested = true;
2713
+ if (parts.length === 4) {
2714
+ const remoteDomain = removeIPv6Brackets(parts[0]);
2715
+ const remotePort = parseInt(parts[1], 10);
2716
+ const localDomain = removeIPv6Brackets(parts[2] || "localhost");
2717
+ const localPort = parseInt(parts[3], 10);
2718
+ return { remoteDomain, remotePort, localDomain, localPort };
2719
+ }
2720
+ return new Error("forwarding address incorrect");
2721
+ }
2722
+ function parseAdditionalForwarding(forwarding) {
2723
+ const toPort = (v) => {
2724
+ if (!v) return null;
2725
+ const n = parseInt(v, 10);
2726
+ return Number.isNaN(n) ? null : n;
2552
2727
  };
2553
- process.once("SIGINT", sigintHandler);
2554
- const logConnecting = () => {
2555
- printer_default.print(`Connecting to ${wsHost}`);
2556
- logger.info("Connecting to remote management", { wsUrl });
2728
+ const parsed = ipv6SafeSplitColon(forwarding);
2729
+ if (parsed.length !== 4) {
2730
+ return new Error(
2731
+ "forwarding must be in format: [schema//]hostname[/port][@forwardingId]:<placeholder>:<forwardingAddress>:<forwardingPort>"
2732
+ );
2733
+ }
2734
+ const firstPart = parsed[0];
2735
+ const [hostPart] = firstPart.split("@");
2736
+ let protocol = "http";
2737
+ let remoteDomainRaw;
2738
+ let remotePort = 0;
2739
+ if (hostPart.includes("//")) {
2740
+ const [schema, rest] = hostPart.split("//");
2741
+ if (!schema || !VALID_PROTOCOLS.includes(schema)) {
2742
+ return new Error(`invalid protocol: ${schema}`);
2743
+ }
2744
+ protocol = schema;
2745
+ const domainAndPort = rest.split("/");
2746
+ if (domainAndPort.length > 2) {
2747
+ return new Error("invalid forwarding address format");
2748
+ }
2749
+ remoteDomainRaw = domainAndPort[0];
2750
+ if (!remoteDomainRaw || !domainRegex.test(remoteDomainRaw)) {
2751
+ return new Error("invalid remote domain");
2752
+ }
2753
+ const parsedRemotePort = toPort(domainAndPort[1]);
2754
+ if (protocol === "http") {
2755
+ remotePort = 0;
2756
+ } else {
2757
+ if (parsedRemotePort === null || !isValidPort(parsedRemotePort)) {
2758
+ return new Error(
2759
+ `${protocol} forwarding requires port in format ${protocol}//domain/remotePort`
2760
+ );
2761
+ }
2762
+ remotePort = parsedRemotePort;
2763
+ }
2764
+ } else {
2765
+ remoteDomainRaw = hostPart;
2766
+ if (!domainRegex.test(remoteDomainRaw)) {
2767
+ return new Error("invalid remote domain");
2768
+ }
2769
+ protocol = "http";
2770
+ remotePort = 0;
2771
+ }
2772
+ const localDomain = removeIPv6Brackets(parsed[2] || "localhost");
2773
+ const localPort = toPort(parsed[3]);
2774
+ if (localPort === null || !isValidPort(localPort)) {
2775
+ return new Error("forwarding address incorrect: invalid local port");
2776
+ }
2777
+ return {
2778
+ protocol,
2779
+ remoteDomain: remoteDomainRaw,
2780
+ remotePort,
2781
+ localDomain,
2782
+ localPort
2557
2783
  };
2558
- while (!_stopRequested) {
2559
- logConnecting();
2560
- setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
2561
- try {
2562
- await handleWebSocketConnection(wsUrl, wsHost, token);
2563
- } catch (error) {
2564
- logger.warn("Remote management connection error", { error: String(error) });
2784
+ }
2785
+ function parseReverseTunnelAddr(finalConfig, values) {
2786
+ const reverseTunnel = values.R;
2787
+ if ((!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) && !values.localport && !finalConfig.forwarding) {
2788
+ return new Error("local port not specified. Please use '-h' option for help.");
2789
+ }
2790
+ if (!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) {
2791
+ return null;
2792
+ }
2793
+ for (const forwarding of reverseTunnel) {
2794
+ const slicedForwarding = ipv6SafeSplitColon(forwarding);
2795
+ if (slicedForwarding.length === 3) {
2796
+ const parsed = parseDefaultForwarding(forwarding);
2797
+ if (parsed instanceof Error) return parsed;
2798
+ finalConfig.forwarding = `${parsed.localDomain}:${parsed.localPort}`;
2799
+ } else if (slicedForwarding.length === 4) {
2800
+ finalConfig.additionalForwarding ?? (finalConfig.additionalForwarding = []);
2801
+ const parsed = parseAdditionalForwarding(forwarding);
2802
+ if (parsed instanceof Error) return parsed;
2803
+ finalConfig.additionalForwarding.push(parsed);
2804
+ } else {
2805
+ return new Error(
2806
+ "Incorrect command line arguments: reverse tunnel address incorrect. Please use '-h' option for help."
2807
+ );
2565
2808
  }
2566
- if (_stopRequested) break;
2567
- printer_default.warn(`Remote management disconnected. Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds...`);
2568
- logger.info("Reconnecting to remote management after disconnect");
2569
- await sleep(RECONNECT_SLEEP_MS);
2570
2809
  }
2571
- process.removeListener("SIGINT", sigintHandler);
2572
- logger.info("Remote management stopped.");
2573
- return getRemoteManagementState();
2810
+ return null;
2574
2811
  }
2575
- async function handleWebSocketConnection(wsUrl, wsHost, token) {
2576
- return new Promise((resolve) => {
2577
- const ws = new import_ws.default(wsUrl, {
2578
- headers: { Authorization: `Bearer ${token}` }
2579
- });
2580
- currentWs = ws;
2581
- let heartbeat = null;
2582
- let firstMessage = true;
2583
- const cleanup = () => {
2584
- if (heartbeat) clearInterval(heartbeat);
2585
- currentWs = null;
2586
- resolve();
2587
- };
2588
- ws.once("open", () => {
2589
- printer_default.success(`Connected to ${wsHost}`);
2590
- heartbeat = setInterval(() => {
2591
- if (ws.readyState === import_ws.default.OPEN) ws.ping();
2592
- }, PING_INTERVAL_MS);
2593
- });
2594
- ws.on("ping", () => ws.pong());
2595
- ws.on("message", async (data) => {
2596
- try {
2597
- if (firstMessage) {
2598
- firstMessage = false;
2599
- const ok = handleConnectionStatusMessage(data);
2600
- if (!ok) ws.close();
2601
- return;
2602
- }
2603
- setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
2604
- const req = JSON.parse(data.toString("utf8"));
2605
- await new WebSocketCommandHandler().handle(ws, req);
2606
- } catch (e) {
2607
- logger.warn("Failed handling websocket message", { error: String(e) });
2608
- }
2609
- });
2610
- ws.on("unexpected-response", (_, res) => {
2611
- setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
2612
- if (res.statusCode === 401) {
2613
- printer_default.error("Unauthorized. Please enter a valid token.");
2614
- logger.error("Unauthorized (401) on remote management connect");
2615
- } else {
2616
- printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
2617
- logger.warn("Unexpected HTTP response", { statusCode: res.statusCode });
2618
- }
2619
- ws.close();
2620
- });
2621
- ws.on("close", (code, reason) => {
2622
- setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2623
- logger.info("WebSocket closed", { code, reason: reason.toString() });
2624
- printer_default.warn(`Disconnected (code: ${code}). Retrying...`);
2625
- cleanup();
2626
- });
2627
- ws.on("error", (err) => {
2628
- setRemoteManagementState({ status: RemoteManagementStatus.Error, errorMessage: err.message });
2629
- logger.warn("WebSocket error", { error: err.message });
2630
- printer_default.error(err);
2631
- cleanup();
2632
- });
2633
- });
2812
+ function parseLocalTunnelAddr(finalConfig, values) {
2813
+ if (!Array.isArray(values.L) || values.L.length === 0) return null;
2814
+ const firstL = values.L[0];
2815
+ const parts = firstL.split(":");
2816
+ if (parts.length === 3) {
2817
+ const lp = parseInt(parts[0], 10);
2818
+ if (!Number.isNaN(lp) && isValidPort(lp)) {
2819
+ finalConfig.webDebugger = `localhost:${lp}`;
2820
+ } else {
2821
+ return new Error(`Invalid debugger port ${lp}`);
2822
+ }
2823
+ } else {
2824
+ return new Error("Incorrect command line arguments: web debugger address incorrect. Please use '-h' option for help.");
2825
+ }
2634
2826
  }
2635
- async function closeRemoteManagement(timeoutMs = 1e4) {
2636
- _stopRequested = true;
2637
- try {
2638
- if (currentWs) {
2639
- try {
2640
- setRemoteManagementState({ status: RemoteManagementStatus.Disconnecting, errorMessage: "" });
2641
- currentWs.close();
2642
- } catch (e) {
2643
- logger.warn("Error while closing current remote management websocket", { error: String(e) });
2644
- }
2827
+ function parseDebugger(finalConfig, values) {
2828
+ let dbg = values.debugger;
2829
+ if (typeof dbg !== "string") return;
2830
+ dbg = dbg.startsWith(":") ? dbg.slice(1) : dbg;
2831
+ const d = parseInt(dbg, 10);
2832
+ if (!Number.isNaN(d) && isValidPort(d)) {
2833
+ finalConfig.webDebugger = `localhost:${d}`;
2834
+ } else {
2835
+ logger.error("Invalid debugger port:", dbg);
2836
+ return new Error(`Invalid debugger port ${dbg}. Please use '-h' option for help.`);
2837
+ }
2838
+ }
2839
+ function parseToken(finalConfig, explicitToken) {
2840
+ if (typeof explicitToken === "string" && explicitToken) {
2841
+ finalConfig.token = explicitToken;
2842
+ }
2843
+ }
2844
+ function parseArgs(finalConfig, remainingPositionals) {
2845
+ parseExtendedOptions(remainingPositionals, finalConfig);
2846
+ }
2847
+ function storeJson(config, saveconf) {
2848
+ if (saveconf) {
2849
+ const path5 = saveconf;
2850
+ try {
2851
+ import_fs3.default.writeFileSync(path5, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
2852
+ logger.info(`Configuration saved to ${path5}`);
2853
+ } catch (err) {
2854
+ const msg = err instanceof Error ? err.message : String(err);
2855
+ logger.error("Error loading configuration:", msg);
2645
2856
  }
2646
- const start = Date.now();
2647
- while (_remoteManagementState.status === "RUNNING") {
2648
- if (Date.now() - start > timeoutMs) {
2649
- logger.warn("Timed out waiting for remote management to stop");
2650
- break;
2651
- }
2652
- await sleep(200);
2857
+ }
2858
+ }
2859
+ function loadJsonConfig(config) {
2860
+ const configpath = config["conf"];
2861
+ if (typeof configpath === "string" && configpath.trim().length > 0) {
2862
+ const filepath = import_path3.default.resolve(configpath);
2863
+ try {
2864
+ const data = import_fs3.default.readFileSync(filepath, { encoding: "utf-8" });
2865
+ const json = JSON.parse(data);
2866
+ return json;
2867
+ } catch (err) {
2868
+ logger.error("Error loading configuration:", err);
2653
2869
  }
2654
- } finally {
2655
- currentWs = null;
2656
- setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2657
- return getRemoteManagementState();
2658
2870
  }
2871
+ return null;
2872
+ }
2873
+ function isSaveConfOption(values) {
2874
+ const saveconf = values["saveconf"];
2875
+ if (typeof saveconf === "string" && saveconf.trim().length > 0) {
2876
+ return saveconf;
2877
+ }
2878
+ return null;
2659
2879
  }
2660
- function getRemoteManagementState() {
2661
- return _remoteManagementState;
2880
+ function parseServe(finalConfig, values) {
2881
+ const sv = values.serve;
2882
+ if (typeof sv !== "string" || sv.trim().length === 0) return null;
2883
+ finalConfig.serve = sv;
2884
+ return null;
2662
2885
  }
2663
- function setRemoteManagementState(state, errorMessage) {
2664
- _remoteManagementState = {
2665
- status: state.status,
2666
- errorMessage: errorMessage || ""
2886
+ function parseAutoReconnect(finalConfig, values) {
2887
+ const autoReconnectValue = values.autoreconnect;
2888
+ if (typeof autoReconnectValue === "string") {
2889
+ const trimmed = autoReconnectValue.trim().toLowerCase();
2890
+ if (trimmed === "true" || trimmed === "") {
2891
+ finalConfig.autoReconnect = true;
2892
+ } else if (trimmed === "false") {
2893
+ finalConfig.autoReconnect = false;
2894
+ } else {
2895
+ return new Error(`Invalid autoreconnect value: ${autoReconnectValue}. Use true or false.`);
2896
+ }
2897
+ }
2898
+ return null;
2899
+ }
2900
+ async function buildFinalConfig(values, positionals) {
2901
+ let token;
2902
+ let server;
2903
+ let type;
2904
+ let forceFlag = false;
2905
+ let qrCode = false;
2906
+ let finalConfig = new Object();
2907
+ let saveconf = isSaveConfOption(values);
2908
+ const configFromFile = loadJsonConfig(values);
2909
+ const userParse = parseUsers(positionals, values.token);
2910
+ token = userParse.token;
2911
+ server = userParse.server;
2912
+ type = userParse.type;
2913
+ forceFlag = userParse.forceFlag;
2914
+ qrCode = userParse.qrCode;
2915
+ const remainingPositionals = userParse.remaining;
2916
+ const initialTunnel = type || values.type;
2917
+ finalConfig = {
2918
+ ...defaultOptions,
2919
+ ...configFromFile || {},
2920
+ // Apply loaded config on top of defaults
2921
+ configid: getRandomId(),
2922
+ token: token || (configFromFile?.token || (typeof values.token === "string" ? values.token : "")),
2923
+ serverAddress: server || (configFromFile?.serverAddress || defaultOptions.serverAddress),
2924
+ tunnelType: initialTunnel ? [initialTunnel] : configFromFile?.tunnelType || [import_pinggy5.TunnelType.Http],
2925
+ NoTUI: values.notui || (configFromFile?.NoTUI || false),
2926
+ qrCode: qrCode || (configFromFile?.qrCode || false),
2927
+ autoReconnect: configFromFile?.autoReconnect ? configFromFile.autoReconnect : defaultOptions.autoReconnect
2667
2928
  };
2929
+ parseType(finalConfig, values, type);
2930
+ parseToken(finalConfig, token || values.token);
2931
+ const dbgErr = parseDebugger(finalConfig, values);
2932
+ if (dbgErr instanceof Error) throw dbgErr;
2933
+ const lpErr = parseLocalPort(finalConfig, values);
2934
+ if (lpErr instanceof Error) throw lpErr;
2935
+ const rErr = parseReverseTunnelAddr(finalConfig, values);
2936
+ if (rErr instanceof Error) throw rErr;
2937
+ const lErr = parseLocalTunnelAddr(finalConfig, values);
2938
+ if (lErr instanceof Error) throw lErr;
2939
+ const serveErr = parseServe(finalConfig, values);
2940
+ if (serveErr instanceof Error) throw serveErr;
2941
+ const autoReconnectErr = parseAutoReconnect(finalConfig, values);
2942
+ if (autoReconnectErr instanceof Error) throw autoReconnectErr;
2943
+ if (forceFlag) finalConfig.force = true;
2944
+ parseArgs(finalConfig, remainingPositionals);
2945
+ storeJson(finalConfig, saveconf);
2946
+ return finalConfig;
2668
2947
  }
2669
- var import_ws, RECONNECT_SLEEP_MS, PING_INTERVAL_MS, _remoteManagementState, _stopRequested, currentWs;
2670
- var init_remoteManagement = __esm({
2671
- "src/remote_management/remoteManagement.ts"() {
2948
+ var import_pinggy5, import_fs3, import_path3, domainRegex, KEYWORDS, VALID_PROTOCOLS;
2949
+ var init_buildConfig = __esm({
2950
+ "src/cli/buildConfig.ts"() {
2672
2951
  "use strict";
2673
2952
  init_cjs_shims();
2674
- import_ws = __toESM(require("ws"), 1);
2953
+ init_defaults();
2954
+ init_extendedOptions();
2675
2955
  init_logger();
2676
- init_websocket_handlers();
2677
- init_printer();
2678
- init_types();
2679
- RECONNECT_SLEEP_MS = 5e3;
2680
- PING_INTERVAL_MS = 3e4;
2681
- _remoteManagementState = {
2682
- status: "NOT_RUNNING",
2683
- errorMessage: ""
2684
- };
2685
- _stopRequested = false;
2686
- currentWs = null;
2956
+ init_util();
2957
+ import_pinggy5 = require("@pinggy/pinggy");
2958
+ import_fs3 = __toESM(require("fs"), 1);
2959
+ import_path3 = __toESM(require("path"), 1);
2960
+ domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
2961
+ KEYWORDS = /* @__PURE__ */ new Set([
2962
+ import_pinggy5.TunnelType.Http,
2963
+ import_pinggy5.TunnelType.Tcp,
2964
+ import_pinggy5.TunnelType.Tls,
2965
+ import_pinggy5.TunnelType.Udp,
2966
+ import_pinggy5.TunnelType.TlsTcp,
2967
+ "force",
2968
+ "qr"
2969
+ ]);
2970
+ VALID_PROTOCOLS = ["http", "tcp", "udp", "tls"];
2687
2971
  }
2688
2972
  });
2689
2973
 
@@ -3486,6 +3770,91 @@ function closeDisconnectModal(screen, manager) {
3486
3770
  manager.inDisconnectView = false;
3487
3771
  screen.render();
3488
3772
  }
3773
+ function showReconnectingModal(screen, manager, retryCnt, message) {
3774
+ if (manager.reconnectModal) {
3775
+ manager.reconnectModal.destroy();
3776
+ manager.reconnectModal = null;
3777
+ }
3778
+ manager.inReconnectView = true;
3779
+ manager.reconnectModal = import_blessed2.default.box({
3780
+ parent: screen,
3781
+ top: "center",
3782
+ left: "center",
3783
+ width: "50%",
3784
+ height: "20%",
3785
+ border: {
3786
+ type: "line"
3787
+ },
3788
+ style: {
3789
+ border: {
3790
+ fg: "yellow"
3791
+ }
3792
+ },
3793
+ padding: { left: 2, right: 2, top: 1, bottom: 1 },
3794
+ tags: true,
3795
+ align: "center",
3796
+ valign: "middle"
3797
+ });
3798
+ const content = `{yellow-fg}{bold}Reconnecting...{/bold}{/yellow-fg}
3799
+
3800
+ ${message || `Attempt #${retryCnt} \u2014 trying to re-establish tunnel...`}
3801
+
3802
+ {gray-fg}Please wait{/gray-fg}`;
3803
+ manager.reconnectModal.setContent(content);
3804
+ manager.reconnectModal.focus();
3805
+ screen.render();
3806
+ }
3807
+ function closeReconnectingModal(screen, manager) {
3808
+ if (manager.reconnectModal) {
3809
+ manager.reconnectModal.destroy();
3810
+ manager.reconnectModal = null;
3811
+ }
3812
+ manager.inReconnectView = false;
3813
+ screen.render();
3814
+ }
3815
+ function showReconnectionFailedModal(screen, manager, retryCnt, onClose) {
3816
+ closeReconnectingModal(screen, manager);
3817
+ manager.inReconnectView = true;
3818
+ manager.reconnectModal = import_blessed2.default.box({
3819
+ parent: screen,
3820
+ top: "center",
3821
+ left: "center",
3822
+ width: "50%",
3823
+ height: "20%",
3824
+ border: {
3825
+ type: "line"
3826
+ },
3827
+ style: {
3828
+ border: {
3829
+ fg: "red"
3830
+ }
3831
+ },
3832
+ padding: { left: 2, right: 2, top: 1, bottom: 1 },
3833
+ tags: true,
3834
+ align: "center",
3835
+ valign: "middle"
3836
+ });
3837
+ const content = `{red-fg}{bold}Reconnection Failed{/bold}{/red-fg}
3838
+
3839
+ Failed to reconnect after ${retryCnt} attempts.
3840
+ Tunnel will be closed.
3841
+
3842
+ {white-bg}{black-fg}Closing in 5 seconds...{/black-fg}{/white-bg}`;
3843
+ manager.reconnectModal.setContent(content);
3844
+ manager.reconnectModal.focus();
3845
+ screen.render();
3846
+ const timeout = setTimeout(() => {
3847
+ closeReconnectingModal(screen, manager);
3848
+ if (onClose) onClose();
3849
+ }, 5e3);
3850
+ const keyHandler = () => {
3851
+ clearTimeout(timeout);
3852
+ closeReconnectingModal(screen, manager);
3853
+ if (onClose) onClose();
3854
+ };
3855
+ manager.reconnectModal.key(["escape", "enter", "space"], keyHandler);
3856
+ screen.key(["escape", "enter", "space"], keyHandler);
3857
+ }
3489
3858
  function showLoadingModal(screen, modalManager, message = "Loading...") {
3490
3859
  if (modalManager.loadingView) return;
3491
3860
  modalManager.loadingBox = import_blessed2.default.box({
@@ -3793,9 +4162,11 @@ var init_TunnelTui = __esm({
3793
4162
  detailModal: null,
3794
4163
  keyBindingsModal: null,
3795
4164
  disconnectModal: null,
4165
+ reconnectModal: null,
3796
4166
  inDetailView: false,
3797
4167
  keyBindingView: false,
3798
4168
  inDisconnectView: false,
4169
+ inReconnectView: false,
3799
4170
  loadingBox: null,
3800
4171
  loadingView: false,
3801
4172
  fetchAbortController: null
@@ -3996,6 +4367,25 @@ Tunnel will be closed.` : info.messages?.join("\n") || "Disconnect request recei
3996
4367
  );
3997
4368
  }
3998
4369
  }
4370
+ updateReconnectingInfo(retryCnt, message) {
4371
+ showReconnectingModal(
4372
+ this.screen,
4373
+ this.modalManager,
4374
+ retryCnt,
4375
+ message
4376
+ );
4377
+ }
4378
+ closeReconnectingInfo() {
4379
+ closeReconnectingModal(this.screen, this.modalManager);
4380
+ }
4381
+ updateReconnectionFailed(retryCnt) {
4382
+ showReconnectionFailedModal(
4383
+ this.screen,
4384
+ this.modalManager,
4385
+ retryCnt,
4386
+ () => this.destroy()
4387
+ );
4388
+ }
3999
4389
  start() {
4000
4390
  this.screen.render();
4001
4391
  }
@@ -4096,6 +4486,35 @@ async function startCli(finalConfig, manager) {
4096
4486
  }
4097
4487
  printer_default.print(import_picocolors3.default.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"));
4098
4488
  printer_default.print(import_picocolors3.default.gray("\nPress Ctrl+C to stop the tunnel.\n"));
4489
+ manager2.registerWillReconnectListener(tunnel.tunnelid, (tunnelId, error, messages) => {
4490
+ if (activeTui) {
4491
+ const msg = messages?.join("\n") || error || "Tunnel disconnected, reconnecting...";
4492
+ activeTui.updateReconnectingInfo(0, msg);
4493
+ } else if (finalConfig.autoReconnect) {
4494
+ printer_default.warn(error || "Tunnel connection reset");
4495
+ printer_default.startSpinner(messages?.join("\n"));
4496
+ }
4497
+ });
4498
+ manager2.registerReconnectingListener(tunnel.tunnelid, (tunnelId, retryCnt) => {
4499
+ if (activeTui) {
4500
+ activeTui.updateReconnectingInfo(retryCnt);
4501
+ } else if (finalConfig.autoReconnect) {
4502
+ printer_default.startSpinner(`Reconnecting to Pinggy (attempt #${retryCnt})`);
4503
+ }
4504
+ });
4505
+ manager2.registerReconnectionCompletedListener(tunnel.tunnelid, (tunnelId, urls) => {
4506
+ if (activeTui) {
4507
+ activeTui.closeReconnectingInfo();
4508
+ }
4509
+ });
4510
+ manager2.registerReconnectionFailedListener(tunnel.tunnelid, (tunnelId, retryCnt) => {
4511
+ if (activeTui) {
4512
+ activeTui.updateReconnectionFailed(retryCnt);
4513
+ } else {
4514
+ printer_default.stopSpinnerFail(`Reconnection failed after ${retryCnt} attempts`);
4515
+ process.exit(1);
4516
+ }
4517
+ });
4099
4518
  manager2.registerDisconnectListener(tunnel.tunnelid, async (tunnelId, error, messages) => {
4100
4519
  if (activeTui) {
4101
4520
  disconnectState = {
@@ -4273,6 +4692,19 @@ var init_main = __esm({
4273
4692
  });
4274
4693
 
4275
4694
  // src/index.ts
4695
+ var index_exports = {};
4696
+ __export(index_exports, {
4697
+ TunnelErrorCodeType: () => TunnelErrorCodeType,
4698
+ TunnelManager: () => TunnelManager,
4699
+ TunnelOperations: () => TunnelOperations,
4700
+ TunnelStateType: () => TunnelStateType,
4701
+ TunnelWarningCode: () => TunnelWarningCode,
4702
+ closeRemoteManagement: () => closeRemoteManagement,
4703
+ enablePackageLogging: () => enablePackageLogging,
4704
+ getRemoteManagementState: () => getRemoteManagementState,
4705
+ initiateRemoteManagement: () => initiateRemoteManagement
4706
+ });
4707
+ module.exports = __toCommonJS(index_exports);
4276
4708
  init_cjs_shims();
4277
4709
 
4278
4710
  // src/utils/detect_vc_redist_on_windows.ts
@@ -4357,6 +4789,11 @@ async function openDownloadPage() {
4357
4789
 
4358
4790
  // src/index.ts
4359
4791
  init_printer();
4792
+ init_TunnelManager();
4793
+ init_handler();
4794
+ init_logger();
4795
+ init_remoteManagement();
4796
+ init_types();
4360
4797
  async function verifyAndLoad() {
4361
4798
  if (process.platform === "win32") {
4362
4799
  const vcRedist = checkVCRedist();
@@ -4374,3 +4811,15 @@ verifyAndLoad().catch((err) => {
4374
4811
  printer_default.error(`Failed to start CLI:, ${err}`);
4375
4812
  process.exit(1);
4376
4813
  });
4814
+ // Annotate the CommonJS export names for ESM import in node:
4815
+ 0 && (module.exports = {
4816
+ TunnelErrorCodeType,
4817
+ TunnelManager,
4818
+ TunnelOperations,
4819
+ TunnelStateType,
4820
+ TunnelWarningCode,
4821
+ closeRemoteManagement,
4822
+ enablePackageLogging,
4823
+ getRemoteManagementState,
4824
+ initiateRemoteManagement
4825
+ });