pinggy 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-65R2GMKQ.js +2101 -0
  3. package/dist/index.cjs +1814 -1362
  4. package/dist/index.d.cts +616 -0
  5. package/dist/index.d.ts +616 -0
  6. package/dist/index.js +38 -55
  7. package/dist/{main-CZY6GID4.js → main-2QDG7PWL.js} +229 -1726
  8. package/package.json +3 -4
  9. package/.github/workflows/npm-publish-github-packages.yml +0 -34
  10. package/.github/workflows/publish-binaries.yml +0 -223
  11. package/Makefile +0 -4
  12. package/caxa_build.js +0 -24
  13. package/dist/chunk-T5ESYDJY.js +0 -121
  14. package/ent.plist +0 -14
  15. package/jest.config.js +0 -19
  16. package/src/_tests_/build_config.test.ts +0 -91
  17. package/src/cli/buildConfig.ts +0 -475
  18. package/src/cli/defaults.ts +0 -20
  19. package/src/cli/extendedOptions.ts +0 -153
  20. package/src/cli/help.ts +0 -43
  21. package/src/cli/options.ts +0 -50
  22. package/src/cli/starCli.ts +0 -229
  23. package/src/index.ts +0 -30
  24. package/src/logger.ts +0 -138
  25. package/src/main.ts +0 -87
  26. package/src/remote_management/handler.ts +0 -244
  27. package/src/remote_management/remoteManagement.ts +0 -226
  28. package/src/remote_management/remote_schema.ts +0 -176
  29. package/src/remote_management/websocket_handlers.ts +0 -180
  30. package/src/tui/blessed/TunnelTui.ts +0 -340
  31. package/src/tui/blessed/components/DisplayUpdaters.ts +0 -189
  32. package/src/tui/blessed/components/KeyBindings.ts +0 -236
  33. package/src/tui/blessed/components/Modals.ts +0 -302
  34. package/src/tui/blessed/components/UIComponents.ts +0 -306
  35. package/src/tui/blessed/components/index.ts +0 -4
  36. package/src/tui/blessed/config.ts +0 -53
  37. package/src/tui/blessed/headerFetcher.ts +0 -42
  38. package/src/tui/blessed/index.ts +0 -2
  39. package/src/tui/blessed/qrCodeGenerator.ts +0 -20
  40. package/src/tui/blessed/webDebuggerConnection.ts +0 -128
  41. package/src/tui/ink/asciArt.ts +0 -7
  42. package/src/tui/ink/hooks/useQrCodes.ts +0 -27
  43. package/src/tui/ink/hooks/useReqResHeaders.ts +0 -27
  44. package/src/tui/ink/hooks/useTerminalSize.ts +0 -26
  45. package/src/tui/ink/hooks/useTerminalStats.ts +0 -24
  46. package/src/tui/ink/hooks/useWebDebugger.ts +0 -98
  47. package/src/tui/ink/index.tsx +0 -243
  48. package/src/tui/ink/layout/Borders.tsx +0 -15
  49. package/src/tui/ink/layout/Container.tsx +0 -15
  50. package/src/tui/ink/sections/DebuggerDetailModal.tsx +0 -53
  51. package/src/tui/ink/sections/KeyBindings.tsx +0 -58
  52. package/src/tui/ink/sections/QrCodeSection.tsx +0 -28
  53. package/src/tui/ink/sections/StatsSection.tsx +0 -20
  54. package/src/tui/ink/sections/URLsSection.tsx +0 -53
  55. package/src/tui/ink/utils/utils.ts +0 -35
  56. package/src/tui/spinner/spinner.ts +0 -64
  57. package/src/tunnel_manager/TunnelManager.ts +0 -1212
  58. package/src/types.ts +0 -255
  59. package/src/utils/FileServer.ts +0 -112
  60. package/src/utils/detect_vc_redist_on_windows.ts +0 -167
  61. package/src/utils/getFreePort.ts +0 -41
  62. package/src/utils/htmlTemplates.ts +0 -146
  63. package/src/utils/parseArgs.ts +0 -79
  64. package/src/utils/printer.ts +0 -81
  65. package/src/utils/util.ts +0 -18
  66. package/src/workers/file_serve_worker.ts +0 -33
  67. package/tsconfig.json +0 -17
  68. package/tsup.config.ts +0 -12
@@ -0,0 +1,2101 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-HUN2MRZO.js";
4
+
5
+ // src/utils/printer.ts
6
+ import pico2 from "picocolors";
7
+
8
+ // src/tui/spinner/spinner.ts
9
+ import pico from "picocolors";
10
+ var spinners = {
11
+ dots: {
12
+ interval: 80,
13
+ frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]
14
+ }
15
+ };
16
+ var currentTimer = null;
17
+ var currentText = "";
18
+ function startSpinner(name = "dots", text = "Loading") {
19
+ const spinner = spinners[name];
20
+ let i = 0;
21
+ currentText = text;
22
+ if (currentTimer) {
23
+ clearInterval(currentTimer);
24
+ }
25
+ currentTimer = setInterval(() => {
26
+ const frame = spinner.frames[i = ++i % spinner.frames.length];
27
+ process.stdout.write(`\r${pico.cyan(frame)} ${text}`);
28
+ }, spinner.interval);
29
+ return () => stopSpinner();
30
+ }
31
+ function stopSpinner() {
32
+ if (currentTimer) {
33
+ clearInterval(currentTimer);
34
+ currentTimer = null;
35
+ process.stdout.write("\r\x1B[K");
36
+ }
37
+ }
38
+ function stopSpinnerSuccess(message) {
39
+ if (currentTimer) {
40
+ clearInterval(currentTimer);
41
+ currentTimer = null;
42
+ const finalMessage = message || currentText;
43
+ process.stdout.write(`\r${pico.green("\u2714")} ${finalMessage}
44
+ `);
45
+ }
46
+ }
47
+ function stopSpinnerFail(message) {
48
+ if (currentTimer) {
49
+ clearInterval(currentTimer);
50
+ currentTimer = null;
51
+ const finalMessage = message || currentText;
52
+ process.stdout.write(`\r${pico.red("\u2716")} ${finalMessage}
53
+ `);
54
+ }
55
+ }
56
+
57
+ // src/utils/printer.ts
58
+ var _CLIPrinter = class _CLIPrinter {
59
+ static isCLIError(err) {
60
+ return err instanceof Error;
61
+ }
62
+ static print(message, ...args) {
63
+ console.log(message, ...args);
64
+ }
65
+ static error(err) {
66
+ const def = this.errorDefinitions.find((d) => d.match(err));
67
+ const msg = def.message(err);
68
+ console.error(pico2.red(pico2.bold("\u2716 Error:")), pico2.red(msg));
69
+ process.exit(1);
70
+ }
71
+ static warn(message) {
72
+ console.warn(pico2.yellow(pico2.bold("\u26A0 Warning:")), pico2.yellow(message));
73
+ }
74
+ static warnTxt(message) {
75
+ console.warn(pico2.yellow(pico2.bold("\u26A0 Warning:")), pico2.yellow(message));
76
+ }
77
+ static success(message) {
78
+ console.log(pico2.green(pico2.bold(" \u2714 Success:")), pico2.green(message));
79
+ }
80
+ static async info(message) {
81
+ console.log(pico2.blue(message));
82
+ }
83
+ static startSpinner(message) {
84
+ startSpinner("dots", message);
85
+ }
86
+ static stopSpinnerSuccess(message) {
87
+ stopSpinnerSuccess(message);
88
+ }
89
+ static stopSpinnerFail(message) {
90
+ stopSpinnerFail(message);
91
+ }
92
+ };
93
+ _CLIPrinter.errorDefinitions = [
94
+ {
95
+ match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION",
96
+ message: (err) => {
97
+ const match = /Unknown option '(.+?)'/.exec(err.message);
98
+ const option = match ? match[1] : "(unknown)";
99
+ return `Unknown option '${option}'. Please check your command or use pinggy --h for guidance.`;
100
+ }
101
+ },
102
+ {
103
+ match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_MISSING_OPTION_VALUE",
104
+ message: (err) => `Missing required argument for option '${err.option}'.`
105
+ },
106
+ {
107
+ match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE",
108
+ message: (err) => `Invalid argument'${err.message}'.`
109
+ },
110
+ {
111
+ match: (err) => _CLIPrinter.isCLIError(err) && err.code === "ENOENT",
112
+ message: (err) => `File or directory not found: ${err.message}`
113
+ },
114
+ {
115
+ match: () => true,
116
+ // fallback
117
+ message: (err) => _CLIPrinter.isCLIError(err) ? err.message : String(err)
118
+ }
119
+ ];
120
+ var CLIPrinter = _CLIPrinter;
121
+ var printer_default = CLIPrinter;
122
+
123
+ // src/utils/util.ts
124
+ import { createRequire } from "module";
125
+ import { randomUUID } from "crypto";
126
+ function getRandomId() {
127
+ return randomUUID();
128
+ }
129
+ function isValidPort(p) {
130
+ return Number.isInteger(p) && p > 0 && p < 65536;
131
+ }
132
+ var require2 = createRequire(import.meta.url);
133
+ var pkg = require2("../package.json");
134
+ function getVersion() {
135
+ return pkg.version ?? "";
136
+ }
137
+
138
+ // src/tunnel_manager/TunnelManager.ts
139
+ import { pinggy } from "@pinggy/pinggy";
140
+ import path from "path";
141
+ import { Worker } from "worker_threads";
142
+ import { fileURLToPath } from "url";
143
+ var __filename2 = fileURLToPath(import.meta.url);
144
+ var __dirname2 = path.dirname(__filename2);
145
+ var TunnelManager = class _TunnelManager {
146
+ constructor() {
147
+ this.tunnelsByTunnelId = /* @__PURE__ */ new Map();
148
+ this.tunnelsByConfigId = /* @__PURE__ */ new Map();
149
+ this.tunnelStats = /* @__PURE__ */ new Map();
150
+ this.tunnelStatsListeners = /* @__PURE__ */ new Map();
151
+ this.tunnelErrorListeners = /* @__PURE__ */ new Map();
152
+ this.tunnelDisconnectListeners = /* @__PURE__ */ new Map();
153
+ this.tunnelWorkerErrorListeners = /* @__PURE__ */ new Map();
154
+ this.tunnelStartListeners = /* @__PURE__ */ new Map();
155
+ this.tunnelWillReconnectListeners = /* @__PURE__ */ new Map();
156
+ this.tunnelReconnectingListeners = /* @__PURE__ */ new Map();
157
+ this.tunnelReconnectionCompletedListeners = /* @__PURE__ */ new Map();
158
+ this.tunnelReconnectionFailedListeners = /* @__PURE__ */ new Map();
159
+ }
160
+ static getInstance() {
161
+ if (!_TunnelManager.instance) {
162
+ _TunnelManager.instance = new _TunnelManager();
163
+ }
164
+ return _TunnelManager.instance;
165
+ }
166
+ /**
167
+ * Creates a new managed tunnel instance with the given configuration.
168
+ * Builds the config with forwarding rules and creates the tunnel instance.
169
+ *
170
+ * @param config - The tunnel configuration options
171
+ * @param config.configid - Unique identifier for the tunnel configuration
172
+ * @param config.tunnelid - Optional custom tunnel identifier. If not provided, a random UUID will be generated
173
+ * @param config.additionalForwarding - Optional array of additional forwarding configurations
174
+ *
175
+ * @throws {Error} When configId is invalid or empty
176
+ * @throws {Error} When a tunnel with the given configId already exists
177
+ *
178
+ * @returns {ManagedTunnel} A new managed tunnel instance containing the tunnel details,
179
+ * status information, and statistics
180
+ */
181
+ async createTunnel(config) {
182
+ const { configid, additionalForwarding, tunnelName } = config;
183
+ if (configid === void 0 || configid.trim().length === 0) {
184
+ throw new Error(`Invalid configId: "${configid}"`);
185
+ }
186
+ if (this.tunnelsByConfigId.has(configid)) {
187
+ throw new Error(`Tunnel with configId "${configid}" already exists`);
188
+ }
189
+ const tunnelid = config.tunnelid || getRandomId();
190
+ const configWithForwarding = this.buildPinggyConfig(config, additionalForwarding);
191
+ return this._createTunnelWithProcessedConfig({
192
+ configid,
193
+ tunnelid,
194
+ tunnelName,
195
+ originalConfig: config,
196
+ configWithForwarding,
197
+ additionalForwarding,
198
+ serve: config.serve,
199
+ autoReconnect: config.autoReconnect !== void 0 ? config.autoReconnect : false
200
+ });
201
+ }
202
+ /**
203
+ * Internal method to create a tunnel with an already-processed configuration.
204
+ * This is used by createTunnel, restartTunnel, and updateConfig to avoid config processing.
205
+ *
206
+ * @param params - Configuration parameters with already-processed forwarding rules
207
+ * @returns The created ManagedTunnel instance
208
+ * @private
209
+ */
210
+ async _createTunnelWithProcessedConfig(params) {
211
+ let instance;
212
+ try {
213
+ instance = await pinggy.createTunnel(params.configWithForwarding);
214
+ } catch (e) {
215
+ logger.error("Error creating tunnel instance:", e);
216
+ throw e;
217
+ }
218
+ const now = (/* @__PURE__ */ new Date()).toISOString();
219
+ const managed = {
220
+ tunnelid: params.tunnelid,
221
+ configid: params.configid,
222
+ tunnelName: params.tunnelName,
223
+ instance,
224
+ tunnelConfig: params.originalConfig,
225
+ configWithForwarding: params.configWithForwarding,
226
+ additionalForwarding: params.additionalForwarding,
227
+ serve: params.serve,
228
+ warnings: [],
229
+ isStopped: false,
230
+ createdAt: now,
231
+ startedAt: null,
232
+ stoppedAt: null,
233
+ autoReconnect: params.autoReconnect
234
+ };
235
+ instance.setTunnelEstablishedCallback(({}) => {
236
+ managed.startedAt = (/* @__PURE__ */ new Date()).toISOString();
237
+ });
238
+ this.setupStatsCallback(params.tunnelid, managed);
239
+ this.setupErrorCallback(params.tunnelid, managed);
240
+ this.setupDisconnectCallback(params.tunnelid, managed);
241
+ this.setupWillReconnectCallback(params.tunnelid, managed);
242
+ this.setupReconnectingCallback(params.tunnelid, managed);
243
+ this.setupReconnectionCompletedCallback(params.tunnelid, managed);
244
+ this.setupReconnectionFailedCallback(params.tunnelid, managed);
245
+ this.setUpTunnelWorkerErrorCallback(params.tunnelid, managed);
246
+ this.tunnelsByTunnelId.set(params.tunnelid, managed);
247
+ this.tunnelsByConfigId.set(params.configid, managed);
248
+ logger.info("Tunnel created", { configid: params.configid, tunnelid: params.tunnelid });
249
+ return managed;
250
+ }
251
+ /**
252
+ * Builds the Pinggy configuration by merging the default forwarding rule
253
+ * with additional forwarding rules from additionalForwarding array.
254
+ *
255
+ * @param config - The base Pinggy configuration
256
+ * @param additionalForwarding - Optional array of additional forwarding rules
257
+ * @returns Modified PinggyOptions
258
+ */
259
+ buildPinggyConfig(config, additionalForwarding) {
260
+ const forwardingRules = [];
261
+ if (config.forwarding) {
262
+ forwardingRules.push({
263
+ type: config.tunnelType && config.tunnelType[0] || "http",
264
+ address: config.forwarding
265
+ });
266
+ }
267
+ if (Array.isArray(additionalForwarding) && additionalForwarding.length > 0) {
268
+ for (const rule of additionalForwarding) {
269
+ if (rule && rule.localDomain && rule.localPort && rule.remoteDomain && isValidPort(rule.localPort)) {
270
+ const forwardingRule = {
271
+ type: rule.protocol,
272
+ // In Future we can make this dynamic based on user input
273
+ address: `${rule.localDomain}:${rule.localPort}`,
274
+ listenAddress: rule.remotePort && isValidPort(rule.remotePort) ? `${rule.remoteDomain}:${rule.remotePort}` : rule.remoteDomain
275
+ };
276
+ forwardingRules.push(forwardingRule);
277
+ }
278
+ }
279
+ }
280
+ return {
281
+ ...config,
282
+ forwarding: forwardingRules.length > 0 ? forwardingRules : config.forwarding
283
+ };
284
+ }
285
+ /**
286
+ * Start a tunnel that was created but not yet started
287
+ */
288
+ async startTunnel(tunnelId) {
289
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
290
+ if (!managed) throw new Error(`Tunnel with id "${tunnelId}" not found`);
291
+ logger.info("Starting tunnel", { tunnelId });
292
+ let urls;
293
+ try {
294
+ urls = await managed.instance.start();
295
+ } catch (error) {
296
+ logger.error("Failed to start tunnel", { tunnelId, error });
297
+ throw error;
298
+ }
299
+ logger.info("Tunnel started", { tunnelId, urls });
300
+ if (managed.serve) {
301
+ this.startStaticFileServer(managed);
302
+ }
303
+ try {
304
+ const startListeners = this.tunnelStartListeners.get(tunnelId);
305
+ if (startListeners) {
306
+ for (const [id, listener] of startListeners) {
307
+ try {
308
+ listener(tunnelId, urls);
309
+ } catch (err) {
310
+ logger.debug("Error in start-listener callback", { listenerId: id, tunnelId, err });
311
+ }
312
+ }
313
+ }
314
+ } catch (e) {
315
+ logger.warn("Failed to notify start listeners", { tunnelId, e });
316
+ }
317
+ return urls;
318
+ }
319
+ /**
320
+ * Stops a running tunnel and updates its status.
321
+ *
322
+ * @param tunnelId - The unique identifier of the tunnel to stop
323
+ * @throws {Error} If the tunnel with the given tunnelId is not found
324
+ * @remarks
325
+ * - Clears the tunnel's remote URLs
326
+ * - Updates the tunnel's state to Exited if stopped successfully
327
+ * - Logs the stop operation with tunnelId and configId
328
+ */
329
+ stopTunnel(tunnelId) {
330
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
331
+ if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
332
+ logger.info("Stopping tunnel", { tunnelId, configId: managed.configid });
333
+ try {
334
+ managed.instance.stop();
335
+ if (managed.serveWorker) {
336
+ logger.info("terminating serveWorker");
337
+ managed.serveWorker.terminate();
338
+ }
339
+ this.tunnelStats.delete(tunnelId);
340
+ this.tunnelStatsListeners.delete(tunnelId);
341
+ this.tunnelStats.delete(tunnelId);
342
+ this.tunnelStatsListeners.delete(tunnelId);
343
+ this.tunnelErrorListeners.delete(tunnelId);
344
+ this.tunnelDisconnectListeners.delete(tunnelId);
345
+ this.tunnelWorkerErrorListeners.delete(tunnelId);
346
+ this.tunnelStartListeners.delete(tunnelId);
347
+ this.tunnelWillReconnectListeners.delete(tunnelId);
348
+ this.tunnelReconnectingListeners.delete(tunnelId);
349
+ this.tunnelReconnectionCompletedListeners.delete(tunnelId);
350
+ this.tunnelReconnectionFailedListeners.delete(tunnelId);
351
+ managed.serveWorker = null;
352
+ managed.warnings = managed.warnings ?? [];
353
+ managed.isStopped = true;
354
+ managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
355
+ logger.info("Tunnel stopped", { tunnelId, configId: managed.configid });
356
+ return { configid: managed.configid, tunnelid: managed.tunnelid };
357
+ } catch (error) {
358
+ logger.error("Failed to stop tunnel", { tunnelId, error });
359
+ throw error;
360
+ }
361
+ }
362
+ /**
363
+ * Get all public URLs for a tunnel
364
+ */
365
+ async getTunnelUrls(tunnelId) {
366
+ try {
367
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
368
+ if (!managed || managed.isStopped) {
369
+ logger.error(`Tunnel "${tunnelId}" not found when fetching URLs`);
370
+ return [];
371
+ }
372
+ const urls = await managed.instance.urls();
373
+ logger.debug("Queried tunnel URLs", { tunnelId, urls });
374
+ return urls;
375
+ } catch (error) {
376
+ logger.error("Error fetching tunnel URLs", { tunnelId, error });
377
+ throw error;
378
+ }
379
+ }
380
+ /**
381
+ * Get all TunnelStatus currently managed by this TunnelManager
382
+ * @returns An array of all TunnelStatus objects
383
+ */
384
+ async getAllTunnels() {
385
+ try {
386
+ const tunnelList = await Promise.all(Array.from(this.tunnelsByTunnelId.values()).map(async (tunnel) => {
387
+ return {
388
+ tunnelid: tunnel.tunnelid,
389
+ configid: tunnel.configid,
390
+ tunnelName: tunnel.tunnelName,
391
+ tunnelConfig: tunnel.tunnelConfig,
392
+ remoteurls: !tunnel.isStopped ? await this.getTunnelUrls(tunnel.tunnelid) : [],
393
+ additionalForwarding: tunnel.additionalForwarding,
394
+ serve: tunnel.serve
395
+ };
396
+ }));
397
+ return tunnelList;
398
+ } catch (err) {
399
+ logger.error("Error fetching tunnels", { error: err });
400
+ return [];
401
+ }
402
+ }
403
+ /**
404
+ * Get status of a tunnel
405
+ */
406
+ async getTunnelStatus(tunnelId) {
407
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
408
+ if (!managed) {
409
+ logger.error(`Tunnel "${tunnelId}" not found when fetching status`);
410
+ throw new Error(`Tunnel "${tunnelId}" not found`);
411
+ }
412
+ if (managed.isStopped) {
413
+ return "exited";
414
+ }
415
+ const status = await managed.instance.getStatus();
416
+ logger.debug("Queried tunnel status", { tunnelId, status });
417
+ return status;
418
+ }
419
+ /**
420
+ * Stop all tunnels
421
+ */
422
+ stopAllTunnels() {
423
+ for (const { instance } of this.tunnelsByTunnelId.values()) {
424
+ try {
425
+ instance.stop();
426
+ } catch (e) {
427
+ logger.warn("Error stopping tunnel instance", e);
428
+ }
429
+ }
430
+ this.tunnelsByTunnelId.clear();
431
+ this.tunnelsByConfigId.clear();
432
+ this.tunnelStats.clear();
433
+ this.tunnelStatsListeners.clear();
434
+ logger.info("All tunnels stopped and cleared");
435
+ }
436
+ /**
437
+ * Remove a stopped tunnel's records so it will no longer be returned by list methods.
438
+ *
439
+ *
440
+ * @param tunnelId - the tunnel id to remove
441
+ * @returns true if the record was removed, false otherwise
442
+ */
443
+ removeStoppedTunnelByTunnelId(tunnelId) {
444
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
445
+ if (!managed) {
446
+ logger.debug("Attempted to remove non-existent tunnel", { tunnelId });
447
+ return false;
448
+ }
449
+ if (!managed.isStopped) {
450
+ logger.warn("Attempted to remove tunnel that is not stopped", { tunnelId });
451
+ return false;
452
+ }
453
+ this._cleanupTunnelRecords(managed);
454
+ logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.configid });
455
+ return true;
456
+ }
457
+ /**
458
+ * Remove a stopped tunnel by its config id.
459
+ * @param configId - the config id to remove
460
+ * @returns true if the record was removed, false otherwise
461
+ */
462
+ removeStoppedTunnelByConfigId(configId) {
463
+ const managed = this.tunnelsByConfigId.get(configId);
464
+ if (!managed) {
465
+ logger.debug("Attempted to remove non-existent tunnel by configId", { configId });
466
+ return false;
467
+ }
468
+ return this.removeStoppedTunnelByTunnelId(managed.tunnelid);
469
+ }
470
+ _cleanupTunnelRecords(managed) {
471
+ if (!managed.isStopped) {
472
+ throw new Error(`Active tunnel "${managed.tunnelid}" cannot be removed`);
473
+ }
474
+ try {
475
+ if (managed.serveWorker) {
476
+ managed.serveWorker = null;
477
+ }
478
+ this.tunnelStats.delete(managed.tunnelid);
479
+ this.tunnelStatsListeners.delete(managed.tunnelid);
480
+ this.tunnelErrorListeners.delete(managed.tunnelid);
481
+ this.tunnelDisconnectListeners.delete(managed.tunnelid);
482
+ this.tunnelWorkerErrorListeners.delete(managed.tunnelid);
483
+ this.tunnelStartListeners.delete(managed.tunnelid);
484
+ this.tunnelWillReconnectListeners.delete(managed.tunnelid);
485
+ this.tunnelReconnectingListeners.delete(managed.tunnelid);
486
+ this.tunnelReconnectionCompletedListeners.delete(managed.tunnelid);
487
+ this.tunnelReconnectionFailedListeners.delete(managed.tunnelid);
488
+ this.tunnelsByTunnelId.delete(managed.tunnelid);
489
+ this.tunnelsByConfigId.delete(managed.configid);
490
+ } catch (e) {
491
+ logger.warn("Failed cleaning up tunnel records", { tunnelId: managed.tunnelid, error: e });
492
+ }
493
+ }
494
+ /**
495
+ * Get tunnel instance by either configId or tunnelId
496
+ * @param configId - The configuration ID of the tunnel
497
+ * @param tunnelId - The tunnel ID
498
+ * @returns The tunnel instance
499
+ * @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
500
+ */
501
+ getTunnelInstance(configId, tunnelId) {
502
+ if (configId) {
503
+ const managed = this.tunnelsByConfigId.get(configId);
504
+ if (!managed) throw new Error(`Tunnel "${configId}" not found`);
505
+ return managed.instance;
506
+ }
507
+ if (tunnelId) {
508
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
509
+ if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
510
+ return managed.instance;
511
+ }
512
+ throw new Error(`Either configId or tunnelId must be provided`);
513
+ }
514
+ /**
515
+ * Get tunnel config by either configId or tunnelId
516
+ * @param configId - The configuration ID of the tunnel
517
+ * @param tunnelId - The tunnel ID
518
+ * @returns The tunnel config
519
+ * @throws Error if neither configId nor tunnelId is provided, or if tunnel is not found
520
+ */
521
+ async getTunnelConfig(configId, tunnelId) {
522
+ if (configId) {
523
+ const managed = this.tunnelsByConfigId.get(configId);
524
+ if (!managed) {
525
+ throw new Error(`Tunnel with configId "${configId}" not found`);
526
+ }
527
+ return managed.instance.getConfig();
528
+ }
529
+ if (tunnelId) {
530
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
531
+ if (!managed) {
532
+ throw new Error(`Tunnel with tunnelId "${tunnelId}" not found`);
533
+ }
534
+ return managed.instance.getConfig();
535
+ }
536
+ throw new Error(`Either configId or tunnelId must be provided`);
537
+ }
538
+ /**
539
+ * Restarts a tunnel with its current configuration.
540
+ * This function will stop the tunnel if it's running and start it again.
541
+ * All configurations including additional forwarding rules are preserved.
542
+ */
543
+ async restartTunnel(tunnelid) {
544
+ const existingTunnel = this.tunnelsByTunnelId.get(tunnelid);
545
+ if (!existingTunnel) {
546
+ throw new Error(`Tunnel "${tunnelid}" not found`);
547
+ }
548
+ logger.info("Initiating tunnel restart", {
549
+ tunnelId: tunnelid,
550
+ configId: existingTunnel.configid
551
+ });
552
+ try {
553
+ const tunnelName = existingTunnel.tunnelName;
554
+ const currentConfigId = existingTunnel.configid;
555
+ const currentConfig = existingTunnel.tunnelConfig;
556
+ const configWithForwarding = existingTunnel.configWithForwarding;
557
+ const additionalForwarding = existingTunnel.additionalForwarding;
558
+ const currentServe = existingTunnel.serve;
559
+ const autoReconnect = existingTunnel.autoReconnect || false;
560
+ this.tunnelsByTunnelId.delete(tunnelid);
561
+ this.tunnelsByConfigId.delete(existingTunnel.configid);
562
+ this.tunnelStats.delete(tunnelid);
563
+ this.tunnelStatsListeners.delete(tunnelid);
564
+ this.tunnelErrorListeners.delete(tunnelid);
565
+ this.tunnelDisconnectListeners.delete(tunnelid);
566
+ this.tunnelWorkerErrorListeners.delete(tunnelid);
567
+ this.tunnelStartListeners.delete(tunnelid);
568
+ this.tunnelWillReconnectListeners.delete(tunnelid);
569
+ this.tunnelReconnectingListeners.delete(tunnelid);
570
+ this.tunnelReconnectionCompletedListeners.delete(tunnelid);
571
+ this.tunnelReconnectionFailedListeners.delete(tunnelid);
572
+ const newTunnel = await this._createTunnelWithProcessedConfig({
573
+ configid: currentConfigId,
574
+ tunnelid,
575
+ tunnelName,
576
+ originalConfig: currentConfig,
577
+ configWithForwarding,
578
+ additionalForwarding,
579
+ serve: currentServe,
580
+ autoReconnect
581
+ });
582
+ if (existingTunnel.createdAt) {
583
+ newTunnel.createdAt = existingTunnel.createdAt;
584
+ }
585
+ await this.startTunnel(newTunnel.tunnelid);
586
+ } catch (error) {
587
+ logger.error("Failed to restart tunnel", {
588
+ tunnelid,
589
+ error: error instanceof Error ? error.message : "Unknown error"
590
+ });
591
+ throw new Error(`Failed to restart tunnel: ${error instanceof Error ? error.message : "Unknown error"}`);
592
+ }
593
+ }
594
+ /**
595
+ * Updates the configuration of an existing tunnel.
596
+ *
597
+ * This method handles the process of updating a tunnel's configuration while preserving
598
+ * its state. If the tunnel is running, it will be stopped, updated, and restarted.
599
+ * In case of failure, it attempts to restore the original configuration.
600
+ *
601
+ * @param newConfig - The new configuration to apply, including configid and optional additional forwarding
602
+ *
603
+ * @returns Promise resolving to the updated ManagedTunnel
604
+ * @throws Error if the tunnel is not found or if the update process fails
605
+ */
606
+ async updateConfig(newConfig) {
607
+ const { configid, tunnelName: newTunnelName, additionalForwarding } = newConfig;
608
+ if (!configid || configid.trim().length === 0) {
609
+ throw new Error(`Invalid configid: "${configid}"`);
610
+ }
611
+ const existingTunnel = this.tunnelsByConfigId.get(configid);
612
+ if (!existingTunnel) {
613
+ throw new Error(`Tunnel with config id "${configid}" not found`);
614
+ }
615
+ const isStopped = existingTunnel.isStopped;
616
+ const currentTunnelConfig = existingTunnel.tunnelConfig;
617
+ const currentConfigWithForwarding = existingTunnel.configWithForwarding;
618
+ const currentTunnelId = existingTunnel.tunnelid;
619
+ const currentTunnelConfigId = existingTunnel.configid;
620
+ const currentAdditionalForwarding = existingTunnel.additionalForwarding;
621
+ const currentTunnelName = existingTunnel.tunnelName;
622
+ const currentServe = existingTunnel.serve;
623
+ const currentAutoReconnect = existingTunnel.autoReconnect || false;
624
+ try {
625
+ if (!isStopped) {
626
+ existingTunnel.instance.stop();
627
+ }
628
+ this.tunnelsByTunnelId.delete(currentTunnelId);
629
+ this.tunnelsByConfigId.delete(currentTunnelConfigId);
630
+ const mergedBaseConfig = {
631
+ ...newConfig,
632
+ configid,
633
+ tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
634
+ serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe
635
+ };
636
+ const newConfigWithForwarding = this.buildPinggyConfig(
637
+ mergedBaseConfig,
638
+ additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding
639
+ );
640
+ const newTunnel = await this._createTunnelWithProcessedConfig({
641
+ configid,
642
+ tunnelid: currentTunnelId,
643
+ tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
644
+ originalConfig: mergedBaseConfig,
645
+ configWithForwarding: newConfigWithForwarding,
646
+ additionalForwarding: additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding,
647
+ serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe,
648
+ autoReconnect: currentAutoReconnect
649
+ });
650
+ if (!isStopped) {
651
+ await this.startTunnel(newTunnel.tunnelid);
652
+ }
653
+ logger.info("Tunnel configuration updated", {
654
+ tunnelId: newTunnel.tunnelid,
655
+ configId: newTunnel.configid,
656
+ isStopped
657
+ });
658
+ return newTunnel;
659
+ } catch (error) {
660
+ logger.error("Error updating tunnel configuration", {
661
+ configId: configid,
662
+ error: error instanceof Error ? error.message : String(error)
663
+ });
664
+ try {
665
+ const originalTunnel = await this._createTunnelWithProcessedConfig({
666
+ configid: currentTunnelConfigId,
667
+ tunnelid: currentTunnelId,
668
+ tunnelName: currentTunnelName,
669
+ originalConfig: currentTunnelConfig,
670
+ configWithForwarding: currentConfigWithForwarding,
671
+ additionalForwarding: currentAdditionalForwarding,
672
+ serve: currentServe,
673
+ autoReconnect: currentAutoReconnect
674
+ });
675
+ if (!isStopped) {
676
+ await this.startTunnel(originalTunnel.tunnelid);
677
+ }
678
+ logger.warn("Restored original tunnel configuration after update failure", {
679
+ currentTunnelId,
680
+ error: error instanceof Error ? error.message : "Unknown error"
681
+ });
682
+ } catch (restoreError) {
683
+ logger.error("Failed to restore original tunnel configuration", {
684
+ currentTunnelId,
685
+ error: restoreError instanceof Error ? restoreError.message : "Unknown error"
686
+ });
687
+ }
688
+ throw error;
689
+ }
690
+ }
691
+ /**
692
+ * Retrieve the ManagedTunnel object by either configId or tunnelId.
693
+ * Throws an error if neither id is provided or the tunnel is not found.
694
+ */
695
+ getManagedTunnel(configId, tunnelId) {
696
+ if (configId) {
697
+ const managed = this.tunnelsByConfigId.get(configId);
698
+ if (!managed) throw new Error(`Tunnel "${configId}" not found`);
699
+ return managed;
700
+ }
701
+ if (tunnelId) {
702
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
703
+ if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
704
+ return managed;
705
+ }
706
+ throw new Error(`Either configId or tunnelId must be provided`);
707
+ }
708
+ async getTunnelGreetMessage(tunnelId) {
709
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
710
+ if (!managed) {
711
+ logger.error(`Tunnel "${tunnelId}" not found when fetching greet message`);
712
+ return null;
713
+ }
714
+ try {
715
+ if (managed.isStopped) {
716
+ return null;
717
+ }
718
+ const messages = await managed.instance.getGreetMessage();
719
+ if (Array.isArray(messages)) {
720
+ return messages.join(" ");
721
+ }
722
+ return messages ?? null;
723
+ } catch (e) {
724
+ logger.error(
725
+ `Error fetching greet message for tunnel "${tunnelId}": ${e instanceof Error ? e.message : String(e)}`
726
+ );
727
+ return null;
728
+ }
729
+ }
730
+ getTunnelStats(tunnelId) {
731
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
732
+ if (!managed) {
733
+ return null;
734
+ }
735
+ const stats = this.tunnelStats.get(tunnelId);
736
+ return stats || null;
737
+ }
738
+ getLatestTunnelStats(tunnelId) {
739
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
740
+ if (!managed) {
741
+ return null;
742
+ }
743
+ const stats = this.tunnelStats.get(tunnelId);
744
+ if (stats && stats.length > 0) {
745
+ return stats[stats.length - 1];
746
+ }
747
+ return null;
748
+ }
749
+ /**
750
+ * Registers a listener function to receive tunnel statistics updates.
751
+ * The listener will be called whenever any tunnel's stats are updated.
752
+ *
753
+ * @param tunnelId - The tunnel ID to listen to stats for
754
+ * @param listener - Function that receives tunnelId and stats when updates occur
755
+ * @returns A unique listener ID that can be used to deregister the listener and tunnelId
756
+ *
757
+ * @throws {Error} When the specified tunnelId does not exist
758
+ */
759
+ async registerStatsListener(tunnelId, listener) {
760
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
761
+ if (!managed) {
762
+ throw new Error(`Tunnel "${tunnelId}" not found`);
763
+ }
764
+ if (!this.tunnelStatsListeners.has(tunnelId)) {
765
+ this.tunnelStatsListeners.set(tunnelId, /* @__PURE__ */ new Map());
766
+ }
767
+ const listenerId = getRandomId();
768
+ const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
769
+ tunnelListeners.set(listenerId, listener);
770
+ logger.info("Stats listener registered for tunnel", { tunnelId, listenerId });
771
+ return [listenerId, tunnelId];
772
+ }
773
+ async registerErrorListener(tunnelId, listener) {
774
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
775
+ if (!managed) {
776
+ throw new Error(`Tunnel "${tunnelId}" not found`);
777
+ }
778
+ if (!this.tunnelErrorListeners.has(tunnelId)) {
779
+ this.tunnelErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
780
+ }
781
+ const listenerId = getRandomId();
782
+ const tunnelErrorListeners = this.tunnelErrorListeners.get(tunnelId);
783
+ tunnelErrorListeners.set(listenerId, listener);
784
+ logger.info("Error listener registered for tunnel", { tunnelId, listenerId });
785
+ return listenerId;
786
+ }
787
+ async registerDisconnectListener(tunnelId, listener) {
788
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
789
+ if (!managed) {
790
+ throw new Error(`Tunnel "${tunnelId}" not found`);
791
+ }
792
+ if (!this.tunnelDisconnectListeners.has(tunnelId)) {
793
+ this.tunnelDisconnectListeners.set(tunnelId, /* @__PURE__ */ new Map());
794
+ }
795
+ const listenerId = getRandomId();
796
+ const tunnelDisconnectListeners = this.tunnelDisconnectListeners.get(tunnelId);
797
+ tunnelDisconnectListeners.set(listenerId, listener);
798
+ logger.info("Disconnect listener registered for tunnel", { tunnelId, listenerId });
799
+ return listenerId;
800
+ }
801
+ async registerWorkerErrorListner(tunnelId, listener) {
802
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
803
+ if (!managed) {
804
+ throw new Error(`Tunnel "${tunnelId}" not found`);
805
+ }
806
+ if (!this.tunnelWorkerErrorListeners.has(tunnelId)) {
807
+ this.tunnelWorkerErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
808
+ }
809
+ const listenerId = getRandomId();
810
+ const tunnelWorkerErrorListner = this.tunnelWorkerErrorListeners.get(tunnelId);
811
+ tunnelWorkerErrorListner?.set(listenerId, listener);
812
+ logger.info("TunnelWorker error listener registered for tunnel", { tunnelId, listenerId });
813
+ }
814
+ async registerStartListener(tunnelId, listener) {
815
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
816
+ if (!managed) {
817
+ throw new Error(`Tunnel "${tunnelId}" not found`);
818
+ }
819
+ if (!this.tunnelStartListeners.has(tunnelId)) {
820
+ this.tunnelStartListeners.set(tunnelId, /* @__PURE__ */ new Map());
821
+ }
822
+ const listenerId = getRandomId();
823
+ const listeners = this.tunnelStartListeners.get(tunnelId);
824
+ listeners.set(listenerId, listener);
825
+ logger.info("Start listener registered for tunnel", { tunnelId, listenerId });
826
+ return listenerId;
827
+ }
828
+ async registerWillReconnectListener(tunnelId, listener) {
829
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
830
+ if (!managed) {
831
+ throw new Error(`Tunnel "${tunnelId}" not found`);
832
+ }
833
+ if (!this.tunnelWillReconnectListeners.has(tunnelId)) {
834
+ this.tunnelWillReconnectListeners.set(tunnelId, /* @__PURE__ */ new Map());
835
+ }
836
+ const listenerId = getRandomId();
837
+ this.tunnelWillReconnectListeners.get(tunnelId).set(listenerId, listener);
838
+ logger.info("WillReconnect listener registered for tunnel", { tunnelId, listenerId });
839
+ return listenerId;
840
+ }
841
+ async registerReconnectingListener(tunnelId, listener) {
842
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
843
+ if (!managed) {
844
+ throw new Error(`Tunnel "${tunnelId}" not found`);
845
+ }
846
+ if (!this.tunnelReconnectingListeners.has(tunnelId)) {
847
+ this.tunnelReconnectingListeners.set(tunnelId, /* @__PURE__ */ new Map());
848
+ }
849
+ const listenerId = getRandomId();
850
+ this.tunnelReconnectingListeners.get(tunnelId).set(listenerId, listener);
851
+ logger.info("Reconnecting listener registered for tunnel", { tunnelId, listenerId });
852
+ return listenerId;
853
+ }
854
+ async registerReconnectionCompletedListener(tunnelId, listener) {
855
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
856
+ if (!managed) {
857
+ throw new Error(`Tunnel "${tunnelId}" not found`);
858
+ }
859
+ if (!this.tunnelReconnectionCompletedListeners.has(tunnelId)) {
860
+ this.tunnelReconnectionCompletedListeners.set(tunnelId, /* @__PURE__ */ new Map());
861
+ }
862
+ const listenerId = getRandomId();
863
+ this.tunnelReconnectionCompletedListeners.get(tunnelId).set(listenerId, listener);
864
+ logger.info("ReconnectionCompleted listener registered for tunnel", { tunnelId, listenerId });
865
+ return listenerId;
866
+ }
867
+ async registerReconnectionFailedListener(tunnelId, listener) {
868
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
869
+ if (!managed) {
870
+ throw new Error(`Tunnel "${tunnelId}" not found`);
871
+ }
872
+ if (!this.tunnelReconnectionFailedListeners.has(tunnelId)) {
873
+ this.tunnelReconnectionFailedListeners.set(tunnelId, /* @__PURE__ */ new Map());
874
+ }
875
+ const listenerId = getRandomId();
876
+ this.tunnelReconnectionFailedListeners.get(tunnelId).set(listenerId, listener);
877
+ logger.info("ReconnectionFailed listener registered for tunnel", { tunnelId, listenerId });
878
+ return listenerId;
879
+ }
880
+ /**
881
+ * Removes a previously registered stats listener.
882
+ *
883
+ * @param tunnelId - The tunnel ID the listener was registered for
884
+ * @param listenerId - The unique ID returned when the listener was registered
885
+ */
886
+ deregisterStatsListener(tunnelId, listenerId) {
887
+ const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
888
+ if (!tunnelListeners) {
889
+ logger.warn("No listeners found for tunnel", { tunnelId });
890
+ return;
891
+ }
892
+ const removed = tunnelListeners.delete(listenerId);
893
+ if (removed) {
894
+ logger.info("Stats listener deregistered", { tunnelId, listenerId });
895
+ if (tunnelListeners.size === 0) {
896
+ this.tunnelStatsListeners.delete(tunnelId);
897
+ }
898
+ } else {
899
+ logger.warn("Attempted to deregister non-existent stats listener", { tunnelId, listenerId });
900
+ }
901
+ }
902
+ deregisterErrorListener(tunnelId, listenerId) {
903
+ const listeners = this.tunnelErrorListeners.get(tunnelId);
904
+ if (!listeners) {
905
+ logger.warn("No error listeners found for tunnel", { tunnelId });
906
+ return;
907
+ }
908
+ const removed = listeners.delete(listenerId);
909
+ if (removed) {
910
+ logger.info("Error listener deregistered", { tunnelId, listenerId });
911
+ if (listeners.size === 0) {
912
+ this.tunnelErrorListeners.delete(tunnelId);
913
+ }
914
+ } else {
915
+ logger.warn("Attempted to deregister non-existent error listener", { tunnelId, listenerId });
916
+ }
917
+ }
918
+ deregisterDisconnectListener(tunnelId, listenerId) {
919
+ const listeners = this.tunnelDisconnectListeners.get(tunnelId);
920
+ if (!listeners) {
921
+ logger.warn("No disconnect listeners found for tunnel", { tunnelId });
922
+ return;
923
+ }
924
+ const removed = listeners.delete(listenerId);
925
+ if (removed) {
926
+ logger.info("Disconnect listener deregistered", { tunnelId, listenerId });
927
+ if (listeners.size === 0) {
928
+ this.tunnelDisconnectListeners.delete(tunnelId);
929
+ }
930
+ } else {
931
+ logger.warn("Attempted to deregister non-existent disconnect listener", { tunnelId, listenerId });
932
+ }
933
+ }
934
+ deregisterWillReconnectListener(tunnelId, listenerId) {
935
+ const listeners = this.tunnelWillReconnectListeners.get(tunnelId);
936
+ if (!listeners) {
937
+ logger.warn("No will-reconnect listeners found for tunnel", { tunnelId });
938
+ return;
939
+ }
940
+ ;
941
+ const removed = listeners.delete(listenerId);
942
+ if (removed) {
943
+ logger.info("WillReconnect listener deregistered", { tunnelId, listenerId });
944
+ if (listeners.size === 0) {
945
+ this.tunnelWillReconnectListeners.delete(tunnelId);
946
+ }
947
+ } else {
948
+ logger.warn("Attempted to deregister non-existent will-reconnect listener", { tunnelId, listenerId });
949
+ }
950
+ }
951
+ deregisterReconnectingListener(tunnelId, listenerId) {
952
+ const listeners = this.tunnelReconnectingListeners.get(tunnelId);
953
+ if (!listeners) {
954
+ logger.warn("No reconnecting listeners found for tunnel", { tunnelId });
955
+ return;
956
+ }
957
+ ;
958
+ const removed = listeners.delete(listenerId);
959
+ if (removed) {
960
+ logger.info("Reconnecting listener deregistered", { tunnelId, listenerId });
961
+ if (listeners.size === 0) {
962
+ this.tunnelReconnectingListeners.delete(tunnelId);
963
+ }
964
+ } else {
965
+ logger.warn("Attempted to deregister non-existent reconnecting listener", { tunnelId, listenerId });
966
+ }
967
+ }
968
+ deregisterReconnectionCompletedListener(tunnelId, listenerId) {
969
+ const listeners = this.tunnelReconnectionCompletedListeners.get(tunnelId);
970
+ if (!listeners) {
971
+ logger.warn("No reconnection completed listeners found for tunnel", { tunnelId });
972
+ return;
973
+ }
974
+ const removed = listeners.delete(listenerId);
975
+ if (removed) {
976
+ logger.info("Reconnection completed listener deregistered", { tunnelId, listenerId });
977
+ if (listeners.size === 0) {
978
+ this.tunnelReconnectionCompletedListeners.delete(tunnelId);
979
+ }
980
+ } else {
981
+ logger.warn("Attempted to deregister non-existent reconnection completed listener", { tunnelId, listenerId });
982
+ }
983
+ }
984
+ deregisterReconnectionFailedListener(tunnelId, listenerId) {
985
+ const listeners = this.tunnelReconnectionFailedListeners.get(tunnelId);
986
+ if (!listeners) {
987
+ logger.warn("No reconnection failed listeners found for tunnel", { tunnelId });
988
+ return;
989
+ }
990
+ const removed = listeners.delete(listenerId);
991
+ if (removed) {
992
+ logger.info("Reconnection failed listener deregistered", { tunnelId, listenerId });
993
+ if (listeners.size === 0) {
994
+ this.tunnelReconnectionFailedListeners.delete(tunnelId);
995
+ }
996
+ } else {
997
+ logger.warn("Attempted to deregister non-existent reconnection failed listener", { tunnelId, listenerId });
998
+ }
999
+ }
1000
+ async getLocalserverTlsInfo(tunnelId) {
1001
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
1002
+ if (!managed) {
1003
+ logger.error(`Tunnel "${tunnelId}" not found when fetching local server TLS info`);
1004
+ return false;
1005
+ }
1006
+ try {
1007
+ if (managed.isStopped) {
1008
+ return false;
1009
+ }
1010
+ const tlsInfo = await managed.instance.getLocalServerTls();
1011
+ if (tlsInfo) {
1012
+ return tlsInfo;
1013
+ }
1014
+ return false;
1015
+ } catch (e) {
1016
+ logger.error(`Error fetching TLS info for tunnel "${tunnelId}": ${e instanceof Error ? e.message : e}`);
1017
+ return false;
1018
+ }
1019
+ }
1020
+ /**
1021
+ * Sets up the stats callback for a tunnel during creation.
1022
+ * This callback will update stored stats and notify all registered listeners.
1023
+ */
1024
+ setupStatsCallback(tunnelId, managed) {
1025
+ try {
1026
+ const callback = (usage) => {
1027
+ this.updateStats(tunnelId, usage);
1028
+ };
1029
+ managed.instance.setUsageUpdateCallback(callback);
1030
+ logger.debug("Stats callback set up for tunnel", { tunnelId });
1031
+ } catch (error) {
1032
+ logger.warn("Failed to set up stats callback", { tunnelId, error });
1033
+ }
1034
+ }
1035
+ notifyErrorListeners(tunnelId, errorMsg, isFatal) {
1036
+ try {
1037
+ const listeners = this.tunnelErrorListeners.get(tunnelId);
1038
+ if (!listeners) return;
1039
+ for (const [id, listener] of listeners) {
1040
+ try {
1041
+ listener(tunnelId, errorMsg, isFatal);
1042
+ } catch (err) {
1043
+ logger.debug("Error in error-listener callback", { listenerId: id, tunnelId, err });
1044
+ }
1045
+ }
1046
+ } catch (err) {
1047
+ logger.debug("Failed to notify error listeners", { tunnelId, err });
1048
+ }
1049
+ }
1050
+ setupErrorCallback(tunnelId, managed) {
1051
+ try {
1052
+ const callback = ({ errorNo, error, recoverable }) => {
1053
+ try {
1054
+ const msg = typeof error === "string" ? error : String(error);
1055
+ const isFatal = true;
1056
+ logger.debug("Tunnel reported error", { tunnelId, errorNo, errorMsg: msg, recoverable });
1057
+ this.notifyErrorListeners(tunnelId, msg, isFatal);
1058
+ } catch (e) {
1059
+ logger.warn("Error handling tunnel error callback", { tunnelId, e });
1060
+ }
1061
+ };
1062
+ managed.instance.setTunnelErrorCallback(callback);
1063
+ logger.debug("Error callback set up for tunnel", { tunnelId });
1064
+ } catch (error) {
1065
+ logger.warn("Failed to set up error callback", { tunnelId, error });
1066
+ }
1067
+ }
1068
+ setupDisconnectCallback(tunnelId, managed) {
1069
+ try {
1070
+ const callback = ({ error, messages }) => {
1071
+ try {
1072
+ logger.debug("Tunnel disconnected", { tunnelId, error, messages });
1073
+ const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1074
+ if (managedTunnel) {
1075
+ managedTunnel.isStopped = true;
1076
+ managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
1077
+ }
1078
+ const listeners = this.tunnelDisconnectListeners.get(tunnelId);
1079
+ if (!listeners) return;
1080
+ for (const [id, listener] of listeners) {
1081
+ try {
1082
+ listener(tunnelId, error, messages);
1083
+ } catch (err) {
1084
+ logger.debug("Error in disconnect-listener callback", { listenerId: id, tunnelId, err });
1085
+ }
1086
+ }
1087
+ } catch (e) {
1088
+ logger.warn("Error handling tunnel disconnect callback", { tunnelId, e });
1089
+ }
1090
+ };
1091
+ managed.instance.setTunnelDisconnectedCallback(callback);
1092
+ logger.debug("Disconnect callback set up for tunnel", { tunnelId });
1093
+ } catch (error) {
1094
+ logger.warn("Failed to set up disconnect callback", { tunnelId, error });
1095
+ }
1096
+ }
1097
+ /**
1098
+ * Called when the tunnel disconnects and the SDK is about to start reconnecting.
1099
+ * Notifies registered will-reconnect listeners.
1100
+ */
1101
+ setupWillReconnectCallback(tunnelId, managed) {
1102
+ try {
1103
+ const callback = ({ error, messages }) => {
1104
+ try {
1105
+ logger.info("Tunnel will reconnect", { tunnelId, error, messages });
1106
+ const listeners = this.tunnelWillReconnectListeners.get(tunnelId);
1107
+ if (!listeners) return;
1108
+ for (const [id, listener] of listeners) {
1109
+ try {
1110
+ listener(tunnelId, error, messages);
1111
+ } catch (err) {
1112
+ logger.debug("Error in will-reconnect-listener callback", { listenerId: id, tunnelId, err });
1113
+ }
1114
+ }
1115
+ } catch (e) {
1116
+ logger.warn("Error handling will-reconnect callback", { tunnelId, e });
1117
+ }
1118
+ };
1119
+ managed.instance.setWillReconnectCallback(callback);
1120
+ logger.debug("WillReconnect callback set up for tunnel", { tunnelId });
1121
+ } catch (error) {
1122
+ logger.warn("Failed to set up will-reconnect callback", { tunnelId, error });
1123
+ }
1124
+ }
1125
+ /**
1126
+ * Called for each reconnection attempt with the current retry count.
1127
+ * Notifies registered reconnecting listeners.
1128
+ */
1129
+ setupReconnectingCallback(tunnelId, managed) {
1130
+ try {
1131
+ const callback = ({ retryCnt }) => {
1132
+ try {
1133
+ logger.info("Tunnel reconnecting", { tunnelId, retryCnt });
1134
+ const listeners = this.tunnelReconnectingListeners.get(tunnelId);
1135
+ if (!listeners) return;
1136
+ for (const [id, listener] of listeners) {
1137
+ try {
1138
+ listener(tunnelId, retryCnt);
1139
+ } catch (err) {
1140
+ logger.debug("Error in reconnecting-listener callback", { listenerId: id, tunnelId, err });
1141
+ }
1142
+ }
1143
+ } catch (e) {
1144
+ logger.warn("Error handling reconnecting callback", { tunnelId, e });
1145
+ }
1146
+ };
1147
+ managed.instance.setReconnectingCallback(callback);
1148
+ logger.debug("Reconnecting callback set up for tunnel", { tunnelId });
1149
+ } catch (error) {
1150
+ logger.warn("Failed to set up reconnecting callback", { tunnelId, error });
1151
+ }
1152
+ }
1153
+ /**
1154
+ * Called when reconnection succeeds. Updates tunnel state back to active,
1155
+ * and notifies registered reconnection-completed and start listeners with new URLs.
1156
+ */
1157
+ setupReconnectionCompletedCallback(tunnelId, managed) {
1158
+ try {
1159
+ const callback = ({ urls }) => {
1160
+ try {
1161
+ logger.info("Tunnel reconnection completed", { tunnelId, urls });
1162
+ const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1163
+ if (managedTunnel) {
1164
+ managedTunnel.isStopped = false;
1165
+ managedTunnel.startedAt = (/* @__PURE__ */ new Date()).toISOString();
1166
+ managedTunnel.stoppedAt = null;
1167
+ }
1168
+ const listeners = this.tunnelReconnectionCompletedListeners.get(tunnelId);
1169
+ if (listeners) {
1170
+ for (const [id, listener] of listeners) {
1171
+ try {
1172
+ listener(tunnelId, urls);
1173
+ } catch (err) {
1174
+ logger.debug("Error in reconnection-completed-listener callback", { listenerId: id, tunnelId, err });
1175
+ }
1176
+ }
1177
+ }
1178
+ const startListeners = this.tunnelStartListeners.get(tunnelId);
1179
+ if (startListeners) {
1180
+ for (const [id, listener] of startListeners) {
1181
+ try {
1182
+ listener(tunnelId, urls);
1183
+ } catch (err) {
1184
+ logger.debug("Error in start-listener callback on reconnection", { listenerId: id, tunnelId, err });
1185
+ }
1186
+ }
1187
+ }
1188
+ } catch (e) {
1189
+ logger.warn("Error handling reconnection-completed callback", { tunnelId, e });
1190
+ }
1191
+ };
1192
+ managed.instance.setReconnectionCompletedCallback(callback);
1193
+ logger.debug("ReconnectionCompleted callback set up for tunnel", { tunnelId });
1194
+ } catch (error) {
1195
+ logger.warn("Failed to set up reconnection-completed callback", { tunnelId, error });
1196
+ }
1197
+ }
1198
+ /**
1199
+ * Called when all reconnection attempts are exhausted.
1200
+ * Marks the tunnel as stopped and notifies registered reconnection-failed listeners.
1201
+ */
1202
+ setupReconnectionFailedCallback(tunnelId, managed) {
1203
+ try {
1204
+ const callback = ({ retryCnt }) => {
1205
+ try {
1206
+ logger.error("Tunnel reconnection failed", { tunnelId, retryCnt });
1207
+ const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1208
+ if (managedTunnel) {
1209
+ managedTunnel.isStopped = true;
1210
+ managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
1211
+ }
1212
+ const listeners = this.tunnelReconnectionFailedListeners.get(tunnelId);
1213
+ if (!listeners) return;
1214
+ for (const [id, listener] of listeners) {
1215
+ try {
1216
+ listener(tunnelId, retryCnt);
1217
+ } catch (err) {
1218
+ logger.debug("Error in reconnection-failed-listener callback", { listenerId: id, tunnelId, err });
1219
+ }
1220
+ }
1221
+ } catch (e) {
1222
+ logger.warn("Error handling reconnection-failed callback", { tunnelId, e });
1223
+ }
1224
+ };
1225
+ managed.instance.setReconnectionFailedCallback(callback);
1226
+ logger.debug("ReconnectionFailed callback set up for tunnel", { tunnelId });
1227
+ } catch (error) {
1228
+ logger.warn("Failed to set up reconnection-failed callback", { tunnelId, error });
1229
+ }
1230
+ }
1231
+ setUpTunnelWorkerErrorCallback(tunnelId, managed) {
1232
+ try {
1233
+ const callback = (error) => {
1234
+ try {
1235
+ logger.debug("Error in Tunnel Worker", { tunnelId, errorMessage: error.message });
1236
+ const listeners = this.tunnelWorkerErrorListeners.get(tunnelId);
1237
+ if (!listeners) return;
1238
+ for (const [id, listener] of listeners) {
1239
+ try {
1240
+ listener(tunnelId, error);
1241
+ } catch (err) {
1242
+ logger.debug("Error in worker-error-listener callback", { listenerId: id, tunnelId, err });
1243
+ }
1244
+ }
1245
+ } catch (e) {
1246
+ logger.warn("Error handling tunnel worker error callback", { tunnelId, e });
1247
+ }
1248
+ };
1249
+ managed.instance.setWorkerErrorCallback(callback);
1250
+ logger.debug("Disconnect callback set up for tunnel", { tunnelId });
1251
+ } catch (error) {
1252
+ logger.warn("Failed to setup tunnel worker error callback");
1253
+ }
1254
+ }
1255
+ /**
1256
+ * Updates the stored stats for a tunnel and notifies all registered listeners.
1257
+ */
1258
+ updateStats(tunnelId, rawUsage) {
1259
+ try {
1260
+ const normalizedStats = this.normalizeStats(rawUsage);
1261
+ const existingStats = this.tunnelStats.get(tunnelId) || [];
1262
+ const updatedStats = [...existingStats, normalizedStats];
1263
+ this.tunnelStats.set(tunnelId, updatedStats);
1264
+ const tunnelListeners = this.tunnelStatsListeners.get(tunnelId);
1265
+ if (tunnelListeners) {
1266
+ for (const [listenerId, listener] of tunnelListeners) {
1267
+ try {
1268
+ listener(tunnelId, normalizedStats);
1269
+ } catch (error) {
1270
+ logger.warn("Error in stats listener callback", { listenerId, tunnelId, error });
1271
+ }
1272
+ }
1273
+ }
1274
+ logger.debug("Stats updated and listeners notified", {
1275
+ tunnelId,
1276
+ listenersCount: tunnelListeners?.size || 0
1277
+ });
1278
+ } catch (error) {
1279
+ logger.warn("Error updating stats", { tunnelId, error });
1280
+ }
1281
+ }
1282
+ /**
1283
+ * Normalizes raw usage data from the SDK into a consistent TunnelStats format.
1284
+ */
1285
+ normalizeStats(rawStats) {
1286
+ const elapsed = this.parseNumber(rawStats.elapsedTime ?? 0);
1287
+ const liveConns = this.parseNumber(rawStats.numLiveConnections ?? 0);
1288
+ const totalConns = this.parseNumber(rawStats.numTotalConnections ?? 0);
1289
+ const reqBytes = this.parseNumber(rawStats.numTotalReqBytes ?? 0);
1290
+ const resBytes = this.parseNumber(rawStats.numTotalResBytes ?? 0);
1291
+ const txBytes = this.parseNumber(rawStats.numTotalTxBytes ?? 0);
1292
+ return {
1293
+ elapsedTime: elapsed,
1294
+ numLiveConnections: liveConns,
1295
+ numTotalConnections: totalConns,
1296
+ numTotalReqBytes: reqBytes,
1297
+ numTotalResBytes: resBytes,
1298
+ numTotalTxBytes: txBytes
1299
+ };
1300
+ }
1301
+ parseNumber(value) {
1302
+ const parsed = typeof value === "number" ? value : parseInt(String(value), 10);
1303
+ return isNaN(parsed) ? 0 : parsed;
1304
+ }
1305
+ startStaticFileServer(managed) {
1306
+ try {
1307
+ const __filename3 = fileURLToPath(import.meta.url);
1308
+ const __dirname3 = path.dirname(__filename3);
1309
+ const fileServerWorkerPath = path.join(__dirname3, "workers", "file_serve_worker.cjs");
1310
+ const staticServerWorker = new Worker(fileServerWorkerPath, {
1311
+ workerData: {
1312
+ dir: managed.serve,
1313
+ port: managed.tunnelConfig?.forwarding
1314
+ }
1315
+ });
1316
+ staticServerWorker.on("message", (msg) => {
1317
+ switch (msg.type) {
1318
+ case "started":
1319
+ logger.info("Static file server started", { dir: managed.serve });
1320
+ break;
1321
+ case "warning":
1322
+ if (msg.code === "INVALID_TUNNEL_SERVE_PATH") {
1323
+ managed.warnings = managed.warnings ?? [];
1324
+ managed.warnings.push({ code: msg.code, message: msg.message });
1325
+ }
1326
+ printer_default.warn(msg.message);
1327
+ break;
1328
+ case "error":
1329
+ managed.warnings = managed.warnings ?? [];
1330
+ managed.warnings.push({
1331
+ code: "UNKNOWN_WARNING",
1332
+ message: msg.message
1333
+ });
1334
+ break;
1335
+ }
1336
+ });
1337
+ managed.serveWorker = staticServerWorker;
1338
+ } catch (error) {
1339
+ logger.error("Error starting static file server", error);
1340
+ }
1341
+ }
1342
+ };
1343
+
1344
+ // src/types.ts
1345
+ var TunnelStateType = /* @__PURE__ */ ((TunnelStateType2) => {
1346
+ TunnelStateType2["New"] = "idle";
1347
+ TunnelStateType2["Starting"] = "starting";
1348
+ TunnelStateType2["Running"] = "running";
1349
+ TunnelStateType2["Live"] = "live";
1350
+ TunnelStateType2["Closed"] = "closed";
1351
+ TunnelStateType2["Exited"] = "exited";
1352
+ return TunnelStateType2;
1353
+ })(TunnelStateType || {});
1354
+ var TunnelErrorCodeType = /* @__PURE__ */ ((TunnelErrorCodeType2) => {
1355
+ TunnelErrorCodeType2["NonResponsive"] = "non_responsive";
1356
+ TunnelErrorCodeType2["FailedToConnect"] = "failed_to_connect";
1357
+ TunnelErrorCodeType2["ErrorInAdditionalForwarding"] = "additional_forwarding_error";
1358
+ TunnelErrorCodeType2["WebdebuggerError"] = "webdebugger_error";
1359
+ TunnelErrorCodeType2["NoError"] = "";
1360
+ return TunnelErrorCodeType2;
1361
+ })(TunnelErrorCodeType || {});
1362
+ var TunnelWarningCode = /* @__PURE__ */ ((TunnelWarningCode2) => {
1363
+ TunnelWarningCode2["InvalidTunnelServePath"] = "INVALID_TUNNEL_SERVE_PATH";
1364
+ TunnelWarningCode2["UnknownWarning"] = "UNKNOWN_WARNING";
1365
+ return TunnelWarningCode2;
1366
+ })(TunnelWarningCode || {});
1367
+ var ErrorCode = {
1368
+ InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
1369
+ InvalidRequestBodyError: "COULD_NOT_READ_BODY",
1370
+ InternalServerError: "INTERNAL_SERVER_ERROR",
1371
+ InvalidBodyFormatError: "INVALID_DATA_FORMAT",
1372
+ ErrorStartingTunnel: "ERROR_STARTING_TUNNEL",
1373
+ TunnelNotFound: "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND",
1374
+ TunnelAlreadyRunningError: "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING",
1375
+ WebsocketUpgradeFailError: "WEBSOCKET_UPGRADE_FAILED",
1376
+ RemoteManagementAlreadyRunning: "REMOTE_MANAGEMENT_ALREADY_RUNNING",
1377
+ RemoteManagementNotRunning: "REMOTE_MANAGEMENT_NOT_RUNNING",
1378
+ RemoteManagementDeserializationFailed: "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED"
1379
+ };
1380
+ function isErrorResponse(obj) {
1381
+ return typeof obj === "object" && obj !== null && "code" in obj && "message" in obj && typeof obj.message === "string" && Object.values(ErrorCode).includes(obj.code);
1382
+ }
1383
+ function newErrorResponse(codeOrError, message) {
1384
+ if (typeof codeOrError === "object") {
1385
+ return codeOrError;
1386
+ }
1387
+ return {
1388
+ code: codeOrError,
1389
+ message
1390
+ };
1391
+ }
1392
+ function NewResponseObject(data) {
1393
+ const encoder = new TextEncoder();
1394
+ const bytes = encoder.encode(JSON.stringify(data));
1395
+ return {
1396
+ response: bytes,
1397
+ requestid: "",
1398
+ command: "",
1399
+ error: false,
1400
+ errorresponse: {}
1401
+ };
1402
+ }
1403
+ function NewErrorResponseObject(errorResponse) {
1404
+ return {
1405
+ response: new Uint8Array(),
1406
+ requestid: "",
1407
+ command: "",
1408
+ error: true,
1409
+ errorresponse: errorResponse
1410
+ };
1411
+ }
1412
+ function newStatus(tunnelState, errorCode, errorMsg) {
1413
+ let assignedState = tunnelState;
1414
+ if (tunnelState === "live" /* Live */) {
1415
+ assignedState = "running" /* Running */;
1416
+ } else if (tunnelState === "idle" /* New */) {
1417
+ assignedState = "idle" /* New */;
1418
+ } else if (tunnelState === "closed" /* Closed */) {
1419
+ assignedState = "exited" /* Exited */;
1420
+ }
1421
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1422
+ return {
1423
+ state: assignedState,
1424
+ errorcode: errorCode,
1425
+ errormsg: errorMsg,
1426
+ createdtimestamp: now,
1427
+ starttimestamp: now,
1428
+ endtimestamp: now,
1429
+ warnings: []
1430
+ };
1431
+ }
1432
+ function newStats() {
1433
+ return {
1434
+ numLiveConnections: 0,
1435
+ numTotalConnections: 0,
1436
+ numTotalReqBytes: 0,
1437
+ numTotalResBytes: 0,
1438
+ numTotalTxBytes: 0,
1439
+ elapsedTime: 0
1440
+ };
1441
+ }
1442
+ var RemoteManagementStatus = {
1443
+ Connecting: "CONNECTING",
1444
+ Disconnecting: "DISCONNECTING",
1445
+ Reconnecting: "RECONNECTING",
1446
+ Running: "RUNNING",
1447
+ NotRunning: "NOT_RUNNING",
1448
+ Error: "ERROR"
1449
+ };
1450
+
1451
+ // src/remote_management/remote_schema.ts
1452
+ import { TunnelType as TunnelType2 } from "@pinggy/pinggy";
1453
+ import { z } from "zod";
1454
+ var HeaderModificationSchema = z.object({
1455
+ key: z.string(),
1456
+ value: z.array(z.string()).optional(),
1457
+ type: z.enum(["add", "remove", "update"])
1458
+ });
1459
+ var AdditionalForwardingSchema = z.object({
1460
+ remoteDomain: z.string().optional(),
1461
+ remotePort: z.number().optional(),
1462
+ localDomain: z.string(),
1463
+ localPort: z.number()
1464
+ });
1465
+ var TunnelConfigSchema = z.object({
1466
+ allowPreflight: z.boolean().optional(),
1467
+ // primary key
1468
+ allowpreflight: z.boolean().optional(),
1469
+ // legacy key
1470
+ autoreconnect: z.boolean(),
1471
+ basicauth: z.array(z.object({ username: z.string(), password: z.string() })).nullable(),
1472
+ bearerauth: z.string().nullable(),
1473
+ configid: z.string(),
1474
+ configname: z.string(),
1475
+ greetmsg: z.string().optional(),
1476
+ force: z.boolean(),
1477
+ forwardedhost: z.string(),
1478
+ fullRequestUrl: z.boolean(),
1479
+ headermodification: z.array(HeaderModificationSchema),
1480
+ httpsOnly: z.boolean(),
1481
+ internalwebdebuggerport: z.number(),
1482
+ ipwhitelist: z.array(z.string()).nullable(),
1483
+ localport: z.number(),
1484
+ localsservertls: z.union([z.boolean(), z.string()]),
1485
+ localservertlssni: z.string().nullable(),
1486
+ regioncode: z.string(),
1487
+ noReverseProxy: z.boolean(),
1488
+ serveraddress: z.string(),
1489
+ serverport: z.number(),
1490
+ statusCheckInterval: z.number(),
1491
+ token: z.string(),
1492
+ tunnelTimeout: z.number(),
1493
+ type: z.enum([
1494
+ TunnelType2.Http,
1495
+ TunnelType2.Tcp,
1496
+ TunnelType2.Udp,
1497
+ TunnelType2.Tls,
1498
+ TunnelType2.TlsTcp
1499
+ ]),
1500
+ webdebuggerport: z.number(),
1501
+ xff: z.string(),
1502
+ additionalForwarding: z.array(AdditionalForwardingSchema).optional(),
1503
+ serve: z.string().optional()
1504
+ }).superRefine((data, ctx) => {
1505
+ if (data.allowPreflight === void 0 && data.allowpreflight === void 0) {
1506
+ ctx.addIssue({
1507
+ code: "custom",
1508
+ message: "Either allowPreflight or allowpreflight is required",
1509
+ path: ["allowPreflight"]
1510
+ });
1511
+ }
1512
+ }).transform((data) => ({
1513
+ ...data,
1514
+ allowPreflight: data.allowPreflight ?? data.allowpreflight,
1515
+ allowpreflight: data.allowPreflight ?? data.allowpreflight
1516
+ }));
1517
+ var StartSchema = z.object({
1518
+ tunnelID: z.string().nullable().optional(),
1519
+ tunnelConfig: TunnelConfigSchema
1520
+ });
1521
+ var StopSchema = z.object({
1522
+ tunnelID: z.string().min(1)
1523
+ });
1524
+ var GetSchema = StopSchema;
1525
+ var RestartSchema = StopSchema;
1526
+ var UpdateConfigSchema = z.object({
1527
+ tunnelConfig: TunnelConfigSchema
1528
+ });
1529
+ function tunnelConfigToPinggyOptions(config) {
1530
+ return {
1531
+ token: config.token || "",
1532
+ serverAddress: config.serveraddress || "free.pinggy.io",
1533
+ forwarding: `${config.forwardedhost || "localhost"}:${config.localport}`,
1534
+ webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
1535
+ ipWhitelist: config.ipwhitelist || [],
1536
+ basicAuth: config.basicauth ? config.basicauth : [],
1537
+ bearerTokenAuth: config.bearerauth ? [config.bearerauth] : [],
1538
+ headerModification: config.headermodification,
1539
+ xForwardedFor: !!config.xff,
1540
+ httpsOnly: config.httpsOnly,
1541
+ originalRequestUrl: config.fullRequestUrl,
1542
+ allowPreflight: config.allowPreflight,
1543
+ reverseProxy: config.noReverseProxy,
1544
+ force: config.force,
1545
+ autoReconnect: config.autoreconnect,
1546
+ optional: {
1547
+ sniServerName: config.localservertlssni || ""
1548
+ }
1549
+ };
1550
+ }
1551
+ function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg, additionalForwarding, serve) {
1552
+ const forwarding = Array.isArray(opts.forwarding) ? String(opts.forwarding[0].address).replace("//", "").replace(/\/$/, "") : String(opts.forwarding).replace("//", "").replace(/\/$/, "");
1553
+ const parsedForwardedHost = forwarding.split(":").length == 3 ? forwarding.split(":")[1] : forwarding.split(":")[0];
1554
+ const parsedLocalPort = forwarding.split(":").length == 3 ? parseInt(forwarding.split(":")[2], 10) : parseInt(forwarding.split(":")[1], 10);
1555
+ const tunnelType = (Array.isArray(opts.forwarding) ? opts.forwarding[0]?.type : void 0) ?? TunnelType2.Http;
1556
+ const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
1557
+ return {
1558
+ allowPreflight: opts.allowPreflight ?? false,
1559
+ allowpreflight: opts.allowPreflight ?? false,
1560
+ autoreconnect: opts.autoReconnect ?? false,
1561
+ basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
1562
+ bearerauth: parsedTokens.length ? parsedTokens.join(",") : null,
1563
+ configid,
1564
+ configname: configName,
1565
+ greetmsg: greetMsg || "",
1566
+ force: opts.force ?? false,
1567
+ forwardedhost: parsedForwardedHost || "localhost",
1568
+ fullRequestUrl: opts.originalRequestUrl ?? false,
1569
+ headermodification: opts.headerModification || [],
1570
+ //structured list
1571
+ httpsOnly: opts.httpsOnly ?? false,
1572
+ internalwebdebuggerport: 0,
1573
+ ipwhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : null,
1574
+ localport: parsedLocalPort || 0,
1575
+ localservertlssni: null,
1576
+ regioncode: "",
1577
+ noReverseProxy: opts.reverseProxy ?? false,
1578
+ serveraddress: opts.serverAddress || "free.pinggy.io",
1579
+ serverport: 0,
1580
+ statusCheckInterval: 0,
1581
+ token: opts.token || "",
1582
+ tunnelTimeout: 0,
1583
+ type: tunnelType,
1584
+ webdebuggerport: Number(opts.webDebugger?.split(":")[0]) || 0,
1585
+ xff: opts.xForwardedFor ? "1" : "",
1586
+ localsservertls: localserverTls || false,
1587
+ additionalForwarding: additionalForwarding || [],
1588
+ serve: serve || ""
1589
+ };
1590
+ }
1591
+
1592
+ // src/remote_management/handler.ts
1593
+ import { TunnelType as TunnelType3 } from "@pinggy/pinggy";
1594
+ var TunnelOperations = class {
1595
+ constructor() {
1596
+ this.tunnelManager = TunnelManager.getInstance();
1597
+ }
1598
+ buildStatus(tunnelId, state, errorCode) {
1599
+ const status = newStatus(state, errorCode, "");
1600
+ try {
1601
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelId);
1602
+ if (managed) {
1603
+ status.createdtimestamp = managed.createdAt || "";
1604
+ status.starttimestamp = managed.startedAt || "";
1605
+ status.endtimestamp = managed.stoppedAt || "";
1606
+ }
1607
+ } catch (e) {
1608
+ }
1609
+ return status;
1610
+ }
1611
+ // --- Helper to construct TunnelResponse ---
1612
+ async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, additionalForwarding, serve) {
1613
+ const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
1614
+ this.tunnelManager.getTunnelStatus(tunnelid),
1615
+ this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
1616
+ this.tunnelManager.getLocalserverTlsInfo(tunnelid),
1617
+ this.tunnelManager.getTunnelGreetMessage(tunnelid),
1618
+ this.tunnelManager.getTunnelUrls(tunnelid)
1619
+ ]);
1620
+ return {
1621
+ tunnelid,
1622
+ remoteurls,
1623
+ tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg, additionalForwarding),
1624
+ status: this.buildStatus(tunnelid, status, "" /* NoError */),
1625
+ stats
1626
+ };
1627
+ }
1628
+ error(code, err, fallback) {
1629
+ return newErrorResponse({
1630
+ code,
1631
+ message: err instanceof Error ? err.message : fallback
1632
+ });
1633
+ }
1634
+ // --- Operations ---
1635
+ async handleStart(config) {
1636
+ try {
1637
+ const opts = tunnelConfigToPinggyOptions(config);
1638
+ const additionalForwardingParsed = config.additionalForwarding || [];
1639
+ const { tunnelid, instance, tunnelName, additionalForwarding, serve } = await this.tunnelManager.createTunnel({
1640
+ ...opts,
1641
+ tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [TunnelType3.Http],
1642
+ // Temporary fix in future we will not use this field.
1643
+ configid: config.configid,
1644
+ tunnelName: config.configname,
1645
+ additionalForwarding: additionalForwardingParsed,
1646
+ serve: config.serve
1647
+ });
1648
+ this.tunnelManager.startTunnel(tunnelid);
1649
+ const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
1650
+ const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, additionalForwarding, serve);
1651
+ return resp;
1652
+ } catch (err) {
1653
+ return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
1654
+ }
1655
+ }
1656
+ async handleUpdateConfig(config) {
1657
+ try {
1658
+ const opts = tunnelConfigToPinggyOptions(config);
1659
+ const tunnel = await this.tunnelManager.updateConfig({
1660
+ ...opts,
1661
+ tunnelType: Array.isArray(config.type) ? config.type : config.type ? [config.type] : [TunnelType3.Http],
1662
+ // // Temporary fix in future we will not use this field.
1663
+ configid: config.configid,
1664
+ tunnelName: config.configname,
1665
+ additionalForwarding: config.additionalForwarding || [],
1666
+ serve: config.serve
1667
+ });
1668
+ if (!tunnel.instance || !tunnel.tunnelConfig)
1669
+ throw new Error("Invalid tunnel state after configuration update");
1670
+ return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.additionalForwarding, tunnel.serve);
1671
+ } catch (err) {
1672
+ return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
1673
+ }
1674
+ }
1675
+ async handleList() {
1676
+ try {
1677
+ const tunnels = await this.tunnelManager.getAllTunnels();
1678
+ if (tunnels.length === 0) {
1679
+ return [];
1680
+ }
1681
+ return Promise.all(
1682
+ tunnels.map(async (t) => {
1683
+ const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
1684
+ const [status, tlsInfo, greetMsg] = await Promise.all([
1685
+ this.tunnelManager.getTunnelStatus(t.tunnelid),
1686
+ this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
1687
+ this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
1688
+ ]);
1689
+ const pinggyOptions = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
1690
+ const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configid, t.tunnelName, tlsInfo, greetMsg, t.additionalForwarding, t.serve);
1691
+ return {
1692
+ tunnelid: t.tunnelid,
1693
+ remoteurls: t.remoteurls,
1694
+ status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
1695
+ stats: rawStats,
1696
+ tunnelconfig: tunnelConfig
1697
+ };
1698
+ })
1699
+ );
1700
+ } catch (err) {
1701
+ return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
1702
+ }
1703
+ }
1704
+ async handleStop(tunnelid) {
1705
+ try {
1706
+ const { configid } = this.tunnelManager.stopTunnel(tunnelid);
1707
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1708
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
1709
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
1710
+ } catch (err) {
1711
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
1712
+ }
1713
+ }
1714
+ async handleGet(tunnelid) {
1715
+ try {
1716
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1717
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
1718
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
1719
+ } catch (err) {
1720
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
1721
+ }
1722
+ }
1723
+ async handleRestart(tunnelid) {
1724
+ try {
1725
+ await this.tunnelManager.restartTunnel(tunnelid);
1726
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1727
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
1728
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configid, managed.tunnelName, managed.additionalForwarding, managed.serve);
1729
+ } catch (err) {
1730
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
1731
+ }
1732
+ }
1733
+ handleRegisterStatsListener(tunnelid, listener) {
1734
+ this.tunnelManager.registerStatsListener(tunnelid, listener);
1735
+ }
1736
+ handleUnregisterStatsListener(tunnelid, listnerId) {
1737
+ this.tunnelManager.deregisterStatsListener(tunnelid, listnerId);
1738
+ }
1739
+ handleGetTunnelStats(tunnelid) {
1740
+ try {
1741
+ const stats = this.tunnelManager.getTunnelStats(tunnelid);
1742
+ if (!stats) {
1743
+ return [newStats()];
1744
+ }
1745
+ return stats;
1746
+ } catch (err) {
1747
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats");
1748
+ }
1749
+ }
1750
+ handleRegisterDisconnectListener(tunnelid, listener) {
1751
+ this.tunnelManager.registerDisconnectListener(tunnelid, listener);
1752
+ }
1753
+ handleRemoveStoppedTunnelByConfigId(configId) {
1754
+ try {
1755
+ return this.tunnelManager.removeStoppedTunnelByConfigId(configId);
1756
+ } catch (err) {
1757
+ return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by configId");
1758
+ }
1759
+ }
1760
+ handleRemoveStoppedTunnelByTunnelId(tunnelId) {
1761
+ try {
1762
+ return this.tunnelManager.removeStoppedTunnelByTunnelId(tunnelId);
1763
+ } catch (err) {
1764
+ return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by tunnelId");
1765
+ }
1766
+ }
1767
+ };
1768
+
1769
+ // src/remote_management/remoteManagement.ts
1770
+ import WebSocket from "ws";
1771
+
1772
+ // src/remote_management/websocket_handlers.ts
1773
+ import z2 from "zod";
1774
+ var WebSocketCommandHandler = class {
1775
+ constructor() {
1776
+ this.tunnelHandler = new TunnelOperations();
1777
+ }
1778
+ safeParse(text) {
1779
+ if (!text) return void 0;
1780
+ try {
1781
+ return JSON.parse(text);
1782
+ } catch (e) {
1783
+ logger.warn("Invalid JSON payload", { error: String(e), text });
1784
+ return void 0;
1785
+ }
1786
+ }
1787
+ sendResponse(ws, resp) {
1788
+ const payload = {
1789
+ ...resp,
1790
+ response: Buffer.from(resp.response || []).toString("base64")
1791
+ };
1792
+ ws.send(JSON.stringify(payload));
1793
+ }
1794
+ sendError(ws, req, message, code = ErrorCode.InternalServerError) {
1795
+ const resp = NewErrorResponseObject({ code, message });
1796
+ resp.command = req.command || "";
1797
+ resp.requestid = req.requestid || "";
1798
+ this.sendResponse(ws, resp);
1799
+ }
1800
+ async handleStartReq(req, raw) {
1801
+ const dc = StartSchema.parse(raw);
1802
+ printer_default.info("Starting tunnel with config name: " + dc.tunnelConfig.configname);
1803
+ const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
1804
+ return this.wrapResponse(result, req);
1805
+ }
1806
+ async handleStopReq(req, raw) {
1807
+ const dc = StopSchema.parse(raw);
1808
+ printer_default.info("Stopping tunnel with ID: " + dc.tunnelID);
1809
+ const result = await this.tunnelHandler.handleStop(dc.tunnelID);
1810
+ return this.wrapResponse(result, req);
1811
+ }
1812
+ async handleGetReq(req, raw) {
1813
+ const dc = GetSchema.parse(raw);
1814
+ const result = await this.tunnelHandler.handleGet(dc.tunnelID);
1815
+ return this.wrapResponse(result, req);
1816
+ }
1817
+ async handleRestartReq(req, raw) {
1818
+ const dc = RestartSchema.parse(raw);
1819
+ const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
1820
+ return this.wrapResponse(result, req);
1821
+ }
1822
+ async handleUpdateConfigReq(req, raw) {
1823
+ const dc = UpdateConfigSchema.parse(raw);
1824
+ const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
1825
+ return this.wrapResponse(result, req);
1826
+ }
1827
+ async handleListReq(req) {
1828
+ const result = await this.tunnelHandler.handleList();
1829
+ return this.wrapResponse(result, req);
1830
+ }
1831
+ wrapResponse(result, req) {
1832
+ if (isErrorResponse(result)) {
1833
+ const errResp = NewErrorResponseObject(result);
1834
+ errResp.command = req.command;
1835
+ errResp.requestid = req.requestid;
1836
+ return errResp;
1837
+ }
1838
+ const finalResult = JSON.parse(JSON.stringify(result));
1839
+ if (Array.isArray(finalResult)) {
1840
+ finalResult.forEach((item) => {
1841
+ if (item?.tunnelconfig) {
1842
+ delete item.tunnelconfig.allowPreflight;
1843
+ }
1844
+ });
1845
+ } else if (finalResult?.tunnelconfig) {
1846
+ delete finalResult.tunnelconfig.allowPreflight;
1847
+ }
1848
+ const respObj = NewResponseObject(finalResult);
1849
+ respObj.command = req.command;
1850
+ respObj.requestid = req.requestid;
1851
+ return respObj;
1852
+ }
1853
+ async handle(ws, req) {
1854
+ const cmd = (req.command || "").toLowerCase();
1855
+ const raw = this.safeParse(req.data);
1856
+ try {
1857
+ let response;
1858
+ switch (cmd) {
1859
+ case "start": {
1860
+ response = await this.handleStartReq(req, raw);
1861
+ break;
1862
+ }
1863
+ case "stop": {
1864
+ response = await this.handleStopReq(req, raw);
1865
+ break;
1866
+ }
1867
+ case "get": {
1868
+ response = await this.handleGetReq(req, raw);
1869
+ break;
1870
+ }
1871
+ case "restart": {
1872
+ response = await this.handleRestartReq(req, raw);
1873
+ break;
1874
+ }
1875
+ case "updateconfig": {
1876
+ response = await this.handleUpdateConfigReq(req, raw);
1877
+ break;
1878
+ }
1879
+ case "list": {
1880
+ response = await this.handleListReq(req);
1881
+ break;
1882
+ }
1883
+ default:
1884
+ if (typeof req.command === "string") {
1885
+ logger.warn("Unknown command", { command: req.command });
1886
+ }
1887
+ return this.sendError(ws, req, "Invalid command");
1888
+ }
1889
+ logger.debug("Sending response", { command: response.command, requestid: response.requestid });
1890
+ this.sendResponse(ws, response);
1891
+ } catch (e) {
1892
+ if (e instanceof z2.ZodError) {
1893
+ logger.warn("Validation failed", { cmd, issues: e.issues });
1894
+ return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
1895
+ }
1896
+ logger.error("Error handling command", { cmd, error: String(e) });
1897
+ return this.sendError(ws, req, e?.message || "Internal error");
1898
+ }
1899
+ }
1900
+ };
1901
+ function handleConnectionStatusMessage(firstMessage) {
1902
+ try {
1903
+ const text = typeof firstMessage === "string" ? firstMessage : firstMessage.toString();
1904
+ const cs = JSON.parse(text);
1905
+ if (!cs.success) {
1906
+ const msg = cs.error_msg || "Connection failed";
1907
+ printer_default.warn(`Connection failed: ${msg}`);
1908
+ logger.warn("Remote management connection failed", { error_code: cs.error_code, error_msg: msg });
1909
+ return false;
1910
+ }
1911
+ return true;
1912
+ } catch (e) {
1913
+ logger.warn("Failed to parse connection status message", { error: String(e) });
1914
+ return true;
1915
+ }
1916
+ }
1917
+
1918
+ // src/remote_management/remoteManagement.ts
1919
+ var RECONNECT_SLEEP_MS = 5e3;
1920
+ var PING_INTERVAL_MS = 3e4;
1921
+ var _remoteManagementState = {
1922
+ status: "NOT_RUNNING",
1923
+ errorMessage: ""
1924
+ };
1925
+ var _stopRequested = false;
1926
+ var currentWs = null;
1927
+ function buildRemoteManagementWsUrl(manage) {
1928
+ let baseUrl = (manage || "dashboard.pinggy.io").trim();
1929
+ if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
1930
+ baseUrl = "wss://" + baseUrl;
1931
+ }
1932
+ const trimmed = baseUrl.replace(/\/$/, "");
1933
+ return `${trimmed}/backend/api/v1/remote-management/connect`;
1934
+ }
1935
+ function extractHostname(u) {
1936
+ try {
1937
+ const url = new URL(u);
1938
+ return url.host;
1939
+ } catch {
1940
+ return u;
1941
+ }
1942
+ }
1943
+ function sleep(ms) {
1944
+ return new Promise((res) => setTimeout(res, ms));
1945
+ }
1946
+ async function parseRemoteManagement(values) {
1947
+ const rmToken = values["remote-management"];
1948
+ if (typeof rmToken === "string" && rmToken.trim().length > 0) {
1949
+ const manageHost = values["manage"];
1950
+ try {
1951
+ await initiateRemoteManagement(rmToken, manageHost);
1952
+ return { ok: true };
1953
+ } catch (e) {
1954
+ logger.error("Failed to initiate remote management:", e);
1955
+ return { ok: false, error: e };
1956
+ }
1957
+ }
1958
+ }
1959
+ async function initiateRemoteManagement(token, manage) {
1960
+ if (!token || token.trim().length === 0) {
1961
+ throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
1962
+ }
1963
+ const wsUrl = buildRemoteManagementWsUrl(manage);
1964
+ const wsHost = extractHostname(wsUrl);
1965
+ logger.info("Remote management mode enabled.");
1966
+ _stopRequested = false;
1967
+ const sigintHandler = () => {
1968
+ _stopRequested = true;
1969
+ };
1970
+ process.once("SIGINT", sigintHandler);
1971
+ const logConnecting = () => {
1972
+ printer_default.print(`Connecting to ${wsHost}`);
1973
+ logger.info("Connecting to remote management", { wsUrl });
1974
+ };
1975
+ while (!_stopRequested) {
1976
+ logConnecting();
1977
+ setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
1978
+ try {
1979
+ await handleWebSocketConnection(wsUrl, wsHost, token);
1980
+ } catch (error) {
1981
+ logger.warn("Remote management connection error", { error: String(error) });
1982
+ }
1983
+ if (_stopRequested) break;
1984
+ printer_default.warn(`Remote management disconnected. Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds...`);
1985
+ logger.info("Reconnecting to remote management after disconnect");
1986
+ await sleep(RECONNECT_SLEEP_MS);
1987
+ }
1988
+ process.removeListener("SIGINT", sigintHandler);
1989
+ logger.info("Remote management stopped.");
1990
+ return getRemoteManagementState();
1991
+ }
1992
+ async function handleWebSocketConnection(wsUrl, wsHost, token) {
1993
+ return new Promise((resolve) => {
1994
+ const ws = new WebSocket(wsUrl, {
1995
+ headers: { Authorization: `Bearer ${token}` }
1996
+ });
1997
+ currentWs = ws;
1998
+ let heartbeat = null;
1999
+ let firstMessage = true;
2000
+ const cleanup = () => {
2001
+ if (heartbeat) clearInterval(heartbeat);
2002
+ currentWs = null;
2003
+ resolve();
2004
+ };
2005
+ ws.once("open", () => {
2006
+ printer_default.success(`Connected to ${wsHost}`);
2007
+ heartbeat = setInterval(() => {
2008
+ if (ws.readyState === WebSocket.OPEN) ws.ping();
2009
+ }, PING_INTERVAL_MS);
2010
+ });
2011
+ ws.on("ping", () => ws.pong());
2012
+ ws.on("message", async (data) => {
2013
+ try {
2014
+ if (firstMessage) {
2015
+ firstMessage = false;
2016
+ const ok = handleConnectionStatusMessage(data);
2017
+ if (!ok) ws.close();
2018
+ return;
2019
+ }
2020
+ setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
2021
+ const req = JSON.parse(data.toString("utf8"));
2022
+ await new WebSocketCommandHandler().handle(ws, req);
2023
+ } catch (e) {
2024
+ logger.warn("Failed handling websocket message", { error: String(e) });
2025
+ }
2026
+ });
2027
+ ws.on("unexpected-response", (_, res) => {
2028
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
2029
+ if (res.statusCode === 401) {
2030
+ printer_default.error("Unauthorized. Please enter a valid token.");
2031
+ logger.error("Unauthorized (401) on remote management connect");
2032
+ } else {
2033
+ printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
2034
+ logger.warn("Unexpected HTTP response", { statusCode: res.statusCode });
2035
+ }
2036
+ ws.close();
2037
+ });
2038
+ ws.on("close", (code, reason) => {
2039
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2040
+ logger.info("WebSocket closed", { code, reason: reason.toString() });
2041
+ printer_default.warn(`Disconnected (code: ${code}). Retrying...`);
2042
+ cleanup();
2043
+ });
2044
+ ws.on("error", (err) => {
2045
+ setRemoteManagementState({ status: RemoteManagementStatus.Error, errorMessage: err.message });
2046
+ logger.warn("WebSocket error", { error: err.message });
2047
+ printer_default.error(err);
2048
+ cleanup();
2049
+ });
2050
+ });
2051
+ }
2052
+ async function closeRemoteManagement(timeoutMs = 1e4) {
2053
+ _stopRequested = true;
2054
+ try {
2055
+ if (currentWs) {
2056
+ try {
2057
+ setRemoteManagementState({ status: RemoteManagementStatus.Disconnecting, errorMessage: "" });
2058
+ currentWs.close();
2059
+ } catch (e) {
2060
+ logger.warn("Error while closing current remote management websocket", { error: String(e) });
2061
+ }
2062
+ }
2063
+ const start = Date.now();
2064
+ while (_remoteManagementState.status === "RUNNING") {
2065
+ if (Date.now() - start > timeoutMs) {
2066
+ logger.warn("Timed out waiting for remote management to stop");
2067
+ break;
2068
+ }
2069
+ await sleep(200);
2070
+ }
2071
+ } finally {
2072
+ currentWs = null;
2073
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2074
+ return getRemoteManagementState();
2075
+ }
2076
+ }
2077
+ function getRemoteManagementState() {
2078
+ return _remoteManagementState;
2079
+ }
2080
+ function setRemoteManagementState(state, errorMessage) {
2081
+ _remoteManagementState = {
2082
+ status: state.status,
2083
+ errorMessage: errorMessage || ""
2084
+ };
2085
+ }
2086
+
2087
+ export {
2088
+ printer_default,
2089
+ getRandomId,
2090
+ isValidPort,
2091
+ getVersion,
2092
+ TunnelManager,
2093
+ TunnelStateType,
2094
+ TunnelErrorCodeType,
2095
+ TunnelWarningCode,
2096
+ TunnelOperations,
2097
+ parseRemoteManagement,
2098
+ initiateRemoteManagement,
2099
+ closeRemoteManagement,
2100
+ getRemoteManagementState
2101
+ };