pinggy 0.3.7 → 0.3.9

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.
@@ -68,6 +68,9 @@ var _CLIPrinter = class _CLIPrinter {
68
68
  console.error(pico2.red(pico2.bold("\u2716 Error:")), pico2.red(msg));
69
69
  process.exit(1);
70
70
  }
71
+ static red(message) {
72
+ return pico2.red(message);
73
+ }
71
74
  static warn(message) {
72
75
  console.warn(pico2.yellow(pico2.bold("\u26A0 Warning:")), pico2.yellow(message));
73
76
  }
@@ -121,27 +124,36 @@ var CLIPrinter = _CLIPrinter;
121
124
  var printer_default = CLIPrinter;
122
125
 
123
126
  // src/utils/util.ts
124
- import { createRequire } from "module";
127
+ import { readFileSync } from "fs";
125
128
  import { randomUUID } from "crypto";
129
+ import { fileURLToPath } from "url";
130
+ import { dirname, join } from "path";
126
131
  function getRandomId() {
127
132
  return randomUUID();
128
133
  }
129
134
  function isValidPort(p) {
130
135
  return Number.isInteger(p) && p > 0 && p < 65536;
131
136
  }
132
- var require2 = createRequire(import.meta.url);
133
- var pkg = require2("../package.json");
137
+ var __filename2 = fileURLToPath(import.meta.url);
138
+ var __dirname2 = dirname(__filename2);
134
139
  function getVersion() {
135
- return pkg.version ?? "";
140
+ try {
141
+ const packageJsonPath = join(__dirname2, "../package.json");
142
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
143
+ return pkg.version ?? "";
144
+ } catch (error) {
145
+ printer_default.error("Error reading version info");
146
+ return "";
147
+ }
136
148
  }
137
149
 
138
150
  // src/tunnel_manager/TunnelManager.ts
139
151
  import { pinggy } from "@pinggy/pinggy";
140
152
  import path from "path";
141
153
  import { Worker } from "worker_threads";
142
- import { fileURLToPath } from "url";
143
- var __filename2 = fileURLToPath(import.meta.url);
144
- var __dirname2 = path.dirname(__filename2);
154
+ import { fileURLToPath as fileURLToPath2 } from "url";
155
+ var __filename3 = fileURLToPath2(import.meta.url);
156
+ var __dirname3 = path.dirname(__filename3);
145
157
  var TunnelManager = class _TunnelManager {
146
158
  constructor() {
147
159
  this.tunnelsByTunnelId = /* @__PURE__ */ new Map();
@@ -149,6 +161,7 @@ var TunnelManager = class _TunnelManager {
149
161
  this.tunnelStats = /* @__PURE__ */ new Map();
150
162
  this.tunnelStatsListeners = /* @__PURE__ */ new Map();
151
163
  this.tunnelErrorListeners = /* @__PURE__ */ new Map();
164
+ this.tunnelPollingErrorListeners = /* @__PURE__ */ new Map();
152
165
  this.tunnelDisconnectListeners = /* @__PURE__ */ new Map();
153
166
  this.tunnelWorkerErrorListeners = /* @__PURE__ */ new Map();
154
167
  this.tunnelStartListeners = /* @__PURE__ */ new Map();
@@ -165,12 +178,9 @@ var TunnelManager = class _TunnelManager {
165
178
  }
166
179
  /**
167
180
  * Creates a new managed tunnel instance with the given configuration.
168
- * Builds the config with forwarding rules and creates the tunnel instance.
181
+ * Optionally builds the config with forwarding rules based on buildConfig flag.
169
182
  *
170
183
  * @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
184
  *
175
185
  * @throws {Error} When configId is invalid or empty
176
186
  * @throws {Error} When a tunnel with the given configId already exists
@@ -179,24 +189,19 @@ var TunnelManager = class _TunnelManager {
179
189
  * status information, and statistics
180
190
  */
181
191
  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`);
192
+ const { configId, tunnelid: requestedTunnelId, tunnelName, name, serve } = config;
193
+ const tunnelid = requestedTunnelId || getRandomId();
194
+ const autoReconnect = config.autoReconnect || false;
195
+ if (!configId || typeof configId !== "string" || configId.trim() === "") {
196
+ throw new Error("configId is required and must be a non-empty string");
188
197
  }
189
- const tunnelid = config.tunnelid || getRandomId();
190
- const configWithForwarding = this.buildPinggyConfig(config, additionalForwarding);
191
198
  return this._createTunnelWithProcessedConfig({
192
- configid,
199
+ configId,
193
200
  tunnelid,
194
- tunnelName,
201
+ tunnelName: tunnelName || name,
195
202
  originalConfig: config,
196
- configWithForwarding,
197
- additionalForwarding,
198
- serve: config.serve,
199
- autoReconnect: config.autoReconnect !== void 0 ? config.autoReconnect : false
203
+ serve,
204
+ autoReconnect
200
205
  });
201
206
  }
202
207
  /**
@@ -210,7 +215,8 @@ var TunnelManager = class _TunnelManager {
210
215
  async _createTunnelWithProcessedConfig(params) {
211
216
  let instance;
212
217
  try {
213
- instance = await pinggy.createTunnel(params.configWithForwarding);
218
+ logger.debug("Creating tunnel instance with processed config", params.originalConfig);
219
+ instance = await pinggy.createTunnel(params.originalConfig);
214
220
  } catch (e) {
215
221
  logger.error("Error creating tunnel instance:", e);
216
222
  throw e;
@@ -218,12 +224,10 @@ var TunnelManager = class _TunnelManager {
218
224
  const now = (/* @__PURE__ */ new Date()).toISOString();
219
225
  const managed = {
220
226
  tunnelid: params.tunnelid,
221
- configid: params.configid,
227
+ configId: params.configId,
222
228
  tunnelName: params.tunnelName,
223
229
  instance,
224
230
  tunnelConfig: params.originalConfig,
225
- configWithForwarding: params.configWithForwarding,
226
- additionalForwarding: params.additionalForwarding,
227
231
  serve: params.serve,
228
232
  warnings: [],
229
233
  isStopped: false,
@@ -237,6 +241,7 @@ var TunnelManager = class _TunnelManager {
237
241
  });
238
242
  this.setupStatsCallback(params.tunnelid, managed);
239
243
  this.setupErrorCallback(params.tunnelid, managed);
244
+ this.setupTunnelPollingErrorCallback(params.tunnelid, managed);
240
245
  this.setupDisconnectCallback(params.tunnelid, managed);
241
246
  this.setupWillReconnectCallback(params.tunnelid, managed);
242
247
  this.setupReconnectingCallback(params.tunnelid, managed);
@@ -244,44 +249,10 @@ var TunnelManager = class _TunnelManager {
244
249
  this.setupReconnectionFailedCallback(params.tunnelid, managed);
245
250
  this.setUpTunnelWorkerErrorCallback(params.tunnelid, managed);
246
251
  this.tunnelsByTunnelId.set(params.tunnelid, managed);
247
- this.tunnelsByConfigId.set(params.configid, managed);
248
- logger.info("Tunnel created", { configid: params.configid, tunnelid: params.tunnelid });
252
+ this.tunnelsByConfigId.set(params.configId, managed);
253
+ logger.info("Tunnel created", { configId: params.configId, tunnelId: params.tunnelid });
249
254
  return managed;
250
255
  }
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
256
  /**
286
257
  * Start a tunnel that was created but not yet started
287
258
  */
@@ -293,7 +264,7 @@ var TunnelManager = class _TunnelManager {
293
264
  try {
294
265
  urls = await managed.instance.start();
295
266
  } catch (error) {
296
- logger.error("Failed to start tunnel", { tunnelId, error });
267
+ logger.warn("Failed to start tunnel", { tunnelId, error });
297
268
  throw error;
298
269
  }
299
270
  logger.info("Tunnel started", { tunnelId, urls });
@@ -329,7 +300,7 @@ var TunnelManager = class _TunnelManager {
329
300
  stopTunnel(tunnelId) {
330
301
  const managed = this.tunnelsByTunnelId.get(tunnelId);
331
302
  if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
332
- logger.info("Stopping tunnel", { tunnelId, configId: managed.configid });
303
+ logger.info("Stopping tunnel", { tunnelId, configId: managed.configId });
333
304
  try {
334
305
  managed.instance.stop();
335
306
  if (managed.serveWorker) {
@@ -341,6 +312,7 @@ var TunnelManager = class _TunnelManager {
341
312
  this.tunnelStats.delete(tunnelId);
342
313
  this.tunnelStatsListeners.delete(tunnelId);
343
314
  this.tunnelErrorListeners.delete(tunnelId);
315
+ this.tunnelPollingErrorListeners.delete(tunnelId);
344
316
  this.tunnelDisconnectListeners.delete(tunnelId);
345
317
  this.tunnelWorkerErrorListeners.delete(tunnelId);
346
318
  this.tunnelStartListeners.delete(tunnelId);
@@ -352,8 +324,8 @@ var TunnelManager = class _TunnelManager {
352
324
  managed.warnings = managed.warnings ?? [];
353
325
  managed.isStopped = true;
354
326
  managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
355
- logger.info("Tunnel stopped", { tunnelId, configId: managed.configid });
356
- return { configid: managed.configid, tunnelid: managed.tunnelid };
327
+ logger.info("Tunnel stopped", { tunnelId, configId: managed.configId });
328
+ return { configId: managed.configId, tunnelid: managed.tunnelid };
357
329
  } catch (error) {
358
330
  logger.error("Failed to stop tunnel", { tunnelId, error });
359
331
  throw error;
@@ -386,11 +358,10 @@ var TunnelManager = class _TunnelManager {
386
358
  const tunnelList = await Promise.all(Array.from(this.tunnelsByTunnelId.values()).map(async (tunnel) => {
387
359
  return {
388
360
  tunnelid: tunnel.tunnelid,
389
- configid: tunnel.configid,
361
+ configId: tunnel.configId,
390
362
  tunnelName: tunnel.tunnelName,
391
363
  tunnelConfig: tunnel.tunnelConfig,
392
364
  remoteurls: !tunnel.isStopped ? await this.getTunnelUrls(tunnel.tunnelid) : [],
393
- additionalForwarding: tunnel.additionalForwarding,
394
365
  serve: tunnel.serve
395
366
  };
396
367
  }));
@@ -431,6 +402,15 @@ var TunnelManager = class _TunnelManager {
431
402
  this.tunnelsByConfigId.clear();
432
403
  this.tunnelStats.clear();
433
404
  this.tunnelStatsListeners.clear();
405
+ this.tunnelErrorListeners.clear();
406
+ this.tunnelPollingErrorListeners.clear();
407
+ this.tunnelDisconnectListeners.clear();
408
+ this.tunnelWorkerErrorListeners.clear();
409
+ this.tunnelStartListeners.clear();
410
+ this.tunnelWillReconnectListeners.clear();
411
+ this.tunnelReconnectingListeners.clear();
412
+ this.tunnelReconnectionCompletedListeners.clear();
413
+ this.tunnelReconnectionFailedListeners.clear();
434
414
  logger.info("All tunnels stopped and cleared");
435
415
  }
436
416
  /**
@@ -451,7 +431,7 @@ var TunnelManager = class _TunnelManager {
451
431
  return false;
452
432
  }
453
433
  this._cleanupTunnelRecords(managed);
454
- logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.configid });
434
+ logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.configId });
455
435
  return true;
456
436
  }
457
437
  /**
@@ -478,6 +458,7 @@ var TunnelManager = class _TunnelManager {
478
458
  this.tunnelStats.delete(managed.tunnelid);
479
459
  this.tunnelStatsListeners.delete(managed.tunnelid);
480
460
  this.tunnelErrorListeners.delete(managed.tunnelid);
461
+ this.tunnelPollingErrorListeners.delete(managed.tunnelid);
481
462
  this.tunnelDisconnectListeners.delete(managed.tunnelid);
482
463
  this.tunnelWorkerErrorListeners.delete(managed.tunnelid);
483
464
  this.tunnelStartListeners.delete(managed.tunnelid);
@@ -486,7 +467,7 @@ var TunnelManager = class _TunnelManager {
486
467
  this.tunnelReconnectionCompletedListeners.delete(managed.tunnelid);
487
468
  this.tunnelReconnectionFailedListeners.delete(managed.tunnelid);
488
469
  this.tunnelsByTunnelId.delete(managed.tunnelid);
489
- this.tunnelsByConfigId.delete(managed.configid);
470
+ this.tunnelsByConfigId.delete(managed.configId);
490
471
  } catch (e) {
491
472
  logger.warn("Failed cleaning up tunnel records", { tunnelId: managed.tunnelid, error: e });
492
473
  }
@@ -547,21 +528,20 @@ var TunnelManager = class _TunnelManager {
547
528
  }
548
529
  logger.info("Initiating tunnel restart", {
549
530
  tunnelId: tunnelid,
550
- configId: existingTunnel.configid
531
+ configId: existingTunnel.configId
551
532
  });
552
533
  try {
553
534
  const tunnelName = existingTunnel.tunnelName;
554
- const currentConfigId = existingTunnel.configid;
535
+ const currentConfigId = existingTunnel.configId;
555
536
  const currentConfig = existingTunnel.tunnelConfig;
556
- const configWithForwarding = existingTunnel.configWithForwarding;
557
- const additionalForwarding = existingTunnel.additionalForwarding;
558
537
  const currentServe = existingTunnel.serve;
559
538
  const autoReconnect = existingTunnel.autoReconnect || false;
560
539
  this.tunnelsByTunnelId.delete(tunnelid);
561
- this.tunnelsByConfigId.delete(existingTunnel.configid);
540
+ this.tunnelsByConfigId.delete(existingTunnel.configId);
562
541
  this.tunnelStats.delete(tunnelid);
563
542
  this.tunnelStatsListeners.delete(tunnelid);
564
543
  this.tunnelErrorListeners.delete(tunnelid);
544
+ this.tunnelPollingErrorListeners.delete(tunnelid);
565
545
  this.tunnelDisconnectListeners.delete(tunnelid);
566
546
  this.tunnelWorkerErrorListeners.delete(tunnelid);
567
547
  this.tunnelStartListeners.delete(tunnelid);
@@ -570,12 +550,10 @@ var TunnelManager = class _TunnelManager {
570
550
  this.tunnelReconnectionCompletedListeners.delete(tunnelid);
571
551
  this.tunnelReconnectionFailedListeners.delete(tunnelid);
572
552
  const newTunnel = await this._createTunnelWithProcessedConfig({
573
- configid: currentConfigId,
553
+ configId: currentConfigId,
574
554
  tunnelid,
575
555
  tunnelName,
576
556
  originalConfig: currentConfig,
577
- configWithForwarding,
578
- additionalForwarding,
579
557
  serve: currentServe,
580
558
  autoReconnect
581
559
  });
@@ -604,20 +582,18 @@ var TunnelManager = class _TunnelManager {
604
582
  * @throws Error if the tunnel is not found or if the update process fails
605
583
  */
606
584
  async updateConfig(newConfig) {
607
- const { configid, tunnelName: newTunnelName, additionalForwarding } = newConfig;
608
- if (!configid || configid.trim().length === 0) {
609
- throw new Error(`Invalid configid: "${configid}"`);
585
+ const { configId, tunnelName: newTunnelName } = newConfig;
586
+ if (!configId || configId.trim().length === 0) {
587
+ throw new Error(`Invalid configId: "${configId}"`);
610
588
  }
611
- const existingTunnel = this.tunnelsByConfigId.get(configid);
589
+ const existingTunnel = this.tunnelsByConfigId.get(configId);
612
590
  if (!existingTunnel) {
613
- throw new Error(`Tunnel with config id "${configid}" not found`);
591
+ throw new Error(`Tunnel with config id "${configId}" not found`);
614
592
  }
615
593
  const isStopped = existingTunnel.isStopped;
616
594
  const currentTunnelConfig = existingTunnel.tunnelConfig;
617
- const currentConfigWithForwarding = existingTunnel.configWithForwarding;
618
595
  const currentTunnelId = existingTunnel.tunnelid;
619
- const currentTunnelConfigId = existingTunnel.configid;
620
- const currentAdditionalForwarding = existingTunnel.additionalForwarding;
596
+ const currentTunnelConfigId = existingTunnel.configId;
621
597
  const currentTunnelName = existingTunnel.tunnelName;
622
598
  const currentServe = existingTunnel.serve;
623
599
  const currentAutoReconnect = existingTunnel.autoReconnect || false;
@@ -629,22 +605,19 @@ var TunnelManager = class _TunnelManager {
629
605
  this.tunnelsByConfigId.delete(currentTunnelConfigId);
630
606
  const mergedBaseConfig = {
631
607
  ...newConfig,
632
- configid,
608
+ configId,
633
609
  tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
634
610
  serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe
635
611
  };
636
- const newConfigWithForwarding = this.buildPinggyConfig(
637
- mergedBaseConfig,
638
- additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding
639
- );
612
+ const effectiveServe = newConfig.serve !== void 0 ? newConfig.serve : currentServe;
613
+ const effectiveTunnelName = newTunnelName !== void 0 ? newTunnelName : currentTunnelName;
614
+ let configWithForwarding;
640
615
  const newTunnel = await this._createTunnelWithProcessedConfig({
641
- configid,
616
+ configId,
642
617
  tunnelid: currentTunnelId,
643
- tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
618
+ tunnelName: effectiveTunnelName,
644
619
  originalConfig: mergedBaseConfig,
645
- configWithForwarding: newConfigWithForwarding,
646
- additionalForwarding: additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding,
647
- serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe,
620
+ serve: effectiveServe,
648
621
  autoReconnect: currentAutoReconnect
649
622
  });
650
623
  if (!isStopped) {
@@ -652,23 +625,21 @@ var TunnelManager = class _TunnelManager {
652
625
  }
653
626
  logger.info("Tunnel configuration updated", {
654
627
  tunnelId: newTunnel.tunnelid,
655
- configId: newTunnel.configid,
628
+ configId: newTunnel.configId,
656
629
  isStopped
657
630
  });
658
631
  return newTunnel;
659
632
  } catch (error) {
660
633
  logger.error("Error updating tunnel configuration", {
661
- configId: configid,
634
+ configId,
662
635
  error: error instanceof Error ? error.message : String(error)
663
636
  });
664
637
  try {
665
638
  const originalTunnel = await this._createTunnelWithProcessedConfig({
666
- configid: currentTunnelConfigId,
639
+ configId: currentTunnelConfigId,
667
640
  tunnelid: currentTunnelId,
668
641
  tunnelName: currentTunnelName,
669
642
  originalConfig: currentTunnelConfig,
670
- configWithForwarding: currentConfigWithForwarding,
671
- additionalForwarding: currentAdditionalForwarding,
672
643
  serve: currentServe,
673
644
  autoReconnect: currentAutoReconnect
674
645
  });
@@ -784,6 +755,19 @@ var TunnelManager = class _TunnelManager {
784
755
  logger.info("Error listener registered for tunnel", { tunnelId, listenerId });
785
756
  return listenerId;
786
757
  }
758
+ async registerPollingErrorListener(tunnelId, listener) {
759
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
760
+ if (!managed) {
761
+ throw new Error(`Tunnel "${tunnelId}" not found`);
762
+ }
763
+ if (!this.tunnelPollingErrorListeners.has(tunnelId)) {
764
+ this.tunnelPollingErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
765
+ }
766
+ const listenerId = getRandomId();
767
+ this.tunnelPollingErrorListeners.get(tunnelId).set(listenerId, listener);
768
+ logger.info("Polling error listener registered for tunnel", { tunnelId, listenerId });
769
+ return listenerId;
770
+ }
787
771
  async registerDisconnectListener(tunnelId, listener) {
788
772
  const managed = this.tunnelsByTunnelId.get(tunnelId);
789
773
  if (!managed) {
@@ -915,6 +899,22 @@ var TunnelManager = class _TunnelManager {
915
899
  logger.warn("Attempted to deregister non-existent error listener", { tunnelId, listenerId });
916
900
  }
917
901
  }
902
+ deregisterPollingErrorListener(tunnelId, listenerId) {
903
+ const listeners = this.tunnelPollingErrorListeners.get(tunnelId);
904
+ if (!listeners) {
905
+ logger.warn("No polling error listeners found for tunnel", { tunnelId });
906
+ return;
907
+ }
908
+ const removed = listeners.delete(listenerId);
909
+ if (removed) {
910
+ logger.info("Polling error listener deregistered", { tunnelId, listenerId });
911
+ if (listeners.size === 0) {
912
+ this.tunnelPollingErrorListeners.delete(tunnelId);
913
+ }
914
+ } else {
915
+ logger.warn("Attempted to deregister non-existent polling error listener", { tunnelId, listenerId });
916
+ }
917
+ }
918
918
  deregisterDisconnectListener(tunnelId, listenerId) {
919
919
  const listeners = this.tunnelDisconnectListeners.get(tunnelId);
920
920
  if (!listeners) {
@@ -1032,6 +1032,38 @@ var TunnelManager = class _TunnelManager {
1032
1032
  logger.warn("Failed to set up stats callback", { tunnelId, error });
1033
1033
  }
1034
1034
  }
1035
+ setupTunnelPollingErrorCallback(tunnelId, managed) {
1036
+ try {
1037
+ const callback = ({ error }) => {
1038
+ try {
1039
+ const errorMessage = error instanceof Error ? error.message : String(error);
1040
+ logger.info("Tunnel reported polling error", { tunnelId, errorMessage });
1041
+ this.notifyPollingErrorListeners(tunnelId, errorMessage);
1042
+ } catch (e) {
1043
+ logger.warn("Error handling tunnel polling error callback", { tunnelId, e });
1044
+ }
1045
+ };
1046
+ managed.instance.setPollingErrorCallback(callback);
1047
+ logger.debug("Tunnel polling error callback set up for tunnel", { tunnelId });
1048
+ } catch (error) {
1049
+ logger.warn("Failed to set up tunnel polling error callback", { tunnelId, error });
1050
+ }
1051
+ }
1052
+ notifyPollingErrorListeners(tunnelId, errorMsg) {
1053
+ try {
1054
+ const listeners = this.tunnelPollingErrorListeners.get(tunnelId);
1055
+ if (!listeners) return;
1056
+ for (const [id, listener] of listeners) {
1057
+ try {
1058
+ listener(tunnelId, errorMsg);
1059
+ } catch (err) {
1060
+ logger.debug("Error in polling-error-listener callback", { listenerId: id, tunnelId, err });
1061
+ }
1062
+ }
1063
+ } catch (err) {
1064
+ logger.debug("Failed to notify polling error listeners", { tunnelId, err });
1065
+ }
1066
+ }
1035
1067
  notifyErrorListeners(tunnelId, errorMsg, isFatal) {
1036
1068
  try {
1037
1069
  const listeners = this.tunnelErrorListeners.get(tunnelId);
@@ -1304,9 +1336,9 @@ var TunnelManager = class _TunnelManager {
1304
1336
  }
1305
1337
  startStaticFileServer(managed) {
1306
1338
  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");
1339
+ const __filename4 = fileURLToPath2(import.meta.url);
1340
+ const __dirname4 = path.dirname(__filename4);
1341
+ const fileServerWorkerPath = path.join(__dirname4, "workers", "file_serve_worker.cjs");
1310
1342
  const staticServerWorker = new Worker(fileServerWorkerPath, {
1311
1343
  workerData: {
1312
1344
  dir: managed.serve,
@@ -1449,11 +1481,11 @@ var RemoteManagementStatus = {
1449
1481
  };
1450
1482
 
1451
1483
  // src/remote_management/remote_schema.ts
1452
- import { TunnelType as TunnelType2 } from "@pinggy/pinggy";
1484
+ import { TunnelType } from "@pinggy/pinggy";
1453
1485
  import { z } from "zod";
1454
1486
  var HeaderModificationSchema = z.object({
1455
1487
  key: z.string(),
1456
- value: z.array(z.string()).optional(),
1488
+ value: z.array(z.string()).nullable().optional(),
1457
1489
  type: z.enum(["add", "remove", "update"])
1458
1490
  });
1459
1491
  var AdditionalForwardingSchema = z.object({
@@ -1469,7 +1501,7 @@ var TunnelConfigSchema = z.object({
1469
1501
  // legacy key
1470
1502
  autoreconnect: z.boolean(),
1471
1503
  basicauth: z.array(z.object({ username: z.string(), password: z.string() })).nullable(),
1472
- bearerauth: z.string().nullable(),
1504
+ bearerauth: z.array(z.string()).nullable(),
1473
1505
  configid: z.string(),
1474
1506
  configname: z.string(),
1475
1507
  greetmsg: z.string().optional(),
@@ -1491,11 +1523,11 @@ var TunnelConfigSchema = z.object({
1491
1523
  token: z.string(),
1492
1524
  tunnelTimeout: z.number(),
1493
1525
  type: z.enum([
1494
- TunnelType2.Http,
1495
- TunnelType2.Tcp,
1496
- TunnelType2.Udp,
1497
- TunnelType2.Tls,
1498
- TunnelType2.TlsTcp
1526
+ TunnelType.Http,
1527
+ TunnelType.Tcp,
1528
+ TunnelType.Udp,
1529
+ TunnelType.Tls,
1530
+ TunnelType.TlsTcp
1499
1531
  ]),
1500
1532
  webdebuggerport: z.number(),
1501
1533
  xff: z.string(),
@@ -1526,15 +1558,102 @@ var RestartSchema = StopSchema;
1526
1558
  var UpdateConfigSchema = z.object({
1527
1559
  tunnelConfig: TunnelConfigSchema
1528
1560
  });
1561
+ var ForwardingEntryV2Schema = z.object({
1562
+ listenAddress: z.string().optional(),
1563
+ address: z.string(),
1564
+ type: z.enum([TunnelType.Http, TunnelType.Tcp, TunnelType.Udp, TunnelType.Tls, TunnelType.TlsTcp]).optional()
1565
+ });
1566
+ var TunnelConfigV1Schema = z.object({
1567
+ // Meta Info
1568
+ version: z.string(),
1569
+ name: z.string(),
1570
+ configId: z.string(),
1571
+ // General tunnel configurations
1572
+ serverAddress: z.string().optional(),
1573
+ token: z.string().optional(),
1574
+ autoReconnect: z.boolean().optional(),
1575
+ reconnectInterval: z.number().optional(),
1576
+ maxReconnectAttempts: z.number().optional(),
1577
+ force: z.boolean(),
1578
+ keepAliveInterval: z.number().optional(),
1579
+ webDebugger: z.string(),
1580
+ //Forwarding
1581
+ // Either a URL string (e.g. "https://localhost:5555") or an array of forwarding entries.
1582
+ forwarding: z.union([
1583
+ z.string(),
1584
+ z.array(ForwardingEntryV2Schema)
1585
+ ]),
1586
+ // IP whitelist
1587
+ ipWhitelist: z.array(z.string()).optional(),
1588
+ basicAuth: z.array(z.object({ username: z.string(), password: z.string() })).optional(),
1589
+ bearerTokenAuth: z.array(z.string()).optional(),
1590
+ headerModification: z.array(HeaderModificationSchema).optional(),
1591
+ reverseProxy: z.boolean().optional(),
1592
+ xForwardedFor: z.boolean().optional(),
1593
+ httpsOnly: z.boolean().optional(),
1594
+ originalRequestUrl: z.boolean().optional(),
1595
+ allowPreflight: z.boolean().optional(),
1596
+ serve: z.string().optional(),
1597
+ optional: z.record(z.string(), z.unknown()).optional()
1598
+ });
1599
+ var StartV2Schema = z.object({
1600
+ tunnelID: z.string().nullable().optional(),
1601
+ tunnelConfig: TunnelConfigV1Schema
1602
+ });
1603
+ var UpdateConfigV2Schema = z.object({
1604
+ tunnelConfig: TunnelConfigV1Schema
1605
+ });
1606
+ function pinggyOptionsToTunnelConfigV1(opts, configStoredInCli) {
1607
+ const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
1608
+ return {
1609
+ version: configStoredInCli.version || "1.0",
1610
+ name: configStoredInCli.name || "",
1611
+ configId: configStoredInCli.configId || "",
1612
+ serverAddress: opts.serverAddress || "a.pinggy.io:443",
1613
+ token: opts.token || "",
1614
+ autoReconnect: opts.autoReconnect ?? true,
1615
+ force: opts.force ?? false,
1616
+ webDebugger: opts.webDebugger || "",
1617
+ forwarding: opts.forwarding ? opts.forwarding : "",
1618
+ ipWhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : [],
1619
+ basicAuth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : void 0,
1620
+ bearerTokenAuth: parsedTokens.length ? parsedTokens : void 0,
1621
+ headerModification: opts.headerModification || [],
1622
+ reverseProxy: opts.reverseProxy ?? false,
1623
+ xForwardedFor: !!opts.xForwardedFor,
1624
+ httpsOnly: opts.httpsOnly ?? false,
1625
+ originalRequestUrl: opts.originalRequestUrl ?? false,
1626
+ allowPreflight: opts.allowPreflight ?? false,
1627
+ optional: opts.optional || {}
1628
+ };
1629
+ }
1529
1630
  function tunnelConfigToPinggyOptions(config) {
1631
+ const forwardingData = [];
1632
+ forwardingData.push({
1633
+ address: `${config.forwardedhost}:${config.localport}`,
1634
+ type: config.type || TunnelType.Http
1635
+ // Default to HTTP for the primary forwarding entry
1636
+ });
1637
+ if (config.additionalForwarding && Array.isArray(config.additionalForwarding)) {
1638
+ config.additionalForwarding.forEach((entry) => {
1639
+ if (entry.localDomain && entry.localPort && entry.remoteDomain) {
1640
+ const listenAddress = entry.remotePort && isValidPort(entry.remotePort) ? `${entry.remoteDomain}:${entry.remotePort}` : entry.remoteDomain;
1641
+ forwardingData.push({
1642
+ address: `${entry.localDomain}:${entry.localPort}`,
1643
+ listenAddress,
1644
+ type: TunnelType.Http
1645
+ });
1646
+ }
1647
+ });
1648
+ }
1530
1649
  return {
1531
1650
  token: config.token || "",
1532
1651
  serverAddress: config.serveraddress || "free.pinggy.io",
1533
- forwarding: `${config.forwardedhost || "localhost"}:${config.localport}`,
1652
+ forwarding: forwardingData,
1534
1653
  webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
1535
1654
  ipWhitelist: config.ipwhitelist || [],
1536
1655
  basicAuth: config.basicauth ? config.basicauth : [],
1537
- bearerTokenAuth: config.bearerauth ? [config.bearerauth] : [],
1656
+ bearerTokenAuth: config.bearerauth || [],
1538
1657
  headerModification: config.headermodification,
1539
1658
  xForwardedFor: !!config.xff,
1540
1659
  httpsOnly: config.httpsOnly,
@@ -1548,18 +1667,38 @@ function tunnelConfigToPinggyOptions(config) {
1548
1667
  }
1549
1668
  };
1550
1669
  }
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;
1670
+ function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg, serve) {
1671
+ let primaryEntry;
1672
+ let additionalEntries = [];
1673
+ if (Array.isArray(opts.forwarding)) {
1674
+ primaryEntry = opts.forwarding.find((e) => !e.listenAddress) ?? opts.forwarding[0];
1675
+ additionalEntries = opts.forwarding.filter(
1676
+ (e) => e !== primaryEntry && Boolean(e.listenAddress)
1677
+ );
1678
+ }
1679
+ const forwarding = primaryEntry ? String(primaryEntry.address) : String(opts.forwarding);
1680
+ const [parsedForwardedHost, portStr] = forwarding.split(":");
1681
+ const parsedLocalPort = parseInt(portStr, 10);
1682
+ const tunnelType = primaryEntry?.type ?? TunnelType.Http;
1683
+ const additionalForwarding = additionalEntries.map((e) => {
1684
+ const [localDomain, localPortStr] = String(e.address).split(":");
1685
+ const [remoteDomain, remotePortStr] = String(e.listenAddress).split(":");
1686
+ const localPort = parseInt(localPortStr, 10);
1687
+ const remotePort = parseInt(remotePortStr, 10);
1688
+ return {
1689
+ localDomain,
1690
+ localPort: isNaN(localPort) ? 0 : localPort,
1691
+ remoteDomain,
1692
+ remotePort: isNaN(remotePort) ? 0 : remotePort
1693
+ };
1694
+ });
1556
1695
  const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
1557
1696
  return {
1558
1697
  allowPreflight: opts.allowPreflight ?? false,
1559
1698
  allowpreflight: opts.allowPreflight ?? false,
1560
1699
  autoreconnect: opts.autoReconnect ?? false,
1561
1700
  basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
1562
- bearerauth: parsedTokens.length ? parsedTokens.join(",") : null,
1701
+ bearerauth: parsedTokens.length ? [parsedTokens.join(",")] : null,
1563
1702
  configid,
1564
1703
  configname: configName,
1565
1704
  greetmsg: greetMsg || "",
@@ -1590,7 +1729,6 @@ function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls,
1590
1729
  }
1591
1730
 
1592
1731
  // src/remote_management/handler.ts
1593
- import { TunnelType as TunnelType3 } from "@pinggy/pinggy";
1594
1732
  var TunnelOperations = class {
1595
1733
  constructor() {
1596
1734
  this.tunnelManager = TunnelManager.getInstance();
@@ -1609,7 +1747,7 @@ var TunnelOperations = class {
1609
1747
  return status;
1610
1748
  }
1611
1749
  // --- Helper to construct TunnelResponse ---
1612
- async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, additionalForwarding, serve) {
1750
+ async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, serve) {
1613
1751
  const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
1614
1752
  this.tunnelManager.getTunnelStatus(tunnelid),
1615
1753
  this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
@@ -1620,11 +1758,27 @@ var TunnelOperations = class {
1620
1758
  return {
1621
1759
  tunnelid,
1622
1760
  remoteurls,
1623
- tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg, additionalForwarding),
1761
+ tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg),
1624
1762
  status: this.buildStatus(tunnelid, status, "" /* NoError */),
1625
1763
  stats
1626
1764
  };
1627
1765
  }
1766
+ async buildTunnelResponseV2(tunnelid, tunnelConfig, configFromCli, configid, tunnelName, serve) {
1767
+ const [status, stats, greetMsg, remoteurls] = await Promise.all([
1768
+ this.tunnelManager.getTunnelStatus(tunnelid),
1769
+ this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
1770
+ this.tunnelManager.getTunnelGreetMessage(tunnelid),
1771
+ this.tunnelManager.getTunnelUrls(tunnelid)
1772
+ ]);
1773
+ return {
1774
+ tunnelid,
1775
+ remoteurls,
1776
+ tunnelconfig: pinggyOptionsToTunnelConfigV1(tunnelConfig, configFromCli),
1777
+ status: this.buildStatus(tunnelid, status, "" /* NoError */),
1778
+ stats,
1779
+ greetmsg: greetMsg
1780
+ };
1781
+ }
1628
1782
  error(code, err, fallback) {
1629
1783
  return newErrorResponse({
1630
1784
  code,
@@ -1635,19 +1789,28 @@ var TunnelOperations = class {
1635
1789
  async handleStart(config) {
1636
1790
  try {
1637
1791
  const opts = tunnelConfigToPinggyOptions(config);
1638
- const additionalForwardingParsed = config.additionalForwarding || [];
1639
- const { tunnelid, instance, tunnelName, additionalForwarding, serve } = await this.tunnelManager.createTunnel({
1792
+ const { tunnelid, instance, tunnelName, serve, tunnelConfig } = await this.tunnelManager.createTunnel({
1640
1793
  ...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
1794
+ configId: config.configid,
1795
+ name: config.configname,
1796
+ optional: {
1797
+ serve: config.serve
1798
+ }
1647
1799
  });
1648
- this.tunnelManager.startTunnel(tunnelid);
1800
+ await this.tunnelManager.startTunnel(tunnelid);
1801
+ const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
1802
+ const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, serve);
1803
+ return resp;
1804
+ } catch (err) {
1805
+ return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
1806
+ }
1807
+ }
1808
+ async handleStartV2(config) {
1809
+ try {
1810
+ const { tunnelid, instance, serve } = await this.tunnelManager.createTunnel(config);
1811
+ await this.tunnelManager.startTunnel(tunnelid);
1649
1812
  const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
1650
- const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, additionalForwarding, serve);
1813
+ const resp = this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.serve);
1651
1814
  return resp;
1652
1815
  } catch (err) {
1653
1816
  return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
@@ -1658,20 +1821,59 @@ var TunnelOperations = class {
1658
1821
  const opts = tunnelConfigToPinggyOptions(config);
1659
1822
  const tunnel = await this.tunnelManager.updateConfig({
1660
1823
  ...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
1824
+ configId: config.configid,
1825
+ name: config.configname,
1826
+ optional: {
1827
+ serve: config.serve
1828
+ }
1667
1829
  });
1668
1830
  if (!tunnel.instance || !tunnel.tunnelConfig)
1669
1831
  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);
1832
+ return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.serve);
1833
+ } catch (err) {
1834
+ return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
1835
+ }
1836
+ }
1837
+ async handleUpdateConfigV2(config) {
1838
+ try {
1839
+ const tunnel = await this.tunnelManager.updateConfig(config);
1840
+ if (!tunnel.instance || !tunnel.tunnelConfig)
1841
+ throw new Error("Invalid tunnel state after configuration update");
1842
+ return this.buildTunnelResponseV2(tunnel.tunnelid, tunnel.tunnelConfig, config, config.configId, tunnel.tunnelName, tunnel.serve);
1671
1843
  } catch (err) {
1672
1844
  return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
1673
1845
  }
1674
1846
  }
1847
+ async handleListV2() {
1848
+ try {
1849
+ const tunnels = await this.tunnelManager.getAllTunnels();
1850
+ if (tunnels.length === 0) {
1851
+ return [];
1852
+ }
1853
+ return Promise.all(
1854
+ tunnels.map(async (t) => {
1855
+ const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
1856
+ const [status, tlsInfo, greetMsg] = await Promise.all([
1857
+ this.tunnelManager.getTunnelStatus(t.tunnelid),
1858
+ this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
1859
+ this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
1860
+ ]);
1861
+ const tunnelConfguration = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
1862
+ const tunnelConfig = pinggyOptionsToTunnelConfigV1(tunnelConfguration, t.tunnelConfig);
1863
+ return {
1864
+ tunnelid: t.tunnelid,
1865
+ remoteurls: t.remoteurls,
1866
+ status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
1867
+ stats: rawStats,
1868
+ tunnelconfig: tunnelConfig,
1869
+ greetmsg: greetMsg
1870
+ };
1871
+ })
1872
+ );
1873
+ } catch (err) {
1874
+ return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
1875
+ }
1876
+ }
1675
1877
  async handleList() {
1676
1878
  try {
1677
1879
  const tunnels = await this.tunnelManager.getAllTunnels();
@@ -1687,7 +1889,7 @@ var TunnelOperations = class {
1687
1889
  this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
1688
1890
  ]);
1689
1891
  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);
1892
+ const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configId, t.tunnelName, tlsInfo, greetMsg, t.serve);
1691
1893
  return {
1692
1894
  tunnelid: t.tunnelid,
1693
1895
  remoteurls: t.remoteurls,
@@ -1703,10 +1905,10 @@ var TunnelOperations = class {
1703
1905
  }
1704
1906
  async handleStop(tunnelid) {
1705
1907
  try {
1706
- const { configid } = this.tunnelManager.stopTunnel(tunnelid);
1908
+ const { configId } = this.tunnelManager.stopTunnel(tunnelid);
1707
1909
  const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1708
1910
  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);
1911
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configId, managed.tunnelName, managed.serve);
1710
1912
  } catch (err) {
1711
1913
  return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
1712
1914
  }
@@ -1715,7 +1917,7 @@ var TunnelOperations = class {
1715
1917
  try {
1716
1918
  const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1717
1919
  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);
1920
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configId, managed.tunnelName, managed.serve);
1719
1921
  } catch (err) {
1720
1922
  return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
1721
1923
  }
@@ -1725,7 +1927,7 @@ var TunnelOperations = class {
1725
1927
  await this.tunnelManager.restartTunnel(tunnelid);
1726
1928
  const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1727
1929
  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);
1930
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configId, managed.tunnelName, managed.serve);
1729
1931
  } catch (err) {
1730
1932
  return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
1731
1933
  }
@@ -1769,11 +1971,198 @@ var TunnelOperations = class {
1769
1971
  // src/remote_management/remoteManagement.ts
1770
1972
  import WebSocket from "ws";
1771
1973
 
1974
+ // src/remote_management/websocket_printer.ts
1975
+ import pico3 from "picocolors";
1976
+ var PENDING_START_TIMEOUT_MS = 5 * 60 * 1e3;
1977
+ var RemoteManagementWebSocketPrinter = class {
1978
+ constructor() {
1979
+ this.tunnelManager = TunnelManager.getInstance();
1980
+ this.pendingStarts = /* @__PURE__ */ new Map();
1981
+ }
1982
+ setTunnelHandler(tunnelHandler) {
1983
+ this.tunnelHandler = tunnelHandler;
1984
+ }
1985
+ queueStart(config) {
1986
+ this.cleanupExpiredPendingStarts();
1987
+ const entry = {
1988
+ configId: this.getConfigIdFromRequest(config),
1989
+ configName: this.getConfigNameFromRequest(config),
1990
+ queuedAt: Date.now()
1991
+ };
1992
+ this.latestPendingConfigId = entry.configId;
1993
+ this.pendingStarts.set(entry.configId, entry);
1994
+ printer_default.startSpinner("Starting tunnel with config name: " + entry.configName);
1995
+ }
1996
+ failQueuedStart(config, reason) {
1997
+ const configId = this.getConfigIdFromRequest(config);
1998
+ const pending = this.pendingStarts.get(configId);
1999
+ const configName = pending?.configName || this.getConfigNameFromRequest(config);
2000
+ this.pendingStarts.delete(configId);
2001
+ if (this.latestPendingConfigId === configId) {
2002
+ this.latestPendingConfigId = void 0;
2003
+ printer_default.stopSpinnerFail(`Failed to start tunnel with config name: ${configName}. ${reason}`);
2004
+ }
2005
+ }
2006
+ handleStartResult(config, result) {
2007
+ this.cleanupExpiredPendingStarts();
2008
+ const requestedConfigId = this.getConfigIdFromRequest(config);
2009
+ if (this.latestPendingConfigId && requestedConfigId !== this.latestPendingConfigId) {
2010
+ this.pendingStarts.delete(requestedConfigId);
2011
+ return;
2012
+ }
2013
+ if (isErrorResponse(result)) {
2014
+ this.failQueuedStart(config, result.message);
2015
+ return;
2016
+ }
2017
+ const configId = this.getConfigIdFromTunnel(result);
2018
+ const pending = this.pendingStarts.get(requestedConfigId) || {
2019
+ configId: requestedConfigId,
2020
+ configName: this.getConfigNameFromRequest(config),
2021
+ queuedAt: Date.now()
2022
+ };
2023
+ pending.tunnelId = result.tunnelid;
2024
+ this.pendingStarts.set(requestedConfigId, pending);
2025
+ if (result.remoteurls.length > 0) {
2026
+ this.completePendingStart(pending, result.remoteurls);
2027
+ }
2028
+ }
2029
+ printStopRequested(tunnelId) {
2030
+ const details = this.resolveTunnelDetails(tunnelId);
2031
+ printer_default.startSpinner("Stopping tunnel with config name: " + details.configName);
2032
+ }
2033
+ handleStopResult(tunnelId, result) {
2034
+ const details = this.resolveTunnelDetails(tunnelId, result);
2035
+ if (isErrorResponse(result)) {
2036
+ printer_default.stopSpinnerFail("Failed to stop tunnel with config name: " + details.configName);
2037
+ return;
2038
+ }
2039
+ this.pendingStarts.delete(details.configId);
2040
+ printer_default.stopSpinnerSuccess("Stopped tunnel with config name: " + details.configName);
2041
+ }
2042
+ printRestartRequested(tunnelId) {
2043
+ const details = this.resolveTunnelDetails(tunnelId);
2044
+ printer_default.startSpinner("Restarting tunnel with config name: " + details.configName);
2045
+ }
2046
+ handleRestartResult(tunnelId, result) {
2047
+ const details = this.resolveTunnelDetails(tunnelId, result);
2048
+ if (isErrorResponse(result)) {
2049
+ printer_default.warn(`Failed to restart tunnel with config name: ${details.configName}. ${result.message}`);
2050
+ printer_default.stopSpinnerFail("Failed to restart tunnel with config name: " + details.configName);
2051
+ return;
2052
+ }
2053
+ printer_default.stopSpinnerSuccess("Restarted tunnel with config name: " + details.configName);
2054
+ if (result.remoteurls?.length > 0) {
2055
+ printer_default.info(pico3.cyanBright("Remote URLs:"));
2056
+ (result.remoteurls ?? []).forEach(
2057
+ (url) => printer_default.print(" " + pico3.magentaBright(url))
2058
+ );
2059
+ }
2060
+ }
2061
+ monitorList(result) {
2062
+ this.cleanupExpiredPendingStarts();
2063
+ if (!Array.isArray(result) || this.pendingStarts.size === 0 || !this.latestPendingConfigId) {
2064
+ return;
2065
+ }
2066
+ for (const tunnel of result) {
2067
+ const pending = this.findPendingStart(tunnel);
2068
+ if (!pending) {
2069
+ continue;
2070
+ }
2071
+ if (pending.configId !== this.latestPendingConfigId) {
2072
+ continue;
2073
+ }
2074
+ pending.tunnelId = tunnel.tunnelid;
2075
+ this.pendingStarts.set(pending.configId, pending);
2076
+ if (tunnel.remoteurls.length > 0) {
2077
+ this.completePendingStart(pending, tunnel.remoteurls);
2078
+ continue;
2079
+ }
2080
+ if (tunnel.status.state === "exited" /* Exited */) {
2081
+ const reason = tunnel.status.errormsg || "Tunnel exited before a public URL was assigned";
2082
+ this.pendingStarts.delete(pending.configId);
2083
+ this.latestPendingConfigId = void 0;
2084
+ printer_default.stopSpinnerFail(`Tunnel start did not complete for config name: ${pending.configName}. ${reason}`);
2085
+ }
2086
+ }
2087
+ }
2088
+ completePendingStart(entry, urls) {
2089
+ if (this.latestPendingConfigId && entry.configId !== this.latestPendingConfigId) {
2090
+ this.pendingStarts.delete(entry.configId);
2091
+ return;
2092
+ }
2093
+ this.pendingStarts.delete(entry.configId);
2094
+ this.latestPendingConfigId = void 0;
2095
+ printer_default.stopSpinnerSuccess(`Tunnel started with config name: ${entry.configName}.`);
2096
+ printer_default.info(pico3.cyanBright("Remote URLs:"));
2097
+ (urls ?? []).forEach(
2098
+ (url) => printer_default.print(" " + pico3.magentaBright(url))
2099
+ );
2100
+ }
2101
+ cleanupExpiredPendingStarts() {
2102
+ const now = Date.now();
2103
+ for (const [configId, entry] of this.pendingStarts.entries()) {
2104
+ if (now - entry.queuedAt <= PENDING_START_TIMEOUT_MS) {
2105
+ continue;
2106
+ }
2107
+ this.pendingStarts.delete(configId);
2108
+ printer_default.warn(`Timed out while waiting for tunnel URL for config name: ${entry.configName}`);
2109
+ logger.warn("Pending websocket start entry expired", { configId, tunnelId: entry.tunnelId });
2110
+ }
2111
+ }
2112
+ findPendingStart(tunnel) {
2113
+ const configId = this.getConfigIdFromTunnel(tunnel);
2114
+ const byConfigId = this.pendingStarts.get(configId);
2115
+ if (byConfigId) {
2116
+ return byConfigId;
2117
+ }
2118
+ for (const entry of this.pendingStarts.values()) {
2119
+ if (entry.tunnelId === tunnel.tunnelid) {
2120
+ return entry;
2121
+ }
2122
+ }
2123
+ return void 0;
2124
+ }
2125
+ resolveTunnelDetails(tunnelId, result) {
2126
+ try {
2127
+ const managed = this.tunnelManager.getManagedTunnel(void 0, tunnelId);
2128
+ return {
2129
+ configId: managed.configId,
2130
+ configName: managed.tunnelName || managed.configId || tunnelId
2131
+ };
2132
+ } catch {
2133
+ if (result && !isErrorResponse(result)) {
2134
+ return {
2135
+ configId: this.getConfigIdFromTunnel(result),
2136
+ configName: this.getConfigNameFromTunnel(result)
2137
+ };
2138
+ }
2139
+ return {
2140
+ configId: tunnelId,
2141
+ configName: tunnelId
2142
+ };
2143
+ }
2144
+ }
2145
+ getConfigIdFromRequest(config) {
2146
+ return "configid" in config ? config.configid : config.configId;
2147
+ }
2148
+ getConfigNameFromRequest(config) {
2149
+ return "configname" in config ? config.configname : config.name;
2150
+ }
2151
+ getConfigIdFromTunnel(tunnel) {
2152
+ return "configid" in tunnel.tunnelconfig ? tunnel.tunnelconfig.configid : tunnel.tunnelconfig.configId;
2153
+ }
2154
+ getConfigNameFromTunnel(tunnel) {
2155
+ return "configname" in tunnel.tunnelconfig ? tunnel.tunnelconfig.configname : tunnel.tunnelconfig.name;
2156
+ }
2157
+ };
2158
+ var remoteManagementWebSocketPrinter = new RemoteManagementWebSocketPrinter();
2159
+
1772
2160
  // src/remote_management/websocket_handlers.ts
1773
2161
  import z2 from "zod";
1774
2162
  var WebSocketCommandHandler = class {
1775
2163
  constructor() {
1776
2164
  this.tunnelHandler = new TunnelOperations();
2165
+ remoteManagementWebSocketPrinter.setTunnelHandler(this.tunnelHandler);
1777
2166
  }
1778
2167
  safeParse(text) {
1779
2168
  if (!text) return void 0;
@@ -1798,35 +2187,157 @@ var WebSocketCommandHandler = class {
1798
2187
  this.sendResponse(ws, resp);
1799
2188
  }
1800
2189
  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);
2190
+ let queuedConfig;
2191
+ try {
2192
+ const dc = StartSchema.parse(raw);
2193
+ queuedConfig = dc.tunnelConfig;
2194
+ remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
2195
+ const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
2196
+ remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
2197
+ return this.wrapResponse(result, req);
2198
+ } catch (e) {
2199
+ if (queuedConfig) {
2200
+ remoteManagementWebSocketPrinter.failQueuedStart(queuedConfig, String(e));
2201
+ }
2202
+ if (e instanceof z2.ZodError) {
2203
+ printer_default.warn("Validation failed for start request");
2204
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2205
+ }
2206
+ printer_default.warn(`Error in handleStartReq error: ${String(e)}`);
2207
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2208
+ }
2209
+ }
2210
+ async handleStartV2Req(req, raw) {
2211
+ let queuedConfig;
2212
+ try {
2213
+ const dc = StartV2Schema.parse(raw);
2214
+ queuedConfig = dc.tunnelConfig;
2215
+ remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
2216
+ const result = await this.tunnelHandler.handleStartV2(dc.tunnelConfig);
2217
+ remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
2218
+ return this.wrapResponse(result, req);
2219
+ } catch (e) {
2220
+ if (queuedConfig) {
2221
+ remoteManagementWebSocketPrinter.failQueuedStart(queuedConfig, String(e));
2222
+ }
2223
+ if (e instanceof z2.ZodError) {
2224
+ printer_default.warn("Validation failed for start-v2 request");
2225
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2226
+ }
2227
+ printer_default.warn(`Error in handleStartV2Req error: ${String(e)}`);
2228
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2229
+ }
1805
2230
  }
1806
2231
  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);
2232
+ try {
2233
+ const dc = StopSchema.parse(raw);
2234
+ remoteManagementWebSocketPrinter.printStopRequested(dc.tunnelID);
2235
+ const result = await this.tunnelHandler.handleStop(dc.tunnelID);
2236
+ remoteManagementWebSocketPrinter.handleStopResult(dc.tunnelID, result);
2237
+ return this.wrapResponse(result, req);
2238
+ } catch (e) {
2239
+ if (e instanceof z2.ZodError) {
2240
+ printer_default.warn("Validation failed for stop request");
2241
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2242
+ }
2243
+ printer_default.warn(`Error in handleStopReq error: ${String(e)}`);
2244
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2245
+ }
1811
2246
  }
1812
2247
  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);
2248
+ try {
2249
+ const dc = GetSchema.parse(raw);
2250
+ const result = await this.tunnelHandler.handleGet(dc.tunnelID);
2251
+ return this.wrapResponse(result, req);
2252
+ } catch (e) {
2253
+ if (e instanceof z2.ZodError) {
2254
+ printer_default.warn("Validation failed for get request");
2255
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2256
+ }
2257
+ printer_default.warn(`Error in handleGetReq error: ${String(e)}`);
2258
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2259
+ }
1816
2260
  }
1817
2261
  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);
2262
+ try {
2263
+ const dc = RestartSchema.parse(raw);
2264
+ remoteManagementWebSocketPrinter.printRestartRequested(dc.tunnelID);
2265
+ const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
2266
+ remoteManagementWebSocketPrinter.handleRestartResult(dc.tunnelID, result);
2267
+ return this.wrapResponse(result, req);
2268
+ } catch (e) {
2269
+ if (e instanceof z2.ZodError) {
2270
+ printer_default.warn("Validation failed for restart request");
2271
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2272
+ }
2273
+ printer_default.warn(`Error in handleRestartReq error: ${String(e)}`);
2274
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2275
+ }
1821
2276
  }
1822
2277
  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);
2278
+ try {
2279
+ const dc = UpdateConfigSchema.parse(raw);
2280
+ const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
2281
+ return this.wrapResponse(result, req);
2282
+ } catch (e) {
2283
+ if (e instanceof z2.ZodError) {
2284
+ printer_default.warn("Validation failed for updateconfig request");
2285
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2286
+ }
2287
+ printer_default.warn(`Error in handleUpdateConfigReq error: ${String(e)}`);
2288
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2289
+ }
2290
+ }
2291
+ async handleUpdateConfigV2Req(req, raw) {
2292
+ try {
2293
+ const dc = UpdateConfigV2Schema.parse(raw);
2294
+ const result = await this.tunnelHandler.handleUpdateConfigV2(dc.tunnelConfig);
2295
+ return this.wrapResponse(result, req);
2296
+ } catch (e) {
2297
+ if (e instanceof z2.ZodError) {
2298
+ printer_default.warn("Validation failed for update-config-v2 request");
2299
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2300
+ }
2301
+ printer_default.warn(`Error in handleUpdateConfigV2Req error: ${String(e)}`);
2302
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2303
+ }
1826
2304
  }
1827
2305
  async handleListReq(req) {
1828
- const result = await this.tunnelHandler.handleList();
1829
- return this.wrapResponse(result, req);
2306
+ try {
2307
+ const result = await this.tunnelHandler.handleList();
2308
+ remoteManagementWebSocketPrinter.monitorList(result);
2309
+ return this.wrapResponse(result, req);
2310
+ } catch (e) {
2311
+ printer_default.warn(`Error in handleListReq error: ${String(e)}`);
2312
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2313
+ }
2314
+ }
2315
+ async handleListV2Req(req) {
2316
+ try {
2317
+ const result = await this.tunnelHandler.handleListV2();
2318
+ remoteManagementWebSocketPrinter.monitorList(result);
2319
+ return this.wrapResponse(result, req);
2320
+ } catch (e) {
2321
+ printer_default.warn(`Error in handleListV2Req error: ${String(e)}`);
2322
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2323
+ }
2324
+ }
2325
+ async handleGetVersionReq(ws, req) {
2326
+ try {
2327
+ const versionResponse = {
2328
+ cli_version: getVersion()
2329
+ };
2330
+ const payload = {
2331
+ command: req.command,
2332
+ requestid: req.requestid,
2333
+ response: JSON.stringify(versionResponse),
2334
+ error: false
2335
+ };
2336
+ ws.send(JSON.stringify(payload));
2337
+ } catch (e) {
2338
+ printer_default.warn(`Error in handleGetVersionReq error: ${String(e)}`);
2339
+ this.sendError(ws, req, String(e));
2340
+ }
1830
2341
  }
1831
2342
  wrapResponse(result, req) {
1832
2343
  if (isErrorResponse(result)) {
@@ -1860,6 +2371,10 @@ var WebSocketCommandHandler = class {
1860
2371
  response = await this.handleStartReq(req, raw);
1861
2372
  break;
1862
2373
  }
2374
+ case "start-v2": {
2375
+ response = await this.handleStartV2Req(req, raw);
2376
+ break;
2377
+ }
1863
2378
  case "stop": {
1864
2379
  response = await this.handleStopReq(req, raw);
1865
2380
  break;
@@ -1876,10 +2391,22 @@ var WebSocketCommandHandler = class {
1876
2391
  response = await this.handleUpdateConfigReq(req, raw);
1877
2392
  break;
1878
2393
  }
2394
+ case "update-config-v2": {
2395
+ response = await this.handleUpdateConfigV2Req(req, raw);
2396
+ break;
2397
+ }
1879
2398
  case "list": {
1880
2399
  response = await this.handleListReq(req);
1881
2400
  break;
1882
2401
  }
2402
+ case "list-v2": {
2403
+ response = await this.handleListV2Req(req);
2404
+ break;
2405
+ }
2406
+ case "get-version": {
2407
+ await this.handleGetVersionReq(ws, req);
2408
+ return;
2409
+ }
1883
2410
  default:
1884
2411
  if (typeof req.command === "string") {
1885
2412
  logger.warn("Unknown command", { command: req.command });
@@ -1898,6 +2425,18 @@ var WebSocketCommandHandler = class {
1898
2425
  }
1899
2426
  }
1900
2427
  };
2428
+ function sendVersionResponse(ws) {
2429
+ const versionResponse = {
2430
+ cli_version: getVersion()
2431
+ };
2432
+ const payload = {
2433
+ command: "get-version",
2434
+ requestid: "0",
2435
+ response: JSON.stringify(versionResponse),
2436
+ error: false
2437
+ };
2438
+ ws.send(JSON.stringify(payload));
2439
+ }
1901
2440
  function handleConnectionStatusMessage(firstMessage) {
1902
2441
  try {
1903
2442
  const text = typeof firstMessage === "string" ? firstMessage : firstMessage.toString();
@@ -1948,7 +2487,11 @@ async function parseRemoteManagement(values) {
1948
2487
  if (typeof rmToken === "string" && rmToken.trim().length > 0) {
1949
2488
  const manageHost = values["manage"];
1950
2489
  try {
1951
- await initiateRemoteManagement(rmToken, manageHost);
2490
+ const remoteManagementConfig = {
2491
+ apiKey: rmToken,
2492
+ serverUrl: buildRemoteManagementWsUrl(manageHost)
2493
+ };
2494
+ await initiateRemoteManagement(remoteManagementConfig);
1952
2495
  return { ok: true };
1953
2496
  } catch (e) {
1954
2497
  logger.error("Failed to initiate remote management:", e);
@@ -1956,11 +2499,11 @@ async function parseRemoteManagement(values) {
1956
2499
  }
1957
2500
  }
1958
2501
  }
1959
- async function initiateRemoteManagement(token, manage) {
1960
- if (!token || token.trim().length === 0) {
2502
+ async function initiateRemoteManagement(remoteManagementConfig) {
2503
+ if (!remoteManagementConfig.apiKey || remoteManagementConfig.apiKey.trim().length === 0) {
1961
2504
  throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
1962
2505
  }
1963
- const wsUrl = buildRemoteManagementWsUrl(manage);
2506
+ const wsUrl = remoteManagementConfig.serverUrl;
1964
2507
  const wsHost = extractHostname(wsUrl);
1965
2508
  logger.info("Remote management mode enabled.");
1966
2509
  _stopRequested = false;
@@ -1976,7 +2519,7 @@ async function initiateRemoteManagement(token, manage) {
1976
2519
  logConnecting();
1977
2520
  setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
1978
2521
  try {
1979
- await handleWebSocketConnection(wsUrl, wsHost, token);
2522
+ await handleWebSocketConnection(wsUrl, wsHost, remoteManagementConfig.apiKey);
1980
2523
  } catch (error) {
1981
2524
  logger.warn("Remote management connection error", { error: String(error) });
1982
2525
  }
@@ -2004,6 +2547,7 @@ async function handleWebSocketConnection(wsUrl, wsHost, token) {
2004
2547
  };
2005
2548
  ws.once("open", () => {
2006
2549
  printer_default.success(`Connected to ${wsHost}`);
2550
+ setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
2007
2551
  heartbeat = setInterval(() => {
2008
2552
  if (ws.readyState === WebSocket.OPEN) ws.ping();
2009
2553
  }, PING_INTERVAL_MS);
@@ -2014,7 +2558,11 @@ async function handleWebSocketConnection(wsUrl, wsHost, token) {
2014
2558
  if (firstMessage) {
2015
2559
  firstMessage = false;
2016
2560
  const ok = handleConnectionStatusMessage(data);
2017
- if (!ok) ws.close();
2561
+ if (!ok) {
2562
+ ws.close();
2563
+ return;
2564
+ }
2565
+ sendVersionResponse(ws);
2018
2566
  return;
2019
2567
  }
2020
2568
  setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
@@ -2025,20 +2573,21 @@ async function handleWebSocketConnection(wsUrl, wsHost, token) {
2025
2573
  }
2026
2574
  });
2027
2575
  ws.on("unexpected-response", (_, res) => {
2028
- setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
2029
2576
  if (res.statusCode === 401) {
2577
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
2030
2578
  printer_default.error("Unauthorized. Please enter a valid token.");
2031
2579
  logger.error("Unauthorized (401) on remote management connect");
2580
+ ws.close();
2032
2581
  } else {
2582
+ logger.warn("Unexpected HTTP response ", { statusCode: res.statusCode });
2033
2583
  printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
2034
- logger.warn("Unexpected HTTP response", { statusCode: res.statusCode });
2584
+ cleanup();
2035
2585
  }
2036
- ws.close();
2037
2586
  });
2038
2587
  ws.on("close", (code, reason) => {
2039
2588
  setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2040
2589
  logger.info("WebSocket closed", { code, reason: reason.toString() });
2041
- printer_default.warn(`Disconnected (code: ${code}). Retrying...`);
2590
+ printer_default.warn(`Disconnected (code: ${code}). Retrying in ${RECONNECT_SLEEP_MS / 1e3}s...`);
2042
2591
  cleanup();
2043
2592
  });
2044
2593
  ws.on("error", (err) => {