pinggy 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -6,6 +6,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
9
12
  var __export = (target, all) => {
10
13
  for (var name in all)
11
14
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -26,40 +29,155 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
29
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
30
  mod
28
31
  ));
29
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
32
 
31
- // src/index.ts
32
- var index_exports = {};
33
- __export(index_exports, {
34
- TunnelManager: () => TunnelManager,
35
- TunnelOperations: () => TunnelOperations,
36
- closeRemoteManagement: () => closeRemoteManagement,
37
- enablePackageLogging: () => enablePackageLogging,
38
- getRemoteManagementState: () => getRemoteManagementState,
39
- initiateRemoteManagement: () => initiateRemoteManagement
33
+ // node_modules/tsup/assets/cjs_shims.js
34
+ var getImportMetaUrl, importMetaUrl;
35
+ var init_cjs_shims = __esm({
36
+ "node_modules/tsup/assets/cjs_shims.js"() {
37
+ "use strict";
38
+ getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
39
+ importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
40
+ }
40
41
  });
41
- module.exports = __toCommonJS(index_exports);
42
42
 
43
- // node_modules/tsup/assets/cjs_shims.js
44
- var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
45
- var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
43
+ // src/tui/spinner/spinner.ts
44
+ function startSpinner(name = "dots", text = "Loading") {
45
+ const spinner = spinners[name];
46
+ let i = 0;
47
+ currentText = text;
48
+ if (currentTimer) {
49
+ clearInterval(currentTimer);
50
+ }
51
+ currentTimer = setInterval(() => {
52
+ const frame = spinner.frames[i = ++i % spinner.frames.length];
53
+ process.stdout.write(`\r${import_picocolors.default.cyan(frame)} ${text}`);
54
+ }, spinner.interval);
55
+ return () => stopSpinner();
56
+ }
57
+ function stopSpinner() {
58
+ if (currentTimer) {
59
+ clearInterval(currentTimer);
60
+ currentTimer = null;
61
+ process.stdout.write("\r\x1B[K");
62
+ }
63
+ }
64
+ function stopSpinnerSuccess(message) {
65
+ if (currentTimer) {
66
+ clearInterval(currentTimer);
67
+ currentTimer = null;
68
+ const finalMessage = message || currentText;
69
+ process.stdout.write(`\r${import_picocolors.default.green("\u2714")} ${finalMessage}
70
+ `);
71
+ }
72
+ }
73
+ function stopSpinnerFail(message) {
74
+ if (currentTimer) {
75
+ clearInterval(currentTimer);
76
+ currentTimer = null;
77
+ const finalMessage = message || currentText;
78
+ process.stdout.write(`\r${import_picocolors.default.red("\u2716")} ${finalMessage}
79
+ `);
80
+ }
81
+ }
82
+ var import_picocolors, spinners, currentTimer, currentText;
83
+ var init_spinner = __esm({
84
+ "src/tui/spinner/spinner.ts"() {
85
+ "use strict";
86
+ init_cjs_shims();
87
+ import_picocolors = __toESM(require("picocolors"), 1);
88
+ spinners = {
89
+ dots: {
90
+ interval: 80,
91
+ frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]
92
+ }
93
+ };
94
+ currentTimer = null;
95
+ currentText = "";
96
+ }
97
+ });
46
98
 
47
- // src/tunnel_manager/TunnelManager.ts
48
- var import_pinggy2 = require("@pinggy/pinggy");
99
+ // src/utils/printer.ts
100
+ var import_picocolors2, _CLIPrinter, CLIPrinter, printer_default;
101
+ var init_printer = __esm({
102
+ "src/utils/printer.ts"() {
103
+ "use strict";
104
+ init_cjs_shims();
105
+ import_picocolors2 = __toESM(require("picocolors"), 1);
106
+ init_spinner();
107
+ _CLIPrinter = class _CLIPrinter {
108
+ static isCLIError(err) {
109
+ return err instanceof Error;
110
+ }
111
+ static print(message, ...args) {
112
+ console.log(message, ...args);
113
+ }
114
+ static error(err) {
115
+ const def = this.errorDefinitions.find((d) => d.match(err));
116
+ const msg = def.message(err);
117
+ console.error(import_picocolors2.default.red(import_picocolors2.default.bold("\u2716 Error:")), import_picocolors2.default.red(msg));
118
+ process.exit(1);
119
+ }
120
+ static warn(message) {
121
+ console.warn(import_picocolors2.default.yellow(import_picocolors2.default.bold("\u26A0 Warning:")), import_picocolors2.default.yellow(message));
122
+ }
123
+ static warnTxt(message) {
124
+ console.warn(import_picocolors2.default.yellow(import_picocolors2.default.bold("\u26A0 Warning:")), import_picocolors2.default.yellow(message));
125
+ }
126
+ static success(message) {
127
+ console.log(import_picocolors2.default.green(import_picocolors2.default.bold(" \u2714 Success:")), import_picocolors2.default.green(message));
128
+ }
129
+ static async info(message) {
130
+ console.log(import_picocolors2.default.blue(message));
131
+ }
132
+ static startSpinner(message) {
133
+ startSpinner("dots", message);
134
+ }
135
+ static stopSpinnerSuccess(message) {
136
+ stopSpinnerSuccess(message);
137
+ }
138
+ static stopSpinnerFail(message) {
139
+ stopSpinnerFail(message);
140
+ }
141
+ };
142
+ _CLIPrinter.errorDefinitions = [
143
+ {
144
+ match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION",
145
+ message: (err) => {
146
+ const match = /Unknown option '(.+?)'/.exec(err.message);
147
+ const option = match ? match[1] : "(unknown)";
148
+ return `Unknown option '${option}'. Please check your command or use pinggy --h for guidance.`;
149
+ }
150
+ },
151
+ {
152
+ match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_MISSING_OPTION_VALUE",
153
+ message: (err) => `Missing required argument for option '${err.option}'.`
154
+ },
155
+ {
156
+ match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE",
157
+ message: (err) => `Invalid argument'${err.message}'.`
158
+ },
159
+ {
160
+ match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ENOENT",
161
+ message: (err) => `File or directory not found: ${err.message}`
162
+ },
163
+ {
164
+ match: () => true,
165
+ // fallback
166
+ message: (err) => _CLIPrinter.isCLIError(err) ? err.message : String(err)
167
+ }
168
+ ];
169
+ CLIPrinter = _CLIPrinter;
170
+ printer_default = CLIPrinter;
171
+ }
172
+ });
49
173
 
50
174
  // src/logger.ts
51
- var import_winston = __toESM(require("winston"), 1);
52
- var import_fs = __toESM(require("fs"), 1);
53
- var import_path = __toESM(require("path"), 1);
54
- var import_pinggy = require("@pinggy/pinggy");
55
- var _logger = null;
56
175
  function getLogger() {
57
176
  if (!_logger) {
58
177
  _logger = import_winston.default.createLogger({ level: "info", silent: true });
59
178
  }
60
179
  return _logger;
61
180
  }
62
- var logger = getLogger();
63
181
  function applyLoggingConfig(cfg) {
64
182
  const {
65
183
  level,
@@ -73,8 +191,8 @@ function applyLoggingConfig(cfg) {
73
191
  enableLoggingByLogLevelInSdk(level ?? "info", filePath);
74
192
  }
75
193
  if (filePath) {
76
- const dir = import_path.default.dirname(filePath);
77
- if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
194
+ const dir = import_path2.default.dirname(filePath);
195
+ if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
78
196
  }
79
197
  const transports = [];
80
198
  if (stdout) {
@@ -142,1126 +260,1044 @@ function enableLoggingByLogLevelInSdk(loglevel, logFilePath) {
142
260
  import_pinggy.pinggy.setDebugLogging(true, import_pinggy.LogLevel.INFO, logFilePath);
143
261
  }
144
262
  }
145
-
146
- // src/tunnel_manager/TunnelManager.ts
147
- var import_node_path = __toESM(require("path"), 1);
148
- var import_node_worker_threads = require("worker_threads");
149
- var import_node_url = require("url");
150
-
151
- // src/utils/printer.ts
152
- var import_picocolors2 = __toESM(require("picocolors"), 1);
153
-
154
- // src/tui/spinner/spinner.ts
155
- var import_picocolors = __toESM(require("picocolors"), 1);
156
- var spinners = {
157
- dots: {
158
- interval: 80,
159
- frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]
263
+ var import_winston, import_fs2, import_path2, import_pinggy, _logger, logger;
264
+ var init_logger = __esm({
265
+ "src/logger.ts"() {
266
+ "use strict";
267
+ init_cjs_shims();
268
+ import_winston = __toESM(require("winston"), 1);
269
+ import_fs2 = __toESM(require("fs"), 1);
270
+ import_path2 = __toESM(require("path"), 1);
271
+ import_pinggy = require("@pinggy/pinggy");
272
+ _logger = null;
273
+ logger = getLogger();
160
274
  }
161
- };
162
- var currentTimer = null;
163
- var currentText = "";
164
- function startSpinner(name = "dots", text = "Loading") {
165
- const spinner = spinners[name];
166
- let i = 0;
167
- currentText = text;
168
- if (currentTimer) {
169
- clearInterval(currentTimer);
170
- }
171
- currentTimer = setInterval(() => {
172
- const frame = spinner.frames[i = ++i % spinner.frames.length];
173
- process.stdout.write(`\r${import_picocolors.default.cyan(frame)} ${text}`);
174
- }, spinner.interval);
175
- return () => stopSpinner();
176
- }
177
- function stopSpinner() {
178
- if (currentTimer) {
179
- clearInterval(currentTimer);
180
- currentTimer = null;
181
- process.stdout.write("\r\x1B[K");
182
- }
183
- }
184
- function stopSpinnerSuccess(message) {
185
- if (currentTimer) {
186
- clearInterval(currentTimer);
187
- currentTimer = null;
188
- const finalMessage = message || currentText;
189
- process.stdout.write(`\r${import_picocolors.default.green("\u2714")} ${finalMessage}
190
- `);
191
- }
192
- }
193
- function stopSpinnerFail(message) {
194
- if (currentTimer) {
195
- clearInterval(currentTimer);
196
- currentTimer = null;
197
- const finalMessage = message || currentText;
198
- process.stdout.write(`\r${import_picocolors.default.red("\u2716")} ${finalMessage}
199
- `);
200
- }
201
- }
202
-
203
- // src/utils/printer.ts
204
- var _CLIPrinter = class _CLIPrinter {
205
- static isCLIError(err) {
206
- return err instanceof Error;
207
- }
208
- static print(message, ...args) {
209
- console.log(message, ...args);
210
- }
211
- static error(err) {
212
- const def = this.errorDefinitions.find((d) => d.match(err));
213
- const msg = def.message(err);
214
- console.error(import_picocolors2.default.red(import_picocolors2.default.bold("\u2716 Error:")), import_picocolors2.default.red(msg));
215
- process.exit(1);
216
- }
217
- static warn(message) {
218
- console.warn(import_picocolors2.default.yellow(import_picocolors2.default.bold("\u26A0 Warning:")), import_picocolors2.default.yellow(message));
219
- }
220
- static warnTxt(message) {
221
- console.warn(import_picocolors2.default.yellow(import_picocolors2.default.bold("\u26A0 Warning:")), import_picocolors2.default.yellow(message));
222
- }
223
- static success(message) {
224
- console.log(import_picocolors2.default.green(import_picocolors2.default.bold(" \u2714 Success:")), import_picocolors2.default.green(message));
225
- }
226
- static async info(message) {
227
- console.log(import_picocolors2.default.blue(message));
228
- }
229
- static startSpinner(message) {
230
- startSpinner("dots", message);
231
- }
232
- static stopSpinnerSuccess(message) {
233
- stopSpinnerSuccess(message);
234
- }
235
- static stopSpinnerFail(message) {
236
- stopSpinnerFail(message);
237
- }
238
- };
239
- _CLIPrinter.errorDefinitions = [
240
- {
241
- match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION",
242
- message: (err) => {
243
- const match = /Unknown option '(.+?)'/.exec(err.message);
244
- const option = match ? match[1] : "(unknown)";
245
- return `Unknown option '${option}'. Please check your command or use pinggy --h for guidance.`;
246
- }
247
- },
248
- {
249
- match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_MISSING_OPTION_VALUE",
250
- message: (err) => `Missing required argument for option '${err.option}'.`
251
- },
252
- {
253
- match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE",
254
- message: (err) => `Invalid argument'${err.message}'.`
255
- },
256
- {
257
- match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ENOENT",
258
- message: (err) => `File or directory not found: ${err.message}`
259
- },
260
- {
261
- match: () => true,
262
- // fallback
263
- message: (err) => _CLIPrinter.isCLIError(err) ? err.message : String(err)
264
- }
265
- ];
266
- var CLIPrinter = _CLIPrinter;
267
- var printer_default = CLIPrinter;
275
+ });
268
276
 
269
277
  // src/utils/util.ts
270
- var import_module = require("module");
271
- var import_crypto = require("crypto");
272
278
  function getRandomId() {
273
279
  return (0, import_crypto.randomUUID)();
274
280
  }
275
281
  function isValidPort(p) {
276
282
  return Number.isInteger(p) && p > 0 && p < 65536;
277
283
  }
278
- var require2 = (0, import_module.createRequire)(importMetaUrl);
279
- var pkg = require2("../package.json");
280
284
  function getVersion() {
281
285
  return pkg.version ?? "";
282
286
  }
287
+ var import_module, import_crypto, require2, pkg;
288
+ var init_util = __esm({
289
+ "src/utils/util.ts"() {
290
+ "use strict";
291
+ init_cjs_shims();
292
+ import_module = require("module");
293
+ import_crypto = require("crypto");
294
+ require2 = (0, import_module.createRequire)(importMetaUrl);
295
+ pkg = require2("../package.json");
296
+ }
297
+ });
283
298
 
284
299
  // src/tunnel_manager/TunnelManager.ts
285
- var __filename2 = (0, import_node_url.fileURLToPath)(importMetaUrl);
286
- var __dirname = import_node_path.default.dirname(__filename2);
287
- var TunnelManager = class _TunnelManager {
288
- constructor() {
289
- this.tunnelsByTunnelId = /* @__PURE__ */ new Map();
290
- this.tunnelsByConfigId = /* @__PURE__ */ new Map();
291
- this.tunnelStats = /* @__PURE__ */ new Map();
292
- this.tunnelStatsListeners = /* @__PURE__ */ new Map();
293
- this.tunnelErrorListeners = /* @__PURE__ */ new Map();
294
- this.tunnelDisconnectListeners = /* @__PURE__ */ new Map();
295
- this.tunnelWorkerErrorListeners = /* @__PURE__ */ new Map();
296
- this.tunnelStartListeners = /* @__PURE__ */ new Map();
297
- }
298
- static getInstance() {
299
- if (!_TunnelManager.instance) {
300
- _TunnelManager.instance = new _TunnelManager();
301
- }
302
- return _TunnelManager.instance;
303
- }
304
- /**
305
- * Creates a new managed tunnel instance with the given configuration.
306
- * Builds the config with forwarding rules and creates the tunnel instance.
307
- *
308
- * @param config - The tunnel configuration options
309
- * @param config.configid - Unique identifier for the tunnel configuration
310
- * @param config.tunnelid - Optional custom tunnel identifier. If not provided, a random UUID will be generated
311
- * @param config.additionalForwarding - Optional array of additional forwarding configurations
312
- *
313
- * @throws {Error} When configId is invalid or empty
314
- * @throws {Error} When a tunnel with the given configId already exists
315
- *
316
- * @returns {ManagedTunnel} A new managed tunnel instance containing the tunnel details,
317
- * status information, and statistics
318
- */
319
- async createTunnel(config) {
320
- const { configid, additionalForwarding, tunnelName } = config;
321
- if (configid === void 0 || configid.trim().length === 0) {
322
- throw new Error(`Invalid configId: "${configid}"`);
323
- }
324
- if (this.tunnelsByConfigId.has(configid)) {
325
- throw new Error(`Tunnel with configId "${configid}" already exists`);
326
- }
327
- const tunnelid = config.tunnelid || getRandomId();
328
- const configWithForwarding = this.buildPinggyConfig(config, additionalForwarding);
329
- return this._createTunnelWithProcessedConfig({
330
- configid,
331
- tunnelid,
332
- tunnelName,
333
- originalConfig: config,
334
- configWithForwarding,
335
- additionalForwarding,
336
- serve: config.serve,
337
- autoReconnect: config.autoReconnect !== void 0 ? config.autoReconnect : false
338
- });
339
- }
340
- /**
341
- * Internal method to create a tunnel with an already-processed configuration.
342
- * This is used by createTunnel, restartTunnel, and updateConfig to avoid config processing.
343
- *
344
- * @param params - Configuration parameters with already-processed forwarding rules
345
- * @returns The created ManagedTunnel instance
346
- * @private
347
- */
348
- async _createTunnelWithProcessedConfig(params) {
349
- let instance;
350
- try {
351
- instance = await import_pinggy2.pinggy.createTunnel(params.configWithForwarding);
352
- } catch (e) {
353
- logger.error("Error creating tunnel instance:", e);
354
- throw e;
355
- }
356
- const now = (/* @__PURE__ */ new Date()).toISOString();
357
- const managed = {
358
- tunnelid: params.tunnelid,
359
- configid: params.configid,
360
- tunnelName: params.tunnelName,
361
- instance,
362
- tunnelConfig: params.originalConfig,
363
- configWithForwarding: params.configWithForwarding,
364
- additionalForwarding: params.additionalForwarding,
365
- serve: params.serve,
366
- warnings: [],
367
- isStopped: false,
368
- createdAt: now,
369
- startedAt: null,
370
- stoppedAt: null,
371
- autoReconnect: params.autoReconnect
372
- };
373
- instance.setTunnelEstablishedCallback(({}) => {
374
- managed.startedAt = (/* @__PURE__ */ new Date()).toISOString();
375
- });
376
- this.setupStatsCallback(params.tunnelid, managed);
377
- this.setupErrorCallback(params.tunnelid, managed);
378
- this.setupDisconnectCallback(params.tunnelid, managed);
379
- this.setUpTunnelWorkerErrorCallback(params.tunnelid, managed);
380
- this.tunnelsByTunnelId.set(params.tunnelid, managed);
381
- this.tunnelsByConfigId.set(params.configid, managed);
382
- logger.info("Tunnel created", { configid: params.configid, tunnelid: params.tunnelid });
383
- return managed;
384
- }
385
- /**
386
- * Builds the Pinggy configuration by merging the default forwarding rule
387
- * with additional forwarding rules from additionalForwarding array.
388
- *
389
- * @param config - The base Pinggy configuration
390
- * @param additionalForwarding - Optional array of additional forwarding rules
391
- * @returns Modified PinggyOptions
392
- */
393
- buildPinggyConfig(config, additionalForwarding) {
394
- const forwardingRules = [];
395
- if (config.forwarding) {
396
- forwardingRules.push({
397
- type: config.tunnelType && config.tunnelType[0] || "http",
398
- address: config.forwarding
399
- });
400
- }
401
- if (Array.isArray(additionalForwarding) && additionalForwarding.length > 0) {
402
- for (const rule of additionalForwarding) {
403
- if (rule && rule.localDomain && rule.localPort && rule.remoteDomain && isValidPort(rule.localPort)) {
404
- const forwardingRule = {
405
- type: rule.protocol,
406
- // In Future we can make this dynamic based on user input
407
- address: `${rule.localDomain}:${rule.localPort}`,
408
- listenAddress: rule.remotePort && isValidPort(rule.remotePort) ? `${rule.remoteDomain}:${rule.remotePort}` : rule.remoteDomain
409
- };
410
- forwardingRules.push(forwardingRule);
300
+ var import_pinggy2, import_node_path, import_node_worker_threads, import_node_url, __filename2, __dirname, TunnelManager;
301
+ var init_TunnelManager = __esm({
302
+ "src/tunnel_manager/TunnelManager.ts"() {
303
+ "use strict";
304
+ init_cjs_shims();
305
+ import_pinggy2 = require("@pinggy/pinggy");
306
+ init_logger();
307
+ import_node_path = __toESM(require("path"), 1);
308
+ import_node_worker_threads = require("worker_threads");
309
+ import_node_url = require("url");
310
+ init_printer();
311
+ init_util();
312
+ __filename2 = (0, import_node_url.fileURLToPath)(importMetaUrl);
313
+ __dirname = import_node_path.default.dirname(__filename2);
314
+ TunnelManager = class _TunnelManager {
315
+ constructor() {
316
+ this.tunnelsByTunnelId = /* @__PURE__ */ new Map();
317
+ this.tunnelsByConfigId = /* @__PURE__ */ new Map();
318
+ this.tunnelStats = /* @__PURE__ */ new Map();
319
+ this.tunnelStatsListeners = /* @__PURE__ */ new Map();
320
+ this.tunnelErrorListeners = /* @__PURE__ */ new Map();
321
+ this.tunnelDisconnectListeners = /* @__PURE__ */ new Map();
322
+ this.tunnelWorkerErrorListeners = /* @__PURE__ */ new Map();
323
+ this.tunnelStartListeners = /* @__PURE__ */ new Map();
324
+ }
325
+ static getInstance() {
326
+ if (!_TunnelManager.instance) {
327
+ _TunnelManager.instance = new _TunnelManager();
411
328
  }
329
+ return _TunnelManager.instance;
412
330
  }
413
- }
414
- return {
415
- ...config,
416
- forwarding: forwardingRules.length > 0 ? forwardingRules : config.forwarding
417
- };
418
- }
419
- /**
420
- * Start a tunnel that was created but not yet started
421
- */
422
- async startTunnel(tunnelId) {
423
- const managed = this.tunnelsByTunnelId.get(tunnelId);
424
- if (!managed) throw new Error(`Tunnel with id "${tunnelId}" not found`);
425
- logger.info("Starting tunnel", { tunnelId });
426
- let urls;
427
- try {
428
- urls = await managed.instance.start();
429
- } catch (error) {
430
- logger.error("Failed to start tunnel", { tunnelId, error });
431
- throw error;
432
- }
433
- logger.info("Tunnel started", { tunnelId, urls });
434
- if (managed.serve) {
435
- this.startStaticFileServer(managed);
436
- }
437
- try {
438
- const startListeners = this.tunnelStartListeners.get(tunnelId);
439
- if (startListeners) {
440
- for (const [id, listener] of startListeners) {
441
- try {
442
- listener(tunnelId, urls);
443
- } catch (err) {
444
- logger.debug("Error in start-listener callback", { listenerId: id, tunnelId, err });
445
- }
331
+ /**
332
+ * Creates a new managed tunnel instance with the given configuration.
333
+ * Builds the config with forwarding rules and creates the tunnel instance.
334
+ *
335
+ * @param config - The tunnel configuration options
336
+ * @param config.configid - Unique identifier for the tunnel configuration
337
+ * @param config.tunnelid - Optional custom tunnel identifier. If not provided, a random UUID will be generated
338
+ * @param config.additionalForwarding - Optional array of additional forwarding configurations
339
+ *
340
+ * @throws {Error} When configId is invalid or empty
341
+ * @throws {Error} When a tunnel with the given configId already exists
342
+ *
343
+ * @returns {ManagedTunnel} A new managed tunnel instance containing the tunnel details,
344
+ * status information, and statistics
345
+ */
346
+ async createTunnel(config) {
347
+ const { configid, additionalForwarding, tunnelName } = config;
348
+ if (configid === void 0 || configid.trim().length === 0) {
349
+ throw new Error(`Invalid configId: "${configid}"`);
350
+ }
351
+ if (this.tunnelsByConfigId.has(configid)) {
352
+ throw new Error(`Tunnel with configId "${configid}" already exists`);
446
353
  }
354
+ const tunnelid = config.tunnelid || getRandomId();
355
+ const configWithForwarding = this.buildPinggyConfig(config, additionalForwarding);
356
+ return this._createTunnelWithProcessedConfig({
357
+ configid,
358
+ tunnelid,
359
+ tunnelName,
360
+ originalConfig: config,
361
+ configWithForwarding,
362
+ additionalForwarding,
363
+ serve: config.serve,
364
+ autoReconnect: config.autoReconnect !== void 0 ? config.autoReconnect : false
365
+ });
447
366
  }
448
- } catch (e) {
449
- logger.warn("Failed to notify start listeners", { tunnelId, e });
450
- }
451
- return urls;
452
- }
453
- /**
454
- * Stops a running tunnel and updates its status.
455
- *
456
- * @param tunnelId - The unique identifier of the tunnel to stop
457
- * @throws {Error} If the tunnel with the given tunnelId is not found
458
- * @remarks
459
- * - Clears the tunnel's remote URLs
460
- * - Updates the tunnel's state to Exited if stopped successfully
461
- * - Logs the stop operation with tunnelId and configId
462
- */
463
- stopTunnel(tunnelId) {
464
- const managed = this.tunnelsByTunnelId.get(tunnelId);
465
- if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
466
- logger.info("Stopping tunnel", { tunnelId, configId: managed.configid });
467
- try {
468
- managed.instance.stop();
469
- if (managed.serveWorker) {
470
- logger.info("terminating serveWorker");
471
- managed.serveWorker.terminate();
472
- }
473
- this.tunnelStats.delete(tunnelId);
474
- this.tunnelStatsListeners.delete(tunnelId);
475
- this.tunnelStats.delete(tunnelId);
476
- this.tunnelStatsListeners.delete(tunnelId);
477
- this.tunnelErrorListeners.delete(tunnelId);
478
- this.tunnelDisconnectListeners.delete(tunnelId);
479
- this.tunnelWorkerErrorListeners.delete(tunnelId);
480
- this.tunnelStartListeners.delete(tunnelId);
481
- managed.serveWorker = null;
482
- managed.warnings = managed.warnings ?? [];
483
- managed.isStopped = true;
484
- managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
485
- logger.info("Tunnel stopped", { tunnelId, configId: managed.configid });
486
- return { configid: managed.configid, tunnelid: managed.tunnelid };
487
- } catch (error) {
488
- logger.error("Failed to stop tunnel", { tunnelId, error });
489
- throw error;
490
- }
491
- }
492
- /**
493
- * Get all public URLs for a tunnel
494
- */
495
- async getTunnelUrls(tunnelId) {
496
- try {
497
- const managed = this.tunnelsByTunnelId.get(tunnelId);
498
- if (!managed || managed.isStopped) {
499
- logger.error(`Tunnel "${tunnelId}" not found when fetching URLs`);
500
- return [];
501
- }
502
- const urls = await managed.instance.urls();
503
- logger.debug("Queried tunnel URLs", { tunnelId, urls });
504
- return urls;
505
- } catch (error) {
506
- logger.error("Error fetching tunnel URLs", { tunnelId, error });
507
- throw error;
508
- }
509
- }
510
- /**
511
- * Get all TunnelStatus currently managed by this TunnelManager
512
- * @returns An array of all TunnelStatus objects
513
- */
514
- async getAllTunnels() {
515
- try {
516
- const tunnelList = await Promise.all(Array.from(this.tunnelsByTunnelId.values()).map(async (tunnel) => {
367
+ /**
368
+ * Internal method to create a tunnel with an already-processed configuration.
369
+ * This is used by createTunnel, restartTunnel, and updateConfig to avoid config processing.
370
+ *
371
+ * @param params - Configuration parameters with already-processed forwarding rules
372
+ * @returns The created ManagedTunnel instance
373
+ * @private
374
+ */
375
+ async _createTunnelWithProcessedConfig(params) {
376
+ let instance;
377
+ try {
378
+ instance = await import_pinggy2.pinggy.createTunnel(params.configWithForwarding);
379
+ } catch (e) {
380
+ logger.error("Error creating tunnel instance:", e);
381
+ throw e;
382
+ }
383
+ const now = (/* @__PURE__ */ new Date()).toISOString();
384
+ const managed = {
385
+ tunnelid: params.tunnelid,
386
+ configid: params.configid,
387
+ tunnelName: params.tunnelName,
388
+ instance,
389
+ tunnelConfig: params.originalConfig,
390
+ configWithForwarding: params.configWithForwarding,
391
+ additionalForwarding: params.additionalForwarding,
392
+ serve: params.serve,
393
+ warnings: [],
394
+ isStopped: false,
395
+ createdAt: now,
396
+ startedAt: null,
397
+ stoppedAt: null,
398
+ autoReconnect: params.autoReconnect
399
+ };
400
+ instance.setTunnelEstablishedCallback(({}) => {
401
+ managed.startedAt = (/* @__PURE__ */ new Date()).toISOString();
402
+ });
403
+ this.setupStatsCallback(params.tunnelid, managed);
404
+ this.setupErrorCallback(params.tunnelid, managed);
405
+ this.setupDisconnectCallback(params.tunnelid, managed);
406
+ this.setUpTunnelWorkerErrorCallback(params.tunnelid, managed);
407
+ this.tunnelsByTunnelId.set(params.tunnelid, managed);
408
+ this.tunnelsByConfigId.set(params.configid, managed);
409
+ logger.info("Tunnel created", { configid: params.configid, tunnelid: params.tunnelid });
410
+ return managed;
411
+ }
412
+ /**
413
+ * Builds the Pinggy configuration by merging the default forwarding rule
414
+ * with additional forwarding rules from additionalForwarding array.
415
+ *
416
+ * @param config - The base Pinggy configuration
417
+ * @param additionalForwarding - Optional array of additional forwarding rules
418
+ * @returns Modified PinggyOptions
419
+ */
420
+ buildPinggyConfig(config, additionalForwarding) {
421
+ const forwardingRules = [];
422
+ if (config.forwarding) {
423
+ forwardingRules.push({
424
+ type: config.tunnelType && config.tunnelType[0] || "http",
425
+ address: config.forwarding
426
+ });
427
+ }
428
+ if (Array.isArray(additionalForwarding) && additionalForwarding.length > 0) {
429
+ for (const rule of additionalForwarding) {
430
+ if (rule && rule.localDomain && rule.localPort && rule.remoteDomain && isValidPort(rule.localPort)) {
431
+ const forwardingRule = {
432
+ type: rule.protocol,
433
+ // In Future we can make this dynamic based on user input
434
+ address: `${rule.localDomain}:${rule.localPort}`,
435
+ listenAddress: rule.remotePort && isValidPort(rule.remotePort) ? `${rule.remoteDomain}:${rule.remotePort}` : rule.remoteDomain
436
+ };
437
+ forwardingRules.push(forwardingRule);
438
+ }
439
+ }
440
+ }
517
441
  return {
518
- tunnelid: tunnel.tunnelid,
519
- configid: tunnel.configid,
520
- tunnelName: tunnel.tunnelName,
521
- tunnelConfig: tunnel.tunnelConfig,
522
- remoteurls: !tunnel.isStopped ? await this.getTunnelUrls(tunnel.tunnelid) : [],
523
- additionalForwarding: tunnel.additionalForwarding,
524
- serve: tunnel.serve
442
+ ...config,
443
+ forwarding: forwardingRules.length > 0 ? forwardingRules : config.forwarding
525
444
  };
526
- }));
527
- return tunnelList;
528
- } catch (err) {
529
- logger.error("Error fetching tunnels", { error: err });
530
- return [];
531
- }
532
- }
533
- /**
534
- * Get status of a tunnel
535
- */
536
- async getTunnelStatus(tunnelId) {
537
- const managed = this.tunnelsByTunnelId.get(tunnelId);
538
- if (!managed) {
539
- logger.error(`Tunnel "${tunnelId}" not found when fetching status`);
540
- throw new Error(`Tunnel "${tunnelId}" not found`);
541
- }
542
- if (managed.isStopped) {
543
- return "exited";
544
- }
545
- const status = await managed.instance.getStatus();
546
- logger.debug("Queried tunnel status", { tunnelId, status });
547
- return status;
548
- }
549
- /**
550
- * Stop all tunnels
551
- */
552
- stopAllTunnels() {
553
- for (const { instance } of this.tunnelsByTunnelId.values()) {
554
- try {
555
- instance.stop();
556
- } catch (e) {
557
- logger.warn("Error stopping tunnel instance", e);
558
445
  }
559
- }
560
- this.tunnelsByTunnelId.clear();
561
- this.tunnelsByConfigId.clear();
562
- this.tunnelStats.clear();
563
- this.tunnelStatsListeners.clear();
564
- logger.info("All tunnels stopped and cleared");
565
- }
566
- /**
567
- * Remove a stopped tunnel's records so it will no longer be returned by list methods.
568
- *
569
- *
570
- * @param tunnelId - the tunnel id to remove
571
- * @returns true if the record was removed, false otherwise
572
- */
573
- removeStoppedTunnelByTunnelId(tunnelId) {
574
- const managed = this.tunnelsByTunnelId.get(tunnelId);
575
- if (!managed) {
576
- logger.debug("Attempted to remove non-existent tunnel", { tunnelId });
577
- return false;
578
- }
579
- if (!managed.isStopped) {
580
- logger.warn("Attempted to remove tunnel that is not stopped", { tunnelId });
581
- return false;
582
- }
583
- this._cleanupTunnelRecords(managed);
584
- logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.configid });
585
- return true;
586
- }
587
- /**
588
- * Remove a stopped tunnel by its config id.
589
- * @param configId - the config id to remove
590
- * @returns true if the record was removed, false otherwise
591
- */
592
- removeStoppedTunnelByConfigId(configId) {
593
- const managed = this.tunnelsByConfigId.get(configId);
594
- if (!managed) {
595
- logger.debug("Attempted to remove non-existent tunnel by configId", { configId });
596
- return false;
597
- }
598
- return this.removeStoppedTunnelByTunnelId(managed.tunnelid);
599
- }
600
- _cleanupTunnelRecords(managed) {
601
- if (!managed.isStopped) {
602
- throw new Error(`Active tunnel "${managed.tunnelid}" cannot be removed`);
603
- }
604
- try {
605
- if (managed.serveWorker) {
606
- managed.serveWorker = null;
607
- }
608
- this.tunnelStats.delete(managed.tunnelid);
609
- this.tunnelStatsListeners.delete(managed.tunnelid);
610
- this.tunnelErrorListeners.delete(managed.tunnelid);
611
- this.tunnelDisconnectListeners.delete(managed.tunnelid);
612
- this.tunnelWorkerErrorListeners.delete(managed.tunnelid);
613
- this.tunnelStartListeners.delete(managed.tunnelid);
614
- this.tunnelsByTunnelId.delete(managed.tunnelid);
615
- this.tunnelsByConfigId.delete(managed.configid);
616
- } catch (e) {
617
- logger.warn("Failed cleaning up tunnel records", { tunnelId: managed.tunnelid, error: e });
618
- }
619
- }
620
- /**
621
- * Get tunnel instance by either configId or tunnelId
622
- * @param configId - The configuration ID of the tunnel
623
- * @param tunnelId - The tunnel ID
624
- * @returns The tunnel instance
625
- * @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
626
- */
627
- getTunnelInstance(configId, tunnelId) {
628
- if (configId) {
629
- const managed = this.tunnelsByConfigId.get(configId);
630
- if (!managed) throw new Error(`Tunnel "${configId}" not found`);
631
- return managed.instance;
632
- }
633
- if (tunnelId) {
634
- const managed = this.tunnelsByTunnelId.get(tunnelId);
635
- if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
636
- return managed.instance;
637
- }
638
- throw new Error(`Either configId or tunnelId must be provided`);
639
- }
640
- /**
641
- * Get tunnel config by either configId or tunnelId
642
- * @param configId - The configuration ID of the tunnel
643
- * @param tunnelId - The tunnel ID
644
- * @returns The tunnel config
645
- * @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
646
- */
647
- async getTunnelConfig(configId, tunnelId) {
648
- if (configId) {
649
- const managed = this.tunnelsByConfigId.get(configId);
650
- if (!managed) {
651
- throw new Error(`Tunnel with configId "${configId}" not found`);
652
- }
653
- return managed.instance.getConfig();
654
- }
655
- if (tunnelId) {
656
- const managed = this.tunnelsByTunnelId.get(tunnelId);
657
- if (!managed) {
658
- throw new Error(`Tunnel with tunnelId "${tunnelId}" not found`);
446
+ /**
447
+ * Start a tunnel that was created but not yet started
448
+ */
449
+ async startTunnel(tunnelId) {
450
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
451
+ if (!managed) throw new Error(`Tunnel with id "${tunnelId}" not found`);
452
+ logger.info("Starting tunnel", { tunnelId });
453
+ let urls;
454
+ try {
455
+ urls = await managed.instance.start();
456
+ } catch (error) {
457
+ logger.error("Failed to start tunnel", { tunnelId, error });
458
+ throw error;
459
+ }
460
+ logger.info("Tunnel started", { tunnelId, urls });
461
+ if (managed.serve) {
462
+ this.startStaticFileServer(managed);
463
+ }
464
+ try {
465
+ const startListeners = this.tunnelStartListeners.get(tunnelId);
466
+ if (startListeners) {
467
+ for (const [id, listener] of startListeners) {
468
+ try {
469
+ listener(tunnelId, urls);
470
+ } catch (err) {
471
+ logger.debug("Error in start-listener callback", { listenerId: id, tunnelId, err });
472
+ }
473
+ }
474
+ }
475
+ } catch (e) {
476
+ logger.warn("Failed to notify start listeners", { tunnelId, e });
477
+ }
478
+ return urls;
659
479
  }
660
- return managed.instance.getConfig();
661
- }
662
- throw new Error(`Either configId or tunnelId must be provided`);
663
- }
664
- /**
665
- * Restarts a tunnel with its current configuration.
666
- * This function will stop the tunnel if it's running and start it again.
667
- * All configurations including additional forwarding rules are preserved.
668
- */
669
- async restartTunnel(tunnelid) {
670
- const existingTunnel = this.tunnelsByTunnelId.get(tunnelid);
671
- if (!existingTunnel) {
672
- throw new Error(`Tunnel "${tunnelid}" not found`);
673
- }
674
- logger.info("Initiating tunnel restart", {
675
- tunnelId: tunnelid,
676
- configId: existingTunnel.configid
677
- });
678
- try {
679
- const tunnelName = existingTunnel.tunnelName;
680
- const currentConfigId = existingTunnel.configid;
681
- const currentConfig = existingTunnel.tunnelConfig;
682
- const configWithForwarding = existingTunnel.configWithForwarding;
683
- const additionalForwarding = existingTunnel.additionalForwarding;
684
- const currentServe = existingTunnel.serve;
685
- const autoReconnect = existingTunnel.autoReconnect || false;
686
- this.tunnelsByTunnelId.delete(tunnelid);
687
- this.tunnelsByConfigId.delete(existingTunnel.configid);
688
- this.tunnelStats.delete(tunnelid);
689
- this.tunnelStatsListeners.delete(tunnelid);
690
- this.tunnelErrorListeners.delete(tunnelid);
691
- this.tunnelDisconnectListeners.delete(tunnelid);
692
- this.tunnelWorkerErrorListeners.delete(tunnelid);
693
- this.tunnelStartListeners.delete(tunnelid);
694
- const newTunnel = await this._createTunnelWithProcessedConfig({
695
- configid: currentConfigId,
696
- tunnelid,
697
- tunnelName,
698
- originalConfig: currentConfig,
699
- configWithForwarding,
700
- additionalForwarding,
701
- serve: currentServe,
702
- autoReconnect
703
- });
704
- if (existingTunnel.createdAt) {
705
- newTunnel.createdAt = existingTunnel.createdAt;
480
+ /**
481
+ * Stops a running tunnel and updates its status.
482
+ *
483
+ * @param tunnelId - The unique identifier of the tunnel to stop
484
+ * @throws {Error} If the tunnel with the given tunnelId is not found
485
+ * @remarks
486
+ * - Clears the tunnel's remote URLs
487
+ * - Updates the tunnel's state to Exited if stopped successfully
488
+ * - Logs the stop operation with tunnelId and configId
489
+ */
490
+ stopTunnel(tunnelId) {
491
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
492
+ if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
493
+ logger.info("Stopping tunnel", { tunnelId, configId: managed.configid });
494
+ try {
495
+ managed.instance.stop();
496
+ if (managed.serveWorker) {
497
+ logger.info("terminating serveWorker");
498
+ managed.serveWorker.terminate();
499
+ }
500
+ this.tunnelStats.delete(tunnelId);
501
+ this.tunnelStatsListeners.delete(tunnelId);
502
+ this.tunnelStats.delete(tunnelId);
503
+ this.tunnelStatsListeners.delete(tunnelId);
504
+ this.tunnelErrorListeners.delete(tunnelId);
505
+ this.tunnelDisconnectListeners.delete(tunnelId);
506
+ this.tunnelWorkerErrorListeners.delete(tunnelId);
507
+ this.tunnelStartListeners.delete(tunnelId);
508
+ managed.serveWorker = null;
509
+ managed.warnings = managed.warnings ?? [];
510
+ managed.isStopped = true;
511
+ managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
512
+ logger.info("Tunnel stopped", { tunnelId, configId: managed.configid });
513
+ return { configid: managed.configid, tunnelid: managed.tunnelid };
514
+ } catch (error) {
515
+ logger.error("Failed to stop tunnel", { tunnelId, error });
516
+ throw error;
517
+ }
706
518
  }
707
- await this.startTunnel(newTunnel.tunnelid);
708
- } catch (error) {
709
- logger.error("Failed to restart tunnel", {
710
- tunnelid,
711
- error: error instanceof Error ? error.message : "Unknown error"
712
- });
713
- throw new Error(`Failed to restart tunnel: ${error instanceof Error ? error.message : "Unknown error"}`);
714
- }
715
- }
716
- /**
717
- * Updates the configuration of an existing tunnel.
718
- *
719
- * This method handles the process of updating a tunnel's configuration while preserving
720
- * its state. If the tunnel is running, it will be stopped, updated, and restarted.
721
- * In case of failure, it attempts to restore the original configuration.
722
- *
723
- * @param newConfig - The new configuration to apply, including configid and optional additional forwarding
724
- *
725
- * @returns Promise resolving to the updated ManagedTunnel
726
- * @throws Error if the tunnel is not found or if the update process fails
727
- */
728
- async updateConfig(newConfig) {
729
- const { configid, tunnelName: newTunnelName, additionalForwarding } = newConfig;
730
- if (!configid || configid.trim().length === 0) {
731
- throw new Error(`Invalid configid: "${configid}"`);
732
- }
733
- const existingTunnel = this.tunnelsByConfigId.get(configid);
734
- if (!existingTunnel) {
735
- throw new Error(`Tunnel with config id "${configid}" not found`);
736
- }
737
- const isStopped = existingTunnel.isStopped;
738
- const currentTunnelConfig = existingTunnel.tunnelConfig;
739
- const currentConfigWithForwarding = existingTunnel.configWithForwarding;
740
- const currentTunnelId = existingTunnel.tunnelid;
741
- const currentTunnelConfigId = existingTunnel.configid;
742
- const currentAdditionalForwarding = existingTunnel.additionalForwarding;
743
- const currentTunnelName = existingTunnel.tunnelName;
744
- const currentServe = existingTunnel.serve;
745
- const currentAutoReconnect = existingTunnel.autoReconnect || false;
746
- try {
747
- if (!isStopped) {
748
- existingTunnel.instance.stop();
749
- }
750
- this.tunnelsByTunnelId.delete(currentTunnelId);
751
- this.tunnelsByConfigId.delete(currentTunnelConfigId);
752
- const mergedBaseConfig = {
753
- ...newConfig,
754
- configid,
755
- tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
756
- serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe
757
- };
758
- const newConfigWithForwarding = this.buildPinggyConfig(
759
- mergedBaseConfig,
760
- additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding
761
- );
762
- const newTunnel = await this._createTunnelWithProcessedConfig({
763
- configid,
764
- tunnelid: currentTunnelId,
765
- tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
766
- originalConfig: mergedBaseConfig,
767
- configWithForwarding: newConfigWithForwarding,
768
- additionalForwarding: additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding,
769
- serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe,
770
- autoReconnect: currentAutoReconnect
771
- });
772
- if (!isStopped) {
773
- await this.startTunnel(newTunnel.tunnelid);
519
+ /**
520
+ * Get all public URLs for a tunnel
521
+ */
522
+ async getTunnelUrls(tunnelId) {
523
+ try {
524
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
525
+ if (!managed || managed.isStopped) {
526
+ logger.error(`Tunnel "${tunnelId}" not found when fetching URLs`);
527
+ return [];
528
+ }
529
+ const urls = await managed.instance.urls();
530
+ logger.debug("Queried tunnel URLs", { tunnelId, urls });
531
+ return urls;
532
+ } catch (error) {
533
+ logger.error("Error fetching tunnel URLs", { tunnelId, error });
534
+ throw error;
535
+ }
774
536
  }
775
- logger.info("Tunnel configuration updated", {
776
- tunnelId: newTunnel.tunnelid,
777
- configId: newTunnel.configid,
778
- isStopped
779
- });
780
- return newTunnel;
781
- } catch (error) {
782
- logger.error("Error updating tunnel configuration", {
783
- configId: configid,
784
- error: error instanceof Error ? error.message : String(error)
785
- });
786
- try {
787
- const originalTunnel = await this._createTunnelWithProcessedConfig({
788
- configid: currentTunnelConfigId,
789
- tunnelid: currentTunnelId,
790
- tunnelName: currentTunnelName,
791
- originalConfig: currentTunnelConfig,
792
- configWithForwarding: currentConfigWithForwarding,
793
- additionalForwarding: currentAdditionalForwarding,
794
- serve: currentServe,
795
- autoReconnect: currentAutoReconnect
796
- });
797
- if (!isStopped) {
798
- await this.startTunnel(originalTunnel.tunnelid);
537
+ /**
538
+ * Get all TunnelStatus currently managed by this TunnelManager
539
+ * @returns An array of all TunnelStatus objects
540
+ */
541
+ async getAllTunnels() {
542
+ try {
543
+ const tunnelList = await Promise.all(Array.from(this.tunnelsByTunnelId.values()).map(async (tunnel) => {
544
+ return {
545
+ tunnelid: tunnel.tunnelid,
546
+ configid: tunnel.configid,
547
+ tunnelName: tunnel.tunnelName,
548
+ tunnelConfig: tunnel.tunnelConfig,
549
+ remoteurls: !tunnel.isStopped ? await this.getTunnelUrls(tunnel.tunnelid) : [],
550
+ additionalForwarding: tunnel.additionalForwarding,
551
+ serve: tunnel.serve
552
+ };
553
+ }));
554
+ return tunnelList;
555
+ } catch (err) {
556
+ logger.error("Error fetching tunnels", { error: err });
557
+ return [];
799
558
  }
800
- logger.warn("Restored original tunnel configuration after update failure", {
801
- currentTunnelId,
802
- error: error instanceof Error ? error.message : "Unknown error"
803
- });
804
- } catch (restoreError) {
805
- logger.error("Failed to restore original tunnel configuration", {
806
- currentTunnelId,
807
- error: restoreError instanceof Error ? restoreError.message : "Unknown error"
808
- });
809
559
  }
810
- throw error;
811
- }
812
- }
813
- /**
814
- * Retrieve the ManagedTunnel object by either configId or tunnelId.
815
- * Throws an error if neither id is provided or the tunnel is not found.
816
- */
817
- getManagedTunnel(configId, tunnelId) {
818
- if (configId) {
819
- const managed = this.tunnelsByConfigId.get(configId);
820
- if (!managed) throw new Error(`Tunnel "${configId}" not found`);
821
- return managed;
822
- }
823
- if (tunnelId) {
824
- const managed = this.tunnelsByTunnelId.get(tunnelId);
825
- if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
826
- return managed;
827
- }
828
- throw new Error(`Either configId or tunnelId must be provided`);
829
- }
830
- async getTunnelGreetMessage(tunnelId) {
831
- const managed = this.tunnelsByTunnelId.get(tunnelId);
832
- if (!managed) {
833
- logger.error(`Tunnel "${tunnelId}" not found when fetching greet message`);
834
- return null;
835
- }
836
- try {
837
- if (managed.isStopped) {
838
- return null;
560
+ /**
561
+ * Get status of a tunnel
562
+ */
563
+ async getTunnelStatus(tunnelId) {
564
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
565
+ if (!managed) {
566
+ logger.error(`Tunnel "${tunnelId}" not found when fetching status`);
567
+ throw new Error(`Tunnel "${tunnelId}" not found`);
568
+ }
569
+ if (managed.isStopped) {
570
+ return "exited";
571
+ }
572
+ const status = await managed.instance.getStatus();
573
+ logger.debug("Queried tunnel status", { tunnelId, status });
574
+ return status;
839
575
  }
840
- const messages = await managed.instance.getGreetMessage();
841
- if (Array.isArray(messages)) {
842
- return messages.join(" ");
576
+ /**
577
+ * Stop all tunnels
578
+ */
579
+ stopAllTunnels() {
580
+ for (const { instance } of this.tunnelsByTunnelId.values()) {
581
+ try {
582
+ instance.stop();
583
+ } catch (e) {
584
+ logger.warn("Error stopping tunnel instance", e);
585
+ }
586
+ }
587
+ this.tunnelsByTunnelId.clear();
588
+ this.tunnelsByConfigId.clear();
589
+ this.tunnelStats.clear();
590
+ this.tunnelStatsListeners.clear();
591
+ logger.info("All tunnels stopped and cleared");
843
592
  }
844
- return messages ?? null;
845
- } catch (e) {
846
- logger.error(
847
- `Error fetching greet message for tunnel "${tunnelId}": ${e instanceof Error ? e.message : String(e)}`
848
- );
849
- return null;
850
- }
851
- }
852
- getTunnelStats(tunnelId) {
853
- const managed = this.tunnelsByTunnelId.get(tunnelId);
854
- if (!managed) {
855
- return null;
856
- }
857
- const stats = this.tunnelStats.get(tunnelId);
858
- return stats || null;
859
- }
860
- getLatestTunnelStats(tunnelId) {
861
- const managed = this.tunnelsByTunnelId.get(tunnelId);
862
- if (!managed) {
863
- return null;
864
- }
865
- const stats = this.tunnelStats.get(tunnelId);
866
- if (stats && stats.length > 0) {
867
- return stats[stats.length - 1];
868
- }
869
- return null;
870
- }
871
- /**
872
- * Registers a listener function to receive tunnel statistics updates.
873
- * The listener will be called whenever any tunnel's stats are updated.
874
- *
875
- * @param tunnelId - The tunnel ID to listen to stats for
876
- * @param listener - Function that receives tunnelId and stats when updates occur
877
- * @returns A unique listener ID that can be used to deregister the listener and tunnelId
878
- *
879
- * @throws {Error} When the specified tunnelId does not exist
880
- */
881
- async registerStatsListener(tunnelId, listener) {
882
- const managed = this.tunnelsByTunnelId.get(tunnelId);
883
- if (!managed) {
884
- throw new Error(`Tunnel "${tunnelId}" not found`);
885
- }
886
- if (!this.tunnelStatsListeners.has(tunnelId)) {
887
- this.tunnelStatsListeners.set(tunnelId, /* @__PURE__ */ new Map());
888
- }
889
- const listenerId = getRandomId();
890
- const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
891
- tunnelListeners.set(listenerId, listener);
892
- logger.info("Stats listener registered for tunnel", { tunnelId, listenerId });
893
- return [listenerId, tunnelId];
894
- }
895
- async registerErrorListener(tunnelId, listener) {
896
- const managed = this.tunnelsByTunnelId.get(tunnelId);
897
- if (!managed) {
898
- throw new Error(`Tunnel "${tunnelId}" not found`);
899
- }
900
- if (!this.tunnelErrorListeners.has(tunnelId)) {
901
- this.tunnelErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
902
- }
903
- const listenerId = getRandomId();
904
- const tunnelErrorListeners = this.tunnelErrorListeners.get(tunnelId);
905
- tunnelErrorListeners.set(listenerId, listener);
906
- logger.info("Error listener registered for tunnel", { tunnelId, listenerId });
907
- return listenerId;
908
- }
909
- async registerDisconnectListener(tunnelId, listener) {
910
- const managed = this.tunnelsByTunnelId.get(tunnelId);
911
- if (!managed) {
912
- throw new Error(`Tunnel "${tunnelId}" not found`);
913
- }
914
- if (!this.tunnelDisconnectListeners.has(tunnelId)) {
915
- this.tunnelDisconnectListeners.set(tunnelId, /* @__PURE__ */ new Map());
916
- }
917
- const listenerId = getRandomId();
918
- const tunnelDisconnectListeners = this.tunnelDisconnectListeners.get(tunnelId);
919
- tunnelDisconnectListeners.set(listenerId, listener);
920
- logger.info("Disconnect listener registered for tunnel", { tunnelId, listenerId });
921
- return listenerId;
922
- }
923
- async registerWorkerErrorListner(tunnelId, listener) {
924
- const managed = this.tunnelsByTunnelId.get(tunnelId);
925
- if (!managed) {
926
- throw new Error(`Tunnel "${tunnelId}" not found`);
927
- }
928
- if (!this.tunnelWorkerErrorListeners.has(tunnelId)) {
929
- this.tunnelWorkerErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
930
- }
931
- const listenerId = getRandomId();
932
- const tunnelWorkerErrorListner = this.tunnelWorkerErrorListeners.get(tunnelId);
933
- tunnelWorkerErrorListner?.set(listenerId, listener);
934
- logger.info("TunnelWorker error listener registered for tunnel", { tunnelId, listenerId });
935
- }
936
- async registerStartListener(tunnelId, listener) {
937
- const managed = this.tunnelsByTunnelId.get(tunnelId);
938
- if (!managed) {
939
- throw new Error(`Tunnel "${tunnelId}" not found`);
940
- }
941
- if (!this.tunnelStartListeners.has(tunnelId)) {
942
- this.tunnelStartListeners.set(tunnelId, /* @__PURE__ */ new Map());
943
- }
944
- const listenerId = getRandomId();
945
- const listeners = this.tunnelStartListeners.get(tunnelId);
946
- listeners.set(listenerId, listener);
947
- logger.info("Start listener registered for tunnel", { tunnelId, listenerId });
948
- return listenerId;
949
- }
950
- /**
951
- * Removes a previously registered stats listener.
952
- *
953
- * @param tunnelId - The tunnel ID the listener was registered for
954
- * @param listenerId - The unique ID returned when the listener was registered
955
- */
956
- deregisterStatsListener(tunnelId, listenerId) {
957
- const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
958
- if (!tunnelListeners) {
959
- logger.warn("No listeners found for tunnel", { tunnelId });
960
- return;
961
- }
962
- const removed = tunnelListeners.delete(listenerId);
963
- if (removed) {
964
- logger.info("Stats listener deregistered", { tunnelId, listenerId });
965
- if (tunnelListeners.size === 0) {
966
- this.tunnelStatsListeners.delete(tunnelId);
593
+ /**
594
+ * Remove a stopped tunnel's records so it will no longer be returned by list methods.
595
+ *
596
+ *
597
+ * @param tunnelId - the tunnel id to remove
598
+ * @returns true if the record was removed, false otherwise
599
+ */
600
+ removeStoppedTunnelByTunnelId(tunnelId) {
601
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
602
+ if (!managed) {
603
+ logger.debug("Attempted to remove non-existent tunnel", { tunnelId });
604
+ return false;
605
+ }
606
+ if (!managed.isStopped) {
607
+ logger.warn("Attempted to remove tunnel that is not stopped", { tunnelId });
608
+ return false;
609
+ }
610
+ this._cleanupTunnelRecords(managed);
611
+ logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.configid });
612
+ return true;
967
613
  }
968
- } else {
969
- logger.warn("Attempted to deregister non-existent stats listener", { tunnelId, listenerId });
970
- }
971
- }
972
- deregisterErrorListener(tunnelId, listenerId) {
973
- const listeners = this.tunnelErrorListeners.get(tunnelId);
974
- if (!listeners) {
975
- logger.warn("No error listeners found for tunnel", { tunnelId });
976
- return;
977
- }
978
- const removed = listeners.delete(listenerId);
979
- if (removed) {
980
- logger.info("Error listener deregistered", { tunnelId, listenerId });
981
- if (listeners.size === 0) {
982
- this.tunnelErrorListeners.delete(tunnelId);
614
+ /**
615
+ * Remove a stopped tunnel by its config id.
616
+ * @param configId - the config id to remove
617
+ * @returns true if the record was removed, false otherwise
618
+ */
619
+ removeStoppedTunnelByConfigId(configId) {
620
+ const managed = this.tunnelsByConfigId.get(configId);
621
+ if (!managed) {
622
+ logger.debug("Attempted to remove non-existent tunnel by configId", { configId });
623
+ return false;
624
+ }
625
+ return this.removeStoppedTunnelByTunnelId(managed.tunnelid);
983
626
  }
984
- } else {
985
- logger.warn("Attempted to deregister non-existent error listener", { tunnelId, listenerId });
986
- }
987
- }
988
- deregisterDisconnectListener(tunnelId, listenerId) {
989
- const listeners = this.tunnelDisconnectListeners.get(tunnelId);
990
- if (!listeners) {
991
- logger.warn("No disconnect listeners found for tunnel", { tunnelId });
992
- return;
993
- }
994
- const removed = listeners.delete(listenerId);
995
- if (removed) {
996
- logger.info("Disconnect listener deregistered", { tunnelId, listenerId });
997
- if (listeners.size === 0) {
998
- this.tunnelDisconnectListeners.delete(tunnelId);
627
+ _cleanupTunnelRecords(managed) {
628
+ if (!managed.isStopped) {
629
+ throw new Error(`Active tunnel "${managed.tunnelid}" cannot be removed`);
630
+ }
631
+ try {
632
+ if (managed.serveWorker) {
633
+ managed.serveWorker = null;
634
+ }
635
+ this.tunnelStats.delete(managed.tunnelid);
636
+ this.tunnelStatsListeners.delete(managed.tunnelid);
637
+ this.tunnelErrorListeners.delete(managed.tunnelid);
638
+ this.tunnelDisconnectListeners.delete(managed.tunnelid);
639
+ this.tunnelWorkerErrorListeners.delete(managed.tunnelid);
640
+ this.tunnelStartListeners.delete(managed.tunnelid);
641
+ this.tunnelsByTunnelId.delete(managed.tunnelid);
642
+ this.tunnelsByConfigId.delete(managed.configid);
643
+ } catch (e) {
644
+ logger.warn("Failed cleaning up tunnel records", { tunnelId: managed.tunnelid, error: e });
645
+ }
999
646
  }
1000
- } else {
1001
- logger.warn("Attempted to deregister non-existent disconnect listener", { tunnelId, listenerId });
1002
- }
1003
- }
1004
- async getLocalserverTlsInfo(tunnelId) {
1005
- const managed = this.tunnelsByTunnelId.get(tunnelId);
1006
- if (!managed) {
1007
- logger.error(`Tunnel "${tunnelId}" not found when fetching local server TLS info`);
1008
- return false;
1009
- }
1010
- try {
1011
- if (managed.isStopped) {
1012
- return false;
647
+ /**
648
+ * Get tunnel instance by either configId or tunnelId
649
+ * @param configId - The configuration ID of the tunnel
650
+ * @param tunnelId - The tunnel ID
651
+ * @returns The tunnel instance
652
+ * @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
653
+ */
654
+ getTunnelInstance(configId, tunnelId) {
655
+ if (configId) {
656
+ const managed = this.tunnelsByConfigId.get(configId);
657
+ if (!managed) throw new Error(`Tunnel "${configId}" not found`);
658
+ return managed.instance;
659
+ }
660
+ if (tunnelId) {
661
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
662
+ if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
663
+ return managed.instance;
664
+ }
665
+ throw new Error(`Either configId or tunnelId must be provided`);
1013
666
  }
1014
- const tlsInfo = await managed.instance.getLocalServerTls();
1015
- if (tlsInfo) {
1016
- return tlsInfo;
667
+ /**
668
+ * Get tunnel config by either configId or tunnelId
669
+ * @param configId - The configuration ID of the tunnel
670
+ * @param tunnelId - The tunnel ID
671
+ * @returns The tunnel config
672
+ * @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
673
+ */
674
+ async getTunnelConfig(configId, tunnelId) {
675
+ if (configId) {
676
+ const managed = this.tunnelsByConfigId.get(configId);
677
+ if (!managed) {
678
+ throw new Error(`Tunnel with configId "${configId}" not found`);
679
+ }
680
+ return managed.instance.getConfig();
681
+ }
682
+ if (tunnelId) {
683
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
684
+ if (!managed) {
685
+ throw new Error(`Tunnel with tunnelId "${tunnelId}" not found`);
686
+ }
687
+ return managed.instance.getConfig();
688
+ }
689
+ throw new Error(`Either configId or tunnelId must be provided`);
1017
690
  }
1018
- return false;
1019
- } catch (e) {
1020
- logger.error(`Error fetching TLS info for tunnel "${tunnelId}": ${e instanceof Error ? e.message : e}`);
1021
- return false;
1022
- }
1023
- }
1024
- /**
1025
- * Sets up the stats callback for a tunnel during creation.
1026
- * This callback will update stored stats and notify all registered listeners.
1027
- */
1028
- setupStatsCallback(tunnelId, managed) {
1029
- try {
1030
- const callback = (usage) => {
1031
- this.updateStats(tunnelId, usage);
1032
- };
1033
- managed.instance.setUsageUpdateCallback(callback);
1034
- logger.debug("Stats callback set up for tunnel", { tunnelId });
1035
- } catch (error) {
1036
- logger.warn("Failed to set up stats callback", { tunnelId, error });
1037
- }
1038
- }
1039
- notifyErrorListeners(tunnelId, errorMsg, isFatal) {
1040
- try {
1041
- const listeners = this.tunnelErrorListeners.get(tunnelId);
1042
- if (!listeners) return;
1043
- for (const [id, listener] of listeners) {
691
+ /**
692
+ * Restarts a tunnel with its current configuration.
693
+ * This function will stop the tunnel if it's running and start it again.
694
+ * All configurations including additional forwarding rules are preserved.
695
+ */
696
+ async restartTunnel(tunnelid) {
697
+ const existingTunnel = this.tunnelsByTunnelId.get(tunnelid);
698
+ if (!existingTunnel) {
699
+ throw new Error(`Tunnel "${tunnelid}" not found`);
700
+ }
701
+ logger.info("Initiating tunnel restart", {
702
+ tunnelId: tunnelid,
703
+ configId: existingTunnel.configid
704
+ });
1044
705
  try {
1045
- listener(tunnelId, errorMsg, isFatal);
1046
- } catch (err) {
1047
- logger.debug("Error in error-listener callback", { listenerId: id, tunnelId, err });
706
+ const tunnelName = existingTunnel.tunnelName;
707
+ const currentConfigId = existingTunnel.configid;
708
+ const currentConfig = existingTunnel.tunnelConfig;
709
+ const configWithForwarding = existingTunnel.configWithForwarding;
710
+ const additionalForwarding = existingTunnel.additionalForwarding;
711
+ const currentServe = existingTunnel.serve;
712
+ const autoReconnect = existingTunnel.autoReconnect || false;
713
+ this.tunnelsByTunnelId.delete(tunnelid);
714
+ this.tunnelsByConfigId.delete(existingTunnel.configid);
715
+ this.tunnelStats.delete(tunnelid);
716
+ this.tunnelStatsListeners.delete(tunnelid);
717
+ this.tunnelErrorListeners.delete(tunnelid);
718
+ this.tunnelDisconnectListeners.delete(tunnelid);
719
+ this.tunnelWorkerErrorListeners.delete(tunnelid);
720
+ this.tunnelStartListeners.delete(tunnelid);
721
+ const newTunnel = await this._createTunnelWithProcessedConfig({
722
+ configid: currentConfigId,
723
+ tunnelid,
724
+ tunnelName,
725
+ originalConfig: currentConfig,
726
+ configWithForwarding,
727
+ additionalForwarding,
728
+ serve: currentServe,
729
+ autoReconnect
730
+ });
731
+ if (existingTunnel.createdAt) {
732
+ newTunnel.createdAt = existingTunnel.createdAt;
733
+ }
734
+ await this.startTunnel(newTunnel.tunnelid);
735
+ } catch (error) {
736
+ logger.error("Failed to restart tunnel", {
737
+ tunnelid,
738
+ error: error instanceof Error ? error.message : "Unknown error"
739
+ });
740
+ throw new Error(`Failed to restart tunnel: ${error instanceof Error ? error.message : "Unknown error"}`);
1048
741
  }
1049
742
  }
1050
- } catch (err) {
1051
- logger.debug("Failed to notify error listeners", { tunnelId, err });
1052
- }
1053
- }
1054
- setupErrorCallback(tunnelId, managed) {
1055
- try {
1056
- const callback = ({ errorNo, error, recoverable }) => {
743
+ /**
744
+ * Updates the configuration of an existing tunnel.
745
+ *
746
+ * This method handles the process of updating a tunnel's configuration while preserving
747
+ * its state. If the tunnel is running, it will be stopped, updated, and restarted.
748
+ * In case of failure, it attempts to restore the original configuration.
749
+ *
750
+ * @param newConfig - The new configuration to apply, including configid and optional additional forwarding
751
+ *
752
+ * @returns Promise resolving to the updated ManagedTunnel
753
+ * @throws Error if the tunnel is not found or if the update process fails
754
+ */
755
+ async updateConfig(newConfig) {
756
+ const { configid, tunnelName: newTunnelName, additionalForwarding } = newConfig;
757
+ if (!configid || configid.trim().length === 0) {
758
+ throw new Error(`Invalid configid: "${configid}"`);
759
+ }
760
+ const existingTunnel = this.tunnelsByConfigId.get(configid);
761
+ if (!existingTunnel) {
762
+ throw new Error(`Tunnel with config id "${configid}" not found`);
763
+ }
764
+ const isStopped = existingTunnel.isStopped;
765
+ const currentTunnelConfig = existingTunnel.tunnelConfig;
766
+ const currentConfigWithForwarding = existingTunnel.configWithForwarding;
767
+ const currentTunnelId = existingTunnel.tunnelid;
768
+ const currentTunnelConfigId = existingTunnel.configid;
769
+ const currentAdditionalForwarding = existingTunnel.additionalForwarding;
770
+ const currentTunnelName = existingTunnel.tunnelName;
771
+ const currentServe = existingTunnel.serve;
772
+ const currentAutoReconnect = existingTunnel.autoReconnect || false;
773
+ try {
774
+ if (!isStopped) {
775
+ existingTunnel.instance.stop();
776
+ }
777
+ this.tunnelsByTunnelId.delete(currentTunnelId);
778
+ this.tunnelsByConfigId.delete(currentTunnelConfigId);
779
+ const mergedBaseConfig = {
780
+ ...newConfig,
781
+ configid,
782
+ tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
783
+ serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe
784
+ };
785
+ const newConfigWithForwarding = this.buildPinggyConfig(
786
+ mergedBaseConfig,
787
+ additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding
788
+ );
789
+ const newTunnel = await this._createTunnelWithProcessedConfig({
790
+ configid,
791
+ tunnelid: currentTunnelId,
792
+ tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
793
+ originalConfig: mergedBaseConfig,
794
+ configWithForwarding: newConfigWithForwarding,
795
+ additionalForwarding: additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding,
796
+ serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe,
797
+ autoReconnect: currentAutoReconnect
798
+ });
799
+ if (!isStopped) {
800
+ await this.startTunnel(newTunnel.tunnelid);
801
+ }
802
+ logger.info("Tunnel configuration updated", {
803
+ tunnelId: newTunnel.tunnelid,
804
+ configId: newTunnel.configid,
805
+ isStopped
806
+ });
807
+ return newTunnel;
808
+ } catch (error) {
809
+ logger.error("Error updating tunnel configuration", {
810
+ configId: configid,
811
+ error: error instanceof Error ? error.message : String(error)
812
+ });
813
+ try {
814
+ const originalTunnel = await this._createTunnelWithProcessedConfig({
815
+ configid: currentTunnelConfigId,
816
+ tunnelid: currentTunnelId,
817
+ tunnelName: currentTunnelName,
818
+ originalConfig: currentTunnelConfig,
819
+ configWithForwarding: currentConfigWithForwarding,
820
+ additionalForwarding: currentAdditionalForwarding,
821
+ serve: currentServe,
822
+ autoReconnect: currentAutoReconnect
823
+ });
824
+ if (!isStopped) {
825
+ await this.startTunnel(originalTunnel.tunnelid);
826
+ }
827
+ logger.warn("Restored original tunnel configuration after update failure", {
828
+ currentTunnelId,
829
+ error: error instanceof Error ? error.message : "Unknown error"
830
+ });
831
+ } catch (restoreError) {
832
+ logger.error("Failed to restore original tunnel configuration", {
833
+ currentTunnelId,
834
+ error: restoreError instanceof Error ? restoreError.message : "Unknown error"
835
+ });
836
+ }
837
+ throw error;
838
+ }
839
+ }
840
+ /**
841
+ * Retrieve the ManagedTunnel object by either configId or tunnelId.
842
+ * Throws an error if neither id is provided or the tunnel is not found.
843
+ */
844
+ getManagedTunnel(configId, tunnelId) {
845
+ if (configId) {
846
+ const managed = this.tunnelsByConfigId.get(configId);
847
+ if (!managed) throw new Error(`Tunnel "${configId}" not found`);
848
+ return managed;
849
+ }
850
+ if (tunnelId) {
851
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
852
+ if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
853
+ return managed;
854
+ }
855
+ throw new Error(`Either configId or tunnelId must be provided`);
856
+ }
857
+ async getTunnelGreetMessage(tunnelId) {
858
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
859
+ if (!managed) {
860
+ logger.error(`Tunnel "${tunnelId}" not found when fetching greet message`);
861
+ return null;
862
+ }
1057
863
  try {
1058
- const msg = typeof error === "string" ? error : String(error);
1059
- const isFatal = true;
1060
- logger.debug("Tunnel reported error", { tunnelId, errorNo, errorMsg: msg, recoverable });
1061
- this.notifyErrorListeners(tunnelId, msg, isFatal);
864
+ if (managed.isStopped) {
865
+ return null;
866
+ }
867
+ const messages = await managed.instance.getGreetMessage();
868
+ if (Array.isArray(messages)) {
869
+ return messages.join(" ");
870
+ }
871
+ return messages ?? null;
1062
872
  } catch (e) {
1063
- logger.warn("Error handling tunnel error callback", { tunnelId, e });
873
+ logger.error(
874
+ `Error fetching greet message for tunnel "${tunnelId}": ${e instanceof Error ? e.message : String(e)}`
875
+ );
876
+ return null;
877
+ }
878
+ }
879
+ getTunnelStats(tunnelId) {
880
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
881
+ if (!managed) {
882
+ return null;
883
+ }
884
+ const stats = this.tunnelStats.get(tunnelId);
885
+ return stats || null;
886
+ }
887
+ getLatestTunnelStats(tunnelId) {
888
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
889
+ if (!managed) {
890
+ return null;
891
+ }
892
+ const stats = this.tunnelStats.get(tunnelId);
893
+ if (stats && stats.length > 0) {
894
+ return stats[stats.length - 1];
895
+ }
896
+ return null;
897
+ }
898
+ /**
899
+ * Registers a listener function to receive tunnel statistics updates.
900
+ * The listener will be called whenever any tunnel's stats are updated.
901
+ *
902
+ * @param tunnelId - The tunnel ID to listen to stats for
903
+ * @param listener - Function that receives tunnelId and stats when updates occur
904
+ * @returns A unique listener ID that can be used to deregister the listener and tunnelId
905
+ *
906
+ * @throws {Error} When the specified tunnelId does not exist
907
+ */
908
+ async registerStatsListener(tunnelId, listener) {
909
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
910
+ if (!managed) {
911
+ throw new Error(`Tunnel "${tunnelId}" not found`);
912
+ }
913
+ if (!this.tunnelStatsListeners.has(tunnelId)) {
914
+ this.tunnelStatsListeners.set(tunnelId, /* @__PURE__ */ new Map());
915
+ }
916
+ const listenerId = getRandomId();
917
+ const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
918
+ tunnelListeners.set(listenerId, listener);
919
+ logger.info("Stats listener registered for tunnel", { tunnelId, listenerId });
920
+ return [listenerId, tunnelId];
921
+ }
922
+ async registerErrorListener(tunnelId, listener) {
923
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
924
+ if (!managed) {
925
+ throw new Error(`Tunnel "${tunnelId}" not found`);
926
+ }
927
+ if (!this.tunnelErrorListeners.has(tunnelId)) {
928
+ this.tunnelErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
929
+ }
930
+ const listenerId = getRandomId();
931
+ const tunnelErrorListeners = this.tunnelErrorListeners.get(tunnelId);
932
+ tunnelErrorListeners.set(listenerId, listener);
933
+ logger.info("Error listener registered for tunnel", { tunnelId, listenerId });
934
+ return listenerId;
935
+ }
936
+ async registerDisconnectListener(tunnelId, listener) {
937
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
938
+ if (!managed) {
939
+ throw new Error(`Tunnel "${tunnelId}" not found`);
940
+ }
941
+ if (!this.tunnelDisconnectListeners.has(tunnelId)) {
942
+ this.tunnelDisconnectListeners.set(tunnelId, /* @__PURE__ */ new Map());
943
+ }
944
+ const listenerId = getRandomId();
945
+ const tunnelDisconnectListeners = this.tunnelDisconnectListeners.get(tunnelId);
946
+ tunnelDisconnectListeners.set(listenerId, listener);
947
+ logger.info("Disconnect listener registered for tunnel", { tunnelId, listenerId });
948
+ return listenerId;
949
+ }
950
+ async registerWorkerErrorListner(tunnelId, listener) {
951
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
952
+ if (!managed) {
953
+ throw new Error(`Tunnel "${tunnelId}" not found`);
954
+ }
955
+ if (!this.tunnelWorkerErrorListeners.has(tunnelId)) {
956
+ this.tunnelWorkerErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
957
+ }
958
+ const listenerId = getRandomId();
959
+ const tunnelWorkerErrorListner = this.tunnelWorkerErrorListeners.get(tunnelId);
960
+ tunnelWorkerErrorListner?.set(listenerId, listener);
961
+ logger.info("TunnelWorker error listener registered for tunnel", { tunnelId, listenerId });
962
+ }
963
+ async registerStartListener(tunnelId, listener) {
964
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
965
+ if (!managed) {
966
+ throw new Error(`Tunnel "${tunnelId}" not found`);
967
+ }
968
+ if (!this.tunnelStartListeners.has(tunnelId)) {
969
+ this.tunnelStartListeners.set(tunnelId, /* @__PURE__ */ new Map());
970
+ }
971
+ const listenerId = getRandomId();
972
+ const listeners = this.tunnelStartListeners.get(tunnelId);
973
+ listeners.set(listenerId, listener);
974
+ logger.info("Start listener registered for tunnel", { tunnelId, listenerId });
975
+ return listenerId;
976
+ }
977
+ /**
978
+ * Removes a previously registered stats listener.
979
+ *
980
+ * @param tunnelId - The tunnel ID the listener was registered for
981
+ * @param listenerId - The unique ID returned when the listener was registered
982
+ */
983
+ deregisterStatsListener(tunnelId, listenerId) {
984
+ const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
985
+ if (!tunnelListeners) {
986
+ logger.warn("No listeners found for tunnel", { tunnelId });
987
+ return;
988
+ }
989
+ const removed = tunnelListeners.delete(listenerId);
990
+ if (removed) {
991
+ logger.info("Stats listener deregistered", { tunnelId, listenerId });
992
+ if (tunnelListeners.size === 0) {
993
+ this.tunnelStatsListeners.delete(tunnelId);
994
+ }
995
+ } else {
996
+ logger.warn("Attempted to deregister non-existent stats listener", { tunnelId, listenerId });
997
+ }
998
+ }
999
+ deregisterErrorListener(tunnelId, listenerId) {
1000
+ const listeners = this.tunnelErrorListeners.get(tunnelId);
1001
+ if (!listeners) {
1002
+ logger.warn("No error listeners found for tunnel", { tunnelId });
1003
+ return;
1004
+ }
1005
+ const removed = listeners.delete(listenerId);
1006
+ if (removed) {
1007
+ logger.info("Error listener deregistered", { tunnelId, listenerId });
1008
+ if (listeners.size === 0) {
1009
+ this.tunnelErrorListeners.delete(tunnelId);
1010
+ }
1011
+ } else {
1012
+ logger.warn("Attempted to deregister non-existent error listener", { tunnelId, listenerId });
1013
+ }
1014
+ }
1015
+ deregisterDisconnectListener(tunnelId, listenerId) {
1016
+ const listeners = this.tunnelDisconnectListeners.get(tunnelId);
1017
+ if (!listeners) {
1018
+ logger.warn("No disconnect listeners found for tunnel", { tunnelId });
1019
+ return;
1020
+ }
1021
+ const removed = listeners.delete(listenerId);
1022
+ if (removed) {
1023
+ logger.info("Disconnect listener deregistered", { tunnelId, listenerId });
1024
+ if (listeners.size === 0) {
1025
+ this.tunnelDisconnectListeners.delete(tunnelId);
1026
+ }
1027
+ } else {
1028
+ logger.warn("Attempted to deregister non-existent disconnect listener", { tunnelId, listenerId });
1029
+ }
1030
+ }
1031
+ async getLocalserverTlsInfo(tunnelId) {
1032
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
1033
+ if (!managed) {
1034
+ logger.error(`Tunnel "${tunnelId}" not found when fetching local server TLS info`);
1035
+ return false;
1064
1036
  }
1065
- };
1066
- managed.instance.setTunnelErrorCallback(callback);
1067
- logger.debug("Error callback set up for tunnel", { tunnelId });
1068
- } catch (error) {
1069
- logger.warn("Failed to set up error callback", { tunnelId, error });
1070
- }
1071
- }
1072
- setupDisconnectCallback(tunnelId, managed) {
1073
- try {
1074
- const callback = ({ error, messages }) => {
1075
1037
  try {
1076
- logger.debug("Tunnel disconnected", { tunnelId, error, messages });
1077
- const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1078
- if (managedTunnel) {
1079
- managedTunnel.isStopped = true;
1080
- managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
1038
+ if (managed.isStopped) {
1039
+ return false;
1081
1040
  }
1082
- if (managedTunnel && managedTunnel.autoReconnect) {
1083
- logger.info("Auto-reconnecting tunnel", { tunnelId });
1084
- setTimeout(async () => {
1085
- try {
1086
- await this.restartTunnel(tunnelId);
1087
- logger.info("Tunnel auto-reconnected successfully", { tunnelId });
1088
- } catch (e) {
1089
- logger.error("Failed to auto-reconnect tunnel", { tunnelId, e });
1090
- }
1091
- }, 1e4);
1041
+ const tlsInfo = await managed.instance.getLocalServerTls();
1042
+ if (tlsInfo) {
1043
+ return tlsInfo;
1092
1044
  }
1093
- const listeners = this.tunnelDisconnectListeners.get(tunnelId);
1045
+ return false;
1046
+ } catch (e) {
1047
+ logger.error(`Error fetching TLS info for tunnel "${tunnelId}": ${e instanceof Error ? e.message : e}`);
1048
+ return false;
1049
+ }
1050
+ }
1051
+ /**
1052
+ * Sets up the stats callback for a tunnel during creation.
1053
+ * This callback will update stored stats and notify all registered listeners.
1054
+ */
1055
+ setupStatsCallback(tunnelId, managed) {
1056
+ try {
1057
+ const callback = (usage) => {
1058
+ this.updateStats(tunnelId, usage);
1059
+ };
1060
+ managed.instance.setUsageUpdateCallback(callback);
1061
+ logger.debug("Stats callback set up for tunnel", { tunnelId });
1062
+ } catch (error) {
1063
+ logger.warn("Failed to set up stats callback", { tunnelId, error });
1064
+ }
1065
+ }
1066
+ notifyErrorListeners(tunnelId, errorMsg, isFatal) {
1067
+ try {
1068
+ const listeners = this.tunnelErrorListeners.get(tunnelId);
1094
1069
  if (!listeners) return;
1095
1070
  for (const [id, listener] of listeners) {
1096
1071
  try {
1097
- listener(tunnelId, error, messages);
1072
+ listener(tunnelId, errorMsg, isFatal);
1098
1073
  } catch (err) {
1099
- logger.debug("Error in disconnect-listener callback", { listenerId: id, tunnelId, err });
1074
+ logger.debug("Error in error-listener callback", { listenerId: id, tunnelId, err });
1100
1075
  }
1101
1076
  }
1102
- } catch (e) {
1103
- logger.warn("Error handling tunnel disconnect callback", { tunnelId, e });
1077
+ } catch (err) {
1078
+ logger.debug("Failed to notify error listeners", { tunnelId, err });
1104
1079
  }
1105
- };
1106
- managed.instance.setTunnelDisconnectedCallback(callback);
1107
- logger.debug("Disconnect callback set up for tunnel", { tunnelId });
1108
- } catch (error) {
1109
- logger.warn("Failed to set up disconnect callback", { tunnelId, error });
1110
- }
1111
- }
1112
- setUpTunnelWorkerErrorCallback(tunnelId, managed) {
1113
- try {
1114
- const callback = (error) => {
1080
+ }
1081
+ setupErrorCallback(tunnelId, managed) {
1115
1082
  try {
1116
- logger.debug("Error in Tunnel Worker", { tunnelId, errorMessage: error.message });
1117
- const listeners = this.tunnelWorkerErrorListeners.get(tunnelId);
1118
- if (!listeners) return;
1119
- for (const [id, listener] of listeners) {
1083
+ const callback = ({ errorNo, error, recoverable }) => {
1120
1084
  try {
1121
- listener(tunnelId, error);
1122
- } catch (err) {
1123
- logger.debug("Error in worker-error-listener callback", { listenerId: id, tunnelId, err });
1085
+ const msg = typeof error === "string" ? error : String(error);
1086
+ const isFatal = true;
1087
+ logger.debug("Tunnel reported error", { tunnelId, errorNo, errorMsg: msg, recoverable });
1088
+ this.notifyErrorListeners(tunnelId, msg, isFatal);
1089
+ } catch (e) {
1090
+ logger.warn("Error handling tunnel error callback", { tunnelId, e });
1124
1091
  }
1125
- }
1126
- } catch (e) {
1127
- logger.warn("Error handling tunnel worker error callback", { tunnelId, e });
1092
+ };
1093
+ managed.instance.setTunnelErrorCallback(callback);
1094
+ logger.debug("Error callback set up for tunnel", { tunnelId });
1095
+ } catch (error) {
1096
+ logger.warn("Failed to set up error callback", { tunnelId, error });
1128
1097
  }
1129
- };
1130
- managed.instance.setWorkerErrorCallback(callback);
1131
- logger.debug("Disconnect callback set up for tunnel", { tunnelId });
1132
- } catch (error) {
1133
- logger.warn("Failed to setup tunnel worker error callback");
1134
- }
1135
- }
1136
- /**
1137
- * Updates the stored stats for a tunnel and notifies all registered listeners.
1138
- */
1139
- updateStats(tunnelId, rawUsage) {
1140
- try {
1141
- const normalizedStats = this.normalizeStats(rawUsage);
1142
- const existingStats = this.tunnelStats.get(tunnelId) || [];
1143
- const updatedStats = [...existingStats, normalizedStats];
1144
- this.tunnelStats.set(tunnelId, updatedStats);
1145
- const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
1146
- if (tunnelListeners) {
1147
- for (const [listenerId, listener] of tunnelListeners) {
1148
- try {
1149
- listener(tunnelId, normalizedStats);
1150
- } catch (error) {
1151
- logger.warn("Error in stats listener callback", { listenerId, tunnelId, error });
1152
- }
1098
+ }
1099
+ setupDisconnectCallback(tunnelId, managed) {
1100
+ try {
1101
+ const callback = ({ error, messages }) => {
1102
+ try {
1103
+ logger.debug("Tunnel disconnected", { tunnelId, error, messages });
1104
+ const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1105
+ if (managedTunnel) {
1106
+ managedTunnel.isStopped = true;
1107
+ managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
1108
+ }
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
+ const listeners = this.tunnelDisconnectListeners.get(tunnelId);
1121
+ if (!listeners) return;
1122
+ for (const [id, listener] of listeners) {
1123
+ try {
1124
+ listener(tunnelId, error, messages);
1125
+ } catch (err) {
1126
+ logger.debug("Error in disconnect-listener callback", { listenerId: id, tunnelId, err });
1127
+ }
1128
+ }
1129
+ } catch (e) {
1130
+ logger.warn("Error handling tunnel disconnect callback", { tunnelId, e });
1131
+ }
1132
+ };
1133
+ managed.instance.setTunnelDisconnectedCallback(callback);
1134
+ logger.debug("Disconnect callback set up for tunnel", { tunnelId });
1135
+ } catch (error) {
1136
+ logger.warn("Failed to set up disconnect callback", { tunnelId, error });
1153
1137
  }
1154
1138
  }
1155
- logger.debug("Stats updated and listeners notified", {
1156
- tunnelId,
1157
- listenersCount: tunnelListeners?.size || 0
1158
- });
1159
- } catch (error) {
1160
- logger.warn("Error updating stats", { tunnelId, error });
1161
- }
1162
- }
1163
- /**
1164
- * Normalizes raw usage data from the SDK into a consistent TunnelStats format.
1165
- */
1166
- normalizeStats(rawStats) {
1167
- const elapsed = this.parseNumber(rawStats.elapsedTime ?? 0);
1168
- const liveConns = this.parseNumber(rawStats.numLiveConnections ?? 0);
1169
- const totalConns = this.parseNumber(rawStats.numTotalConnections ?? 0);
1170
- const reqBytes = this.parseNumber(rawStats.numTotalReqBytes ?? 0);
1171
- const resBytes = this.parseNumber(rawStats.numTotalResBytes ?? 0);
1172
- const txBytes = this.parseNumber(rawStats.numTotalTxBytes ?? 0);
1173
- return {
1174
- elapsedTime: elapsed,
1175
- numLiveConnections: liveConns,
1176
- numTotalConnections: totalConns,
1177
- numTotalReqBytes: reqBytes,
1178
- numTotalResBytes: resBytes,
1179
- numTotalTxBytes: txBytes
1180
- };
1181
- }
1182
- parseNumber(value) {
1183
- const parsed = typeof value === "number" ? value : parseInt(String(value), 10);
1184
- return isNaN(parsed) ? 0 : parsed;
1185
- }
1186
- startStaticFileServer(managed) {
1187
- try {
1188
- const __filename3 = (0, import_node_url.fileURLToPath)(importMetaUrl);
1189
- const __dirname2 = import_node_path.default.dirname(__filename3);
1190
- const fileServerWorkerPath = import_node_path.default.join(__dirname2, "workers", "file_serve_worker.cjs");
1191
- const staticServerWorker = new import_node_worker_threads.Worker(fileServerWorkerPath, {
1192
- workerData: {
1193
- dir: managed.serve,
1194
- port: managed.tunnelConfig?.forwarding
1139
+ setUpTunnelWorkerErrorCallback(tunnelId, managed) {
1140
+ try {
1141
+ const callback = (error) => {
1142
+ try {
1143
+ logger.debug("Error in Tunnel Worker", { tunnelId, errorMessage: error.message });
1144
+ const listeners = this.tunnelWorkerErrorListeners.get(tunnelId);
1145
+ if (!listeners) return;
1146
+ for (const [id, listener] of listeners) {
1147
+ try {
1148
+ listener(tunnelId, error);
1149
+ } catch (err) {
1150
+ logger.debug("Error in worker-error-listener callback", { listenerId: id, tunnelId, err });
1151
+ }
1152
+ }
1153
+ } catch (e) {
1154
+ logger.warn("Error handling tunnel worker error callback", { tunnelId, e });
1155
+ }
1156
+ };
1157
+ managed.instance.setWorkerErrorCallback(callback);
1158
+ logger.debug("Disconnect callback set up for tunnel", { tunnelId });
1159
+ } catch (error) {
1160
+ logger.warn("Failed to setup tunnel worker error callback");
1195
1161
  }
1196
- });
1197
- staticServerWorker.on("message", (msg) => {
1198
- switch (msg.type) {
1199
- case "started":
1200
- logger.info("Static file server started", { dir: managed.serve });
1201
- break;
1202
- case "warning":
1203
- if (msg.code === "INVALID_TUNNEL_SERVE_PATH") {
1204
- managed.warnings = managed.warnings ?? [];
1205
- managed.warnings.push({ code: msg.code, message: msg.message });
1162
+ }
1163
+ /**
1164
+ * Updates the stored stats for a tunnel and notifies all registered listeners.
1165
+ */
1166
+ updateStats(tunnelId, rawUsage) {
1167
+ try {
1168
+ const normalizedStats = this.normalizeStats(rawUsage);
1169
+ const existingStats = this.tunnelStats.get(tunnelId) || [];
1170
+ const updatedStats = [...existingStats, normalizedStats];
1171
+ this.tunnelStats.set(tunnelId, updatedStats);
1172
+ const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
1173
+ if (tunnelListeners) {
1174
+ for (const [listenerId, listener] of tunnelListeners) {
1175
+ try {
1176
+ listener(tunnelId, normalizedStats);
1177
+ } catch (error) {
1178
+ logger.warn("Error in stats listener callback", { listenerId, tunnelId, error });
1179
+ }
1206
1180
  }
1207
- printer_default.warn(msg.message);
1208
- break;
1209
- case "error":
1210
- managed.warnings = managed.warnings ?? [];
1211
- managed.warnings.push({
1212
- code: "UNKNOWN_WARNING",
1213
- message: msg.message
1214
- });
1215
- break;
1181
+ }
1182
+ logger.debug("Stats updated and listeners notified", {
1183
+ tunnelId,
1184
+ listenersCount: tunnelListeners?.size || 0
1185
+ });
1186
+ } catch (error) {
1187
+ logger.warn("Error updating stats", { tunnelId, error });
1216
1188
  }
1217
- });
1218
- managed.serveWorker = staticServerWorker;
1219
- } catch (error) {
1220
- logger.error("Error starting static file server", error);
1221
- }
1189
+ }
1190
+ /**
1191
+ * Normalizes raw usage data from the SDK into a consistent TunnelStats format.
1192
+ */
1193
+ normalizeStats(rawStats) {
1194
+ const elapsed = this.parseNumber(rawStats.elapsedTime ?? 0);
1195
+ const liveConns = this.parseNumber(rawStats.numLiveConnections ?? 0);
1196
+ const totalConns = this.parseNumber(rawStats.numTotalConnections ?? 0);
1197
+ const reqBytes = this.parseNumber(rawStats.numTotalReqBytes ?? 0);
1198
+ const resBytes = this.parseNumber(rawStats.numTotalResBytes ?? 0);
1199
+ const txBytes = this.parseNumber(rawStats.numTotalTxBytes ?? 0);
1200
+ return {
1201
+ elapsedTime: elapsed,
1202
+ numLiveConnections: liveConns,
1203
+ numTotalConnections: totalConns,
1204
+ numTotalReqBytes: reqBytes,
1205
+ numTotalResBytes: resBytes,
1206
+ numTotalTxBytes: txBytes
1207
+ };
1208
+ }
1209
+ parseNumber(value) {
1210
+ const parsed = typeof value === "number" ? value : parseInt(String(value), 10);
1211
+ return isNaN(parsed) ? 0 : parsed;
1212
+ }
1213
+ startStaticFileServer(managed) {
1214
+ try {
1215
+ const __filename3 = (0, import_node_url.fileURLToPath)(importMetaUrl);
1216
+ const __dirname2 = import_node_path.default.dirname(__filename3);
1217
+ const fileServerWorkerPath = import_node_path.default.join(__dirname2, "workers", "file_serve_worker.cjs");
1218
+ const staticServerWorker = new import_node_worker_threads.Worker(fileServerWorkerPath, {
1219
+ workerData: {
1220
+ dir: managed.serve,
1221
+ port: managed.tunnelConfig?.forwarding
1222
+ }
1223
+ });
1224
+ staticServerWorker.on("message", (msg) => {
1225
+ switch (msg.type) {
1226
+ case "started":
1227
+ logger.info("Static file server started", { dir: managed.serve });
1228
+ break;
1229
+ case "warning":
1230
+ if (msg.code === "INVALID_TUNNEL_SERVE_PATH") {
1231
+ managed.warnings = managed.warnings ?? [];
1232
+ managed.warnings.push({ code: msg.code, message: msg.message });
1233
+ }
1234
+ printer_default.warn(msg.message);
1235
+ break;
1236
+ case "error":
1237
+ managed.warnings = managed.warnings ?? [];
1238
+ managed.warnings.push({
1239
+ code: "UNKNOWN_WARNING",
1240
+ message: msg.message
1241
+ });
1242
+ break;
1243
+ }
1244
+ });
1245
+ managed.serveWorker = staticServerWorker;
1246
+ } catch (error) {
1247
+ logger.error("Error starting static file server", error);
1248
+ }
1249
+ }
1250
+ };
1222
1251
  }
1223
- };
1252
+ });
1224
1253
 
1225
1254
  // src/cli/options.ts
1226
- var cliOptions = {
1227
- // SSH-like options
1228
- R: { type: "string", multiple: true, description: "Local port. Eg. -R0:localhost:3000 will forward tunnel connections to local port 3000." },
1229
- L: { type: "string", multiple: true, description: "Web Debugger address. Eg. -L4300:localhost:4300 will start web debugger on port 4300." },
1230
- o: { type: "string", multiple: true, description: "Options", hidden: true },
1231
- "server-port": { type: "string", short: "p", description: "Pinggy server port. Default: 443" },
1232
- v4: { type: "boolean", short: "4", description: "IPv4 only", hidden: true },
1233
- v6: { type: "boolean", short: "6", description: "IPv6 only", hidden: true },
1234
- // These options appear in the ssh command, but we ignore it in CLI
1235
- t: { type: "boolean", description: "hidden", hidden: true },
1236
- T: { type: "boolean", description: "hidden", hidden: true },
1237
- n: { type: "boolean", description: "hidden", hidden: true },
1238
- N: { type: "boolean", description: "hidden", hidden: true },
1239
- // Better options
1240
- type: { type: "string", description: "Type of the connection. Eg. --type tcp" },
1241
- localport: { type: "string", short: "l", description: "Takes input as [protocol:][host:]port. Eg. --localport https://localhost:8000 OR -l 3000" },
1242
- debugger: { type: "string", short: "d", description: "Port for web debugger. Eg. --debugger 4300 OR -d 4300" },
1243
- token: { type: "string", description: "Token for authentication. Eg. --token TOKEN_VALUE" },
1244
- // Logging options (CLI overrides env)
1245
- loglevel: { type: "string", description: "Logging level: ERROR, INFO, DEBUG. Overrides PINGGY_LOG_LEVEL environment variable" },
1246
- logfile: { type: "string", description: "Path to log file. Overrides PINGGY_LOG_FILE environment variable" },
1247
- v: { type: "boolean", description: "Print logs to stdout for Cli. Overrides PINGGY_LOG_STDOUT environment variable" },
1248
- vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
1249
- vvv: { type: "boolean", description: "Enable all logs from Cli, SDK and internal components." },
1250
- autoreconnect: { type: "boolean", short: "a", description: "Automatically reconnect tunnel on failure." },
1251
- // Save and load config
1252
- saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
1253
- conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
1254
- // File server
1255
- serve: { type: "string", description: "Start a webserver to serve files from the specified path. Eg --serve /path/to/files" },
1256
- // Remote Control
1257
- "remote-management": { type: "string", description: "Enable remote management of tunnels with token. Eg. --remote-management API_KEY" },
1258
- manage: { type: "string", description: "Provide a server address to manage tunnels. Eg --manage dashboard.pinggy.io" },
1259
- notui: { type: "boolean", description: "Disable TUI in remote management mode" },
1260
- // Misc
1261
- version: { type: "boolean", description: "Print version" },
1262
- // Help
1263
- help: { type: "boolean", short: "h", description: "Show this help message" }
1264
- };
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: "boolean", short: "a", description: "Automatically reconnect tunnel on failure." },
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
+ });
1265
1301
 
1266
1302
  // src/cli/help.ts
1267
1303
  function printHelpMessage() {
@@ -1299,29 +1335,42 @@ function printHelpMessage() {
1299
1335
  console.log(" pinggy -R0:localhost:8080 -L4300:localhost:4300 # HTTP tunnel with debugger");
1300
1336
  console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region\n");
1301
1337
  }
1338
+ var init_help = __esm({
1339
+ "src/cli/help.ts"() {
1340
+ "use strict";
1341
+ init_cjs_shims();
1342
+ init_options();
1343
+ }
1344
+ });
1302
1345
 
1303
1346
  // src/cli/defaults.ts
1304
- var defaultOptions = {
1305
- token: void 0,
1306
- // No default token
1307
- serverAddress: "a.pinggy.io",
1308
- forwarding: "localhost:8000",
1309
- webDebugger: "",
1310
- ipWhitelist: [],
1311
- basicAuth: [],
1312
- bearerTokenAuth: [],
1313
- headerModification: [],
1314
- force: false,
1315
- xForwardedFor: false,
1316
- httpsOnly: false,
1317
- originalRequestUrl: false,
1318
- allowPreflight: false,
1319
- reverseProxy: false,
1320
- autoReconnect: false
1321
- };
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: false
1369
+ };
1370
+ }
1371
+ });
1322
1372
 
1323
1373
  // src/cli/extendedOptions.ts
1324
- var import_net = require("net");
1325
1374
  function parseExtendedOptions(options, config) {
1326
1375
  if (!options) return;
1327
1376
  for (const opt of options) {
@@ -1445,12 +1494,18 @@ function isValidIpV6Cidr(input) {
1445
1494
  }
1446
1495
  return false;
1447
1496
  }
1497
+ var import_net;
1498
+ var init_extendedOptions = __esm({
1499
+ "src/cli/extendedOptions.ts"() {
1500
+ "use strict";
1501
+ init_cjs_shims();
1502
+ import_net = require("net");
1503
+ init_logger();
1504
+ init_printer();
1505
+ }
1506
+ });
1448
1507
 
1449
1508
  // src/cli/buildConfig.ts
1450
- var import_pinggy3 = require("@pinggy/pinggy");
1451
- var import_fs2 = __toESM(require("fs"), 1);
1452
- var import_path2 = __toESM(require("path"), 1);
1453
- var domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
1454
1509
  function parseUserAndDomain(str) {
1455
1510
  let token;
1456
1511
  let type;
@@ -1580,7 +1635,6 @@ function ipv6SafeSplitColon(s) {
1580
1635
  result.push(buf);
1581
1636
  return result;
1582
1637
  }
1583
- var VALID_PROTOCOLS = ["http", "tcp", "udp", "tls"];
1584
1638
  function parseDefaultForwarding(forwarding) {
1585
1639
  const parts = ipv6SafeSplitColon(forwarding);
1586
1640
  if (parts.length === 3) {
@@ -1600,77 +1654,63 @@ function parseDefaultForwarding(forwarding) {
1600
1654
  }
1601
1655
  function parseAdditionalForwarding(forwarding) {
1602
1656
  const toPort = (v) => {
1657
+ if (!v) return null;
1603
1658
  const n = parseInt(v, 10);
1604
1659
  return Number.isNaN(n) ? null : n;
1605
1660
  };
1606
- const validateDomain = (d) => d && domainRegex.test(d) ? d : null;
1661
+ const parsed = ipv6SafeSplitColon(forwarding);
1662
+ if (parsed.length !== 4) {
1663
+ return new Error(
1664
+ "forwarding must be in format: [schema//]hostname[/port][@forwardingId]:<placeholder>:<forwardingAddress>:<forwardingPort>"
1665
+ );
1666
+ }
1667
+ const firstPart = parsed[0];
1668
+ const [hostPart] = firstPart.split("@");
1607
1669
  let protocol = "http";
1608
1670
  let remoteDomainRaw;
1609
- const protocolsRequiringDomainPort = ["tcp", "udp"];
1610
- const lowForwarding = forwarding.toLowerCase();
1611
- let remaining = forwarding;
1612
- for (const p of VALID_PROTOCOLS) {
1613
- if (lowForwarding.startsWith(p + "//")) {
1614
- protocol = p;
1615
- remaining = forwarding.slice(p.length + 2);
1616
- break;
1617
- }
1618
- }
1619
- if (protocol === "http" && remaining === forwarding) {
1620
- const parts2 = ipv6SafeSplitColon(remaining);
1621
- if (parts2.length !== 4) {
1622
- return new Error(
1623
- "forwarding must be in format: domain:remotePort:localDomain:localPort"
1624
- );
1625
- }
1626
- const remoteDomain = validateDomain(removeIPv6Brackets(parts2[0]));
1627
- const localDomain2 = removeIPv6Brackets(parts2[2] || "localhost");
1628
- const localPort2 = toPort(parts2[3]);
1629
- if (!remoteDomain) {
1630
- return new Error("forwarding address incorrect: invalid domain");
1671
+ let remotePort = 0;
1672
+ if (hostPart.includes("//")) {
1673
+ const [schema, rest] = hostPart.split("//");
1674
+ if (!schema || !VALID_PROTOCOLS.includes(schema)) {
1675
+ return new Error(`invalid protocol: ${schema}`);
1676
+ }
1677
+ protocol = schema;
1678
+ const domainAndPort = rest.split("/");
1679
+ if (domainAndPort.length > 2) {
1680
+ return new Error("invalid forwarding address format");
1681
+ }
1682
+ remoteDomainRaw = domainAndPort[0];
1683
+ if (!remoteDomainRaw || !domainRegex.test(remoteDomainRaw)) {
1684
+ return new Error("invalid remote domain");
1685
+ }
1686
+ const parsedRemotePort = toPort(domainAndPort[1]);
1687
+ if (protocol === "http") {
1688
+ remotePort = 0;
1689
+ } else {
1690
+ if (parsedRemotePort === null || !isValidPort(parsedRemotePort)) {
1691
+ return new Error(
1692
+ `${protocol} forwarding requires port in format ${protocol}//domain/remotePort`
1693
+ );
1694
+ }
1695
+ remotePort = parsedRemotePort;
1631
1696
  }
1632
- if (localPort2 === null || !isValidPort(localPort2)) {
1633
- return new Error("forwarding address incorrect: invalid local port");
1697
+ } else {
1698
+ remoteDomainRaw = hostPart;
1699
+ if (!domainRegex.test(remoteDomainRaw)) {
1700
+ return new Error("invalid remote domain");
1634
1701
  }
1635
- return {
1636
- protocol: "http",
1637
- remoteDomain,
1638
- remotePort: 0,
1639
- localDomain: localDomain2,
1640
- localPort: localPort2
1641
- };
1642
- }
1643
- const domainPortMatch = remaining.match(/^([^:]+)\/(\d+):(.+)$/);
1644
- if (!domainPortMatch) {
1645
- return new Error(`forwarding must be in format: ${protocol}//domain/remotePort:localDomain:localPort`);
1646
- }
1647
- remoteDomainRaw = removeIPv6Brackets(domainPortMatch[1]);
1648
- const remotePortNum = toPort(domainPortMatch[2]);
1649
- const restParts = domainPortMatch[3];
1650
- if (!remoteDomainRaw || !domainRegex.test(remoteDomainRaw)) {
1651
- return new Error("forwarding address incorrect: invalid domain or remote port");
1652
- }
1653
- if (!remoteDomainRaw || remotePortNum === null || !isValidPort(remotePortNum)) {
1654
- return new Error(`${protocol} forwarding: invalid domain or port in format ${protocol}//domain/remotePort`);
1655
- }
1656
- const parts = ipv6SafeSplitColon(restParts);
1657
- if (parts.length !== 3) {
1658
- return new Error(`forwarding format incorrect: expected ${protocol}//domain/remotePort:placeholder:localDomain:localPort`);
1702
+ protocol = "http";
1703
+ remotePort = 0;
1659
1704
  }
1660
- const localDomain = removeIPv6Brackets(parts[1] || "localhost");
1661
- const localPort = toPort(parts[2]);
1705
+ const localDomain = removeIPv6Brackets(parsed[2] || "localhost");
1706
+ const localPort = toPort(parsed[3]);
1662
1707
  if (localPort === null || !isValidPort(localPort)) {
1663
1708
  return new Error("forwarding address incorrect: invalid local port");
1664
1709
  }
1665
- if (protocolsRequiringDomainPort.includes(protocol)) {
1666
- if (!remoteDomainRaw || !remotePortNum) {
1667
- return new Error(`${protocol} forwarding requires domain and port in format: ${protocol}//domain/remotePort:localDomain:localPort`);
1668
- }
1669
- }
1670
1710
  return {
1671
1711
  protocol,
1672
1712
  remoteDomain: remoteDomainRaw,
1673
- remotePort: remotePortNum,
1713
+ remotePort,
1674
1714
  localDomain,
1675
1715
  localPort
1676
1716
  };
@@ -1683,19 +1723,21 @@ function parseReverseTunnelAddr(finalConfig, values) {
1683
1723
  if (!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) {
1684
1724
  return null;
1685
1725
  }
1686
- const forwarding = parseDefaultForwarding(reverseTunnel[0]);
1687
- if (forwarding instanceof Error) {
1688
- return forwarding;
1689
- }
1690
- finalConfig.forwarding = `${forwarding.localDomain}:${forwarding.localPort}`;
1691
- if (reverseTunnel.length > 1) {
1692
- finalConfig.additionalForwarding = [];
1693
- for (const t of reverseTunnel.slice(1)) {
1694
- const f = parseAdditionalForwarding(t);
1695
- if (f instanceof Error) {
1696
- return f;
1697
- }
1698
- finalConfig.additionalForwarding.push(f);
1726
+ for (const forwarding of reverseTunnel) {
1727
+ const slicedForwarding = ipv6SafeSplitColon(forwarding);
1728
+ if (slicedForwarding.length === 3) {
1729
+ const parsed = parseDefaultForwarding(forwarding);
1730
+ if (parsed instanceof Error) return parsed;
1731
+ finalConfig.forwarding = `${parsed.localDomain}:${parsed.localPort}`;
1732
+ } else if (slicedForwarding.length === 4) {
1733
+ finalConfig.additionalForwarding ?? (finalConfig.additionalForwarding = []);
1734
+ const parsed = parseAdditionalForwarding(forwarding);
1735
+ if (parsed instanceof Error) return parsed;
1736
+ finalConfig.additionalForwarding.push(parsed);
1737
+ } else {
1738
+ return new Error(
1739
+ "Incorrect command line arguments: reverse tunnel address incorrect. Please use '-h' option for help."
1740
+ );
1699
1741
  }
1700
1742
  }
1701
1743
  return null;
@@ -1737,10 +1779,10 @@ function parseArgs(finalConfig, remainingPositionals) {
1737
1779
  }
1738
1780
  function storeJson(config, saveconf) {
1739
1781
  if (saveconf) {
1740
- const path4 = saveconf;
1782
+ const path5 = saveconf;
1741
1783
  try {
1742
- import_fs2.default.writeFileSync(path4, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
1743
- logger.info(`Configuration saved to ${path4}`);
1784
+ import_fs3.default.writeFileSync(path5, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
1785
+ logger.info(`Configuration saved to ${path5}`);
1744
1786
  } catch (err) {
1745
1787
  const msg = err instanceof Error ? err.message : String(err);
1746
1788
  logger.error("Error loading configuration:", msg);
@@ -1750,9 +1792,9 @@ function storeJson(config, saveconf) {
1750
1792
  function loadJsonConfig(config) {
1751
1793
  const configpath = config["conf"];
1752
1794
  if (typeof configpath === "string" && configpath.trim().length > 0) {
1753
- const filepath = import_path2.default.resolve(configpath);
1795
+ const filepath = import_path3.default.resolve(configpath);
1754
1796
  try {
1755
- const data = import_fs2.default.readFileSync(filepath, { encoding: "utf-8" });
1797
+ const data = import_fs3.default.readFileSync(filepath, { encoding: "utf-8" });
1756
1798
  const json = JSON.parse(data);
1757
1799
  return json;
1758
1800
  } catch (err) {
@@ -1820,24 +1862,24 @@ async function buildFinalConfig(values, positionals) {
1820
1862
  storeJson(finalConfig, saveconf);
1821
1863
  return finalConfig;
1822
1864
  }
1823
-
1824
- // src/remote_management/remoteManagement.ts
1825
- var import_ws = __toESM(require("ws"), 1);
1865
+ var import_pinggy3, import_fs3, import_path3, domainRegex, VALID_PROTOCOLS;
1866
+ var init_buildConfig = __esm({
1867
+ "src/cli/buildConfig.ts"() {
1868
+ "use strict";
1869
+ init_cjs_shims();
1870
+ init_defaults();
1871
+ init_extendedOptions();
1872
+ init_logger();
1873
+ init_util();
1874
+ import_pinggy3 = require("@pinggy/pinggy");
1875
+ import_fs3 = __toESM(require("fs"), 1);
1876
+ import_path3 = __toESM(require("path"), 1);
1877
+ domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
1878
+ VALID_PROTOCOLS = ["http", "tcp", "udp", "tls"];
1879
+ }
1880
+ });
1826
1881
 
1827
1882
  // src/types.ts
1828
- var ErrorCode = {
1829
- InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
1830
- InvalidRequestBodyError: "COULD_NOT_READ_BODY",
1831
- InternalServerError: "INTERNAL_SERVER_ERROR",
1832
- InvalidBodyFormatError: "INVALID_DATA_FORMAT",
1833
- ErrorStartingTunnel: "ERROR_STARTING_TUNNEL",
1834
- TunnelNotFound: "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND",
1835
- TunnelAlreadyRunningError: "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING",
1836
- WebsocketUpgradeFailError: "WEBSOCKET_UPGRADE_FAILED",
1837
- RemoteManagementAlreadyRunning: "REMOTE_MANAGEMENT_ALREADY_RUNNING",
1838
- RemoteManagementNotRunning: "REMOTE_MANAGEMENT_NOT_RUNNING",
1839
- RemoteManagementDeserializationFailed: "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED"
1840
- };
1841
1883
  function isErrorResponse(obj) {
1842
1884
  return typeof obj === "object" && obj !== null && "code" in obj && "message" in obj && typeof obj.message === "string" && Object.values(ErrorCode).includes(obj.code);
1843
1885
  }
@@ -1900,93 +1942,36 @@ function newStats() {
1900
1942
  elapsedTime: 0
1901
1943
  };
1902
1944
  }
1903
- var RemoteManagementStatus = {
1904
- Connecting: "CONNECTING",
1905
- Disconnecting: "DISCONNECTING",
1906
- Reconnecting: "RECONNECTING",
1907
- Running: "RUNNING",
1908
- NotRunning: "NOT_RUNNING",
1909
- Error: "ERROR"
1910
- };
1911
-
1912
- // src/remote_management/remote_schema.ts
1913
- var import_pinggy4 = require("@pinggy/pinggy");
1914
- var import_zod = require("zod");
1915
- var HeaderModificationSchema = import_zod.z.object({
1916
- key: import_zod.z.string(),
1917
- value: import_zod.z.array(import_zod.z.string()).optional(),
1918
- type: import_zod.z.enum(["add", "remove", "update"])
1919
- });
1920
- var AdditionalForwardingSchema = import_zod.z.object({
1921
- remoteDomain: import_zod.z.string().optional(),
1922
- remotePort: import_zod.z.number().optional(),
1923
- localDomain: import_zod.z.string(),
1924
- localPort: import_zod.z.number()
1925
- });
1926
- var TunnelConfigSchema = import_zod.z.object({
1927
- allowPreflight: import_zod.z.boolean().optional(),
1928
- // primary key
1929
- allowpreflight: import_zod.z.boolean().optional(),
1930
- // legacy key
1931
- autoreconnect: import_zod.z.boolean(),
1932
- basicauth: import_zod.z.array(import_zod.z.object({ username: import_zod.z.string(), password: import_zod.z.string() })).nullable(),
1933
- bearerauth: import_zod.z.string().nullable(),
1934
- configid: import_zod.z.string(),
1935
- configname: import_zod.z.string(),
1936
- greetmsg: import_zod.z.string().optional(),
1937
- force: import_zod.z.boolean(),
1938
- forwardedhost: import_zod.z.string(),
1939
- fullRequestUrl: import_zod.z.boolean(),
1940
- headermodification: import_zod.z.array(HeaderModificationSchema),
1941
- httpsOnly: import_zod.z.boolean(),
1942
- internalwebdebuggerport: import_zod.z.number(),
1943
- ipwhitelist: import_zod.z.array(import_zod.z.string()).nullable(),
1944
- localport: import_zod.z.number(),
1945
- localsservertls: import_zod.z.union([import_zod.z.boolean(), import_zod.z.string()]),
1946
- localservertlssni: import_zod.z.string().nullable(),
1947
- regioncode: import_zod.z.string(),
1948
- noReverseProxy: import_zod.z.boolean(),
1949
- serveraddress: import_zod.z.string(),
1950
- serverport: import_zod.z.number(),
1951
- statusCheckInterval: import_zod.z.number(),
1952
- token: import_zod.z.string(),
1953
- tunnelTimeout: import_zod.z.number(),
1954
- type: import_zod.z.enum([
1955
- import_pinggy4.TunnelType.Http,
1956
- import_pinggy4.TunnelType.Tcp,
1957
- import_pinggy4.TunnelType.Udp,
1958
- import_pinggy4.TunnelType.Tls,
1959
- import_pinggy4.TunnelType.TlsTcp
1960
- ]),
1961
- webdebuggerport: import_zod.z.number(),
1962
- xff: import_zod.z.string(),
1963
- additionalForwarding: import_zod.z.array(AdditionalForwardingSchema).optional(),
1964
- serve: import_zod.z.string().optional()
1965
- }).superRefine((data, ctx) => {
1966
- if (data.allowPreflight === void 0 && data.allowpreflight === void 0) {
1967
- ctx.addIssue({
1968
- code: "custom",
1969
- message: "Either allowPreflight or allowpreflight is required",
1970
- path: ["allowPreflight"]
1971
- });
1945
+ var ErrorCode, RemoteManagementStatus;
1946
+ var init_types = __esm({
1947
+ "src/types.ts"() {
1948
+ "use strict";
1949
+ init_cjs_shims();
1950
+ ErrorCode = {
1951
+ InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
1952
+ InvalidRequestBodyError: "COULD_NOT_READ_BODY",
1953
+ InternalServerError: "INTERNAL_SERVER_ERROR",
1954
+ InvalidBodyFormatError: "INVALID_DATA_FORMAT",
1955
+ ErrorStartingTunnel: "ERROR_STARTING_TUNNEL",
1956
+ TunnelNotFound: "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND",
1957
+ TunnelAlreadyRunningError: "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING",
1958
+ WebsocketUpgradeFailError: "WEBSOCKET_UPGRADE_FAILED",
1959
+ RemoteManagementAlreadyRunning: "REMOTE_MANAGEMENT_ALREADY_RUNNING",
1960
+ RemoteManagementNotRunning: "REMOTE_MANAGEMENT_NOT_RUNNING",
1961
+ RemoteManagementDeserializationFailed: "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED"
1962
+ };
1963
+ RemoteManagementStatus = {
1964
+ Connecting: "CONNECTING",
1965
+ Disconnecting: "DISCONNECTING",
1966
+ Reconnecting: "RECONNECTING",
1967
+ Running: "RUNNING",
1968
+ NotRunning: "NOT_RUNNING",
1969
+ Error: "ERROR"
1970
+ };
1972
1971
  }
1973
- }).transform((data) => ({
1974
- ...data,
1975
- allowPreflight: data.allowPreflight ?? data.allowpreflight,
1976
- allowpreflight: data.allowPreflight ?? data.allowpreflight
1977
- }));
1978
- var StartSchema = import_zod.z.object({
1979
- tunnelID: import_zod.z.string().nullable().optional(),
1980
- tunnelConfig: TunnelConfigSchema
1981
- });
1982
- var StopSchema = import_zod.z.object({
1983
- tunnelID: import_zod.z.string().min(1)
1984
- });
1985
- var GetSchema = StopSchema;
1986
- var RestartSchema = StopSchema;
1987
- var UpdateConfigSchema = import_zod.z.object({
1988
- tunnelConfig: TunnelConfigSchema
1989
1972
  });
1973
+
1974
+ // src/remote_management/remote_schema.ts
1990
1975
  function tunnelConfigToPinggyOptions(config) {
1991
1976
  return {
1992
1977
  token: config.token || "",
@@ -2049,313 +2034,279 @@ function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls,
2049
2034
  serve: serve || ""
2050
2035
  };
2051
2036
  }
2052
-
2053
- // src/remote_management/handler.ts
2054
- var import_pinggy5 = require("@pinggy/pinggy");
2055
- var TunnelOperations = class {
2056
- constructor() {
2057
- this.tunnelManager = TunnelManager.getInstance();
2058
- }
2059
- buildStatus(tunnelId, state, errorCode) {
2060
- const status = newStatus(state, errorCode, "");
2061
- try {
2062
- const managed = this.tunnelManager.getManagedTunnel("", tunnelId);
2063
- if (managed) {
2064
- status.createdtimestamp = managed.createdAt || "";
2065
- status.starttimestamp = managed.startedAt || "";
2066
- status.endtimestamp = managed.stoppedAt || "";
2067
- }
2068
- } catch (e) {
2069
- }
2070
- return status;
2071
- }
2072
- // --- Helper to construct TunnelResponse ---
2073
- async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, additionalForwarding, serve) {
2074
- const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
2075
- this.tunnelManager.getTunnelStatus(tunnelid),
2076
- this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
2077
- this.tunnelManager.getLocalserverTlsInfo(tunnelid),
2078
- this.tunnelManager.getTunnelGreetMessage(tunnelid),
2079
- this.tunnelManager.getTunnelUrls(tunnelid)
2080
- ]);
2081
- return {
2082
- tunnelid,
2083
- remoteurls,
2084
- tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg, additionalForwarding),
2085
- status: this.buildStatus(tunnelid, status, "" /* NoError */),
2086
- stats
2087
- };
2088
- }
2089
- error(code, err, fallback) {
2090
- return newErrorResponse({
2091
- code,
2092
- message: err instanceof Error ? err.message : fallback
2037
+ var import_pinggy4, import_zod, HeaderModificationSchema, AdditionalForwardingSchema, TunnelConfigSchema, StartSchema, StopSchema, GetSchema, RestartSchema, UpdateConfigSchema;
2038
+ var init_remote_schema = __esm({
2039
+ "src/remote_management/remote_schema.ts"() {
2040
+ "use strict";
2041
+ init_cjs_shims();
2042
+ import_pinggy4 = require("@pinggy/pinggy");
2043
+ import_zod = require("zod");
2044
+ HeaderModificationSchema = import_zod.z.object({
2045
+ key: import_zod.z.string(),
2046
+ value: import_zod.z.array(import_zod.z.string()).optional(),
2047
+ type: import_zod.z.enum(["add", "remove", "update"])
2093
2048
  });
2094
- }
2095
- // --- Operations ---
2096
- async handleStart(config) {
2097
- try {
2098
- const opts = tunnelConfigToPinggyOptions(config);
2099
- const additionalForwardingParsed = config.additionalForwarding || [];
2100
- const { tunnelid, instance, tunnelName, additionalForwarding, serve } = await this.tunnelManager.createTunnel({
2101
- ...opts,
2102
- tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [import_pinggy5.TunnelType.Http],
2103
- // Temporary fix in future we will not use this field.
2104
- configid: config.configid,
2105
- tunnelName: config.configname,
2106
- additionalForwarding: additionalForwardingParsed,
2107
- serve: config.serve
2108
- });
2109
- this.tunnelManager.startTunnel(tunnelid);
2110
- const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
2111
- const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, additionalForwarding, serve);
2112
- return resp;
2113
- } catch (err) {
2114
- return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
2115
- }
2116
- }
2117
- async handleUpdateConfig(config) {
2118
- try {
2119
- const opts = tunnelConfigToPinggyOptions(config);
2120
- const tunnel = await this.tunnelManager.updateConfig({
2121
- ...opts,
2122
- tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [import_pinggy5.TunnelType.Http],
2123
- // // Temporary fix in future we will not use this field.
2124
- configid: config.configid,
2125
- tunnelName: config.configname,
2126
- additionalForwarding: config.additionalForwarding || [],
2127
- serve: config.serve
2128
- });
2129
- if (!tunnel.instance || !tunnel.tunnelConfig)
2130
- throw new Error("Invalid tunnel state after configuration update");
2131
- return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.additionalForwarding, tunnel.serve);
2132
- } catch (err) {
2133
- return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
2134
- }
2135
- }
2136
- async handleList() {
2137
- try {
2138
- const tunnels = await this.tunnelManager.getAllTunnels();
2139
- if (tunnels.length === 0) {
2140
- return [];
2141
- }
2142
- return Promise.all(
2143
- tunnels.map(async (t) => {
2144
- const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
2145
- const [status, tlsInfo, greetMsg] = await Promise.all([
2146
- this.tunnelManager.getTunnelStatus(t.tunnelid),
2147
- this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
2148
- this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
2149
- ]);
2150
- const pinggyOptions = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
2151
- const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configid, t.tunnelName, tlsInfo, greetMsg, t.additionalForwarding, t.serve);
2152
- return {
2153
- tunnelid: t.tunnelid,
2154
- remoteurls: t.remoteurls,
2155
- status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
2156
- stats: rawStats,
2157
- tunnelconfig: tunnelConfig
2158
- };
2159
- })
2160
- );
2161
- } catch (err) {
2162
- return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
2163
- }
2164
- }
2165
- async handleStop(tunnelid) {
2166
- try {
2167
- const { configid } = this.tunnelManager.stopTunnel(tunnelid);
2168
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2169
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2170
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2171
- } catch (err) {
2172
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
2173
- }
2174
- }
2175
- async handleGet(tunnelid) {
2176
- try {
2177
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2178
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2179
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2180
- } catch (err) {
2181
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
2182
- }
2183
- }
2184
- async handleRestart(tunnelid) {
2185
- try {
2186
- await this.tunnelManager.restartTunnel(tunnelid);
2187
- const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2188
- if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2189
- return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2190
- } catch (err) {
2191
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
2192
- }
2193
- }
2194
- handleRegisterStatsListener(tunnelid, listener) {
2195
- this.tunnelManager.registerStatsListener(tunnelid, listener);
2196
- }
2197
- handleUnregisterStatsListener(tunnelid, listnerId) {
2198
- this.tunnelManager.deregisterStatsListener(tunnelid, listnerId);
2199
- }
2200
- handleGetTunnelStats(tunnelid) {
2201
- try {
2202
- const stats = this.tunnelManager.getTunnelStats(tunnelid);
2203
- if (!stats) {
2204
- return [newStats()];
2049
+ AdditionalForwardingSchema = import_zod.z.object({
2050
+ remoteDomain: import_zod.z.string().optional(),
2051
+ remotePort: import_zod.z.number().optional(),
2052
+ localDomain: import_zod.z.string(),
2053
+ localPort: import_zod.z.number()
2054
+ });
2055
+ TunnelConfigSchema = import_zod.z.object({
2056
+ allowPreflight: import_zod.z.boolean().optional(),
2057
+ // primary key
2058
+ allowpreflight: import_zod.z.boolean().optional(),
2059
+ // legacy key
2060
+ autoreconnect: import_zod.z.boolean(),
2061
+ basicauth: import_zod.z.array(import_zod.z.object({ username: import_zod.z.string(), password: import_zod.z.string() })).nullable(),
2062
+ bearerauth: import_zod.z.string().nullable(),
2063
+ configid: import_zod.z.string(),
2064
+ configname: import_zod.z.string(),
2065
+ greetmsg: import_zod.z.string().optional(),
2066
+ force: import_zod.z.boolean(),
2067
+ forwardedhost: import_zod.z.string(),
2068
+ fullRequestUrl: import_zod.z.boolean(),
2069
+ headermodification: import_zod.z.array(HeaderModificationSchema),
2070
+ httpsOnly: import_zod.z.boolean(),
2071
+ internalwebdebuggerport: import_zod.z.number(),
2072
+ ipwhitelist: import_zod.z.array(import_zod.z.string()).nullable(),
2073
+ localport: import_zod.z.number(),
2074
+ localsservertls: import_zod.z.union([import_zod.z.boolean(), import_zod.z.string()]),
2075
+ localservertlssni: import_zod.z.string().nullable(),
2076
+ regioncode: import_zod.z.string(),
2077
+ noReverseProxy: import_zod.z.boolean(),
2078
+ serveraddress: import_zod.z.string(),
2079
+ serverport: import_zod.z.number(),
2080
+ statusCheckInterval: import_zod.z.number(),
2081
+ token: import_zod.z.string(),
2082
+ tunnelTimeout: import_zod.z.number(),
2083
+ type: import_zod.z.enum([
2084
+ import_pinggy4.TunnelType.Http,
2085
+ import_pinggy4.TunnelType.Tcp,
2086
+ import_pinggy4.TunnelType.Udp,
2087
+ import_pinggy4.TunnelType.Tls,
2088
+ import_pinggy4.TunnelType.TlsTcp
2089
+ ]),
2090
+ webdebuggerport: import_zod.z.number(),
2091
+ xff: import_zod.z.string(),
2092
+ additionalForwarding: import_zod.z.array(AdditionalForwardingSchema).optional(),
2093
+ serve: import_zod.z.string().optional()
2094
+ }).superRefine((data, ctx) => {
2095
+ if (data.allowPreflight === void 0 && data.allowpreflight === void 0) {
2096
+ ctx.addIssue({
2097
+ code: "custom",
2098
+ message: "Either allowPreflight or allowpreflight is required",
2099
+ path: ["allowPreflight"]
2100
+ });
2205
2101
  }
2206
- return stats;
2207
- } catch (err) {
2208
- return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats");
2209
- }
2210
- }
2211
- handleRegisterDisconnectListener(tunnelid, listener) {
2212
- this.tunnelManager.registerDisconnectListener(tunnelid, listener);
2213
- }
2214
- handleRemoveStoppedTunnelByConfigId(configId) {
2215
- try {
2216
- return this.tunnelManager.removeStoppedTunnelByConfigId(configId);
2217
- } catch (err) {
2218
- return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by configId");
2219
- }
2220
- }
2221
- handleRemoveStoppedTunnelByTunnelId(tunnelId) {
2222
- try {
2223
- return this.tunnelManager.removeStoppedTunnelByTunnelId(tunnelId);
2224
- } catch (err) {
2225
- return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by tunnelId");
2226
- }
2102
+ }).transform((data) => ({
2103
+ ...data,
2104
+ allowPreflight: data.allowPreflight ?? data.allowpreflight,
2105
+ allowpreflight: data.allowPreflight ?? data.allowpreflight
2106
+ }));
2107
+ StartSchema = import_zod.z.object({
2108
+ tunnelID: import_zod.z.string().nullable().optional(),
2109
+ tunnelConfig: TunnelConfigSchema
2110
+ });
2111
+ StopSchema = import_zod.z.object({
2112
+ tunnelID: import_zod.z.string().min(1)
2113
+ });
2114
+ GetSchema = StopSchema;
2115
+ RestartSchema = StopSchema;
2116
+ UpdateConfigSchema = import_zod.z.object({
2117
+ tunnelConfig: TunnelConfigSchema
2118
+ });
2227
2119
  }
2228
- };
2120
+ });
2229
2121
 
2230
- // src/remote_management/websocket_handlers.ts
2231
- var import_zod2 = __toESM(require("zod"), 1);
2232
- var WebSocketCommandHandler = class {
2233
- constructor() {
2234
- this.tunnelHandler = new TunnelOperations();
2235
- }
2236
- safeParse(text) {
2237
- if (!text) return void 0;
2238
- try {
2239
- return JSON.parse(text);
2240
- } catch (e) {
2241
- logger.warn("Invalid JSON payload", { error: String(e), text });
2242
- return void 0;
2243
- }
2244
- }
2245
- sendResponse(ws, resp) {
2246
- const payload = {
2247
- ...resp,
2248
- response: Buffer.from(resp.response || []).toString("base64")
2249
- };
2250
- ws.send(JSON.stringify(payload));
2251
- }
2252
- sendError(ws, req, message, code = ErrorCode.InternalServerError) {
2253
- const resp = NewErrorResponseObject({ code, message });
2254
- resp.command = req.command || "";
2255
- resp.requestid = req.requestid || "";
2256
- this.sendResponse(ws, resp);
2257
- }
2258
- async handleStartReq(req, raw) {
2259
- const dc = StartSchema.parse(raw);
2260
- printer_default.info("Starting tunnel with config name: " + dc.tunnelConfig.configname);
2261
- const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
2262
- return this.wrapResponse(result, req);
2263
- }
2264
- async handleStopReq(req, raw) {
2265
- const dc = StopSchema.parse(raw);
2266
- printer_default.info("Stopping tunnel with ID: " + dc.tunnelID);
2267
- const result = await this.tunnelHandler.handleStop(dc.tunnelID);
2268
- return this.wrapResponse(result, req);
2269
- }
2270
- async handleGetReq(req, raw) {
2271
- const dc = GetSchema.parse(raw);
2272
- const result = await this.tunnelHandler.handleGet(dc.tunnelID);
2273
- return this.wrapResponse(result, req);
2274
- }
2275
- async handleRestartReq(req, raw) {
2276
- const dc = RestartSchema.parse(raw);
2277
- const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
2278
- return this.wrapResponse(result, req);
2279
- }
2280
- async handleUpdateConfigReq(req, raw) {
2281
- const dc = UpdateConfigSchema.parse(raw);
2282
- const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
2283
- return this.wrapResponse(result, req);
2284
- }
2285
- async handleListReq(req) {
2286
- const result = await this.tunnelHandler.handleList();
2287
- return this.wrapResponse(result, req);
2288
- }
2289
- wrapResponse(result, req) {
2290
- if (isErrorResponse(result)) {
2291
- const errResp = NewErrorResponseObject(result);
2292
- errResp.command = req.command;
2293
- errResp.requestid = req.requestid;
2294
- return errResp;
2295
- }
2296
- const finalResult = JSON.parse(JSON.stringify(result));
2297
- if (Array.isArray(finalResult)) {
2298
- finalResult.forEach((item) => {
2299
- if (item?.tunnelconfig) {
2300
- delete item.tunnelconfig.allowPreflight;
2122
+ // src/remote_management/handler.ts
2123
+ var import_pinggy5, TunnelOperations;
2124
+ var init_handler = __esm({
2125
+ "src/remote_management/handler.ts"() {
2126
+ "use strict";
2127
+ init_cjs_shims();
2128
+ init_types();
2129
+ init_TunnelManager();
2130
+ init_remote_schema();
2131
+ import_pinggy5 = require("@pinggy/pinggy");
2132
+ TunnelOperations = class {
2133
+ constructor() {
2134
+ this.tunnelManager = TunnelManager.getInstance();
2135
+ }
2136
+ buildStatus(tunnelId, state, errorCode) {
2137
+ const status = newStatus(state, errorCode, "");
2138
+ try {
2139
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelId);
2140
+ if (managed) {
2141
+ status.createdtimestamp = managed.createdAt || "";
2142
+ status.starttimestamp = managed.startedAt || "";
2143
+ status.endtimestamp = managed.stoppedAt || "";
2144
+ }
2145
+ } catch (e) {
2301
2146
  }
2302
- });
2303
- } else if (finalResult?.tunnelconfig) {
2304
- delete finalResult.tunnelconfig.allowPreflight;
2305
- }
2306
- const respObj = NewResponseObject(finalResult);
2307
- respObj.command = req.command;
2308
- respObj.requestid = req.requestid;
2309
- return respObj;
2310
- }
2311
- async handle(ws, req) {
2312
- const cmd = (req.command || "").toLowerCase();
2313
- const raw = this.safeParse(req.data);
2314
- try {
2315
- let response;
2316
- switch (cmd) {
2317
- case "start": {
2318
- response = await this.handleStartReq(req, raw);
2319
- break;
2320
- }
2321
- case "stop": {
2322
- response = await this.handleStopReq(req, raw);
2323
- break;
2324
- }
2325
- case "get": {
2326
- response = await this.handleGetReq(req, raw);
2327
- break;
2328
- }
2329
- case "restart": {
2330
- response = await this.handleRestartReq(req, raw);
2331
- break;
2332
- }
2333
- case "updateconfig": {
2334
- response = await this.handleUpdateConfigReq(req, raw);
2335
- break;
2336
- }
2337
- case "list": {
2338
- response = await this.handleListReq(req);
2339
- break;
2340
- }
2341
- default:
2342
- if (typeof req.command === "string") {
2343
- logger.warn("Unknown command", { command: req.command });
2147
+ return status;
2148
+ }
2149
+ // --- Helper to construct TunnelResponse ---
2150
+ async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, additionalForwarding, serve) {
2151
+ const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
2152
+ this.tunnelManager.getTunnelStatus(tunnelid),
2153
+ this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
2154
+ this.tunnelManager.getLocalserverTlsInfo(tunnelid),
2155
+ this.tunnelManager.getTunnelGreetMessage(tunnelid),
2156
+ this.tunnelManager.getTunnelUrls(tunnelid)
2157
+ ]);
2158
+ return {
2159
+ tunnelid,
2160
+ remoteurls,
2161
+ tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg, additionalForwarding),
2162
+ status: this.buildStatus(tunnelid, status, "" /* NoError */),
2163
+ stats
2164
+ };
2165
+ }
2166
+ error(code, err, fallback) {
2167
+ return newErrorResponse({
2168
+ code,
2169
+ message: err instanceof Error ? err.message : fallback
2170
+ });
2171
+ }
2172
+ // --- Operations ---
2173
+ async handleStart(config) {
2174
+ try {
2175
+ const opts = tunnelConfigToPinggyOptions(config);
2176
+ const additionalForwardingParsed = config.additionalForwarding || [];
2177
+ const { tunnelid, instance, tunnelName, additionalForwarding, serve } = await this.tunnelManager.createTunnel({
2178
+ ...opts,
2179
+ tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [import_pinggy5.TunnelType.Http],
2180
+ // Temporary fix in future we will not use this field.
2181
+ configid: config.configid,
2182
+ tunnelName: config.configname,
2183
+ additionalForwarding: additionalForwardingParsed,
2184
+ serve: config.serve
2185
+ });
2186
+ this.tunnelManager.startTunnel(tunnelid);
2187
+ const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
2188
+ const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, additionalForwarding, serve);
2189
+ return resp;
2190
+ } catch (err) {
2191
+ return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
2192
+ }
2193
+ }
2194
+ async handleUpdateConfig(config) {
2195
+ try {
2196
+ const opts = tunnelConfigToPinggyOptions(config);
2197
+ const tunnel = await this.tunnelManager.updateConfig({
2198
+ ...opts,
2199
+ tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [import_pinggy5.TunnelType.Http],
2200
+ // // Temporary fix in future we will not use this field.
2201
+ configid: config.configid,
2202
+ tunnelName: config.configname,
2203
+ additionalForwarding: config.additionalForwarding || [],
2204
+ serve: config.serve
2205
+ });
2206
+ if (!tunnel.instance || !tunnel.tunnelConfig)
2207
+ throw new Error("Invalid tunnel state after configuration update");
2208
+ return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.additionalForwarding, tunnel.serve);
2209
+ } catch (err) {
2210
+ return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
2211
+ }
2212
+ }
2213
+ async handleList() {
2214
+ try {
2215
+ const tunnels = await this.tunnelManager.getAllTunnels();
2216
+ if (tunnels.length === 0) {
2217
+ return [];
2218
+ }
2219
+ return Promise.all(
2220
+ tunnels.map(async (t) => {
2221
+ const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
2222
+ const [status, tlsInfo, greetMsg] = await Promise.all([
2223
+ this.tunnelManager.getTunnelStatus(t.tunnelid),
2224
+ this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
2225
+ this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
2226
+ ]);
2227
+ const pinggyOptions = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
2228
+ const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configid, t.tunnelName, tlsInfo, greetMsg, t.additionalForwarding, t.serve);
2229
+ return {
2230
+ tunnelid: t.tunnelid,
2231
+ remoteurls: t.remoteurls,
2232
+ status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
2233
+ stats: rawStats,
2234
+ tunnelconfig: tunnelConfig
2235
+ };
2236
+ })
2237
+ );
2238
+ } catch (err) {
2239
+ return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
2240
+ }
2241
+ }
2242
+ async handleStop(tunnelid) {
2243
+ try {
2244
+ const { configid } = this.tunnelManager.stopTunnel(tunnelid);
2245
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2246
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2247
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2248
+ } catch (err) {
2249
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
2250
+ }
2251
+ }
2252
+ async handleGet(tunnelid) {
2253
+ try {
2254
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2255
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2256
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2257
+ } catch (err) {
2258
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
2259
+ }
2260
+ }
2261
+ async handleRestart(tunnelid) {
2262
+ try {
2263
+ await this.tunnelManager.restartTunnel(tunnelid);
2264
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
2265
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
2266
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
2267
+ } catch (err) {
2268
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
2269
+ }
2270
+ }
2271
+ handleRegisterStatsListener(tunnelid, listener) {
2272
+ this.tunnelManager.registerStatsListener(tunnelid, listener);
2273
+ }
2274
+ handleUnregisterStatsListener(tunnelid, listnerId) {
2275
+ this.tunnelManager.deregisterStatsListener(tunnelid, listnerId);
2276
+ }
2277
+ handleGetTunnelStats(tunnelid) {
2278
+ try {
2279
+ const stats = this.tunnelManager.getTunnelStats(tunnelid);
2280
+ if (!stats) {
2281
+ return [newStats()];
2344
2282
  }
2345
- return this.sendError(ws, req, "Invalid command");
2283
+ return stats;
2284
+ } catch (err) {
2285
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats");
2286
+ }
2287
+ }
2288
+ handleRegisterDisconnectListener(tunnelid, listener) {
2289
+ this.tunnelManager.registerDisconnectListener(tunnelid, listener);
2290
+ }
2291
+ handleRemoveStoppedTunnelByConfigId(configId) {
2292
+ try {
2293
+ return this.tunnelManager.removeStoppedTunnelByConfigId(configId);
2294
+ } catch (err) {
2295
+ return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by configId");
2296
+ }
2346
2297
  }
2347
- logger.debug("Sending response", { command: response.command, requestid: response.requestid });
2348
- this.sendResponse(ws, response);
2349
- } catch (e) {
2350
- if (e instanceof import_zod2.default.ZodError) {
2351
- logger.warn("Validation failed", { cmd, issues: e.issues });
2352
- return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
2298
+ handleRemoveStoppedTunnelByTunnelId(tunnelId) {
2299
+ try {
2300
+ return this.tunnelManager.removeStoppedTunnelByTunnelId(tunnelId);
2301
+ } catch (err) {
2302
+ return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by tunnelId");
2303
+ }
2353
2304
  }
2354
- logger.error("Error handling command", { cmd, error: String(e) });
2355
- return this.sendError(ws, req, e?.message || "Internal error");
2356
- }
2305
+ };
2357
2306
  }
2358
- };
2307
+ });
2308
+
2309
+ // src/remote_management/websocket_handlers.ts
2359
2310
  function handleConnectionStatusMessage(firstMessage) {
2360
2311
  try {
2361
2312
  const text = typeof firstMessage === "string" ? firstMessage : firstMessage.toString();
@@ -2372,16 +2323,148 @@ function handleConnectionStatusMessage(firstMessage) {
2372
2323
  return true;
2373
2324
  }
2374
2325
  }
2326
+ var import_zod2, WebSocketCommandHandler;
2327
+ var init_websocket_handlers = __esm({
2328
+ "src/remote_management/websocket_handlers.ts"() {
2329
+ "use strict";
2330
+ init_cjs_shims();
2331
+ init_logger();
2332
+ init_types();
2333
+ init_handler();
2334
+ init_remote_schema();
2335
+ import_zod2 = __toESM(require("zod"), 1);
2336
+ init_printer();
2337
+ WebSocketCommandHandler = class {
2338
+ constructor() {
2339
+ this.tunnelHandler = new TunnelOperations();
2340
+ }
2341
+ safeParse(text) {
2342
+ if (!text) return void 0;
2343
+ try {
2344
+ return JSON.parse(text);
2345
+ } catch (e) {
2346
+ logger.warn("Invalid JSON payload", { error: String(e), text });
2347
+ return void 0;
2348
+ }
2349
+ }
2350
+ sendResponse(ws, resp) {
2351
+ const payload = {
2352
+ ...resp,
2353
+ response: Buffer.from(resp.response || []).toString("base64")
2354
+ };
2355
+ ws.send(JSON.stringify(payload));
2356
+ }
2357
+ sendError(ws, req, message, code = ErrorCode.InternalServerError) {
2358
+ const resp = NewErrorResponseObject({ code, message });
2359
+ resp.command = req.command || "";
2360
+ resp.requestid = req.requestid || "";
2361
+ this.sendResponse(ws, resp);
2362
+ }
2363
+ async handleStartReq(req, raw) {
2364
+ const dc = StartSchema.parse(raw);
2365
+ printer_default.info("Starting tunnel with config name: " + dc.tunnelConfig.configname);
2366
+ const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
2367
+ return this.wrapResponse(result, req);
2368
+ }
2369
+ async handleStopReq(req, raw) {
2370
+ const dc = StopSchema.parse(raw);
2371
+ printer_default.info("Stopping tunnel with ID: " + dc.tunnelID);
2372
+ const result = await this.tunnelHandler.handleStop(dc.tunnelID);
2373
+ return this.wrapResponse(result, req);
2374
+ }
2375
+ async handleGetReq(req, raw) {
2376
+ const dc = GetSchema.parse(raw);
2377
+ const result = await this.tunnelHandler.handleGet(dc.tunnelID);
2378
+ return this.wrapResponse(result, req);
2379
+ }
2380
+ async handleRestartReq(req, raw) {
2381
+ const dc = RestartSchema.parse(raw);
2382
+ const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
2383
+ return this.wrapResponse(result, req);
2384
+ }
2385
+ async handleUpdateConfigReq(req, raw) {
2386
+ const dc = UpdateConfigSchema.parse(raw);
2387
+ const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
2388
+ return this.wrapResponse(result, req);
2389
+ }
2390
+ async handleListReq(req) {
2391
+ const result = await this.tunnelHandler.handleList();
2392
+ return this.wrapResponse(result, req);
2393
+ }
2394
+ wrapResponse(result, req) {
2395
+ if (isErrorResponse(result)) {
2396
+ const errResp = NewErrorResponseObject(result);
2397
+ errResp.command = req.command;
2398
+ errResp.requestid = req.requestid;
2399
+ return errResp;
2400
+ }
2401
+ const finalResult = JSON.parse(JSON.stringify(result));
2402
+ if (Array.isArray(finalResult)) {
2403
+ finalResult.forEach((item) => {
2404
+ if (item?.tunnelconfig) {
2405
+ delete item.tunnelconfig.allowPreflight;
2406
+ }
2407
+ });
2408
+ } else if (finalResult?.tunnelconfig) {
2409
+ delete finalResult.tunnelconfig.allowPreflight;
2410
+ }
2411
+ const respObj = NewResponseObject(finalResult);
2412
+ respObj.command = req.command;
2413
+ respObj.requestid = req.requestid;
2414
+ return respObj;
2415
+ }
2416
+ async handle(ws, req) {
2417
+ const cmd = (req.command || "").toLowerCase();
2418
+ const raw = this.safeParse(req.data);
2419
+ try {
2420
+ let response;
2421
+ switch (cmd) {
2422
+ case "start": {
2423
+ response = await this.handleStartReq(req, raw);
2424
+ break;
2425
+ }
2426
+ case "stop": {
2427
+ response = await this.handleStopReq(req, raw);
2428
+ break;
2429
+ }
2430
+ case "get": {
2431
+ response = await this.handleGetReq(req, raw);
2432
+ break;
2433
+ }
2434
+ case "restart": {
2435
+ response = await this.handleRestartReq(req, raw);
2436
+ break;
2437
+ }
2438
+ case "updateconfig": {
2439
+ response = await this.handleUpdateConfigReq(req, raw);
2440
+ break;
2441
+ }
2442
+ case "list": {
2443
+ response = await this.handleListReq(req);
2444
+ break;
2445
+ }
2446
+ default:
2447
+ if (typeof req.command === "string") {
2448
+ logger.warn("Unknown command", { command: req.command });
2449
+ }
2450
+ return this.sendError(ws, req, "Invalid command");
2451
+ }
2452
+ logger.debug("Sending response", { command: response.command, requestid: response.requestid });
2453
+ this.sendResponse(ws, response);
2454
+ } catch (e) {
2455
+ if (e instanceof import_zod2.default.ZodError) {
2456
+ logger.warn("Validation failed", { cmd, issues: e.issues });
2457
+ return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
2458
+ }
2459
+ logger.error("Error handling command", { cmd, error: String(e) });
2460
+ return this.sendError(ws, req, e?.message || "Internal error");
2461
+ }
2462
+ }
2463
+ };
2464
+ }
2465
+ });
2375
2466
 
2376
2467
  // src/remote_management/remoteManagement.ts
2377
- var RECONNECT_SLEEP_MS = 5e3;
2378
- var PING_INTERVAL_MS = 3e4;
2379
- var _remoteManagementState = {
2380
- status: "NOT_RUNNING",
2381
- errorMessage: ""
2382
- };
2383
- var _stopRequested = false;
2384
- var currentWs = null;
2385
2468
  function buildRemoteManagementWsUrl(manage) {
2386
2469
  let baseUrl = (manage || "dashboard.pinggy.io").trim();
2387
2470
  if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
@@ -2541,15 +2624,33 @@ function setRemoteManagementState(state, errorMessage) {
2541
2624
  errorMessage: errorMessage || ""
2542
2625
  };
2543
2626
  }
2627
+ var import_ws, RECONNECT_SLEEP_MS, PING_INTERVAL_MS, _remoteManagementState, _stopRequested, currentWs;
2628
+ var init_remoteManagement = __esm({
2629
+ "src/remote_management/remoteManagement.ts"() {
2630
+ "use strict";
2631
+ init_cjs_shims();
2632
+ import_ws = __toESM(require("ws"), 1);
2633
+ init_logger();
2634
+ init_websocket_handlers();
2635
+ init_printer();
2636
+ init_types();
2637
+ RECONNECT_SLEEP_MS = 5e3;
2638
+ PING_INTERVAL_MS = 3e4;
2639
+ _remoteManagementState = {
2640
+ status: "NOT_RUNNING",
2641
+ errorMessage: ""
2642
+ };
2643
+ _stopRequested = false;
2644
+ currentWs = null;
2645
+ }
2646
+ });
2544
2647
 
2545
2648
  // src/utils/parseArgs.ts
2546
- var import_util3 = require("util");
2547
- var os = __toESM(require("os"), 1);
2548
2649
  function isInlineColonFlag(arg) {
2549
2650
  return /^-([RL])[A-Za-z0-9._-]*:?$/.test(arg);
2550
2651
  }
2551
2652
  function preprocessWindowsArgs(args) {
2552
- if (os.platform() !== "win32") return args;
2653
+ if (os2.platform() !== "win32") return args;
2553
2654
  const out = [];
2554
2655
  let i = 0;
2555
2656
  while (i < args.length) {
@@ -2573,7 +2674,7 @@ function preprocessWindowsArgs(args) {
2573
2674
  function parseCliArgs(options) {
2574
2675
  const rawArgs = process.argv.slice(2);
2575
2676
  const processedArgs = preprocessWindowsArgs(rawArgs);
2576
- const parsed = (0, import_util3.parseArgs)({
2677
+ const parsed = (0, import_util4.parseArgs)({
2577
2678
  args: processedArgs,
2578
2679
  options,
2579
2680
  allowPositionals: true
@@ -2584,9 +2685,17 @@ function parseCliArgs(options) {
2584
2685
  hasAnyArgs
2585
2686
  };
2586
2687
  }
2688
+ var import_util4, os2;
2689
+ var init_parseArgs = __esm({
2690
+ "src/utils/parseArgs.ts"() {
2691
+ "use strict";
2692
+ init_cjs_shims();
2693
+ import_util4 = require("util");
2694
+ os2 = __toESM(require("os"), 1);
2695
+ }
2696
+ });
2587
2697
 
2588
2698
  // src/utils/getFreePort.ts
2589
- var import_net2 = __toESM(require("net"), 1);
2590
2699
  function getFreePort(webDebugger) {
2591
2700
  return new Promise((resolve, reject) => {
2592
2701
  const tryPort = (portToTry) => {
@@ -2616,15 +2725,16 @@ function getFreePort(webDebugger) {
2616
2725
  tryPort(providedPort);
2617
2726
  });
2618
2727
  }
2619
-
2620
- // src/cli/starCli.ts
2621
- var import_picocolors3 = __toESM(require("picocolors"), 1);
2622
-
2623
- // src/tui/blessed/TunnelTui.ts
2624
- var import_blessed3 = __toESM(require("blessed"), 1);
2728
+ var import_net2;
2729
+ var init_getFreePort = __esm({
2730
+ "src/utils/getFreePort.ts"() {
2731
+ "use strict";
2732
+ init_cjs_shims();
2733
+ import_net2 = __toESM(require("net"), 1);
2734
+ }
2735
+ });
2625
2736
 
2626
2737
  // src/tui/blessed/qrCodeGenerator.ts
2627
- var import_qrcode = __toESM(require("qrcode"), 1);
2628
2738
  async function createQrCodes(urls) {
2629
2739
  const codes = [];
2630
2740
  for (const url of urls) {
@@ -2638,17 +2748,16 @@ async function createQrCodes(urls) {
2638
2748
  }
2639
2749
  return codes;
2640
2750
  }
2641
-
2642
- // src/tui/blessed/webDebuggerConnection.ts
2643
- var import_ws2 = __toESM(require("ws"), 1);
2751
+ var import_qrcode;
2752
+ var init_qrCodeGenerator = __esm({
2753
+ "src/tui/blessed/qrCodeGenerator.ts"() {
2754
+ "use strict";
2755
+ init_cjs_shims();
2756
+ import_qrcode = __toESM(require("qrcode"), 1);
2757
+ }
2758
+ });
2644
2759
 
2645
2760
  // src/tui/blessed/config.ts
2646
- var defaultTuiConfig = {
2647
- maxRequestPairs: 100,
2648
- visibleRequestCount: 10,
2649
- viewportScrollMargin: 2,
2650
- inactivityHttpSelectorTimeoutMs: 1e4
2651
- };
2652
2761
  function getTuiConfig() {
2653
2762
  return {
2654
2763
  maxRequestPairs: defaultTuiConfig.maxRequestPairs,
@@ -2657,6 +2766,19 @@ function getTuiConfig() {
2657
2766
  inactivityHttpSelectorTimeoutMs: defaultTuiConfig.inactivityHttpSelectorTimeoutMs
2658
2767
  };
2659
2768
  }
2769
+ var defaultTuiConfig;
2770
+ var init_config = __esm({
2771
+ "src/tui/blessed/config.ts"() {
2772
+ "use strict";
2773
+ init_cjs_shims();
2774
+ defaultTuiConfig = {
2775
+ maxRequestPairs: 100,
2776
+ visibleRequestCount: 10,
2777
+ viewportScrollMargin: 2,
2778
+ inactivityHttpSelectorTimeoutMs: 1e4
2779
+ };
2780
+ }
2781
+ });
2660
2782
 
2661
2783
  // src/tui/blessed/webDebuggerConnection.ts
2662
2784
  function createWebDebuggerConnection(webDebuggerUrl, onUpdate) {
@@ -2750,22 +2872,34 @@ function createWebDebuggerConnection(webDebuggerUrl, onUpdate) {
2750
2872
  }
2751
2873
  };
2752
2874
  }
2753
-
2754
- // src/tui/blessed/components/UIComponents.ts
2755
- var import_blessed = __toESM(require("blessed"), 1);
2875
+ var import_ws2;
2876
+ var init_webDebuggerConnection = __esm({
2877
+ "src/tui/blessed/webDebuggerConnection.ts"() {
2878
+ "use strict";
2879
+ init_cjs_shims();
2880
+ import_ws2 = __toESM(require("ws"), 1);
2881
+ init_logger();
2882
+ init_config();
2883
+ }
2884
+ });
2756
2885
 
2757
2886
  // src/tui/ink/asciArt.ts
2758
- var asciiArtPinggyLogo = `
2887
+ var asciiArtPinggyLogo;
2888
+ var init_asciArt = __esm({
2889
+ "src/tui/ink/asciArt.ts"() {
2890
+ "use strict";
2891
+ init_cjs_shims();
2892
+ asciiArtPinggyLogo = `
2759
2893
  \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557
2760
2894
  \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D
2761
2895
  \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2554\u255D
2762
2896
  \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D
2763
2897
  \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551
2764
2898
  \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D `;
2899
+ }
2900
+ });
2765
2901
 
2766
2902
  // src/tui/blessed/components/UIComponents.ts
2767
- var MIN_WIDTH_WARNING = 60;
2768
- var SIMPLE_LAYOUT_THRESHOLD = 80;
2769
2903
  function colorizeGradient(text) {
2770
2904
  const colors = ["red", "yellow", "green", "cyan", "blue", "magenta"];
2771
2905
  const lines = text.split("\n");
@@ -2987,6 +3121,17 @@ function createSimpleUI(screen, urls, greet) {
2987
3121
  footerBox
2988
3122
  };
2989
3123
  }
3124
+ var import_blessed, MIN_WIDTH_WARNING, SIMPLE_LAYOUT_THRESHOLD;
3125
+ var init_UIComponents = __esm({
3126
+ "src/tui/blessed/components/UIComponents.ts"() {
3127
+ "use strict";
3128
+ init_cjs_shims();
3129
+ import_blessed = __toESM(require("blessed"), 1);
3130
+ init_asciArt();
3131
+ MIN_WIDTH_WARNING = 60;
3132
+ SIMPLE_LAYOUT_THRESHOLD = 80;
3133
+ }
3134
+ });
2990
3135
 
2991
3136
  // src/tui/ink/utils/utils.ts
2992
3137
  function getStatusColor(status) {
@@ -3019,6 +3164,12 @@ function getBytesInt(b) {
3019
3164
  }
3020
3165
  return `${b.toFixed(2)} `;
3021
3166
  }
3167
+ var init_utils = __esm({
3168
+ "src/tui/ink/utils/utils.ts"() {
3169
+ "use strict";
3170
+ init_cjs_shims();
3171
+ }
3172
+ });
3022
3173
 
3023
3174
  // src/tui/blessed/components/DisplayUpdaters.ts
3024
3175
  function updateUrlsDisplay(urlsBox, screen, urls, currentQrIndex) {
@@ -3135,9 +3286,16 @@ function updateQrCodeDisplay(qrCodeBox, screen, qrCodes, urls, currentQrIndex) {
3135
3286
  qrCodeBox.parseContent();
3136
3287
  screen.render();
3137
3288
  }
3289
+ var init_DisplayUpdaters = __esm({
3290
+ "src/tui/blessed/components/DisplayUpdaters.ts"() {
3291
+ "use strict";
3292
+ init_cjs_shims();
3293
+ init_utils();
3294
+ init_config();
3295
+ }
3296
+ });
3138
3297
 
3139
3298
  // src/tui/blessed/components/Modals.ts
3140
- var import_blessed2 = __toESM(require("blessed"), 1);
3141
3299
  function showDetailModal(screen, manager, requestText, responseText) {
3142
3300
  manager.inDetailView = true;
3143
3301
  manager.detailModal = import_blessed2.default.box({
@@ -3340,6 +3498,14 @@ function showErrorModal(screen, modalManager, title = "Error", message) {
3340
3498
  modalManager.loadingView = true;
3341
3499
  screen.render();
3342
3500
  }
3501
+ var import_blessed2;
3502
+ var init_Modals = __esm({
3503
+ "src/tui/blessed/components/Modals.ts"() {
3504
+ "use strict";
3505
+ init_cjs_shims();
3506
+ import_blessed2 = __toESM(require("blessed"), 1);
3507
+ }
3508
+ });
3343
3509
 
3344
3510
  // src/tui/blessed/headerFetcher.ts
3345
3511
  async function fetchReqResHeaders(baseUrl, key, signal) {
@@ -3367,6 +3533,13 @@ async function fetchReqResHeaders(baseUrl, key, signal) {
3367
3533
  throw err;
3368
3534
  }
3369
3535
  }
3536
+ var init_headerFetcher = __esm({
3537
+ "src/tui/blessed/headerFetcher.ts"() {
3538
+ "use strict";
3539
+ init_cjs_shims();
3540
+ init_logger();
3541
+ }
3542
+ });
3370
3543
 
3371
3544
  // src/tui/blessed/components/KeyBindings.ts
3372
3545
  function setupKeyBindings(screen, modalManager, state, callbacks, tunnelConfig) {
@@ -3530,264 +3703,291 @@ function setupKeyBindings(screen, modalManager, state, callbacks, tunnelConfig)
3530
3703
  }
3531
3704
  });
3532
3705
  }
3706
+ var init_KeyBindings = __esm({
3707
+ "src/tui/blessed/components/KeyBindings.ts"() {
3708
+ "use strict";
3709
+ init_cjs_shims();
3710
+ init_headerFetcher();
3711
+ init_logger();
3712
+ init_Modals();
3713
+ init_config();
3714
+ }
3715
+ });
3533
3716
 
3534
3717
  // src/tui/blessed/TunnelTui.ts
3535
- var TunnelTui = class {
3536
- constructor(props) {
3537
- // State
3538
- this.currentQrIndex = 0;
3539
- this.selectedIndex = -1;
3540
- // -1 means no selection
3541
- this.selectedRequestKey = null;
3542
- // Track selected request by key
3543
- this.qrCodes = [];
3544
- this.stats = {
3545
- elapsedTime: 0,
3546
- numLiveConnections: 0,
3547
- numTotalConnections: 0,
3548
- numTotalReqBytes: 0,
3549
- numTotalResBytes: 0,
3550
- numTotalTxBytes: 0
3551
- };
3552
- this.pairs = [];
3553
- this.webDebuggerConnection = null;
3554
- this.modalManager = {
3555
- detailModal: null,
3556
- keyBindingsModal: null,
3557
- disconnectModal: null,
3558
- inDetailView: false,
3559
- keyBindingView: false,
3560
- inDisconnectView: false,
3561
- loadingBox: null,
3562
- loadingView: false,
3563
- fetchAbortController: null
3564
- };
3565
- this.exitPromiseResolve = null;
3566
- this.urls = props.urls;
3567
- this.greet = props.greet || "";
3568
- this.tunnelConfig = props.tunnelConfig;
3569
- this.disconnectInfo = props.disconnectInfo;
3570
- if (props.tunnelInstance) {
3571
- this.tunnelInstance = props.tunnelInstance;
3572
- }
3573
- this.exitPromise = new Promise((resolve) => {
3574
- this.exitPromiseResolve = resolve;
3575
- });
3576
- this.screen = import_blessed3.default.screen({
3577
- smartCSR: true,
3578
- title: "Pinggy Tunnel",
3579
- fullUnicode: true
3580
- });
3581
- this.setupStatsListener();
3582
- this.setupWebDebugger();
3583
- this.generateQrCodes();
3584
- this.createUI();
3585
- this.setupKeyBindings();
3586
- }
3587
- setupStatsListener() {
3588
- globalThis.__PINGGY_TUNNEL_STATS__ = (newStats2) => {
3589
- this.stats = { ...newStats2 };
3590
- this.updateStatsDisplay();
3591
- };
3592
- }
3593
- clearSelection() {
3594
- this.selectedIndex = -1;
3595
- this.selectedRequestKey = null;
3596
- }
3597
- setupWebDebugger() {
3598
- if (this.tunnelConfig?.webDebugger) {
3599
- this.webDebuggerConnection = createWebDebuggerConnection(
3600
- this.tunnelConfig.webDebugger,
3601
- (pairs) => {
3602
- this.pairs = pairs;
3603
- if (this.selectedRequestKey !== null) {
3604
- const newIndex = pairs.findIndex(
3605
- (pair) => pair.request?.key === this.selectedRequestKey
3606
- );
3607
- if (newIndex !== -1) {
3608
- this.selectedIndex = newIndex;
3609
- } else {
3610
- this.clearSelection();
3718
+ var import_blessed3, TunnelTui;
3719
+ var init_TunnelTui = __esm({
3720
+ "src/tui/blessed/TunnelTui.ts"() {
3721
+ "use strict";
3722
+ init_cjs_shims();
3723
+ import_blessed3 = __toESM(require("blessed"), 1);
3724
+ init_qrCodeGenerator();
3725
+ init_webDebuggerConnection();
3726
+ init_TunnelManager();
3727
+ init_UIComponents();
3728
+ init_DisplayUpdaters();
3729
+ init_Modals();
3730
+ init_KeyBindings();
3731
+ TunnelTui = class {
3732
+ constructor(props) {
3733
+ // State
3734
+ this.currentQrIndex = 0;
3735
+ this.selectedIndex = -1;
3736
+ // -1 means no selection
3737
+ this.selectedRequestKey = null;
3738
+ // Track selected request by key
3739
+ this.qrCodes = [];
3740
+ this.stats = {
3741
+ elapsedTime: 0,
3742
+ numLiveConnections: 0,
3743
+ numTotalConnections: 0,
3744
+ numTotalReqBytes: 0,
3745
+ numTotalResBytes: 0,
3746
+ numTotalTxBytes: 0
3747
+ };
3748
+ this.pairs = [];
3749
+ this.webDebuggerConnection = null;
3750
+ this.modalManager = {
3751
+ detailModal: null,
3752
+ keyBindingsModal: null,
3753
+ disconnectModal: null,
3754
+ inDetailView: false,
3755
+ keyBindingView: false,
3756
+ inDisconnectView: false,
3757
+ loadingBox: null,
3758
+ loadingView: false,
3759
+ fetchAbortController: null
3760
+ };
3761
+ this.exitPromiseResolve = null;
3762
+ this.urls = props.urls;
3763
+ this.greet = props.greet || "";
3764
+ this.tunnelConfig = props.tunnelConfig;
3765
+ this.disconnectInfo = props.disconnectInfo;
3766
+ if (props.tunnelInstance) {
3767
+ this.tunnelInstance = props.tunnelInstance;
3768
+ }
3769
+ this.exitPromise = new Promise((resolve) => {
3770
+ this.exitPromiseResolve = resolve;
3771
+ });
3772
+ this.screen = import_blessed3.default.screen({
3773
+ smartCSR: true,
3774
+ title: "Pinggy Tunnel",
3775
+ fullUnicode: true
3776
+ });
3777
+ this.setupStatsListener();
3778
+ this.setupWebDebugger();
3779
+ this.generateQrCodes();
3780
+ this.createUI();
3781
+ this.setupKeyBindings();
3782
+ }
3783
+ setupStatsListener() {
3784
+ globalThis.__PINGGY_TUNNEL_STATS__ = (newStats2) => {
3785
+ this.stats = { ...newStats2 };
3786
+ this.updateStatsDisplay();
3787
+ };
3788
+ }
3789
+ clearSelection() {
3790
+ this.selectedIndex = -1;
3791
+ this.selectedRequestKey = null;
3792
+ }
3793
+ setupWebDebugger() {
3794
+ if (this.tunnelConfig?.webDebugger) {
3795
+ this.webDebuggerConnection = createWebDebuggerConnection(
3796
+ this.tunnelConfig.webDebugger,
3797
+ (pairs) => {
3798
+ this.pairs = pairs;
3799
+ if (this.selectedRequestKey !== null) {
3800
+ const newIndex = pairs.findIndex(
3801
+ (pair) => pair.request?.key === this.selectedRequestKey
3802
+ );
3803
+ if (newIndex !== -1) {
3804
+ this.selectedIndex = newIndex;
3805
+ } else {
3806
+ this.clearSelection();
3807
+ }
3808
+ }
3809
+ this.updateRequestsDisplay();
3611
3810
  }
3811
+ );
3812
+ }
3813
+ }
3814
+ async generateQrCodes() {
3815
+ if (this.tunnelConfig?.qrCode && this.urls.length > 0) {
3816
+ this.qrCodes = await createQrCodes(this.urls);
3817
+ this.updateQrCodeDisplay();
3818
+ }
3819
+ }
3820
+ // Create the UI based on terminal size
3821
+ createUI() {
3822
+ this.buildUI();
3823
+ this.screen.on("resize", () => {
3824
+ this.handleResize();
3825
+ });
3826
+ }
3827
+ buildUI() {
3828
+ const width = this.screen.width;
3829
+ if (width < MIN_WIDTH_WARNING) {
3830
+ this.uiElements = {
3831
+ mainContainer: createWarningUI(this.screen),
3832
+ urlsBox: null,
3833
+ statsBox: null,
3834
+ requestsBox: null,
3835
+ footerBox: null,
3836
+ warningBox: createWarningUI(this.screen)
3837
+ };
3838
+ this.screen.render();
3839
+ return;
3840
+ }
3841
+ if (width < SIMPLE_LAYOUT_THRESHOLD) {
3842
+ this.uiElements = createSimpleUI(this.screen, this.urls, this.greet);
3843
+ } else {
3844
+ this.uiElements = createFullUI(this.screen, this.urls, this.greet, this.tunnelConfig);
3845
+ }
3846
+ this.refreshDisplays();
3847
+ this.screen.render();
3848
+ }
3849
+ refreshDisplays() {
3850
+ this.updateUrlsDisplay();
3851
+ this.updateStatsDisplay();
3852
+ this.updateRequestsDisplay();
3853
+ this.updateQrCodeDisplay();
3854
+ }
3855
+ updateUrlsDisplay() {
3856
+ updateUrlsDisplay(
3857
+ this.uiElements?.urlsBox,
3858
+ this.screen,
3859
+ this.urls,
3860
+ this.currentQrIndex
3861
+ );
3862
+ }
3863
+ updateStatsDisplay() {
3864
+ updateStatsDisplay(
3865
+ this.uiElements?.statsBox,
3866
+ this.screen,
3867
+ this.stats
3868
+ );
3869
+ }
3870
+ updateRequestsDisplay() {
3871
+ const result = updateRequestsDisplay(
3872
+ this.uiElements?.requestsBox,
3873
+ this.screen,
3874
+ this.pairs,
3875
+ this.selectedIndex
3876
+ );
3877
+ if (result.adjustedSelectedIndex !== this.selectedIndex) {
3878
+ if (result.adjustedSelectedIndex === -1) {
3879
+ this.clearSelection();
3880
+ } else {
3881
+ this.selectedIndex = result.adjustedSelectedIndex;
3612
3882
  }
3613
- this.updateRequestsDisplay();
3614
3883
  }
3615
- );
3616
- }
3617
- }
3618
- async generateQrCodes() {
3619
- if (this.tunnelConfig?.qrCode && this.urls.length > 0) {
3620
- this.qrCodes = await createQrCodes(this.urls);
3621
- this.updateQrCodeDisplay();
3622
- }
3623
- }
3624
- // Create the UI based on terminal size
3625
- createUI() {
3626
- this.buildUI();
3627
- this.screen.on("resize", () => {
3628
- this.handleResize();
3629
- });
3630
- }
3631
- buildUI() {
3632
- const width = this.screen.width;
3633
- if (width < MIN_WIDTH_WARNING) {
3634
- this.uiElements = {
3635
- mainContainer: createWarningUI(this.screen),
3636
- urlsBox: null,
3637
- statsBox: null,
3638
- requestsBox: null,
3639
- footerBox: null,
3640
- warningBox: createWarningUI(this.screen)
3641
- };
3642
- this.screen.render();
3643
- return;
3644
- }
3645
- if (width < SIMPLE_LAYOUT_THRESHOLD) {
3646
- this.uiElements = createSimpleUI(this.screen, this.urls, this.greet);
3647
- } else {
3648
- this.uiElements = createFullUI(this.screen, this.urls, this.greet, this.tunnelConfig);
3649
- }
3650
- this.refreshDisplays();
3651
- this.screen.render();
3652
- }
3653
- refreshDisplays() {
3654
- this.updateUrlsDisplay();
3655
- this.updateStatsDisplay();
3656
- this.updateRequestsDisplay();
3657
- this.updateQrCodeDisplay();
3658
- }
3659
- updateUrlsDisplay() {
3660
- updateUrlsDisplay(
3661
- this.uiElements?.urlsBox,
3662
- this.screen,
3663
- this.urls,
3664
- this.currentQrIndex
3665
- );
3666
- }
3667
- updateStatsDisplay() {
3668
- updateStatsDisplay(
3669
- this.uiElements?.statsBox,
3670
- this.screen,
3671
- this.stats
3672
- );
3673
- }
3674
- updateRequestsDisplay() {
3675
- const result = updateRequestsDisplay(
3676
- this.uiElements?.requestsBox,
3677
- this.screen,
3678
- this.pairs,
3679
- this.selectedIndex
3680
- );
3681
- if (result.adjustedSelectedIndex !== this.selectedIndex) {
3682
- if (result.adjustedSelectedIndex === -1) {
3683
- this.clearSelection();
3684
- } else {
3685
- this.selectedIndex = result.adjustedSelectedIndex;
3884
+ if (result.trimmedPairs !== this.pairs) {
3885
+ this.pairs = result.trimmedPairs;
3886
+ }
3686
3887
  }
3687
- }
3688
- if (result.trimmedPairs !== this.pairs) {
3689
- this.pairs = result.trimmedPairs;
3690
- }
3691
- }
3692
- updateQrCodeDisplay() {
3693
- updateQrCodeDisplay(
3694
- this.uiElements?.qrCodeBox,
3695
- this.screen,
3696
- this.qrCodes,
3697
- this.urls,
3698
- this.currentQrIndex
3699
- );
3700
- }
3701
- setupKeyBindings() {
3702
- const self = this;
3703
- const state = {
3704
- get currentQrIndex() {
3705
- return self.currentQrIndex;
3706
- },
3707
- set currentQrIndex(value) {
3708
- self.currentQrIndex = value;
3709
- },
3710
- get selectedIndex() {
3711
- return self.selectedIndex;
3712
- },
3713
- set selectedIndex(value) {
3714
- self.selectedIndex = value;
3715
- },
3716
- get pairs() {
3717
- return self.pairs;
3718
- },
3719
- get urls() {
3720
- return self.urls;
3888
+ updateQrCodeDisplay() {
3889
+ updateQrCodeDisplay(
3890
+ this.uiElements?.qrCodeBox,
3891
+ this.screen,
3892
+ this.qrCodes,
3893
+ this.urls,
3894
+ this.currentQrIndex
3895
+ );
3721
3896
  }
3722
- };
3723
- const callbacks = {
3724
- onQrIndexChange: (index) => {
3725
- self.currentQrIndex = index;
3726
- },
3727
- onSelectedIndexChange: (index, requestKey) => {
3728
- self.selectedIndex = index;
3729
- self.selectedRequestKey = requestKey;
3730
- },
3731
- onDestroy: () => self.destroy(),
3732
- updateUrlsDisplay: () => self.updateUrlsDisplay(),
3733
- updateQrCodeDisplay: () => self.updateQrCodeDisplay(),
3734
- updateRequestsDisplay: () => self.updateRequestsDisplay()
3735
- };
3736
- setupKeyBindings(
3737
- this.screen,
3738
- this.modalManager,
3739
- state,
3740
- callbacks,
3741
- this.tunnelConfig
3742
- );
3743
- }
3744
- handleResize() {
3745
- this.screen.children.forEach((child) => child.destroy());
3746
- this.buildUI();
3747
- }
3748
- updateDisconnectInfo(info) {
3749
- this.disconnectInfo = info;
3750
- if (info?.disconnected) {
3751
- const message = info.error ? `Error: ${info.error}
3897
+ setupKeyBindings() {
3898
+ const self = this;
3899
+ const state = {
3900
+ get currentQrIndex() {
3901
+ return self.currentQrIndex;
3902
+ },
3903
+ set currentQrIndex(value) {
3904
+ self.currentQrIndex = value;
3905
+ },
3906
+ get selectedIndex() {
3907
+ return self.selectedIndex;
3908
+ },
3909
+ set selectedIndex(value) {
3910
+ self.selectedIndex = value;
3911
+ },
3912
+ get pairs() {
3913
+ return self.pairs;
3914
+ },
3915
+ get urls() {
3916
+ return self.urls;
3917
+ }
3918
+ };
3919
+ const callbacks = {
3920
+ onQrIndexChange: (index) => {
3921
+ self.currentQrIndex = index;
3922
+ },
3923
+ onSelectedIndexChange: (index, requestKey) => {
3924
+ self.selectedIndex = index;
3925
+ self.selectedRequestKey = requestKey;
3926
+ },
3927
+ onDestroy: () => self.destroy(),
3928
+ updateUrlsDisplay: () => self.updateUrlsDisplay(),
3929
+ updateQrCodeDisplay: () => self.updateQrCodeDisplay(),
3930
+ updateRequestsDisplay: () => self.updateRequestsDisplay()
3931
+ };
3932
+ setupKeyBindings(
3933
+ this.screen,
3934
+ this.modalManager,
3935
+ state,
3936
+ callbacks,
3937
+ this.tunnelConfig
3938
+ );
3939
+ }
3940
+ handleResize() {
3941
+ this.screen.children.forEach((child) => child.destroy());
3942
+ this.buildUI();
3943
+ }
3944
+ updateDisconnectInfo(info) {
3945
+ this.disconnectInfo = info;
3946
+ if (info?.disconnected) {
3947
+ const message = info.error ? `Error: ${info.error}
3752
3948
  Tunnel will be closed.` : info.messages?.join("\n") || "Disconnect request received. Tunnel will be closed.";
3753
- showDisconnectModal(
3754
- this.screen,
3755
- this.modalManager,
3756
- message,
3757
- () => this.destroy()
3758
- );
3759
- }
3760
- }
3761
- start() {
3762
- this.screen.render();
3763
- }
3764
- waitUntilExit() {
3765
- return this.exitPromise;
3949
+ showDisconnectModal(
3950
+ this.screen,
3951
+ this.modalManager,
3952
+ message,
3953
+ () => this.destroy()
3954
+ );
3955
+ }
3956
+ }
3957
+ start() {
3958
+ this.screen.render();
3959
+ }
3960
+ waitUntilExit() {
3961
+ return this.exitPromise;
3962
+ }
3963
+ destroy() {
3964
+ if (this.tunnelInstance?.tunnelid) {
3965
+ const manager = TunnelManager.getInstance();
3966
+ manager.stopTunnel(this.tunnelInstance.tunnelid);
3967
+ }
3968
+ delete globalThis.__PINGGY_TUNNEL_STATS__;
3969
+ if (this.webDebuggerConnection) {
3970
+ this.webDebuggerConnection.close();
3971
+ }
3972
+ this.screen.destroy();
3973
+ if (this.exitPromiseResolve) {
3974
+ this.exitPromiseResolve();
3975
+ }
3976
+ }
3977
+ };
3766
3978
  }
3767
- destroy() {
3768
- if (this.tunnelInstance?.tunnelid) {
3769
- const manager = TunnelManager.getInstance();
3770
- manager.stopTunnel(this.tunnelInstance.tunnelid);
3771
- }
3772
- delete globalThis.__PINGGY_TUNNEL_STATS__;
3773
- if (this.webDebuggerConnection) {
3774
- this.webDebuggerConnection.close();
3775
- }
3776
- this.screen.destroy();
3777
- if (this.exitPromiseResolve) {
3778
- this.exitPromiseResolve();
3779
- }
3979
+ });
3980
+
3981
+ // src/tui/blessed/index.ts
3982
+ var init_blessed = __esm({
3983
+ "src/tui/blessed/index.ts"() {
3984
+ "use strict";
3985
+ init_cjs_shims();
3986
+ init_TunnelTui();
3780
3987
  }
3781
- };
3988
+ });
3782
3989
 
3783
3990
  // src/cli/starCli.ts
3784
- var TunnelData = {
3785
- urls: null,
3786
- greet: null,
3787
- usage: null
3788
- };
3789
- var activeTui = null;
3790
- var disconnectState = null;
3791
3991
  async function launchTui(finalConfig, urls, greet, tunnel) {
3792
3992
  try {
3793
3993
  const isTTYEnabled = process.stdin.isTTY;
@@ -3930,11 +4130,37 @@ async function startCli(finalConfig, manager) {
3930
4130
  throw err;
3931
4131
  }
3932
4132
  }
4133
+ var import_picocolors3, TunnelData, activeTui, disconnectState;
4134
+ var init_starCli = __esm({
4135
+ "src/cli/starCli.ts"() {
4136
+ "use strict";
4137
+ init_cjs_shims();
4138
+ init_printer();
4139
+ init_TunnelManager();
4140
+ init_getFreePort();
4141
+ init_logger();
4142
+ import_picocolors3 = __toESM(require("picocolors"), 1);
4143
+ init_blessed();
4144
+ TunnelData = {
4145
+ urls: null,
4146
+ greet: null,
4147
+ usage: null
4148
+ };
4149
+ activeTui = null;
4150
+ disconnectState = null;
4151
+ }
4152
+ });
3933
4153
 
3934
- // src/index.ts
3935
- var import_url = require("url");
3936
- var import_process = require("process");
3937
- var import_fs3 = require("fs");
4154
+ // src/main.ts
4155
+ var main_exports = {};
4156
+ __export(main_exports, {
4157
+ TunnelManager: () => TunnelManager,
4158
+ TunnelOperations: () => TunnelOperations,
4159
+ closeRemoteManagement: () => closeRemoteManagement,
4160
+ enablePackageLogging: () => enablePackageLogging,
4161
+ getRemoteManagementState: () => getRemoteManagementState,
4162
+ initiateRemoteManagement: () => initiateRemoteManagement
4163
+ });
3938
4164
  async function main() {
3939
4165
  try {
3940
4166
  const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
@@ -3970,22 +4196,178 @@ async function main() {
3970
4196
  printer_default.error(error);
3971
4197
  }
3972
4198
  }
3973
- var currentFile = (0, import_url.fileURLToPath)(importMetaUrl);
3974
- var entryFile = null;
3975
- try {
3976
- entryFile = import_process.argv[1] ? (0, import_fs3.realpathSync)(import_process.argv[1]) : null;
3977
- } catch (e) {
3978
- entryFile = null;
3979
- }
3980
- if (entryFile && entryFile === currentFile) {
3981
- main();
3982
- }
3983
- // Annotate the CommonJS export names for ESM import in node:
3984
- 0 && (module.exports = {
3985
- TunnelManager,
3986
- TunnelOperations,
3987
- closeRemoteManagement,
3988
- enablePackageLogging,
3989
- getRemoteManagementState,
3990
- initiateRemoteManagement
4199
+ var import_url, import_process, import_fs4, currentFile, entryFile;
4200
+ var init_main = __esm({
4201
+ "src/main.ts"() {
4202
+ "use strict";
4203
+ init_cjs_shims();
4204
+ init_TunnelManager();
4205
+ init_help();
4206
+ init_options();
4207
+ init_buildConfig();
4208
+ init_logger();
4209
+ init_remoteManagement();
4210
+ init_parseArgs();
4211
+ init_printer();
4212
+ init_starCli();
4213
+ init_util();
4214
+ init_handler();
4215
+ import_url = require("url");
4216
+ import_process = require("process");
4217
+ import_fs4 = require("fs");
4218
+ init_logger();
4219
+ init_remoteManagement();
4220
+ currentFile = (0, import_url.fileURLToPath)(importMetaUrl);
4221
+ entryFile = null;
4222
+ try {
4223
+ entryFile = import_process.argv[1] ? (0, import_fs4.realpathSync)(import_process.argv[1]) : null;
4224
+ } catch (e) {
4225
+ entryFile = null;
4226
+ }
4227
+ if (entryFile && entryFile === currentFile) {
4228
+ main();
4229
+ }
4230
+ }
4231
+ });
4232
+
4233
+ // src/index.ts
4234
+ init_cjs_shims();
4235
+
4236
+ // src/utils/detect_vc_redist_on_windows.ts
4237
+ init_cjs_shims();
4238
+ var import_fs = __toESM(require("fs"), 1);
4239
+ var import_path = __toESM(require("path"), 1);
4240
+ var import_child_process = require("child_process");
4241
+ var import_os = __toESM(require("os"), 1);
4242
+ init_printer();
4243
+ var import_util = require("util");
4244
+ var execAsync = (0, import_util.promisify)(import_child_process.exec);
4245
+ var DLLS = ["vcruntime140.dll", "vcruntime140_1.dll", "msvcp140.dll"];
4246
+ var PATHS = ["C:\\Windows\\System32", "C:\\Windows\\SysWOW64"];
4247
+ var REGISTRY_KEYS = [
4248
+ "HKLM\\SOFTWARE\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\X64",
4249
+ "HKLM\\SOFTWARE\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\X86",
4250
+ "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\X64",
4251
+ "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\X86"
4252
+ ];
4253
+ function checkDLLs() {
4254
+ return DLLS.every(
4255
+ (dll) => PATHS.some((p) => {
4256
+ try {
4257
+ return import_fs.default.existsSync(import_path.default.join(p, dll));
4258
+ } catch {
4259
+ return false;
4260
+ }
4261
+ })
4262
+ );
4263
+ }
4264
+ function checkRegistry() {
4265
+ if (import_os.default.platform() !== "win32") return false;
4266
+ try {
4267
+ for (const key of REGISTRY_KEYS) {
4268
+ const cmd = `reg query "${key}" /v Installed 2>nul`;
4269
+ const result = (0, import_child_process.execSync)(cmd, { encoding: "utf8" });
4270
+ if (result.includes("0x1")) {
4271
+ return true;
4272
+ }
4273
+ }
4274
+ } catch {
4275
+ }
4276
+ return false;
4277
+ }
4278
+ function getVCRedistVersion() {
4279
+ if (import_os.default.platform() !== "win32") return null;
4280
+ try {
4281
+ for (const key of REGISTRY_KEYS) {
4282
+ const cmd = `reg query "${key}" /v Version 2>nul`;
4283
+ const result = (0, import_child_process.execSync)(cmd, { encoding: "utf8" });
4284
+ const match = result.match(/Version\s+REG_SZ\s+(\S+)/);
4285
+ if (match) {
4286
+ return match[1];
4287
+ }
4288
+ }
4289
+ } catch {
4290
+ }
4291
+ return null;
4292
+ }
4293
+ function hasVCRedist() {
4294
+ if (import_os.default.platform() !== "win32") {
4295
+ return true;
4296
+ }
4297
+ if (checkRegistry()) {
4298
+ return true;
4299
+ }
4300
+ if (checkDLLs()) {
4301
+ return true;
4302
+ }
4303
+ return false;
4304
+ }
4305
+ function getVCRedistStatus() {
4306
+ if (import_os.default.platform() !== "win32") {
4307
+ return {
4308
+ required: false,
4309
+ installed: true,
4310
+ version: null,
4311
+ method: "non-windows"
4312
+ };
4313
+ }
4314
+ const registryInstalled = checkRegistry();
4315
+ const dllsPresent = checkDLLs();
4316
+ const version = getVCRedistVersion();
4317
+ return {
4318
+ required: true,
4319
+ installed: registryInstalled || dllsPresent,
4320
+ version,
4321
+ registryCheck: registryInstalled,
4322
+ dllCheck: dllsPresent,
4323
+ method: registryInstalled ? "registry" : dllsPresent ? "dll" : "none"
4324
+ };
4325
+ }
4326
+ async function openDownloadPage() {
4327
+ if (process.platform !== "win32") {
4328
+ return;
4329
+ }
4330
+ const url = "https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170";
4331
+ const command = `cmd.exe /c start "" "${url}"`;
4332
+ try {
4333
+ await execAsync(command);
4334
+ printer_default.info("\nOpening Microsoft download page in your browser...");
4335
+ printer_default.info(
4336
+ "Please install the Visual C++ Runtime and restart this application.\n"
4337
+ );
4338
+ } catch (err) {
4339
+ printer_default.info("\nUnable to open your browser automatically.");
4340
+ printer_default.info(
4341
+ "Please visit the following page to download the runtime:\n"
4342
+ );
4343
+ printer_default.info(url + "\n");
4344
+ }
4345
+ }
4346
+ function getVCRedistMessage() {
4347
+ const status = getVCRedistStatus();
4348
+ if (!status.required || status.installed) {
4349
+ return null;
4350
+ }
4351
+ return {
4352
+ error: true,
4353
+ message: "Missing Microsoft Visual C++ Runtime. This application requires the Microsoft Visual C++ Runtime to run on Windows.\nPlease download and install it using the link below, then restart this application.\n"
4354
+ };
4355
+ }
4356
+
4357
+ // src/index.ts
4358
+ init_printer();
4359
+ async function verifyAndLoad() {
4360
+ if (process.platform === "win32" && !hasVCRedist()) {
4361
+ const msg = getVCRedistMessage();
4362
+ printer_default.warn(
4363
+ msg?.message ?? "This application requires the Microsoft Visual C++ Runtime on Windows."
4364
+ );
4365
+ await openDownloadPage();
4366
+ process.exit(1);
4367
+ }
4368
+ await Promise.resolve().then(() => (init_main(), main_exports));
4369
+ }
4370
+ verifyAndLoad().catch((err) => {
4371
+ printer_default.error(`Failed to start CLI:, ${err}`);
4372
+ process.exit(1);
3991
4373
  });