pinggy 0.3.8 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }
@@ -96,7 +99,7 @@ _CLIPrinter.errorDefinitions = [
96
99
  message: (err) => {
97
100
  const match = /Unknown option '(.+?)'/.exec(err.message);
98
101
  const option = match ? match[1] : "(unknown)";
99
- return `Unknown option '${option}'. Please check your command or use pinggy --h for guidance.`;
102
+ return `Unknown option '${option}'. Please check your command or use pinggy -h for guidance.`;
100
103
  }
101
104
  },
102
105
  {
@@ -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}"`);
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");
185
197
  }
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
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,25 +224,25 @@ 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,
230
234
  createdAt: now,
231
235
  startedAt: null,
232
236
  stoppedAt: null,
233
- autoReconnect: params.autoReconnect
237
+ autoReconnect: params.autoReconnect,
238
+ lastError: {}
234
239
  };
235
240
  instance.setTunnelEstablishedCallback(({}) => {
236
241
  managed.startedAt = (/* @__PURE__ */ new Date()).toISOString();
237
242
  });
238
243
  this.setupStatsCallback(params.tunnelid, managed);
239
244
  this.setupErrorCallback(params.tunnelid, managed);
245
+ this.setupTunnelPollingErrorCallback(params.tunnelid, managed);
240
246
  this.setupDisconnectCallback(params.tunnelid, managed);
241
247
  this.setupWillReconnectCallback(params.tunnelid, managed);
242
248
  this.setupReconnectingCallback(params.tunnelid, managed);
@@ -244,44 +250,10 @@ var TunnelManager = class _TunnelManager {
244
250
  this.setupReconnectionFailedCallback(params.tunnelid, managed);
245
251
  this.setUpTunnelWorkerErrorCallback(params.tunnelid, managed);
246
252
  this.tunnelsByTunnelId.set(params.tunnelid, managed);
247
- this.tunnelsByConfigId.set(params.configid, managed);
248
- logger.info("Tunnel created", { configid: params.configid, tunnelid: params.tunnelid });
253
+ this.tunnelsByConfigId.set(params.configId, managed);
254
+ logger.info("Tunnel created", { configId: params.configId, tunnelId: params.tunnelid });
249
255
  return managed;
250
256
  }
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
257
  /**
286
258
  * Start a tunnel that was created but not yet started
287
259
  */
@@ -293,7 +265,14 @@ var TunnelManager = class _TunnelManager {
293
265
  try {
294
266
  urls = await managed.instance.start();
295
267
  } catch (error) {
296
- logger.error("Failed to start tunnel", { tunnelId, error });
268
+ logger.warn("Failed to start tunnel", { tunnelId, error });
269
+ managed.isStopped = true;
270
+ managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
271
+ managed.lastError = {
272
+ message: "Failed to start tunnel",
273
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
274
+ isFatal: true
275
+ };
297
276
  throw error;
298
277
  }
299
278
  logger.info("Tunnel started", { tunnelId, urls });
@@ -329,7 +308,7 @@ var TunnelManager = class _TunnelManager {
329
308
  stopTunnel(tunnelId) {
330
309
  const managed = this.tunnelsByTunnelId.get(tunnelId);
331
310
  if (!managed) throw new Error(`Tunnel "${tunnelId}" not found`);
332
- logger.info("Stopping tunnel", { tunnelId, configId: managed.configid });
311
+ logger.info("Stopping tunnel", { tunnelId, configId: managed.configId });
333
312
  try {
334
313
  managed.instance.stop();
335
314
  if (managed.serveWorker) {
@@ -341,6 +320,7 @@ var TunnelManager = class _TunnelManager {
341
320
  this.tunnelStats.delete(tunnelId);
342
321
  this.tunnelStatsListeners.delete(tunnelId);
343
322
  this.tunnelErrorListeners.delete(tunnelId);
323
+ this.tunnelPollingErrorListeners.delete(tunnelId);
344
324
  this.tunnelDisconnectListeners.delete(tunnelId);
345
325
  this.tunnelWorkerErrorListeners.delete(tunnelId);
346
326
  this.tunnelStartListeners.delete(tunnelId);
@@ -352,8 +332,8 @@ var TunnelManager = class _TunnelManager {
352
332
  managed.warnings = managed.warnings ?? [];
353
333
  managed.isStopped = true;
354
334
  managed.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
355
- logger.info("Tunnel stopped", { tunnelId, configId: managed.configid });
356
- return { configid: managed.configid, tunnelid: managed.tunnelid };
335
+ logger.info("Tunnel stopped", { tunnelId, configId: managed.configId });
336
+ return { configId: managed.configId, tunnelid: managed.tunnelid };
357
337
  } catch (error) {
358
338
  logger.error("Failed to stop tunnel", { tunnelId, error });
359
339
  throw error;
@@ -370,7 +350,6 @@ var TunnelManager = class _TunnelManager {
370
350
  return [];
371
351
  }
372
352
  const urls = await managed.instance.urls();
373
- logger.debug("Queried tunnel URLs", { tunnelId, urls });
374
353
  return urls;
375
354
  } catch (error) {
376
355
  logger.error("Error fetching tunnel URLs", { tunnelId, error });
@@ -386,11 +365,10 @@ var TunnelManager = class _TunnelManager {
386
365
  const tunnelList = await Promise.all(Array.from(this.tunnelsByTunnelId.values()).map(async (tunnel) => {
387
366
  return {
388
367
  tunnelid: tunnel.tunnelid,
389
- configid: tunnel.configid,
368
+ configId: tunnel.configId,
390
369
  tunnelName: tunnel.tunnelName,
391
370
  tunnelConfig: tunnel.tunnelConfig,
392
- remoteurls: !tunnel.isStopped ? await this.getTunnelUrls(tunnel.tunnelid) : [],
393
- additionalForwarding: tunnel.additionalForwarding,
371
+ remoteurls: tunnel.isStopped || tunnel.lastError?.isFatal ? [] : await this.getTunnelUrls(tunnel.tunnelid),
394
372
  serve: tunnel.serve
395
373
  };
396
374
  }));
@@ -413,7 +391,6 @@ var TunnelManager = class _TunnelManager {
413
391
  return "exited";
414
392
  }
415
393
  const status = await managed.instance.getStatus();
416
- logger.debug("Queried tunnel status", { tunnelId, status });
417
394
  return status;
418
395
  }
419
396
  /**
@@ -431,6 +408,15 @@ var TunnelManager = class _TunnelManager {
431
408
  this.tunnelsByConfigId.clear();
432
409
  this.tunnelStats.clear();
433
410
  this.tunnelStatsListeners.clear();
411
+ this.tunnelErrorListeners.clear();
412
+ this.tunnelPollingErrorListeners.clear();
413
+ this.tunnelDisconnectListeners.clear();
414
+ this.tunnelWorkerErrorListeners.clear();
415
+ this.tunnelStartListeners.clear();
416
+ this.tunnelWillReconnectListeners.clear();
417
+ this.tunnelReconnectingListeners.clear();
418
+ this.tunnelReconnectionCompletedListeners.clear();
419
+ this.tunnelReconnectionFailedListeners.clear();
434
420
  logger.info("All tunnels stopped and cleared");
435
421
  }
436
422
  /**
@@ -451,7 +437,7 @@ var TunnelManager = class _TunnelManager {
451
437
  return false;
452
438
  }
453
439
  this._cleanupTunnelRecords(managed);
454
- logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.configid });
440
+ logger.info("Removed stopped tunnel records", { tunnelId, configId: managed.configId });
455
441
  return true;
456
442
  }
457
443
  /**
@@ -478,6 +464,7 @@ var TunnelManager = class _TunnelManager {
478
464
  this.tunnelStats.delete(managed.tunnelid);
479
465
  this.tunnelStatsListeners.delete(managed.tunnelid);
480
466
  this.tunnelErrorListeners.delete(managed.tunnelid);
467
+ this.tunnelPollingErrorListeners.delete(managed.tunnelid);
481
468
  this.tunnelDisconnectListeners.delete(managed.tunnelid);
482
469
  this.tunnelWorkerErrorListeners.delete(managed.tunnelid);
483
470
  this.tunnelStartListeners.delete(managed.tunnelid);
@@ -486,7 +473,7 @@ var TunnelManager = class _TunnelManager {
486
473
  this.tunnelReconnectionCompletedListeners.delete(managed.tunnelid);
487
474
  this.tunnelReconnectionFailedListeners.delete(managed.tunnelid);
488
475
  this.tunnelsByTunnelId.delete(managed.tunnelid);
489
- this.tunnelsByConfigId.delete(managed.configid);
476
+ this.tunnelsByConfigId.delete(managed.configId);
490
477
  } catch (e) {
491
478
  logger.warn("Failed cleaning up tunnel records", { tunnelId: managed.tunnelid, error: e });
492
479
  }
@@ -547,21 +534,20 @@ var TunnelManager = class _TunnelManager {
547
534
  }
548
535
  logger.info("Initiating tunnel restart", {
549
536
  tunnelId: tunnelid,
550
- configId: existingTunnel.configid
537
+ configId: existingTunnel.configId
551
538
  });
552
539
  try {
553
540
  const tunnelName = existingTunnel.tunnelName;
554
- const currentConfigId = existingTunnel.configid;
541
+ const currentConfigId = existingTunnel.configId;
555
542
  const currentConfig = existingTunnel.tunnelConfig;
556
- const configWithForwarding = existingTunnel.configWithForwarding;
557
- const additionalForwarding = existingTunnel.additionalForwarding;
558
543
  const currentServe = existingTunnel.serve;
559
544
  const autoReconnect = existingTunnel.autoReconnect || false;
560
545
  this.tunnelsByTunnelId.delete(tunnelid);
561
- this.tunnelsByConfigId.delete(existingTunnel.configid);
546
+ this.tunnelsByConfigId.delete(existingTunnel.configId);
562
547
  this.tunnelStats.delete(tunnelid);
563
548
  this.tunnelStatsListeners.delete(tunnelid);
564
549
  this.tunnelErrorListeners.delete(tunnelid);
550
+ this.tunnelPollingErrorListeners.delete(tunnelid);
565
551
  this.tunnelDisconnectListeners.delete(tunnelid);
566
552
  this.tunnelWorkerErrorListeners.delete(tunnelid);
567
553
  this.tunnelStartListeners.delete(tunnelid);
@@ -570,12 +556,10 @@ var TunnelManager = class _TunnelManager {
570
556
  this.tunnelReconnectionCompletedListeners.delete(tunnelid);
571
557
  this.tunnelReconnectionFailedListeners.delete(tunnelid);
572
558
  const newTunnel = await this._createTunnelWithProcessedConfig({
573
- configid: currentConfigId,
559
+ configId: currentConfigId,
574
560
  tunnelid,
575
561
  tunnelName,
576
562
  originalConfig: currentConfig,
577
- configWithForwarding,
578
- additionalForwarding,
579
563
  serve: currentServe,
580
564
  autoReconnect
581
565
  });
@@ -604,20 +588,18 @@ var TunnelManager = class _TunnelManager {
604
588
  * @throws Error if the tunnel is not found or if the update process fails
605
589
  */
606
590
  async updateConfig(newConfig) {
607
- const { configid, tunnelName: newTunnelName, additionalForwarding } = newConfig;
608
- if (!configid || configid.trim().length === 0) {
609
- throw new Error(`Invalid configid: "${configid}"`);
591
+ const { configId, tunnelName: newTunnelName } = newConfig;
592
+ if (!configId || configId.trim().length === 0) {
593
+ throw new Error(`Invalid configId: "${configId}"`);
610
594
  }
611
- const existingTunnel = this.tunnelsByConfigId.get(configid);
595
+ const existingTunnel = this.tunnelsByConfigId.get(configId);
612
596
  if (!existingTunnel) {
613
- throw new Error(`Tunnel with config id "${configid}" not found`);
597
+ throw new Error(`Tunnel with config id "${configId}" not found`);
614
598
  }
615
599
  const isStopped = existingTunnel.isStopped;
616
600
  const currentTunnelConfig = existingTunnel.tunnelConfig;
617
- const currentConfigWithForwarding = existingTunnel.configWithForwarding;
618
601
  const currentTunnelId = existingTunnel.tunnelid;
619
- const currentTunnelConfigId = existingTunnel.configid;
620
- const currentAdditionalForwarding = existingTunnel.additionalForwarding;
602
+ const currentTunnelConfigId = existingTunnel.configId;
621
603
  const currentTunnelName = existingTunnel.tunnelName;
622
604
  const currentServe = existingTunnel.serve;
623
605
  const currentAutoReconnect = existingTunnel.autoReconnect || false;
@@ -629,22 +611,19 @@ var TunnelManager = class _TunnelManager {
629
611
  this.tunnelsByConfigId.delete(currentTunnelConfigId);
630
612
  const mergedBaseConfig = {
631
613
  ...newConfig,
632
- configid,
614
+ configId,
633
615
  tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
634
616
  serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe
635
617
  };
636
- const newConfigWithForwarding = this.buildPinggyConfig(
637
- mergedBaseConfig,
638
- additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding
639
- );
618
+ const effectiveServe = newConfig.serve !== void 0 ? newConfig.serve : currentServe;
619
+ const effectiveTunnelName = newTunnelName !== void 0 ? newTunnelName : currentTunnelName;
620
+ let configWithForwarding;
640
621
  const newTunnel = await this._createTunnelWithProcessedConfig({
641
- configid,
622
+ configId,
642
623
  tunnelid: currentTunnelId,
643
- tunnelName: newTunnelName !== void 0 ? newTunnelName : currentTunnelName,
624
+ tunnelName: effectiveTunnelName,
644
625
  originalConfig: mergedBaseConfig,
645
- configWithForwarding: newConfigWithForwarding,
646
- additionalForwarding: additionalForwarding !== void 0 ? additionalForwarding : currentAdditionalForwarding,
647
- serve: newConfig.serve !== void 0 ? newConfig.serve : currentServe,
626
+ serve: effectiveServe,
648
627
  autoReconnect: currentAutoReconnect
649
628
  });
650
629
  if (!isStopped) {
@@ -652,23 +631,21 @@ var TunnelManager = class _TunnelManager {
652
631
  }
653
632
  logger.info("Tunnel configuration updated", {
654
633
  tunnelId: newTunnel.tunnelid,
655
- configId: newTunnel.configid,
634
+ configId: newTunnel.configId,
656
635
  isStopped
657
636
  });
658
637
  return newTunnel;
659
638
  } catch (error) {
660
639
  logger.error("Error updating tunnel configuration", {
661
- configId: configid,
640
+ configId,
662
641
  error: error instanceof Error ? error.message : String(error)
663
642
  });
664
643
  try {
665
644
  const originalTunnel = await this._createTunnelWithProcessedConfig({
666
- configid: currentTunnelConfigId,
645
+ configId: currentTunnelConfigId,
667
646
  tunnelid: currentTunnelId,
668
647
  tunnelName: currentTunnelName,
669
648
  originalConfig: currentTunnelConfig,
670
- configWithForwarding: currentConfigWithForwarding,
671
- additionalForwarding: currentAdditionalForwarding,
672
649
  serve: currentServe,
673
650
  autoReconnect: currentAutoReconnect
674
651
  });
@@ -784,6 +761,19 @@ var TunnelManager = class _TunnelManager {
784
761
  logger.info("Error listener registered for tunnel", { tunnelId, listenerId });
785
762
  return listenerId;
786
763
  }
764
+ async registerPollingErrorListener(tunnelId, listener) {
765
+ const managed = this.tunnelsByTunnelId.get(tunnelId);
766
+ if (!managed) {
767
+ throw new Error(`Tunnel "${tunnelId}" not found`);
768
+ }
769
+ if (!this.tunnelPollingErrorListeners.has(tunnelId)) {
770
+ this.tunnelPollingErrorListeners.set(tunnelId, /* @__PURE__ */ new Map());
771
+ }
772
+ const listenerId = getRandomId();
773
+ this.tunnelPollingErrorListeners.get(tunnelId).set(listenerId, listener);
774
+ logger.info("Polling error listener registered for tunnel", { tunnelId, listenerId });
775
+ return listenerId;
776
+ }
787
777
  async registerDisconnectListener(tunnelId, listener) {
788
778
  const managed = this.tunnelsByTunnelId.get(tunnelId);
789
779
  if (!managed) {
@@ -915,6 +905,22 @@ var TunnelManager = class _TunnelManager {
915
905
  logger.warn("Attempted to deregister non-existent error listener", { tunnelId, listenerId });
916
906
  }
917
907
  }
908
+ deregisterPollingErrorListener(tunnelId, listenerId) {
909
+ const listeners = this.tunnelPollingErrorListeners.get(tunnelId);
910
+ if (!listeners) {
911
+ logger.warn("No polling error listeners found for tunnel", { tunnelId });
912
+ return;
913
+ }
914
+ const removed = listeners.delete(listenerId);
915
+ if (removed) {
916
+ logger.info("Polling error listener deregistered", { tunnelId, listenerId });
917
+ if (listeners.size === 0) {
918
+ this.tunnelPollingErrorListeners.delete(tunnelId);
919
+ }
920
+ } else {
921
+ logger.warn("Attempted to deregister non-existent polling error listener", { tunnelId, listenerId });
922
+ }
923
+ }
918
924
  deregisterDisconnectListener(tunnelId, listenerId) {
919
925
  const listeners = this.tunnelDisconnectListeners.get(tunnelId);
920
926
  if (!listeners) {
@@ -1032,6 +1038,46 @@ var TunnelManager = class _TunnelManager {
1032
1038
  logger.warn("Failed to set up stats callback", { tunnelId, error });
1033
1039
  }
1034
1040
  }
1041
+ setupTunnelPollingErrorCallback(tunnelId, managed) {
1042
+ try {
1043
+ const callback = ({ error }) => {
1044
+ try {
1045
+ const errorMessage = error instanceof Error ? error.message : String(error);
1046
+ logger.info("Tunnel reported polling error", { tunnelId, errorMessage });
1047
+ const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1048
+ if (managedTunnel) {
1049
+ managedTunnel.lastError = {
1050
+ message: errorMessage,
1051
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1052
+ isFatal: true
1053
+ };
1054
+ }
1055
+ this.notifyPollingErrorListeners(tunnelId, errorMessage);
1056
+ } catch (e) {
1057
+ logger.warn("Error handling tunnel polling error callback", { tunnelId, e });
1058
+ }
1059
+ };
1060
+ managed.instance.setPollingErrorCallback(callback);
1061
+ logger.debug("Tunnel polling error callback set up for tunnel", { tunnelId });
1062
+ } catch (error) {
1063
+ logger.warn("Failed to set up tunnel polling error callback", { tunnelId, error });
1064
+ }
1065
+ }
1066
+ notifyPollingErrorListeners(tunnelId, errorMsg) {
1067
+ try {
1068
+ const listeners = this.tunnelPollingErrorListeners.get(tunnelId);
1069
+ if (!listeners) return;
1070
+ for (const [id, listener] of listeners) {
1071
+ try {
1072
+ listener(tunnelId, errorMsg);
1073
+ } catch (err) {
1074
+ logger.debug("Error in polling-error-listener callback", { listenerId: id, tunnelId, err });
1075
+ }
1076
+ }
1077
+ } catch (err) {
1078
+ logger.debug("Failed to notify polling error listeners", { tunnelId, err });
1079
+ }
1080
+ }
1035
1081
  notifyErrorListeners(tunnelId, errorMsg, isFatal) {
1036
1082
  try {
1037
1083
  const listeners = this.tunnelErrorListeners.get(tunnelId);
@@ -1054,6 +1100,14 @@ var TunnelManager = class _TunnelManager {
1054
1100
  const msg = typeof error === "string" ? error : String(error);
1055
1101
  const isFatal = true;
1056
1102
  logger.debug("Tunnel reported error", { tunnelId, errorNo, errorMsg: msg, recoverable });
1103
+ const managedTunnel = this.tunnelsByTunnelId.get(tunnelId);
1104
+ if (managedTunnel) {
1105
+ managedTunnel.lastError = {
1106
+ message: msg,
1107
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1108
+ isFatal: false
1109
+ };
1110
+ }
1057
1111
  this.notifyErrorListeners(tunnelId, msg, isFatal);
1058
1112
  } catch (e) {
1059
1113
  logger.warn("Error handling tunnel error callback", { tunnelId, e });
@@ -1304,9 +1358,9 @@ var TunnelManager = class _TunnelManager {
1304
1358
  }
1305
1359
  startStaticFileServer(managed) {
1306
1360
  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");
1361
+ const __filename4 = fileURLToPath2(import.meta.url);
1362
+ const __dirname4 = path.dirname(__filename4);
1363
+ const fileServerWorkerPath = path.join(__dirname4, "workers", "file_serve_worker.cjs");
1310
1364
  const staticServerWorker = new Worker(fileServerWorkerPath, {
1311
1365
  workerData: {
1312
1366
  dir: managed.serve,
@@ -1449,11 +1503,11 @@ var RemoteManagementStatus = {
1449
1503
  };
1450
1504
 
1451
1505
  // src/remote_management/remote_schema.ts
1452
- import { TunnelType as TunnelType2 } from "@pinggy/pinggy";
1506
+ import { TunnelType } from "@pinggy/pinggy";
1453
1507
  import { z } from "zod";
1454
1508
  var HeaderModificationSchema = z.object({
1455
1509
  key: z.string(),
1456
- value: z.array(z.string()).optional(),
1510
+ value: z.array(z.string()).nullable().optional(),
1457
1511
  type: z.enum(["add", "remove", "update"])
1458
1512
  });
1459
1513
  var AdditionalForwardingSchema = z.object({
@@ -1469,7 +1523,7 @@ var TunnelConfigSchema = z.object({
1469
1523
  // legacy key
1470
1524
  autoreconnect: z.boolean(),
1471
1525
  basicauth: z.array(z.object({ username: z.string(), password: z.string() })).nullable(),
1472
- bearerauth: z.string().nullable(),
1526
+ bearerauth: z.array(z.string()).nullable(),
1473
1527
  configid: z.string(),
1474
1528
  configname: z.string(),
1475
1529
  greetmsg: z.string().optional(),
@@ -1491,11 +1545,11 @@ var TunnelConfigSchema = z.object({
1491
1545
  token: z.string(),
1492
1546
  tunnelTimeout: z.number(),
1493
1547
  type: z.enum([
1494
- TunnelType2.Http,
1495
- TunnelType2.Tcp,
1496
- TunnelType2.Udp,
1497
- TunnelType2.Tls,
1498
- TunnelType2.TlsTcp
1548
+ TunnelType.Http,
1549
+ TunnelType.Tcp,
1550
+ TunnelType.Udp,
1551
+ TunnelType.Tls,
1552
+ TunnelType.TlsTcp
1499
1553
  ]),
1500
1554
  webdebuggerport: z.number(),
1501
1555
  xff: z.string(),
@@ -1526,15 +1580,102 @@ var RestartSchema = StopSchema;
1526
1580
  var UpdateConfigSchema = z.object({
1527
1581
  tunnelConfig: TunnelConfigSchema
1528
1582
  });
1583
+ var ForwardingEntryV2Schema = z.object({
1584
+ listenAddress: z.string().optional(),
1585
+ address: z.string(),
1586
+ type: z.enum([TunnelType.Http, TunnelType.Tcp, TunnelType.Udp, TunnelType.Tls, TunnelType.TlsTcp]).optional()
1587
+ });
1588
+ var TunnelConfigV1Schema = z.object({
1589
+ // Meta Info
1590
+ version: z.string(),
1591
+ name: z.string(),
1592
+ configId: z.string(),
1593
+ // General tunnel configurations
1594
+ serverAddress: z.string().optional(),
1595
+ token: z.string().optional(),
1596
+ autoReconnect: z.boolean().optional(),
1597
+ reconnectInterval: z.number().optional(),
1598
+ maxReconnectAttempts: z.number().optional(),
1599
+ force: z.boolean(),
1600
+ keepAliveInterval: z.number().optional(),
1601
+ webDebugger: z.string(),
1602
+ //Forwarding
1603
+ // Either a URL string (e.g. "https://localhost:5555") or an array of forwarding entries.
1604
+ forwarding: z.union([
1605
+ z.string(),
1606
+ z.array(ForwardingEntryV2Schema)
1607
+ ]),
1608
+ // IP whitelist
1609
+ ipWhitelist: z.array(z.string()).optional(),
1610
+ basicAuth: z.array(z.object({ username: z.string(), password: z.string() })).optional(),
1611
+ bearerTokenAuth: z.array(z.string()).optional(),
1612
+ headerModification: z.array(HeaderModificationSchema).optional(),
1613
+ reverseProxy: z.boolean().optional(),
1614
+ xForwardedFor: z.boolean().optional(),
1615
+ httpsOnly: z.boolean().optional(),
1616
+ originalRequestUrl: z.boolean().optional(),
1617
+ allowPreflight: z.boolean().optional(),
1618
+ serve: z.string().optional(),
1619
+ optional: z.record(z.string(), z.unknown()).optional()
1620
+ });
1621
+ var StartV2Schema = z.object({
1622
+ tunnelID: z.string().nullable().optional(),
1623
+ tunnelConfig: TunnelConfigV1Schema
1624
+ });
1625
+ var UpdateConfigV2Schema = z.object({
1626
+ tunnelConfig: TunnelConfigV1Schema
1627
+ });
1628
+ function pinggyOptionsToTunnelConfigV1(opts, configStoredInCli) {
1629
+ const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
1630
+ return {
1631
+ version: configStoredInCli.version || "1.0",
1632
+ name: configStoredInCli.name || "",
1633
+ configId: configStoredInCli.configId || "",
1634
+ serverAddress: opts.serverAddress || "a.pinggy.io:443",
1635
+ token: opts.token || "",
1636
+ autoReconnect: opts.autoReconnect ?? true,
1637
+ force: opts.force ?? false,
1638
+ webDebugger: opts.webDebugger || "",
1639
+ forwarding: opts.forwarding ? opts.forwarding : "",
1640
+ ipWhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : [],
1641
+ basicAuth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : void 0,
1642
+ bearerTokenAuth: parsedTokens.length ? parsedTokens : void 0,
1643
+ headerModification: opts.headerModification || [],
1644
+ reverseProxy: opts.reverseProxy ?? false,
1645
+ xForwardedFor: !!opts.xForwardedFor,
1646
+ httpsOnly: opts.httpsOnly ?? false,
1647
+ originalRequestUrl: opts.originalRequestUrl ?? false,
1648
+ allowPreflight: opts.allowPreflight ?? false,
1649
+ optional: opts.optional || {}
1650
+ };
1651
+ }
1529
1652
  function tunnelConfigToPinggyOptions(config) {
1653
+ const forwardingData = [];
1654
+ forwardingData.push({
1655
+ address: `${config.forwardedhost}:${config.localport}`,
1656
+ type: config.type || TunnelType.Http
1657
+ // Default to HTTP for the primary forwarding entry
1658
+ });
1659
+ if (config.additionalForwarding && Array.isArray(config.additionalForwarding)) {
1660
+ config.additionalForwarding.forEach((entry) => {
1661
+ if (entry.localDomain && entry.localPort && entry.remoteDomain) {
1662
+ const listenAddress = entry.remotePort && isValidPort(entry.remotePort) ? `${entry.remoteDomain}:${entry.remotePort}` : entry.remoteDomain;
1663
+ forwardingData.push({
1664
+ address: `${entry.localDomain}:${entry.localPort}`,
1665
+ listenAddress,
1666
+ type: TunnelType.Http
1667
+ });
1668
+ }
1669
+ });
1670
+ }
1530
1671
  return {
1531
1672
  token: config.token || "",
1532
1673
  serverAddress: config.serveraddress || "free.pinggy.io",
1533
- forwarding: `${config.forwardedhost || "localhost"}:${config.localport}`,
1674
+ forwarding: forwardingData,
1534
1675
  webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
1535
1676
  ipWhitelist: config.ipwhitelist || [],
1536
1677
  basicAuth: config.basicauth ? config.basicauth : [],
1537
- bearerTokenAuth: config.bearerauth ? [config.bearerauth] : [],
1678
+ bearerTokenAuth: config.bearerauth || [],
1538
1679
  headerModification: config.headermodification,
1539
1680
  xForwardedFor: !!config.xff,
1540
1681
  httpsOnly: config.httpsOnly,
@@ -1548,18 +1689,38 @@ function tunnelConfigToPinggyOptions(config) {
1548
1689
  }
1549
1690
  };
1550
1691
  }
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;
1692
+ function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg, serve) {
1693
+ let primaryEntry;
1694
+ let additionalEntries = [];
1695
+ if (Array.isArray(opts.forwarding)) {
1696
+ primaryEntry = opts.forwarding.find((e) => !e.listenAddress) ?? opts.forwarding[0];
1697
+ additionalEntries = opts.forwarding.filter(
1698
+ (e) => e !== primaryEntry && Boolean(e.listenAddress)
1699
+ );
1700
+ }
1701
+ const forwarding = primaryEntry ? String(primaryEntry.address) : String(opts.forwarding);
1702
+ const [parsedForwardedHost, portStr] = forwarding.split(":");
1703
+ const parsedLocalPort = parseInt(portStr, 10);
1704
+ const tunnelType = primaryEntry?.type ?? TunnelType.Http;
1705
+ const additionalForwarding = additionalEntries.map((e) => {
1706
+ const [localDomain, localPortStr] = String(e.address).split(":");
1707
+ const [remoteDomain, remotePortStr] = String(e.listenAddress).split(":");
1708
+ const localPort = parseInt(localPortStr, 10);
1709
+ const remotePort = parseInt(remotePortStr, 10);
1710
+ return {
1711
+ localDomain,
1712
+ localPort: isNaN(localPort) ? 0 : localPort,
1713
+ remoteDomain,
1714
+ remotePort: isNaN(remotePort) ? 0 : remotePort
1715
+ };
1716
+ });
1556
1717
  const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
1557
1718
  return {
1558
1719
  allowPreflight: opts.allowPreflight ?? false,
1559
1720
  allowpreflight: opts.allowPreflight ?? false,
1560
1721
  autoreconnect: opts.autoReconnect ?? false,
1561
1722
  basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
1562
- bearerauth: parsedTokens.length ? parsedTokens.join(",") : null,
1723
+ bearerauth: parsedTokens.length ? [parsedTokens.join(",")] : null,
1563
1724
  configid,
1564
1725
  configname: configName,
1565
1726
  greetmsg: greetMsg || "",
@@ -1590,7 +1751,6 @@ function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls,
1590
1751
  }
1591
1752
 
1592
1753
  // src/remote_management/handler.ts
1593
- import { TunnelType as TunnelType3 } from "@pinggy/pinggy";
1594
1754
  var TunnelOperations = class {
1595
1755
  constructor() {
1596
1756
  this.tunnelManager = TunnelManager.getInstance();
@@ -1604,12 +1764,15 @@ var TunnelOperations = class {
1604
1764
  status.starttimestamp = managed.startedAt || "";
1605
1765
  status.endtimestamp = managed.stoppedAt || "";
1606
1766
  }
1767
+ if (managed?.lastError) {
1768
+ status.lastError = managed.lastError;
1769
+ }
1607
1770
  } catch (e) {
1608
1771
  }
1609
1772
  return status;
1610
1773
  }
1611
1774
  // --- Helper to construct TunnelResponse ---
1612
- async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, additionalForwarding, serve) {
1775
+ async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, serve) {
1613
1776
  const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
1614
1777
  this.tunnelManager.getTunnelStatus(tunnelid),
1615
1778
  this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
@@ -1620,11 +1783,27 @@ var TunnelOperations = class {
1620
1783
  return {
1621
1784
  tunnelid,
1622
1785
  remoteurls,
1623
- tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg, additionalForwarding),
1786
+ tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg),
1624
1787
  status: this.buildStatus(tunnelid, status, "" /* NoError */),
1625
1788
  stats
1626
1789
  };
1627
1790
  }
1791
+ async buildTunnelResponseV2(tunnelid, tunnelConfig, configFromCli, configid, tunnelName, serve) {
1792
+ const [status, stats, greetMsg, remoteurls] = await Promise.all([
1793
+ this.tunnelManager.getTunnelStatus(tunnelid),
1794
+ this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats(),
1795
+ this.tunnelManager.getTunnelGreetMessage(tunnelid),
1796
+ this.tunnelManager.getTunnelUrls(tunnelid)
1797
+ ]);
1798
+ return {
1799
+ tunnelid,
1800
+ remoteurls,
1801
+ tunnelconfig: pinggyOptionsToTunnelConfigV1(tunnelConfig, configFromCli),
1802
+ status: this.buildStatus(tunnelid, status, "" /* NoError */),
1803
+ stats,
1804
+ greetmsg: greetMsg
1805
+ };
1806
+ }
1628
1807
  error(code, err, fallback) {
1629
1808
  return newErrorResponse({
1630
1809
  code,
@@ -1635,19 +1814,28 @@ var TunnelOperations = class {
1635
1814
  async handleStart(config) {
1636
1815
  try {
1637
1816
  const opts = tunnelConfigToPinggyOptions(config);
1638
- const additionalForwardingParsed = config.additionalForwarding || [];
1639
- const { tunnelid, instance, tunnelName, additionalForwarding, serve } = await this.tunnelManager.createTunnel({
1817
+ const { tunnelid, instance, tunnelName, serve, tunnelConfig } = await this.tunnelManager.createTunnel({
1640
1818
  ...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
1819
+ configId: config.configid,
1820
+ name: config.configname,
1821
+ optional: {
1822
+ serve: config.serve
1823
+ }
1647
1824
  });
1648
- this.tunnelManager.startTunnel(tunnelid);
1825
+ await this.tunnelManager.startTunnel(tunnelid);
1649
1826
  const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
1650
- const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, additionalForwarding, serve);
1827
+ const resp = this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, serve);
1828
+ return resp;
1829
+ } catch (err) {
1830
+ return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
1831
+ }
1832
+ }
1833
+ async handleStartV2(config) {
1834
+ try {
1835
+ const { tunnelid, instance, serve } = await this.tunnelManager.createTunnel(config);
1836
+ await this.tunnelManager.startTunnel(tunnelid);
1837
+ const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
1838
+ const resp = this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.serve);
1651
1839
  return resp;
1652
1840
  } catch (err) {
1653
1841
  return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
@@ -1658,20 +1846,59 @@ var TunnelOperations = class {
1658
1846
  const opts = tunnelConfigToPinggyOptions(config);
1659
1847
  const tunnel = await this.tunnelManager.updateConfig({
1660
1848
  ...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
1849
+ configId: config.configid,
1850
+ name: config.configname,
1851
+ optional: {
1852
+ serve: config.serve
1853
+ }
1667
1854
  });
1668
1855
  if (!tunnel.instance || !tunnel.tunnelConfig)
1669
1856
  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);
1857
+ return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.serve);
1671
1858
  } catch (err) {
1672
1859
  return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
1673
1860
  }
1674
1861
  }
1862
+ async handleUpdateConfigV2(config) {
1863
+ try {
1864
+ const tunnel = await this.tunnelManager.updateConfig(config);
1865
+ if (!tunnel.instance || !tunnel.tunnelConfig)
1866
+ throw new Error("Invalid tunnel state after configuration update");
1867
+ return this.buildTunnelResponseV2(tunnel.tunnelid, tunnel.tunnelConfig, config, config.configId, tunnel.tunnelName, tunnel.serve);
1868
+ } catch (err) {
1869
+ return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
1870
+ }
1871
+ }
1872
+ async handleListV2() {
1873
+ try {
1874
+ const tunnels = await this.tunnelManager.getAllTunnels();
1875
+ if (tunnels.length === 0) {
1876
+ return [];
1877
+ }
1878
+ return Promise.all(
1879
+ tunnels.map(async (t) => {
1880
+ const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
1881
+ const [status, tlsInfo, greetMsg] = await Promise.all([
1882
+ this.tunnelManager.getTunnelStatus(t.tunnelid),
1883
+ this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
1884
+ this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
1885
+ ]);
1886
+ const tunnelConfguration = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
1887
+ const tunnelConfig = pinggyOptionsToTunnelConfigV1(tunnelConfguration, t.tunnelConfig);
1888
+ return {
1889
+ tunnelid: t.tunnelid,
1890
+ remoteurls: t.remoteurls,
1891
+ status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
1892
+ stats: rawStats,
1893
+ tunnelconfig: tunnelConfig,
1894
+ greetmsg: greetMsg
1895
+ };
1896
+ })
1897
+ );
1898
+ } catch (err) {
1899
+ return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
1900
+ }
1901
+ }
1675
1902
  async handleList() {
1676
1903
  try {
1677
1904
  const tunnels = await this.tunnelManager.getAllTunnels();
@@ -1687,7 +1914,7 @@ var TunnelOperations = class {
1687
1914
  this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
1688
1915
  ]);
1689
1916
  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);
1917
+ const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configId, t.tunnelName, tlsInfo, greetMsg, t.serve);
1691
1918
  return {
1692
1919
  tunnelid: t.tunnelid,
1693
1920
  remoteurls: t.remoteurls,
@@ -1703,10 +1930,10 @@ var TunnelOperations = class {
1703
1930
  }
1704
1931
  async handleStop(tunnelid) {
1705
1932
  try {
1706
- const { configid } = this.tunnelManager.stopTunnel(tunnelid);
1933
+ const { configId } = this.tunnelManager.stopTunnel(tunnelid);
1707
1934
  const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1708
1935
  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);
1936
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configId, managed.tunnelName, managed.serve);
1710
1937
  } catch (err) {
1711
1938
  return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
1712
1939
  }
@@ -1715,7 +1942,7 @@ var TunnelOperations = class {
1715
1942
  try {
1716
1943
  const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1717
1944
  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);
1945
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configId, managed.tunnelName, managed.serve);
1719
1946
  } catch (err) {
1720
1947
  return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
1721
1948
  }
@@ -1725,7 +1952,7 @@ var TunnelOperations = class {
1725
1952
  await this.tunnelManager.restartTunnel(tunnelid);
1726
1953
  const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
1727
1954
  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);
1955
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configId, managed.tunnelName, managed.serve);
1729
1956
  } catch (err) {
1730
1957
  return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
1731
1958
  }
@@ -1769,11 +1996,198 @@ var TunnelOperations = class {
1769
1996
  // src/remote_management/remoteManagement.ts
1770
1997
  import WebSocket from "ws";
1771
1998
 
1999
+ // src/remote_management/websocket_printer.ts
2000
+ import pico3 from "picocolors";
2001
+ var PENDING_START_TIMEOUT_MS = 5 * 60 * 1e3;
2002
+ var RemoteManagementWebSocketPrinter = class {
2003
+ constructor() {
2004
+ this.tunnelManager = TunnelManager.getInstance();
2005
+ this.pendingStarts = /* @__PURE__ */ new Map();
2006
+ }
2007
+ setTunnelHandler(tunnelHandler) {
2008
+ this.tunnelHandler = tunnelHandler;
2009
+ }
2010
+ queueStart(config) {
2011
+ this.cleanupExpiredPendingStarts();
2012
+ const entry = {
2013
+ configId: this.getConfigIdFromRequest(config),
2014
+ configName: this.getConfigNameFromRequest(config),
2015
+ queuedAt: Date.now()
2016
+ };
2017
+ this.latestPendingConfigId = entry.configId;
2018
+ this.pendingStarts.set(entry.configId, entry);
2019
+ printer_default.startSpinner("Starting tunnel with config name: " + entry.configName);
2020
+ }
2021
+ failQueuedStart(config, reason) {
2022
+ const configId = this.getConfigIdFromRequest(config);
2023
+ const pending = this.pendingStarts.get(configId);
2024
+ const configName = pending?.configName || this.getConfigNameFromRequest(config);
2025
+ this.pendingStarts.delete(configId);
2026
+ if (this.latestPendingConfigId === configId) {
2027
+ this.latestPendingConfigId = void 0;
2028
+ printer_default.stopSpinnerFail(`Failed to start tunnel with config name: ${configName}. ${reason}`);
2029
+ }
2030
+ }
2031
+ handleStartResult(config, result) {
2032
+ this.cleanupExpiredPendingStarts();
2033
+ const requestedConfigId = this.getConfigIdFromRequest(config);
2034
+ if (this.latestPendingConfigId && requestedConfigId !== this.latestPendingConfigId) {
2035
+ this.pendingStarts.delete(requestedConfigId);
2036
+ return;
2037
+ }
2038
+ if (isErrorResponse(result)) {
2039
+ this.failQueuedStart(config, result.message);
2040
+ return;
2041
+ }
2042
+ const configId = this.getConfigIdFromTunnel(result);
2043
+ const pending = this.pendingStarts.get(requestedConfigId) || {
2044
+ configId: requestedConfigId,
2045
+ configName: this.getConfigNameFromRequest(config),
2046
+ queuedAt: Date.now()
2047
+ };
2048
+ pending.tunnelId = result.tunnelid;
2049
+ this.pendingStarts.set(requestedConfigId, pending);
2050
+ if (result.remoteurls.length > 0) {
2051
+ this.completePendingStart(pending, result.remoteurls);
2052
+ }
2053
+ }
2054
+ printStopRequested(tunnelId) {
2055
+ const details = this.resolveTunnelDetails(tunnelId);
2056
+ printer_default.startSpinner("Stopping tunnel with config name: " + details.configName);
2057
+ }
2058
+ handleStopResult(tunnelId, result) {
2059
+ const details = this.resolveTunnelDetails(tunnelId, result);
2060
+ if (isErrorResponse(result)) {
2061
+ printer_default.stopSpinnerFail("Failed to stop tunnel with config name: " + details.configName);
2062
+ return;
2063
+ }
2064
+ this.pendingStarts.delete(details.configId);
2065
+ printer_default.stopSpinnerSuccess("Stopped tunnel with config name: " + details.configName);
2066
+ }
2067
+ printRestartRequested(tunnelId) {
2068
+ const details = this.resolveTunnelDetails(tunnelId);
2069
+ printer_default.startSpinner("Restarting tunnel with config name: " + details.configName);
2070
+ }
2071
+ handleRestartResult(tunnelId, result) {
2072
+ const details = this.resolveTunnelDetails(tunnelId, result);
2073
+ if (isErrorResponse(result)) {
2074
+ printer_default.warn(`Failed to restart tunnel with config name: ${details.configName}. ${result.message}`);
2075
+ printer_default.stopSpinnerFail("Failed to restart tunnel with config name: " + details.configName);
2076
+ return;
2077
+ }
2078
+ printer_default.stopSpinnerSuccess("Restarted tunnel with config name: " + details.configName);
2079
+ if (result.remoteurls?.length > 0) {
2080
+ printer_default.info(pico3.cyanBright("Remote URLs:"));
2081
+ (result.remoteurls ?? []).forEach(
2082
+ (url) => printer_default.print(" " + pico3.magentaBright(url))
2083
+ );
2084
+ }
2085
+ }
2086
+ monitorList(result) {
2087
+ this.cleanupExpiredPendingStarts();
2088
+ if (!Array.isArray(result) || this.pendingStarts.size === 0 || !this.latestPendingConfigId) {
2089
+ return;
2090
+ }
2091
+ for (const tunnel of result) {
2092
+ const pending = this.findPendingStart(tunnel);
2093
+ if (!pending) {
2094
+ continue;
2095
+ }
2096
+ if (pending.configId !== this.latestPendingConfigId) {
2097
+ continue;
2098
+ }
2099
+ pending.tunnelId = tunnel.tunnelid;
2100
+ this.pendingStarts.set(pending.configId, pending);
2101
+ if (tunnel.remoteurls.length > 0) {
2102
+ this.completePendingStart(pending, tunnel.remoteurls);
2103
+ continue;
2104
+ }
2105
+ if (tunnel.status.state === "exited" /* Exited */) {
2106
+ const reason = tunnel.status.errormsg || "Tunnel exited before a public URL was assigned";
2107
+ this.pendingStarts.delete(pending.configId);
2108
+ this.latestPendingConfigId = void 0;
2109
+ printer_default.stopSpinnerFail(`Tunnel start did not complete for config name: ${pending.configName}. ${reason}`);
2110
+ }
2111
+ }
2112
+ }
2113
+ completePendingStart(entry, urls) {
2114
+ if (this.latestPendingConfigId && entry.configId !== this.latestPendingConfigId) {
2115
+ this.pendingStarts.delete(entry.configId);
2116
+ return;
2117
+ }
2118
+ this.pendingStarts.delete(entry.configId);
2119
+ this.latestPendingConfigId = void 0;
2120
+ printer_default.stopSpinnerSuccess(`Tunnel started with config name: ${entry.configName}.`);
2121
+ printer_default.info(pico3.cyanBright("Remote URLs:"));
2122
+ (urls ?? []).forEach(
2123
+ (url) => printer_default.print(" " + pico3.magentaBright(url))
2124
+ );
2125
+ }
2126
+ cleanupExpiredPendingStarts() {
2127
+ const now = Date.now();
2128
+ for (const [configId, entry] of this.pendingStarts.entries()) {
2129
+ if (now - entry.queuedAt <= PENDING_START_TIMEOUT_MS) {
2130
+ continue;
2131
+ }
2132
+ this.pendingStarts.delete(configId);
2133
+ printer_default.warn(`Timed out while waiting for tunnel URL for config name: ${entry.configName}`);
2134
+ logger.warn("Pending websocket start entry expired", { configId, tunnelId: entry.tunnelId });
2135
+ }
2136
+ }
2137
+ findPendingStart(tunnel) {
2138
+ const configId = this.getConfigIdFromTunnel(tunnel);
2139
+ const byConfigId = this.pendingStarts.get(configId);
2140
+ if (byConfigId) {
2141
+ return byConfigId;
2142
+ }
2143
+ for (const entry of this.pendingStarts.values()) {
2144
+ if (entry.tunnelId === tunnel.tunnelid) {
2145
+ return entry;
2146
+ }
2147
+ }
2148
+ return void 0;
2149
+ }
2150
+ resolveTunnelDetails(tunnelId, result) {
2151
+ try {
2152
+ const managed = this.tunnelManager.getManagedTunnel(void 0, tunnelId);
2153
+ return {
2154
+ configId: managed.configId,
2155
+ configName: managed.tunnelName || managed.configId || tunnelId
2156
+ };
2157
+ } catch {
2158
+ if (result && !isErrorResponse(result)) {
2159
+ return {
2160
+ configId: this.getConfigIdFromTunnel(result),
2161
+ configName: this.getConfigNameFromTunnel(result)
2162
+ };
2163
+ }
2164
+ return {
2165
+ configId: tunnelId,
2166
+ configName: tunnelId
2167
+ };
2168
+ }
2169
+ }
2170
+ getConfigIdFromRequest(config) {
2171
+ return "configid" in config ? config.configid : config.configId;
2172
+ }
2173
+ getConfigNameFromRequest(config) {
2174
+ return "configname" in config ? config.configname : config.name;
2175
+ }
2176
+ getConfigIdFromTunnel(tunnel) {
2177
+ return "configid" in tunnel.tunnelconfig ? tunnel.tunnelconfig.configid : tunnel.tunnelconfig.configId;
2178
+ }
2179
+ getConfigNameFromTunnel(tunnel) {
2180
+ return "configname" in tunnel.tunnelconfig ? tunnel.tunnelconfig.configname : tunnel.tunnelconfig.name;
2181
+ }
2182
+ };
2183
+ var remoteManagementWebSocketPrinter = new RemoteManagementWebSocketPrinter();
2184
+
1772
2185
  // src/remote_management/websocket_handlers.ts
1773
2186
  import z2 from "zod";
1774
2187
  var WebSocketCommandHandler = class {
1775
2188
  constructor() {
1776
2189
  this.tunnelHandler = new TunnelOperations();
2190
+ remoteManagementWebSocketPrinter.setTunnelHandler(this.tunnelHandler);
1777
2191
  }
1778
2192
  safeParse(text) {
1779
2193
  if (!text) return void 0;
@@ -1798,35 +2212,157 @@ var WebSocketCommandHandler = class {
1798
2212
  this.sendResponse(ws, resp);
1799
2213
  }
1800
2214
  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);
2215
+ let queuedConfig;
2216
+ try {
2217
+ const dc = StartSchema.parse(raw);
2218
+ queuedConfig = dc.tunnelConfig;
2219
+ remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
2220
+ const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
2221
+ remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
2222
+ return this.wrapResponse(result, req);
2223
+ } catch (e) {
2224
+ if (queuedConfig) {
2225
+ remoteManagementWebSocketPrinter.failQueuedStart(queuedConfig, String(e));
2226
+ }
2227
+ if (e instanceof z2.ZodError) {
2228
+ printer_default.warn("Validation failed for start request");
2229
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2230
+ }
2231
+ printer_default.warn(`Error in handleStartReq error: ${String(e)}`);
2232
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2233
+ }
2234
+ }
2235
+ async handleStartV2Req(req, raw) {
2236
+ let queuedConfig;
2237
+ try {
2238
+ const dc = StartV2Schema.parse(raw);
2239
+ queuedConfig = dc.tunnelConfig;
2240
+ remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
2241
+ const result = await this.tunnelHandler.handleStartV2(dc.tunnelConfig);
2242
+ remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
2243
+ return this.wrapResponse(result, req);
2244
+ } catch (e) {
2245
+ if (queuedConfig) {
2246
+ remoteManagementWebSocketPrinter.failQueuedStart(queuedConfig, String(e));
2247
+ }
2248
+ if (e instanceof z2.ZodError) {
2249
+ printer_default.warn("Validation failed for start-v2 request");
2250
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2251
+ }
2252
+ printer_default.warn(`Error in handleStartV2Req error: ${String(e)}`);
2253
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2254
+ }
1805
2255
  }
1806
2256
  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);
2257
+ try {
2258
+ const dc = StopSchema.parse(raw);
2259
+ remoteManagementWebSocketPrinter.printStopRequested(dc.tunnelID);
2260
+ const result = await this.tunnelHandler.handleStop(dc.tunnelID);
2261
+ remoteManagementWebSocketPrinter.handleStopResult(dc.tunnelID, result);
2262
+ return this.wrapResponse(result, req);
2263
+ } catch (e) {
2264
+ if (e instanceof z2.ZodError) {
2265
+ printer_default.warn("Validation failed for stop request");
2266
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2267
+ }
2268
+ printer_default.warn(`Error in handleStopReq error: ${String(e)}`);
2269
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2270
+ }
1811
2271
  }
1812
2272
  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);
2273
+ try {
2274
+ const dc = GetSchema.parse(raw);
2275
+ const result = await this.tunnelHandler.handleGet(dc.tunnelID);
2276
+ return this.wrapResponse(result, req);
2277
+ } catch (e) {
2278
+ if (e instanceof z2.ZodError) {
2279
+ printer_default.warn("Validation failed for get request");
2280
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2281
+ }
2282
+ printer_default.warn(`Error in handleGetReq error: ${String(e)}`);
2283
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2284
+ }
1816
2285
  }
1817
2286
  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);
2287
+ try {
2288
+ const dc = RestartSchema.parse(raw);
2289
+ remoteManagementWebSocketPrinter.printRestartRequested(dc.tunnelID);
2290
+ const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
2291
+ remoteManagementWebSocketPrinter.handleRestartResult(dc.tunnelID, result);
2292
+ return this.wrapResponse(result, req);
2293
+ } catch (e) {
2294
+ if (e instanceof z2.ZodError) {
2295
+ printer_default.warn("Validation failed for restart request");
2296
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2297
+ }
2298
+ printer_default.warn(`Error in handleRestartReq error: ${String(e)}`);
2299
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2300
+ }
1821
2301
  }
1822
2302
  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);
2303
+ try {
2304
+ const dc = UpdateConfigSchema.parse(raw);
2305
+ const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
2306
+ return this.wrapResponse(result, req);
2307
+ } catch (e) {
2308
+ if (e instanceof z2.ZodError) {
2309
+ printer_default.warn("Validation failed for updateconfig request");
2310
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2311
+ }
2312
+ printer_default.warn(`Error in handleUpdateConfigReq error: ${String(e)}`);
2313
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2314
+ }
2315
+ }
2316
+ async handleUpdateConfigV2Req(req, raw) {
2317
+ try {
2318
+ const dc = UpdateConfigV2Schema.parse(raw);
2319
+ const result = await this.tunnelHandler.handleUpdateConfigV2(dc.tunnelConfig);
2320
+ return this.wrapResponse(result, req);
2321
+ } catch (e) {
2322
+ if (e instanceof z2.ZodError) {
2323
+ printer_default.warn("Validation failed for update-config-v2 request");
2324
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
2325
+ }
2326
+ printer_default.warn(`Error in handleUpdateConfigV2Req error: ${String(e)}`);
2327
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2328
+ }
1826
2329
  }
1827
2330
  async handleListReq(req) {
1828
- const result = await this.tunnelHandler.handleList();
1829
- return this.wrapResponse(result, req);
2331
+ try {
2332
+ const result = await this.tunnelHandler.handleList();
2333
+ remoteManagementWebSocketPrinter.monitorList(result);
2334
+ return this.wrapResponse(result, req);
2335
+ } catch (e) {
2336
+ printer_default.warn(`Error in handleListReq error: ${String(e)}`);
2337
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2338
+ }
2339
+ }
2340
+ async handleListV2Req(req) {
2341
+ try {
2342
+ const result = await this.tunnelHandler.handleListV2();
2343
+ remoteManagementWebSocketPrinter.monitorList(result);
2344
+ return this.wrapResponse(result, req);
2345
+ } catch (e) {
2346
+ printer_default.warn(`Error in handleListV2Req error: ${String(e)}`);
2347
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
2348
+ }
2349
+ }
2350
+ async handleGetVersionReq(ws, req) {
2351
+ try {
2352
+ const versionResponse = {
2353
+ cli_version: getVersion()
2354
+ };
2355
+ const payload = {
2356
+ command: req.command,
2357
+ requestid: req.requestid,
2358
+ response: JSON.stringify(versionResponse),
2359
+ error: false
2360
+ };
2361
+ ws.send(JSON.stringify(payload));
2362
+ } catch (e) {
2363
+ printer_default.warn(`Error in handleGetVersionReq error: ${String(e)}`);
2364
+ this.sendError(ws, req, String(e));
2365
+ }
1830
2366
  }
1831
2367
  wrapResponse(result, req) {
1832
2368
  if (isErrorResponse(result)) {
@@ -1860,6 +2396,10 @@ var WebSocketCommandHandler = class {
1860
2396
  response = await this.handleStartReq(req, raw);
1861
2397
  break;
1862
2398
  }
2399
+ case "start-v2": {
2400
+ response = await this.handleStartV2Req(req, raw);
2401
+ break;
2402
+ }
1863
2403
  case "stop": {
1864
2404
  response = await this.handleStopReq(req, raw);
1865
2405
  break;
@@ -1876,10 +2416,22 @@ var WebSocketCommandHandler = class {
1876
2416
  response = await this.handleUpdateConfigReq(req, raw);
1877
2417
  break;
1878
2418
  }
2419
+ case "update-config-v2": {
2420
+ response = await this.handleUpdateConfigV2Req(req, raw);
2421
+ break;
2422
+ }
1879
2423
  case "list": {
1880
2424
  response = await this.handleListReq(req);
1881
2425
  break;
1882
2426
  }
2427
+ case "list-v2": {
2428
+ response = await this.handleListV2Req(req);
2429
+ break;
2430
+ }
2431
+ case "get-version": {
2432
+ await this.handleGetVersionReq(ws, req);
2433
+ return;
2434
+ }
1883
2435
  default:
1884
2436
  if (typeof req.command === "string") {
1885
2437
  logger.warn("Unknown command", { command: req.command });
@@ -1898,6 +2450,18 @@ var WebSocketCommandHandler = class {
1898
2450
  }
1899
2451
  }
1900
2452
  };
2453
+ function sendVersionResponse(ws) {
2454
+ const versionResponse = {
2455
+ cli_version: getVersion()
2456
+ };
2457
+ const payload = {
2458
+ command: "get-version",
2459
+ requestid: "0",
2460
+ response: JSON.stringify(versionResponse),
2461
+ error: false
2462
+ };
2463
+ ws.send(JSON.stringify(payload));
2464
+ }
1901
2465
  function handleConnectionStatusMessage(firstMessage) {
1902
2466
  try {
1903
2467
  const text = typeof firstMessage === "string" ? firstMessage : firstMessage.toString();
@@ -1948,7 +2512,11 @@ async function parseRemoteManagement(values) {
1948
2512
  if (typeof rmToken === "string" && rmToken.trim().length > 0) {
1949
2513
  const manageHost = values["manage"];
1950
2514
  try {
1951
- await initiateRemoteManagement(rmToken, manageHost);
2515
+ const remoteManagementConfig = {
2516
+ apiKey: rmToken,
2517
+ serverUrl: buildRemoteManagementWsUrl(manageHost)
2518
+ };
2519
+ await initiateRemoteManagement(remoteManagementConfig);
1952
2520
  return { ok: true };
1953
2521
  } catch (e) {
1954
2522
  logger.error("Failed to initiate remote management:", e);
@@ -1956,11 +2524,11 @@ async function parseRemoteManagement(values) {
1956
2524
  }
1957
2525
  }
1958
2526
  }
1959
- async function initiateRemoteManagement(token, manage) {
1960
- if (!token || token.trim().length === 0) {
2527
+ async function initiateRemoteManagement(remoteManagementConfig) {
2528
+ if (!remoteManagementConfig.apiKey || remoteManagementConfig.apiKey.trim().length === 0) {
1961
2529
  throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
1962
2530
  }
1963
- const wsUrl = buildRemoteManagementWsUrl(manage);
2531
+ const wsUrl = remoteManagementConfig.serverUrl;
1964
2532
  const wsHost = extractHostname(wsUrl);
1965
2533
  logger.info("Remote management mode enabled.");
1966
2534
  _stopRequested = false;
@@ -1976,7 +2544,7 @@ async function initiateRemoteManagement(token, manage) {
1976
2544
  logConnecting();
1977
2545
  setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
1978
2546
  try {
1979
- await handleWebSocketConnection(wsUrl, wsHost, token);
2547
+ await handleWebSocketConnection(wsUrl, wsHost, remoteManagementConfig.apiKey);
1980
2548
  } catch (error) {
1981
2549
  logger.warn("Remote management connection error", { error: String(error) });
1982
2550
  }
@@ -2004,6 +2572,7 @@ async function handleWebSocketConnection(wsUrl, wsHost, token) {
2004
2572
  };
2005
2573
  ws.once("open", () => {
2006
2574
  printer_default.success(`Connected to ${wsHost}`);
2575
+ setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
2007
2576
  heartbeat = setInterval(() => {
2008
2577
  if (ws.readyState === WebSocket.OPEN) ws.ping();
2009
2578
  }, PING_INTERVAL_MS);
@@ -2014,7 +2583,11 @@ async function handleWebSocketConnection(wsUrl, wsHost, token) {
2014
2583
  if (firstMessage) {
2015
2584
  firstMessage = false;
2016
2585
  const ok = handleConnectionStatusMessage(data);
2017
- if (!ok) ws.close();
2586
+ if (!ok) {
2587
+ ws.close();
2588
+ return;
2589
+ }
2590
+ sendVersionResponse(ws);
2018
2591
  return;
2019
2592
  }
2020
2593
  setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
@@ -2025,20 +2598,21 @@ async function handleWebSocketConnection(wsUrl, wsHost, token) {
2025
2598
  }
2026
2599
  });
2027
2600
  ws.on("unexpected-response", (_, res) => {
2028
- setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
2029
2601
  if (res.statusCode === 401) {
2602
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
2030
2603
  printer_default.error("Unauthorized. Please enter a valid token.");
2031
2604
  logger.error("Unauthorized (401) on remote management connect");
2605
+ ws.close();
2032
2606
  } else {
2607
+ logger.warn("Unexpected HTTP response ", { statusCode: res.statusCode });
2033
2608
  printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
2034
- logger.warn("Unexpected HTTP response", { statusCode: res.statusCode });
2609
+ cleanup();
2035
2610
  }
2036
- ws.close();
2037
2611
  });
2038
2612
  ws.on("close", (code, reason) => {
2039
2613
  setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
2040
2614
  logger.info("WebSocket closed", { code, reason: reason.toString() });
2041
- printer_default.warn(`Disconnected (code: ${code}). Retrying...`);
2615
+ printer_default.warn(`Disconnected (code: ${code}). Retrying in ${RECONNECT_SLEEP_MS / 1e3}s...`);
2042
2616
  cleanup();
2043
2617
  });
2044
2618
  ws.on("error", (err) => {