pinggy 0.4.9 → 0.5.1

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.
@@ -0,0 +1,2157 @@
1
+ import {
2
+ IPCClient,
3
+ SessionMode
4
+ } from "./chunk-BFARGPGP.js";
5
+ import {
6
+ TunnelAlreadyRunningError,
7
+ TunnelManager,
8
+ errorMessage,
9
+ getLocalAddress,
10
+ getVersion,
11
+ isValidPort,
12
+ printer_default
13
+ } from "./chunk-DLNUDW6G.js";
14
+ import {
15
+ logger
16
+ } from "./chunk-7G6SJEEA.js";
17
+ import {
18
+ getDaemonInfoPath,
19
+ getDaemonLogPath
20
+ } from "./chunk-GBYF2H4H.js";
21
+
22
+ // src/types.ts
23
+ var TunnelStateType = /* @__PURE__ */ ((TunnelStateType2) => {
24
+ TunnelStateType2["New"] = "idle";
25
+ TunnelStateType2["Starting"] = "starting";
26
+ TunnelStateType2["Running"] = "running";
27
+ TunnelStateType2["Live"] = "live";
28
+ TunnelStateType2["Closed"] = "closed";
29
+ TunnelStateType2["Exited"] = "exited";
30
+ return TunnelStateType2;
31
+ })(TunnelStateType || {});
32
+ var TunnelErrorCodeType = /* @__PURE__ */ ((TunnelErrorCodeType2) => {
33
+ TunnelErrorCodeType2["NonResponsive"] = "non_responsive";
34
+ TunnelErrorCodeType2["FailedToConnect"] = "failed_to_connect";
35
+ TunnelErrorCodeType2["ErrorInAdditionalForwarding"] = "additional_forwarding_error";
36
+ TunnelErrorCodeType2["WebdebuggerError"] = "webdebugger_error";
37
+ TunnelErrorCodeType2["NoError"] = "";
38
+ return TunnelErrorCodeType2;
39
+ })(TunnelErrorCodeType || {});
40
+ var TunnelWarningCode = /* @__PURE__ */ ((TunnelWarningCode2) => {
41
+ TunnelWarningCode2["InvalidTunnelServePath"] = "INVALID_TUNNEL_SERVE_PATH";
42
+ TunnelWarningCode2["UnknownWarning"] = "UNKNOWN_WARNING";
43
+ return TunnelWarningCode2;
44
+ })(TunnelWarningCode || {});
45
+ var ErrorCode = {
46
+ InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
47
+ InvalidRequestBodyError: "COULD_NOT_READ_BODY",
48
+ InternalServerError: "INTERNAL_SERVER_ERROR",
49
+ InvalidBodyFormatError: "INVALID_DATA_FORMAT",
50
+ ErrorStartingTunnel: "ERROR_STARTING_TUNNEL",
51
+ TunnelNotFound: "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND",
52
+ TunnelAlreadyRunningError: "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING",
53
+ WebsocketUpgradeFailError: "WEBSOCKET_UPGRADE_FAILED",
54
+ RemoteManagementAlreadyRunning: "REMOTE_MANAGEMENT_ALREADY_RUNNING",
55
+ RemoteManagementNotRunning: "REMOTE_MANAGEMENT_NOT_RUNNING",
56
+ RemoteManagementDeserializationFailed: "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED"
57
+ };
58
+ function isErrorResponse(obj) {
59
+ return typeof obj === "object" && obj !== null && "code" in obj && "message" in obj && typeof obj.message === "string" && Object.values(ErrorCode).includes(obj.code);
60
+ }
61
+ function newErrorResponse(codeOrError, message) {
62
+ if (typeof codeOrError === "object") {
63
+ return codeOrError;
64
+ }
65
+ return {
66
+ code: codeOrError,
67
+ message
68
+ };
69
+ }
70
+ function NewResponseObject(data) {
71
+ const encoder = new TextEncoder();
72
+ const bytes = encoder.encode(JSON.stringify(data));
73
+ return {
74
+ response: bytes,
75
+ requestid: "",
76
+ command: "",
77
+ error: false,
78
+ errorresponse: {}
79
+ };
80
+ }
81
+ function NewErrorResponseObject(errorResponse) {
82
+ return {
83
+ response: new Uint8Array(),
84
+ requestid: "",
85
+ command: "",
86
+ error: true,
87
+ errorresponse: errorResponse
88
+ };
89
+ }
90
+ function newStatus(tunnelState, errorCode, errorMsg) {
91
+ let assignedState = tunnelState;
92
+ if (tunnelState === "live" /* Live */) {
93
+ assignedState = "running" /* Running */;
94
+ } else if (tunnelState === "idle" /* New */) {
95
+ assignedState = "idle" /* New */;
96
+ } else if (tunnelState === "closed" /* Closed */) {
97
+ assignedState = "exited" /* Exited */;
98
+ }
99
+ const now = (/* @__PURE__ */ new Date()).toISOString();
100
+ return {
101
+ state: assignedState,
102
+ errorcode: errorCode,
103
+ errormsg: errorMsg,
104
+ createdtimestamp: now,
105
+ starttimestamp: now,
106
+ endtimestamp: now,
107
+ warnings: []
108
+ };
109
+ }
110
+ function newStats() {
111
+ return {
112
+ numLiveConnections: 0,
113
+ numTotalConnections: 0,
114
+ numTotalReqBytes: 0,
115
+ numTotalResBytes: 0,
116
+ numTotalTxBytes: 0,
117
+ elapsedTime: 0
118
+ };
119
+ }
120
+ var RemoteManagementStatus = {
121
+ Connecting: "CONNECTING",
122
+ Disconnecting: "DISCONNECTING",
123
+ Reconnecting: "RECONNECTING",
124
+ Running: "RUNNING",
125
+ NotRunning: "NOT_RUNNING",
126
+ Error: "ERROR"
127
+ };
128
+
129
+ // src/remote_management/remote_schema.ts
130
+ import { TunnelType } from "@pinggy/pinggy";
131
+ import { z } from "zod";
132
+ var HeaderModificationSchema = z.object({
133
+ key: z.string(),
134
+ value: z.array(z.string()).nullable().optional(),
135
+ type: z.enum(["add", "remove", "update"])
136
+ });
137
+ var AdditionalForwardingSchema = z.object({
138
+ remoteDomain: z.string().optional(),
139
+ remotePort: z.number().optional(),
140
+ localDomain: z.string(),
141
+ localPort: z.number()
142
+ });
143
+ var TunnelConfigSchema = z.object({
144
+ allowPreflight: z.boolean().optional(),
145
+ // primary key
146
+ allowpreflight: z.boolean().optional(),
147
+ // legacy key
148
+ autoreconnect: z.boolean(),
149
+ basicauth: z.array(z.object({ username: z.string(), password: z.string() })).nullable(),
150
+ bearerauth: z.array(z.string()).nullable(),
151
+ configid: z.string(),
152
+ configname: z.string(),
153
+ greetmsg: z.string().optional(),
154
+ force: z.boolean(),
155
+ forwardedhost: z.string(),
156
+ fullRequestUrl: z.boolean(),
157
+ headermodification: z.array(HeaderModificationSchema),
158
+ httpsOnly: z.boolean(),
159
+ internalwebdebuggerport: z.number(),
160
+ ipwhitelist: z.array(z.string()).nullable(),
161
+ localport: z.number(),
162
+ localsservertls: z.union([z.boolean(), z.string()]),
163
+ localservertlssni: z.string().nullable(),
164
+ regioncode: z.string(),
165
+ noReverseProxy: z.boolean(),
166
+ serveraddress: z.string(),
167
+ serverport: z.number(),
168
+ statusCheckInterval: z.number(),
169
+ token: z.string(),
170
+ tunnelTimeout: z.number(),
171
+ type: z.enum([
172
+ TunnelType.Http,
173
+ TunnelType.Tcp,
174
+ TunnelType.Udp,
175
+ TunnelType.Tls,
176
+ TunnelType.TlsTcp
177
+ ]),
178
+ webdebuggerport: z.number(),
179
+ xff: z.string(),
180
+ additionalForwarding: z.array(AdditionalForwardingSchema).optional(),
181
+ serve: z.string().optional()
182
+ }).superRefine((data, ctx) => {
183
+ if (data.allowPreflight === void 0 && data.allowpreflight === void 0) {
184
+ ctx.addIssue({
185
+ code: "custom",
186
+ message: "Either allowPreflight or allowpreflight is required",
187
+ path: ["allowPreflight"]
188
+ });
189
+ }
190
+ }).transform((data) => ({
191
+ ...data,
192
+ allowPreflight: data.allowPreflight ?? data.allowpreflight,
193
+ allowpreflight: data.allowPreflight ?? data.allowpreflight
194
+ }));
195
+ var StartSchema = z.object({
196
+ tunnelID: z.string().nullable().optional(),
197
+ tunnelConfig: TunnelConfigSchema
198
+ });
199
+ var StopSchema = z.object({
200
+ tunnelID: z.string().min(1)
201
+ });
202
+ var GetSchema = StopSchema;
203
+ var RestartSchema = StopSchema;
204
+ var UpdateConfigSchema = z.object({
205
+ tunnelConfig: TunnelConfigSchema
206
+ });
207
+ var ForwardingEntryV2Schema = z.object({
208
+ listenAddress: z.string().optional(),
209
+ address: z.string(),
210
+ type: z.enum([TunnelType.Http, TunnelType.Tcp, TunnelType.Udp, TunnelType.Tls, TunnelType.TlsTcp])
211
+ });
212
+ var TunnelConfigV1Schema = z.object({
213
+ // Meta Info
214
+ version: z.string(),
215
+ name: z.string(),
216
+ configId: z.string(),
217
+ // General tunnel configurations
218
+ serverAddress: z.string().optional(),
219
+ token: z.string().optional(),
220
+ autoReconnect: z.boolean().optional(),
221
+ reconnectInterval: z.number().optional(),
222
+ maxReconnectAttempts: z.number().optional(),
223
+ force: z.boolean(),
224
+ keepAliveInterval: z.number().optional(),
225
+ webDebugger: z.string(),
226
+ //Forwarding
227
+ // Either a URL string (e.g. "https://localhost:5555") or an array of forwarding entries.
228
+ forwarding: z.union([
229
+ z.string(),
230
+ z.array(ForwardingEntryV2Schema)
231
+ ]),
232
+ // IP whitelist
233
+ ipWhitelist: z.array(z.string()).optional(),
234
+ basicAuth: z.array(z.object({ username: z.string(), password: z.string() })).optional(),
235
+ bearerTokenAuth: z.array(z.string()).optional(),
236
+ headerModification: z.array(HeaderModificationSchema).optional(),
237
+ reverseProxy: z.boolean().optional(),
238
+ xForwardedFor: z.boolean().optional(),
239
+ httpsOnly: z.boolean().optional(),
240
+ originalRequestUrl: z.boolean().optional(),
241
+ allowPreflight: z.boolean().optional(),
242
+ serve: z.string().optional(),
243
+ optional: z.record(z.string(), z.unknown()).optional()
244
+ });
245
+ var StartV2Schema = z.object({
246
+ tunnelID: z.string().nullable().optional(),
247
+ tunnelConfig: TunnelConfigV1Schema
248
+ });
249
+ var UpdateConfigV2Schema = z.object({
250
+ tunnelConfig: TunnelConfigV1Schema
251
+ });
252
+ function pinggyOptionsToTunnelConfigV1(opts, configStoredInCli) {
253
+ const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
254
+ return {
255
+ version: configStoredInCli.version || "1.0",
256
+ name: configStoredInCli.name || "",
257
+ configId: configStoredInCli.configId || "",
258
+ serverAddress: opts.serverAddress || "a.pinggy.io:443",
259
+ token: opts.token || "",
260
+ autoReconnect: opts.autoReconnect ?? true,
261
+ force: opts.force ?? false,
262
+ webDebugger: opts.webDebugger || "",
263
+ forwarding: opts.forwarding ? opts.forwarding : "",
264
+ ipWhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : [],
265
+ basicAuth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : void 0,
266
+ bearerTokenAuth: parsedTokens.length ? parsedTokens : void 0,
267
+ headerModification: opts.headerModification || [],
268
+ reverseProxy: opts.reverseProxy ?? false,
269
+ xForwardedFor: !!opts.xForwardedFor,
270
+ httpsOnly: opts.httpsOnly ?? false,
271
+ originalRequestUrl: opts.originalRequestUrl ?? false,
272
+ allowPreflight: opts.allowPreflight ?? false,
273
+ optional: opts.optional || {}
274
+ };
275
+ }
276
+ function tunnelConfigToPinggyOptions(config) {
277
+ const forwardingData = [];
278
+ forwardingData.push({
279
+ address: `${config.forwardedhost}:${config.localport}`,
280
+ type: config.type || TunnelType.Http
281
+ // Default to HTTP for the primary forwarding entry
282
+ });
283
+ if (config.additionalForwarding && Array.isArray(config.additionalForwarding)) {
284
+ config.additionalForwarding.forEach((entry) => {
285
+ if (entry.localDomain && entry.localPort && entry.remoteDomain) {
286
+ const listenAddress = entry.remotePort && isValidPort(entry.remotePort) ? `${entry.remoteDomain}:${entry.remotePort}` : entry.remoteDomain;
287
+ forwardingData.push({
288
+ address: `${entry.localDomain}:${entry.localPort}`,
289
+ listenAddress,
290
+ type: TunnelType.Http
291
+ });
292
+ }
293
+ });
294
+ }
295
+ return {
296
+ token: config.token || "",
297
+ serverAddress: config.serveraddress || "free.pinggy.io",
298
+ forwarding: forwardingData,
299
+ webDebugger: config.webdebuggerport ? `localhost:${config.webdebuggerport}` : "",
300
+ ipWhitelist: config.ipwhitelist || [],
301
+ basicAuth: config.basicauth ? config.basicauth : [],
302
+ bearerTokenAuth: config.bearerauth || [],
303
+ headerModification: config.headermodification,
304
+ xForwardedFor: !!config.xff,
305
+ httpsOnly: config.httpsOnly,
306
+ originalRequestUrl: config.fullRequestUrl,
307
+ allowPreflight: config.allowPreflight,
308
+ reverseProxy: config.noReverseProxy,
309
+ force: config.force,
310
+ autoReconnect: config.autoreconnect,
311
+ optional: {
312
+ sniServerName: config.localservertlssni || ""
313
+ }
314
+ };
315
+ }
316
+ function pinggyOptionsToTunnelConfig(opts, configid, configName, localserverTls, greetMsg, serve) {
317
+ let primaryEntry;
318
+ let additionalEntries = [];
319
+ if (Array.isArray(opts.forwarding)) {
320
+ primaryEntry = opts.forwarding.find((e) => !e.listenAddress) ?? opts.forwarding[0];
321
+ additionalEntries = opts.forwarding.filter(
322
+ (e) => e !== primaryEntry && Boolean(e.listenAddress)
323
+ );
324
+ }
325
+ const forwarding = primaryEntry ? String(primaryEntry.address) : String(opts.forwarding);
326
+ const [parsedForwardedHost, portStr] = forwarding.split(":");
327
+ const parsedLocalPort = parseInt(portStr, 10);
328
+ const tunnelType = primaryEntry?.type ?? TunnelType.Http;
329
+ const additionalForwarding = additionalEntries.map((e) => {
330
+ const [localDomain, localPortStr] = String(e.address).split(":");
331
+ const [remoteDomain, remotePortStr] = String(e.listenAddress).split(":");
332
+ const localPort = parseInt(localPortStr, 10);
333
+ const remotePort = parseInt(remotePortStr, 10);
334
+ return {
335
+ localDomain,
336
+ localPort: isNaN(localPort) ? 0 : localPort,
337
+ remoteDomain,
338
+ remotePort: isNaN(remotePort) ? 0 : remotePort
339
+ };
340
+ });
341
+ const parsedTokens = opts.bearerTokenAuth ? Array.isArray(opts.bearerTokenAuth) ? opts.bearerTokenAuth : JSON.parse(opts.bearerTokenAuth) : [];
342
+ return {
343
+ allowPreflight: opts.allowPreflight ?? false,
344
+ allowpreflight: opts.allowPreflight ?? false,
345
+ autoreconnect: opts.autoReconnect ?? false,
346
+ basicauth: opts.basicAuth && Object.keys(opts.basicAuth).length ? opts.basicAuth : null,
347
+ bearerauth: parsedTokens.length ? [parsedTokens.join(",")] : null,
348
+ configid,
349
+ configname: configName,
350
+ greetmsg: greetMsg || "",
351
+ force: opts.force ?? false,
352
+ forwardedhost: parsedForwardedHost || "localhost",
353
+ fullRequestUrl: opts.originalRequestUrl ?? false,
354
+ headermodification: opts.headerModification || [],
355
+ //structured list
356
+ httpsOnly: opts.httpsOnly ?? false,
357
+ internalwebdebuggerport: 0,
358
+ ipwhitelist: opts.ipWhitelist ? Array.isArray(opts.ipWhitelist) ? opts.ipWhitelist : JSON.parse(opts.ipWhitelist) : null,
359
+ localport: parsedLocalPort || 0,
360
+ localservertlssni: null,
361
+ regioncode: "",
362
+ noReverseProxy: opts.reverseProxy ?? false,
363
+ serveraddress: opts.serverAddress || "free.pinggy.io",
364
+ serverport: 0,
365
+ statusCheckInterval: 0,
366
+ token: opts.token || "",
367
+ tunnelTimeout: 0,
368
+ type: tunnelType,
369
+ webdebuggerport: Number(opts.webDebugger?.split(":")[0]) || 0,
370
+ xff: opts.xForwardedFor ? "1" : "",
371
+ localsservertls: localserverTls || false,
372
+ additionalForwarding: additionalForwarding || [],
373
+ serve: serve || ""
374
+ };
375
+ }
376
+
377
+ // src/remote_management/handler.ts
378
+ var TunnelOperations = class {
379
+ constructor() {
380
+ this.tunnelManager = TunnelManager.getInstance();
381
+ }
382
+ buildStatus(tunnelId, state, errorCode) {
383
+ const status = newStatus(state, errorCode, "");
384
+ try {
385
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelId);
386
+ if (managed) {
387
+ status.createdtimestamp = managed.createdAt || "";
388
+ status.starttimestamp = managed.startedAt || "";
389
+ status.endtimestamp = managed.stoppedAt || "";
390
+ }
391
+ if (managed?.lastError) {
392
+ status.lastError = managed.lastError;
393
+ }
394
+ } catch (e) {
395
+ }
396
+ return status;
397
+ }
398
+ // --- Placeholder response ---
399
+ buildPendingTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, serve) {
400
+ return {
401
+ tunnelid,
402
+ remoteurls: [],
403
+ tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, false, void 0, serve),
404
+ status: this.buildStatus(tunnelid, "starting" /* Starting */, "" /* NoError */),
405
+ stats: newStats()
406
+ };
407
+ }
408
+ buildPendingTunnelResponseV2(tunnelid, tunnelConfig, configFromCli, configid, tunnelName, serve) {
409
+ return {
410
+ tunnelid,
411
+ remoteurls: [],
412
+ tunnelconfig: pinggyOptionsToTunnelConfigV1(tunnelConfig, configFromCli),
413
+ status: this.buildStatus(tunnelid, "starting" /* Starting */, "" /* NoError */),
414
+ stats: newStats(),
415
+ greetmsg: ""
416
+ };
417
+ }
418
+ // --- Helper to construct TunnelResponse ---
419
+ async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, serve) {
420
+ const stats = this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats();
421
+ const [status, tlsInfo, greetMsg, remoteurls] = await Promise.all([
422
+ this.tunnelManager.getTunnelStatus(tunnelid),
423
+ this.tunnelManager.getLocalserverTlsInfo(tunnelid),
424
+ this.tunnelManager.getTunnelGreetMessage(tunnelid),
425
+ this.tunnelManager.getTunnelUrls(tunnelid)
426
+ ]);
427
+ return {
428
+ tunnelid,
429
+ remoteurls,
430
+ tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, tlsInfo, greetMsg),
431
+ status: this.buildStatus(tunnelid, status, "" /* NoError */),
432
+ stats
433
+ };
434
+ }
435
+ async buildTunnelResponseV2(tunnelid, tunnelConfig, configFromCli, configid, tunnelName, serve) {
436
+ const stats = this.tunnelManager.getLatestTunnelStats(tunnelid) || newStats();
437
+ const [status, greetMsg, remoteurls] = await Promise.all([
438
+ this.tunnelManager.getTunnelStatus(tunnelid),
439
+ this.tunnelManager.getTunnelGreetMessage(tunnelid),
440
+ this.tunnelManager.getTunnelUrls(tunnelid)
441
+ ]);
442
+ return {
443
+ tunnelid,
444
+ remoteurls,
445
+ tunnelconfig: pinggyOptionsToTunnelConfigV1(tunnelConfig, configFromCli),
446
+ status: this.buildStatus(tunnelid, status, "" /* NoError */),
447
+ stats,
448
+ greetmsg: greetMsg
449
+ };
450
+ }
451
+ error(code, err, fallback) {
452
+ return newErrorResponse({
453
+ code,
454
+ message: err instanceof Error ? err.message : fallback
455
+ });
456
+ }
457
+ // --- Operations ---
458
+ async handleStart(config, noWait = false, origin = "cli") {
459
+ try {
460
+ const opts = tunnelConfigToPinggyOptions(config);
461
+ const managed = await this.tunnelManager.createTunnel({
462
+ ...opts,
463
+ configId: config.configid,
464
+ name: config.configname,
465
+ optional: {
466
+ serve: config.serve
467
+ }
468
+ }, origin);
469
+ const { tunnelid, tunnelName, serve, tunnelConfig } = managed;
470
+ const startPromise = this.tunnelManager.startTunnel(tunnelid);
471
+ if (noWait) {
472
+ startPromise.catch((err) => {
473
+ logger.error("No-wait startTunnel failed", { tunnelid, err: String(err) });
474
+ });
475
+ return this.buildPendingTunnelResponse(tunnelid, tunnelConfig, config.configid, tunnelName, serve);
476
+ }
477
+ await startPromise;
478
+ const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
479
+ return this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, serve);
480
+ } catch (err) {
481
+ return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
482
+ }
483
+ }
484
+ async handleStartV2(config, noWait = false, origin = "cli") {
485
+ try {
486
+ const managed = await this.tunnelManager.createTunnel(config, origin);
487
+ const { tunnelid, tunnelConfig } = managed;
488
+ const startPromise = this.tunnelManager.startTunnel(tunnelid);
489
+ if (noWait) {
490
+ startPromise.catch((err) => {
491
+ logger.error("No-wait startTunnel failed", { tunnelid, err: String(err) });
492
+ });
493
+ return this.buildPendingTunnelResponseV2(tunnelid, tunnelConfig, config, config.configId, config.name, config.serve);
494
+ }
495
+ await startPromise;
496
+ const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
497
+ return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.serve);
498
+ } catch (err) {
499
+ if (err instanceof TunnelAlreadyRunningError) {
500
+ return this.error(ErrorCode.TunnelAlreadyRunningError, err, err.message);
501
+ }
502
+ return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
503
+ }
504
+ }
505
+ async handleUpdateConfig(config, noWait = false) {
506
+ try {
507
+ const opts = tunnelConfigToPinggyOptions(config);
508
+ const updateOpts = {
509
+ ...opts,
510
+ configId: config.configid,
511
+ name: config.configname,
512
+ optional: {
513
+ serve: config.serve
514
+ }
515
+ };
516
+ if (noWait) {
517
+ const existing = this.tunnelManager.getManagedTunnel(config.configid);
518
+ if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
519
+ this.tunnelManager.updateConfig(updateOpts).catch((err) => {
520
+ logger.error("No-wait updateConfig failed", { configid: config.configid, err: String(err) });
521
+ });
522
+ return this.buildPendingTunnelResponse(existing.tunnelid, existing.tunnelConfig, config.configid, existing.tunnelName, existing.serve);
523
+ }
524
+ const tunnel = await this.tunnelManager.updateConfig(updateOpts);
525
+ if (!tunnel.instance || !tunnel.tunnelConfig)
526
+ throw new Error("Invalid tunnel state after configuration update");
527
+ return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.serve);
528
+ } catch (err) {
529
+ return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
530
+ }
531
+ }
532
+ async handleUpdateConfigV2(config, noWait = false) {
533
+ try {
534
+ if (noWait) {
535
+ const existing = this.tunnelManager.getManagedTunnel(config.configId);
536
+ if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
537
+ this.tunnelManager.updateConfig(config).catch((err) => {
538
+ logger.error("No-wait updateConfigV2 failed", { configId: config.configId, err: String(err) });
539
+ });
540
+ return this.buildPendingTunnelResponseV2(existing.tunnelid, existing.tunnelConfig, config, config.configId, existing.tunnelName, existing.serve);
541
+ }
542
+ const tunnel = await this.tunnelManager.updateConfig(config);
543
+ if (!tunnel.instance || !tunnel.tunnelConfig)
544
+ throw new Error("Invalid tunnel state after configuration update");
545
+ return this.buildTunnelResponseV2(tunnel.tunnelid, tunnel.tunnelConfig, config, config.configId, tunnel.tunnelName, tunnel.serve);
546
+ } catch (err) {
547
+ return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
548
+ }
549
+ }
550
+ async handleListV2() {
551
+ try {
552
+ const tunnels = await this.tunnelManager.getAllTunnels();
553
+ if (tunnels.length === 0) {
554
+ return [];
555
+ }
556
+ return Promise.all(
557
+ tunnels.map(async (t) => {
558
+ const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
559
+ const [status, tlsInfo, greetMsg] = await Promise.all([
560
+ this.tunnelManager.getTunnelStatus(t.tunnelid),
561
+ this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
562
+ this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
563
+ ]);
564
+ const tunnelConfguration = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
565
+ const tunnelConfig = pinggyOptionsToTunnelConfigV1(tunnelConfguration, t.tunnelConfig);
566
+ return {
567
+ tunnelid: t.tunnelid,
568
+ remoteurls: t.remoteurls,
569
+ status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
570
+ stats: rawStats,
571
+ tunnelconfig: tunnelConfig,
572
+ greetmsg: greetMsg
573
+ };
574
+ })
575
+ );
576
+ } catch (err) {
577
+ return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
578
+ }
579
+ }
580
+ async handleList() {
581
+ try {
582
+ const tunnels = await this.tunnelManager.getAllTunnels();
583
+ if (tunnels.length === 0) {
584
+ return [];
585
+ }
586
+ return Promise.all(
587
+ tunnels.map(async (t) => {
588
+ const rawStats = this.tunnelManager.getLatestTunnelStats(t.tunnelid) || newStats();
589
+ const [status, tlsInfo, greetMsg] = await Promise.all([
590
+ this.tunnelManager.getTunnelStatus(t.tunnelid),
591
+ this.tunnelManager.getLocalserverTlsInfo(t.tunnelid),
592
+ this.tunnelManager.getTunnelGreetMessage(t.tunnelid)
593
+ ]);
594
+ const pinggyOptions = status !== "closed" /* Closed */ && status !== "exited" /* Exited */ ? await this.tunnelManager.getTunnelConfig("", t.tunnelid) : t.tunnelConfig;
595
+ const tunnelConfig = pinggyOptionsToTunnelConfig(pinggyOptions, t.configId, t.tunnelName, tlsInfo, greetMsg, t.serve);
596
+ return {
597
+ tunnelid: t.tunnelid,
598
+ remoteurls: t.remoteurls,
599
+ status: this.buildStatus(t.tunnelid, status, "" /* NoError */),
600
+ stats: rawStats,
601
+ tunnelconfig: tunnelConfig
602
+ };
603
+ })
604
+ );
605
+ } catch (err) {
606
+ return this.error(ErrorCode.InternalServerError, err, "Failed to list tunnels");
607
+ }
608
+ }
609
+ async handleStop(tunnelid) {
610
+ try {
611
+ const { configId } = this.tunnelManager.stopTunnel(tunnelid);
612
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
613
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
614
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, configId, managed.tunnelName, managed.serve);
615
+ } catch (err) {
616
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to stop tunnel");
617
+ }
618
+ }
619
+ async handleGet(tunnelid) {
620
+ try {
621
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
622
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
623
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configId, managed.tunnelName, managed.serve);
624
+ } catch (err) {
625
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
626
+ }
627
+ }
628
+ async handleRestart(tunnelid, noWait = false) {
629
+ try {
630
+ if (noWait) {
631
+ const managed2 = this.tunnelManager.getManagedTunnel("", tunnelid);
632
+ if (!managed2?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
633
+ this.tunnelManager.restartTunnel(tunnelid).catch((err) => {
634
+ logger.error("No-wait restartTunnel failed", { tunnelid, err: String(err) });
635
+ });
636
+ return this.buildPendingTunnelResponse(tunnelid, managed2.tunnelConfig, managed2.configId, managed2.tunnelName, managed2.serve);
637
+ }
638
+ await this.tunnelManager.restartTunnel(tunnelid);
639
+ const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
640
+ if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
641
+ return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configId, managed.tunnelName, managed.serve);
642
+ } catch (err) {
643
+ return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
644
+ }
645
+ }
646
+ handleRegisterStatsListener(tunnelid, listener) {
647
+ void this.tunnelManager.registerStatsListener(tunnelid, listener);
648
+ }
649
+ handleUnregisterStatsListener(tunnelid, listnerId) {
650
+ this.tunnelManager.deregisterStatsListener(tunnelid, listnerId);
651
+ }
652
+ handleGetTunnelStats(tunnelid) {
653
+ try {
654
+ const stats = this.tunnelManager.getTunnelStats(tunnelid);
655
+ if (!stats) {
656
+ return Promise.resolve([newStats()]);
657
+ }
658
+ return Promise.resolve(stats);
659
+ } catch (err) {
660
+ return Promise.resolve(this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel stats"));
661
+ }
662
+ }
663
+ handleRegisterDisconnectListener(tunnelid, listener) {
664
+ void this.tunnelManager.registerDisconnectListener(tunnelid, listener);
665
+ }
666
+ handleRemoveStoppedTunnelByConfigId(configId) {
667
+ try {
668
+ return this.tunnelManager.removeStoppedTunnelByConfigId(configId);
669
+ } catch (err) {
670
+ return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by configId");
671
+ }
672
+ }
673
+ handleRemoveStoppedTunnelByTunnelId(tunnelId) {
674
+ try {
675
+ return this.tunnelManager.removeStoppedTunnelByTunnelId(tunnelId);
676
+ } catch (err) {
677
+ return this.error(ErrorCode.InternalServerError, err, "Failed to remove stopped tunnel by tunnelId");
678
+ }
679
+ }
680
+ };
681
+
682
+ // src/remote_management/remoteManagement.ts
683
+ import WebSocket from "ws";
684
+
685
+ // src/remote_management/websocket_printer.ts
686
+ import pico from "picocolors";
687
+ var PENDING_START_TIMEOUT_MS = 5 * 60 * 1e3;
688
+ var RemoteManagementWebSocketPrinter = class {
689
+ constructor() {
690
+ this.pendingStarts = /* @__PURE__ */ new Map();
691
+ }
692
+ setTunnelHandler(tunnelHandler) {
693
+ this.tunnelHandler = tunnelHandler;
694
+ }
695
+ queueStart(config) {
696
+ this.cleanupExpiredPendingStarts();
697
+ const entry = {
698
+ configId: this.getConfigIdFromRequest(config),
699
+ configName: this.getConfigNameFromRequest(config),
700
+ queuedAt: Date.now()
701
+ };
702
+ this.latestPendingConfigId = entry.configId;
703
+ this.pendingStarts.set(entry.configId, entry);
704
+ printer_default.startSpinner("Starting tunnel with config name: " + entry.configName);
705
+ }
706
+ failQueuedStart(config, reason) {
707
+ const configId = this.getConfigIdFromRequest(config);
708
+ const pending = this.pendingStarts.get(configId);
709
+ const configName = pending?.configName || this.getConfigNameFromRequest(config);
710
+ this.pendingStarts.delete(configId);
711
+ if (this.latestPendingConfigId === configId) {
712
+ this.latestPendingConfigId = void 0;
713
+ printer_default.stopSpinnerFail(`Failed to start tunnel with config name: ${configName}. ${reason}`);
714
+ }
715
+ }
716
+ handleStartResult(config, result) {
717
+ this.cleanupExpiredPendingStarts();
718
+ const requestedConfigId = this.getConfigIdFromRequest(config);
719
+ if (this.latestPendingConfigId && requestedConfigId !== this.latestPendingConfigId) {
720
+ this.pendingStarts.delete(requestedConfigId);
721
+ return;
722
+ }
723
+ if (isErrorResponse(result)) {
724
+ this.failQueuedStart(config, result.message);
725
+ return;
726
+ }
727
+ const configId = this.getConfigIdFromTunnel(result);
728
+ const pending = this.pendingStarts.get(requestedConfigId) || {
729
+ configId: requestedConfigId,
730
+ configName: this.getConfigNameFromRequest(config),
731
+ queuedAt: Date.now()
732
+ };
733
+ pending.tunnelId = result.tunnelid;
734
+ this.pendingStarts.set(requestedConfigId, pending);
735
+ if (result.remoteurls.length > 0) {
736
+ this.completePendingStart(pending, result.remoteurls);
737
+ }
738
+ }
739
+ printStopRequested(tunnelId) {
740
+ const details = this.resolveTunnelDetails(tunnelId);
741
+ printer_default.startSpinner("Stopping tunnel with config name: " + details.configName);
742
+ }
743
+ handleStopResult(tunnelId, result) {
744
+ const details = this.resolveTunnelDetails(tunnelId, result);
745
+ if (isErrorResponse(result)) {
746
+ printer_default.stopSpinnerFail("Failed to stop tunnel with config name: " + details.configName);
747
+ return;
748
+ }
749
+ this.pendingStarts.delete(details.configId);
750
+ printer_default.stopSpinnerSuccess("Stopped tunnel with config name: " + details.configName);
751
+ }
752
+ printRestartRequested(tunnelId) {
753
+ const details = this.resolveTunnelDetails(tunnelId);
754
+ printer_default.startSpinner("Restarting tunnel with config name: " + details.configName);
755
+ }
756
+ handleRestartResult(tunnelId, result) {
757
+ const details = this.resolveTunnelDetails(tunnelId, result);
758
+ if (isErrorResponse(result)) {
759
+ printer_default.warn(`Failed to restart tunnel with config name: ${details.configName}. ${result.message}`);
760
+ printer_default.stopSpinnerFail("Failed to restart tunnel with config name: " + details.configName);
761
+ return;
762
+ }
763
+ printer_default.stopSpinnerSuccess("Restarted tunnel with config name: " + details.configName);
764
+ if (result.remoteurls?.length > 0) {
765
+ printer_default.info(pico.cyanBright("Remote URLs:"));
766
+ (result.remoteurls ?? []).forEach(
767
+ (url) => printer_default.print(" " + pico.magentaBright(url))
768
+ );
769
+ }
770
+ }
771
+ monitorList(result) {
772
+ this.cleanupExpiredPendingStarts();
773
+ if (!Array.isArray(result) || this.pendingStarts.size === 0 || !this.latestPendingConfigId) {
774
+ return;
775
+ }
776
+ for (const tunnel of result) {
777
+ const pending = this.findPendingStart(tunnel);
778
+ if (!pending) {
779
+ continue;
780
+ }
781
+ if (pending.configId !== this.latestPendingConfigId) {
782
+ continue;
783
+ }
784
+ pending.tunnelId = tunnel.tunnelid;
785
+ this.pendingStarts.set(pending.configId, pending);
786
+ if (tunnel.remoteurls.length > 0) {
787
+ this.completePendingStart(pending, tunnel.remoteurls);
788
+ continue;
789
+ }
790
+ if (tunnel.status.state === "exited" /* Exited */) {
791
+ const reason = tunnel.status.errormsg || "Tunnel exited before a public URL was assigned";
792
+ this.pendingStarts.delete(pending.configId);
793
+ this.latestPendingConfigId = void 0;
794
+ printer_default.stopSpinnerFail(`Tunnel start did not complete for config name: ${pending.configName}. ${reason}`);
795
+ }
796
+ }
797
+ }
798
+ completePendingStart(entry, urls) {
799
+ if (this.latestPendingConfigId && entry.configId !== this.latestPendingConfigId) {
800
+ this.pendingStarts.delete(entry.configId);
801
+ return;
802
+ }
803
+ this.pendingStarts.delete(entry.configId);
804
+ this.latestPendingConfigId = void 0;
805
+ printer_default.stopSpinnerSuccess(`Tunnel started with config name: ${entry.configName}.`);
806
+ printer_default.info(pico.cyanBright("Remote URLs:"));
807
+ (urls ?? []).forEach(
808
+ (url) => printer_default.print(" " + pico.magentaBright(url))
809
+ );
810
+ }
811
+ cleanupExpiredPendingStarts() {
812
+ const now = Date.now();
813
+ for (const [configId, entry] of this.pendingStarts.entries()) {
814
+ if (now - entry.queuedAt <= PENDING_START_TIMEOUT_MS) {
815
+ continue;
816
+ }
817
+ this.pendingStarts.delete(configId);
818
+ printer_default.warn(`Timed out while waiting for tunnel URL for config name: ${entry.configName}`);
819
+ logger.warn("Pending websocket start entry expired", { configId, tunnelId: entry.tunnelId });
820
+ }
821
+ }
822
+ findPendingStart(tunnel) {
823
+ const configId = this.getConfigIdFromTunnel(tunnel);
824
+ const byConfigId = this.pendingStarts.get(configId);
825
+ if (byConfigId) {
826
+ return byConfigId;
827
+ }
828
+ for (const entry of this.pendingStarts.values()) {
829
+ if (entry.tunnelId === tunnel.tunnelid) {
830
+ return entry;
831
+ }
832
+ }
833
+ return void 0;
834
+ }
835
+ resolveTunnelDetails(tunnelId, result) {
836
+ if (result && !isErrorResponse(result)) {
837
+ return {
838
+ configId: this.getConfigIdFromTunnel(result),
839
+ configName: this.getConfigNameFromTunnel(result)
840
+ };
841
+ }
842
+ return {
843
+ configId: tunnelId,
844
+ configName: tunnelId
845
+ };
846
+ }
847
+ getConfigIdFromRequest(config) {
848
+ return "configid" in config ? config.configid : config.configId;
849
+ }
850
+ getConfigNameFromRequest(config) {
851
+ return "configname" in config ? config.configname : config.name;
852
+ }
853
+ getConfigIdFromTunnel(tunnel) {
854
+ return "configid" in tunnel.tunnelconfig ? tunnel.tunnelconfig.configid : tunnel.tunnelconfig.configId;
855
+ }
856
+ getConfigNameFromTunnel(tunnel) {
857
+ return "configname" in tunnel.tunnelconfig ? tunnel.tunnelconfig.configname : tunnel.tunnelconfig.name;
858
+ }
859
+ };
860
+ var remoteManagementWebSocketPrinter = new RemoteManagementWebSocketPrinter();
861
+
862
+ // src/remote_management/websocket_handlers.ts
863
+ import z2 from "zod";
864
+ var WsCommand = {
865
+ Start: "start",
866
+ StartV2: "start-v2",
867
+ Stop: "stop",
868
+ Get: "get",
869
+ Restart: "restart",
870
+ UpdateConfig: "updateconfig",
871
+ UpdateConfigV2: "update-config-v2",
872
+ List: "list",
873
+ ListV2: "list-v2",
874
+ GetVersion: "get-version"
875
+ };
876
+ var WebSocketCommandHandler = class {
877
+ constructor(handler) {
878
+ this.tunnelHandler = handler ?? new TunnelOperations();
879
+ remoteManagementWebSocketPrinter.setTunnelHandler(this.tunnelHandler);
880
+ }
881
+ safeParse(text) {
882
+ if (!text) return void 0;
883
+ try {
884
+ return JSON.parse(text);
885
+ } catch (e) {
886
+ logger.warn("Invalid JSON payload", { error: String(e), text });
887
+ return void 0;
888
+ }
889
+ }
890
+ sendResponse(ws, resp) {
891
+ const payload = {
892
+ ...resp,
893
+ response: Buffer.from(resp.response || []).toString("base64")
894
+ };
895
+ ws.send(JSON.stringify(payload));
896
+ }
897
+ sendError(ws, req, message, code = ErrorCode.InternalServerError) {
898
+ const resp = NewErrorResponseObject({ code, message });
899
+ resp.command = req.command || "";
900
+ resp.requestid = req.requestid || "";
901
+ this.sendResponse(ws, resp);
902
+ }
903
+ async handleStartReq(req, raw) {
904
+ let queuedConfig;
905
+ try {
906
+ const dc = StartSchema.parse(raw);
907
+ queuedConfig = dc.tunnelConfig;
908
+ remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
909
+ const result = await this.tunnelHandler.handleStart(dc.tunnelConfig, true);
910
+ remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
911
+ return this.wrapResponse(result, req);
912
+ } catch (e) {
913
+ if (queuedConfig) {
914
+ remoteManagementWebSocketPrinter.failQueuedStart(queuedConfig, String(e));
915
+ }
916
+ if (e instanceof z2.ZodError) {
917
+ printer_default.warn("Validation failed for start request");
918
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
919
+ }
920
+ printer_default.warn(`Error in handleStartReq error: ${String(e)}`);
921
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
922
+ }
923
+ }
924
+ async handleStartV2Req(req, raw) {
925
+ let queuedConfig;
926
+ try {
927
+ const dc = StartV2Schema.parse(raw);
928
+ queuedConfig = dc.tunnelConfig;
929
+ remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
930
+ const result = await this.tunnelHandler.handleStartV2(dc.tunnelConfig, true);
931
+ remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
932
+ return this.wrapResponse(result, req);
933
+ } catch (e) {
934
+ if (queuedConfig) {
935
+ remoteManagementWebSocketPrinter.failQueuedStart(queuedConfig, String(e));
936
+ }
937
+ if (e instanceof z2.ZodError) {
938
+ printer_default.warn("Validation failed for start-v2 request");
939
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
940
+ }
941
+ printer_default.warn(`Error in handleStartV2Req error: ${String(e)}`);
942
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
943
+ }
944
+ }
945
+ async handleStopReq(req, raw) {
946
+ try {
947
+ const dc = StopSchema.parse(raw);
948
+ remoteManagementWebSocketPrinter.printStopRequested(dc.tunnelID);
949
+ const result = await this.tunnelHandler.handleStop(dc.tunnelID);
950
+ remoteManagementWebSocketPrinter.handleStopResult(dc.tunnelID, result);
951
+ return this.wrapResponse(result, req);
952
+ } catch (e) {
953
+ if (e instanceof z2.ZodError) {
954
+ printer_default.warn("Validation failed for stop request");
955
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
956
+ }
957
+ printer_default.warn(`Error in handleStopReq error: ${String(e)}`);
958
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
959
+ }
960
+ }
961
+ async handleGetReq(req, raw) {
962
+ try {
963
+ const dc = GetSchema.parse(raw);
964
+ const result = await this.tunnelHandler.handleGet(dc.tunnelID);
965
+ return this.wrapResponse(result, req);
966
+ } catch (e) {
967
+ if (e instanceof z2.ZodError) {
968
+ printer_default.warn("Validation failed for get request");
969
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
970
+ }
971
+ printer_default.warn(`Error in handleGetReq error: ${String(e)}`);
972
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
973
+ }
974
+ }
975
+ async handleRestartReq(req, raw) {
976
+ try {
977
+ const dc = RestartSchema.parse(raw);
978
+ remoteManagementWebSocketPrinter.printRestartRequested(dc.tunnelID);
979
+ const result = await this.tunnelHandler.handleRestart(dc.tunnelID, true);
980
+ remoteManagementWebSocketPrinter.handleRestartResult(dc.tunnelID, result);
981
+ return this.wrapResponse(result, req);
982
+ } catch (e) {
983
+ if (e instanceof z2.ZodError) {
984
+ printer_default.warn("Validation failed for restart request");
985
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
986
+ }
987
+ printer_default.warn(`Error in handleRestartReq error: ${String(e)}`);
988
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
989
+ }
990
+ }
991
+ async handleUpdateConfigReq(req, raw) {
992
+ try {
993
+ const dc = UpdateConfigSchema.parse(raw);
994
+ const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig, true);
995
+ return this.wrapResponse(result, req);
996
+ } catch (e) {
997
+ if (e instanceof z2.ZodError) {
998
+ printer_default.warn("Validation failed for updateconfig request");
999
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
1000
+ }
1001
+ printer_default.warn(`Error in handleUpdateConfigReq error: ${String(e)}`);
1002
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
1003
+ }
1004
+ }
1005
+ async handleUpdateConfigV2Req(req, raw) {
1006
+ try {
1007
+ const dc = UpdateConfigV2Schema.parse(raw);
1008
+ const result = await this.tunnelHandler.handleUpdateConfigV2(dc.tunnelConfig, true);
1009
+ return this.wrapResponse(result, req);
1010
+ } catch (e) {
1011
+ if (e instanceof z2.ZodError) {
1012
+ printer_default.warn("Validation failed for update-config-v2 request");
1013
+ return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
1014
+ }
1015
+ printer_default.warn(`Error in handleUpdateConfigV2Req error: ${String(e)}`);
1016
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
1017
+ }
1018
+ }
1019
+ async handleListReq(req) {
1020
+ try {
1021
+ const result = await this.tunnelHandler.handleList();
1022
+ remoteManagementWebSocketPrinter.monitorList(result);
1023
+ return this.wrapResponse(result, req);
1024
+ } catch (e) {
1025
+ printer_default.warn(`Error in handleListReq error: ${String(e)}`);
1026
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
1027
+ }
1028
+ }
1029
+ async handleListV2Req(req) {
1030
+ try {
1031
+ const result = await this.tunnelHandler.handleListV2();
1032
+ remoteManagementWebSocketPrinter.monitorList(result);
1033
+ return this.wrapResponse(result, req);
1034
+ } catch (e) {
1035
+ printer_default.warn(`Error in handleListV2Req error: ${String(e)}`);
1036
+ return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
1037
+ }
1038
+ }
1039
+ handleGetVersionReq(ws, req) {
1040
+ try {
1041
+ const versionResponse = {
1042
+ cli_version: getVersion()
1043
+ };
1044
+ const payload = {
1045
+ command: req.command,
1046
+ requestid: req.requestid,
1047
+ response: JSON.stringify(versionResponse),
1048
+ error: false
1049
+ };
1050
+ ws.send(JSON.stringify(payload));
1051
+ } catch (e) {
1052
+ printer_default.warn(`Error in handleGetVersionReq error: ${String(e)}`);
1053
+ this.sendError(ws, req, String(e));
1054
+ }
1055
+ }
1056
+ wrapResponse(result, req) {
1057
+ if (isErrorResponse(result)) {
1058
+ const errResp = NewErrorResponseObject(result);
1059
+ errResp.command = req.command;
1060
+ errResp.requestid = req.requestid;
1061
+ return errResp;
1062
+ }
1063
+ const finalResult = JSON.parse(JSON.stringify(result));
1064
+ if (Array.isArray(finalResult)) {
1065
+ finalResult.forEach((item) => {
1066
+ if (item?.tunnelconfig) {
1067
+ delete item.tunnelconfig.allowPreflight;
1068
+ }
1069
+ });
1070
+ } else if (finalResult?.tunnelconfig) {
1071
+ delete finalResult.tunnelconfig.allowPreflight;
1072
+ }
1073
+ const respObj = NewResponseObject(finalResult);
1074
+ respObj.command = req.command;
1075
+ respObj.requestid = req.requestid;
1076
+ return respObj;
1077
+ }
1078
+ async handle(ws, req) {
1079
+ const cmd = (req.command || "").toLowerCase();
1080
+ const raw = this.safeParse(req.data);
1081
+ try {
1082
+ let response;
1083
+ switch (cmd) {
1084
+ case WsCommand.Start: {
1085
+ response = await this.handleStartReq(req, raw);
1086
+ break;
1087
+ }
1088
+ case WsCommand.StartV2: {
1089
+ response = await this.handleStartV2Req(req, raw);
1090
+ break;
1091
+ }
1092
+ case WsCommand.Stop: {
1093
+ response = await this.handleStopReq(req, raw);
1094
+ break;
1095
+ }
1096
+ case WsCommand.Get: {
1097
+ response = await this.handleGetReq(req, raw);
1098
+ break;
1099
+ }
1100
+ case WsCommand.Restart: {
1101
+ response = await this.handleRestartReq(req, raw);
1102
+ break;
1103
+ }
1104
+ case WsCommand.UpdateConfig: {
1105
+ response = await this.handleUpdateConfigReq(req, raw);
1106
+ break;
1107
+ }
1108
+ case WsCommand.UpdateConfigV2: {
1109
+ response = await this.handleUpdateConfigV2Req(req, raw);
1110
+ break;
1111
+ }
1112
+ case WsCommand.List: {
1113
+ response = await this.handleListReq(req);
1114
+ break;
1115
+ }
1116
+ case WsCommand.ListV2: {
1117
+ response = await this.handleListV2Req(req);
1118
+ break;
1119
+ }
1120
+ case WsCommand.GetVersion: {
1121
+ this.handleGetVersionReq(ws, req);
1122
+ return;
1123
+ }
1124
+ default:
1125
+ if (typeof req.command === "string") {
1126
+ logger.warn("Unknown command", { command: req.command });
1127
+ }
1128
+ return this.sendError(ws, req, "Invalid command");
1129
+ }
1130
+ logger.debug("Sending response", { command: response.command, requestid: response.requestid });
1131
+ this.sendResponse(ws, response);
1132
+ } catch (e) {
1133
+ if (e instanceof z2.ZodError) {
1134
+ logger.warn("Validation failed", { cmd, issues: e.issues });
1135
+ return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
1136
+ }
1137
+ logger.error("Error handling command", { cmd, error: errorMessage(e) });
1138
+ return this.sendError(ws, req, errorMessage(e) || "Internal error");
1139
+ }
1140
+ }
1141
+ };
1142
+ function sendVersionResponse(ws) {
1143
+ const versionResponse = {
1144
+ cli_version: getVersion()
1145
+ };
1146
+ const payload = {
1147
+ command: WsCommand.GetVersion,
1148
+ requestid: "0",
1149
+ response: JSON.stringify(versionResponse),
1150
+ error: false
1151
+ };
1152
+ ws.send(JSON.stringify(payload));
1153
+ }
1154
+ function handleConnectionStatusMessage(firstMessage) {
1155
+ try {
1156
+ const text = typeof firstMessage === "string" ? firstMessage : firstMessage.toString();
1157
+ const cs = JSON.parse(text);
1158
+ if (!cs.success) {
1159
+ const msg = cs.error_msg || "Connection failed";
1160
+ printer_default.warn(`Connection failed: ${msg}`);
1161
+ logger.warn("Remote management connection failed", { error_code: cs.error_code, error_msg: msg });
1162
+ return false;
1163
+ }
1164
+ return true;
1165
+ } catch (e) {
1166
+ logger.warn("Failed to parse connection status message", { error: String(e) });
1167
+ return true;
1168
+ }
1169
+ }
1170
+
1171
+ // src/remote_management/remoteManagement.ts
1172
+ var RECONNECT_SLEEP_MS = 5e3;
1173
+ var PING_INTERVAL_MS = 3e4;
1174
+ var RemoteManagementUnauthorizedError = class extends Error {
1175
+ constructor() {
1176
+ super("Unauthorized. Please enter a valid token.");
1177
+ this.name = "RemoteManagementUnauthorizedError";
1178
+ }
1179
+ };
1180
+ var _remoteManagementState = {
1181
+ status: "NOT_RUNNING",
1182
+ errorMessage: ""
1183
+ };
1184
+ var _stopRequested = false;
1185
+ var currentWs = null;
1186
+ function buildRemoteManagementWsUrl(manage) {
1187
+ let baseUrl = (manage || "dashboard.pinggy.io").trim();
1188
+ if (!(baseUrl.startsWith("ws://") || baseUrl.startsWith("wss://"))) {
1189
+ baseUrl = "wss://" + baseUrl;
1190
+ }
1191
+ const trimmed = baseUrl.replace(/\/$/, "");
1192
+ return `${trimmed}/backend/api/v1/remote-management/connect`;
1193
+ }
1194
+ function extractHostname(u) {
1195
+ try {
1196
+ const url = new URL(u);
1197
+ return url.host;
1198
+ } catch {
1199
+ return u;
1200
+ }
1201
+ }
1202
+ function sleep(ms) {
1203
+ return new Promise((res) => setTimeout(res, ms));
1204
+ }
1205
+ async function initiateRemoteManagement(remoteManagementConfig, tunnelHandler) {
1206
+ if (!remoteManagementConfig.apiKey || remoteManagementConfig.apiKey.trim().length === 0) {
1207
+ throw new Error("Remote management token is required (use --remote-management <TOKEN>)");
1208
+ }
1209
+ const wsUrl = remoteManagementConfig.serverUrl;
1210
+ const wsHost = extractHostname(wsUrl);
1211
+ logger.info("Remote management mode enabled.");
1212
+ _stopRequested = false;
1213
+ const sigintHandler = () => {
1214
+ _stopRequested = true;
1215
+ };
1216
+ process.once("SIGINT", sigintHandler);
1217
+ const logConnecting = () => {
1218
+ printer_default.print(`Connecting to ${wsHost}`);
1219
+ logger.info("Connecting to remote management", { wsUrl });
1220
+ };
1221
+ while (!_stopRequested) {
1222
+ logConnecting();
1223
+ setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
1224
+ try {
1225
+ await handleWebSocketConnection(wsUrl, wsHost, remoteManagementConfig.apiKey, void 0, tunnelHandler);
1226
+ } catch (error) {
1227
+ if (error instanceof RemoteManagementUnauthorizedError) {
1228
+ throw error;
1229
+ }
1230
+ logger.warn("Remote management connection error", { error: String(error) });
1231
+ }
1232
+ if (_stopRequested) {
1233
+ break;
1234
+ }
1235
+ printer_default.warn(`Remote management disconnected. Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds...`);
1236
+ logger.info("Reconnecting to remote management after disconnect");
1237
+ await sleep(RECONNECT_SLEEP_MS);
1238
+ }
1239
+ process.removeListener("SIGINT", sigintHandler);
1240
+ logger.info("Remote management stopped.");
1241
+ return getRemoteManagementState();
1242
+ }
1243
+ async function handleWebSocketConnection(wsUrl, wsHost, token, onOpenCallback, tunnelHandler) {
1244
+ return new Promise((resolve, reject) => {
1245
+ const ws = new WebSocket(wsUrl, {
1246
+ headers: { Authorization: `Bearer ${token}` }
1247
+ });
1248
+ currentWs = ws;
1249
+ let heartbeat = null;
1250
+ let firstMessage = true;
1251
+ let settled = false;
1252
+ const cleanup = (err) => {
1253
+ if (settled) {
1254
+ return;
1255
+ }
1256
+ settled = true;
1257
+ if (heartbeat) {
1258
+ clearInterval(heartbeat);
1259
+ }
1260
+ currentWs = null;
1261
+ if (err) {
1262
+ reject(err);
1263
+ } else {
1264
+ resolve();
1265
+ }
1266
+ };
1267
+ ws.once("open", () => {
1268
+ printer_default.success(`Connected to ${wsHost}`);
1269
+ setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
1270
+ onOpenCallback?.();
1271
+ heartbeat = setInterval(() => {
1272
+ if (ws.readyState === WebSocket.OPEN) ws.ping();
1273
+ }, PING_INTERVAL_MS);
1274
+ });
1275
+ ws.on("ping", () => ws.pong());
1276
+ ws.on("message", async (data) => {
1277
+ try {
1278
+ if (firstMessage) {
1279
+ firstMessage = false;
1280
+ const ok = handleConnectionStatusMessage(data);
1281
+ if (!ok) {
1282
+ ws.close();
1283
+ return;
1284
+ }
1285
+ sendVersionResponse(ws);
1286
+ return;
1287
+ }
1288
+ setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
1289
+ const req = JSON.parse(data.toString("utf8"));
1290
+ await new WebSocketCommandHandler(tunnelHandler).handle(ws, req);
1291
+ } catch (e) {
1292
+ logger.warn("Failed handling websocket message", { error: String(e) });
1293
+ }
1294
+ });
1295
+ ws.on("unexpected-response", (_, res) => {
1296
+ if (res.statusCode === 401) {
1297
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
1298
+ logger.error("Unauthorized (401) on remote management connect");
1299
+ cleanup(new RemoteManagementUnauthorizedError());
1300
+ ws.close();
1301
+ } else {
1302
+ logger.warn("Unexpected HTTP response ", { statusCode: res.statusCode });
1303
+ printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
1304
+ cleanup();
1305
+ ws.close();
1306
+ }
1307
+ });
1308
+ ws.on("close", (code, reason) => {
1309
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
1310
+ logger.info("WebSocket closed", { code, reason: reason.toString() });
1311
+ printer_default.warn(`Disconnected (code: ${code}). Retrying in ${RECONNECT_SLEEP_MS / 1e3}s...`);
1312
+ cleanup();
1313
+ });
1314
+ ws.on("error", (err) => {
1315
+ setRemoteManagementState({ status: RemoteManagementStatus.Error, errorMessage: err.message });
1316
+ logger.warn("WebSocket error", { error: err.message });
1317
+ printer_default.warn(err.message);
1318
+ cleanup();
1319
+ });
1320
+ });
1321
+ }
1322
+ async function closeRemoteManagement(timeoutMs = 1e4) {
1323
+ _stopRequested = true;
1324
+ try {
1325
+ if (currentWs) {
1326
+ try {
1327
+ setRemoteManagementState({ status: RemoteManagementStatus.Disconnecting, errorMessage: "" });
1328
+ currentWs.close();
1329
+ } catch (e) {
1330
+ logger.warn("Error while closing current remote management websocket", { error: String(e) });
1331
+ }
1332
+ }
1333
+ const start = Date.now();
1334
+ while (_remoteManagementState.status === "RUNNING") {
1335
+ if (Date.now() - start > timeoutMs) {
1336
+ logger.warn("Timed out waiting for remote management to stop");
1337
+ break;
1338
+ }
1339
+ await sleep(200);
1340
+ }
1341
+ } finally {
1342
+ currentWs = null;
1343
+ setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: "" });
1344
+ return getRemoteManagementState();
1345
+ }
1346
+ }
1347
+ function startRemoteManagement(remoteManagementConfig, tunnelHandler) {
1348
+ if (!remoteManagementConfig.apiKey || remoteManagementConfig.apiKey.trim().length === 0) {
1349
+ return Promise.reject(new Error("Remote management token is required"));
1350
+ }
1351
+ const wsUrl = remoteManagementConfig.serverUrl;
1352
+ const wsHost = extractHostname(wsUrl);
1353
+ logger.info("Remote management mode enabled.");
1354
+ _stopRequested = false;
1355
+ return new Promise((resolve, reject) => {
1356
+ let firstSettled = false;
1357
+ const settleOnce = (err) => {
1358
+ if (firstSettled) {
1359
+ return;
1360
+ }
1361
+ firstSettled = true;
1362
+ if (err) {
1363
+ reject(err);
1364
+ } else {
1365
+ resolve(getRemoteManagementState());
1366
+ }
1367
+ };
1368
+ const runLoop = async () => {
1369
+ const sigintHandler = () => {
1370
+ _stopRequested = true;
1371
+ };
1372
+ process.once("SIGINT", sigintHandler);
1373
+ while (!_stopRequested) {
1374
+ logger.info("Connecting to remote management", { wsUrl });
1375
+ setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
1376
+ try {
1377
+ await handleWebSocketConnection(wsUrl, wsHost, remoteManagementConfig.apiKey, () => settleOnce(), tunnelHandler);
1378
+ } catch (error) {
1379
+ if (error instanceof RemoteManagementUnauthorizedError) {
1380
+ settleOnce(error);
1381
+ process.removeListener("SIGINT", sigintHandler);
1382
+ return;
1383
+ }
1384
+ settleOnce();
1385
+ logger.warn("Remote management connection error", { error: String(error) });
1386
+ }
1387
+ if (_stopRequested) {
1388
+ break;
1389
+ }
1390
+ logger.info("Reconnecting to remote management after disconnect");
1391
+ await sleep(RECONNECT_SLEEP_MS);
1392
+ }
1393
+ process.removeListener("SIGINT", sigintHandler);
1394
+ logger.info("Remote management stopped.");
1395
+ };
1396
+ runLoop().catch((err) => settleOnce(err instanceof Error ? err : new Error(String(err))));
1397
+ });
1398
+ }
1399
+ function getRemoteManagementState() {
1400
+ return _remoteManagementState;
1401
+ }
1402
+ function setRemoteManagementState(state, errorMessage2) {
1403
+ _remoteManagementState = {
1404
+ status: state.status,
1405
+ errorMessage: errorMessage2 || ""
1406
+ };
1407
+ }
1408
+
1409
+ // src/daemon/lifecycle/daemonManager.ts
1410
+ import os from "os";
1411
+ import fs from "fs";
1412
+ import { spawn } from "child_process";
1413
+ var inProcessHandle = null;
1414
+ var DAEMON_SPAWN_TIMEOUT_MS = 8e3;
1415
+ var DAEMON_POLL_INTERVAL_MS = 200;
1416
+ function isProcessAlive(pid) {
1417
+ try {
1418
+ process.kill(pid, 0);
1419
+ return true;
1420
+ } catch {
1421
+ return false;
1422
+ }
1423
+ }
1424
+ function getDaemonInfo() {
1425
+ const infoPath = getDaemonInfoPath();
1426
+ if (!fs.existsSync(infoPath)) return null;
1427
+ try {
1428
+ const data = JSON.parse(fs.readFileSync(infoPath, "utf-8"));
1429
+ if (!data.pid || !data.port) return null;
1430
+ if (!isProcessAlive(data.pid)) {
1431
+ logger.info("Stale daemon.json found, cleaning up", { stalePid: data.pid });
1432
+ try {
1433
+ fs.unlinkSync(infoPath);
1434
+ } catch {
1435
+ }
1436
+ return null;
1437
+ }
1438
+ return data;
1439
+ } catch {
1440
+ return null;
1441
+ }
1442
+ }
1443
+ function isDaemonRunning() {
1444
+ return getDaemonInfo() !== null;
1445
+ }
1446
+ function getDaemonSpawnArgs() {
1447
+ return {
1448
+ command: process.execPath,
1449
+ args: [process.argv[1], "--_daemon-child"],
1450
+ env: { ...process.env }
1451
+ };
1452
+ }
1453
+ async function startDaemon() {
1454
+ const existing = getDaemonInfo();
1455
+ if (existing) {
1456
+ return existing;
1457
+ }
1458
+ const { command, args, env } = getDaemonSpawnArgs();
1459
+ logger.info("Spawning daemon child", { command, args });
1460
+ let stderrOutput = "";
1461
+ let exited = false;
1462
+ let exitCode = null;
1463
+ const child = spawn(command, args, {
1464
+ detached: true,
1465
+ stdio: ["ignore", "ignore", "pipe"],
1466
+ env,
1467
+ ...os.platform() === "win32" ? { windowsHide: true } : {}
1468
+ });
1469
+ child.stderr?.on("data", (chunk) => {
1470
+ stderrOutput += chunk.toString("utf-8");
1471
+ });
1472
+ child.on("exit", (code) => {
1473
+ exited = true;
1474
+ exitCode = code;
1475
+ });
1476
+ child.unref();
1477
+ const info = await pollForDaemonInfo(DAEMON_SPAWN_TIMEOUT_MS, () => exited);
1478
+ if (!info) {
1479
+ const logPath = getDaemonLogPath();
1480
+ if (exited) {
1481
+ const detail = stderrOutput.trim() || `Check ${logPath} for details.`;
1482
+ throw new Error(`Daemon child exited with code ${exitCode}. ${detail}`);
1483
+ }
1484
+ throw new Error(`Daemon failed to start within timeout. Check ${logPath} for details.`);
1485
+ }
1486
+ child.stderr?.removeAllListeners();
1487
+ child.stderr?.destroy();
1488
+ return info;
1489
+ }
1490
+ async function ensureDaemonRunning() {
1491
+ const existing = getDaemonInfo();
1492
+ if (existing) return existing;
1493
+ if (process.versions.electron) {
1494
+ const { runDaemonChild } = await import("./daemonChild-KXERF36J.js");
1495
+ const handle = await runDaemonChild({
1496
+ installSignalHandlers: false,
1497
+ exitOnFailure: false
1498
+ });
1499
+ inProcessHandle = handle;
1500
+ return getDaemonInfo() ?? {
1501
+ pid: handle.pid,
1502
+ port: handle.port,
1503
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
1504
+ };
1505
+ }
1506
+ return startDaemon();
1507
+ }
1508
+ function getInProcessDaemonHandle() {
1509
+ return inProcessHandle;
1510
+ }
1511
+ async function getActiveTunnelSummaries(origin = "app") {
1512
+ const info = getDaemonInfo();
1513
+ if (!info) return [];
1514
+ const { IPCClient: IPCClient2 } = await import("./ipcClient-LZQCCNMR.js");
1515
+ const client = new IPCClient2(info.port, origin);
1516
+ let tunnels;
1517
+ try {
1518
+ tunnels = await client.listTunnels();
1519
+ } catch (err) {
1520
+ logger.warn("Failed to list tunnels from daemon", { error: errorMessage(err) });
1521
+ return [];
1522
+ }
1523
+ if (!Array.isArray(tunnels)) return [];
1524
+ return tunnels.filter((t) => {
1525
+ const state = t?.status?.state;
1526
+ return state !== "closed" /* Closed */ && state !== "exited" /* Exited */;
1527
+ }).map((t) => ({
1528
+ tunnelId: t.tunnelid,
1529
+ name: t.tunnelconfig?.name || t.tunnelid.slice(0, 8),
1530
+ localAddress: getLocalAddress(t.tunnelconfig),
1531
+ urls: t.remoteurls ?? []
1532
+ }));
1533
+ }
1534
+ function pollForDaemonInfo(timeoutMs, hasExited) {
1535
+ return new Promise((resolve) => {
1536
+ const start = Date.now();
1537
+ const check = () => {
1538
+ const info = getDaemonInfo();
1539
+ if (info) {
1540
+ resolve(info);
1541
+ return;
1542
+ }
1543
+ if (hasExited?.()) {
1544
+ resolve(null);
1545
+ return;
1546
+ }
1547
+ if (Date.now() - start > timeoutMs) {
1548
+ resolve(null);
1549
+ return;
1550
+ }
1551
+ setTimeout(check, DAEMON_POLL_INTERVAL_MS);
1552
+ };
1553
+ check();
1554
+ });
1555
+ }
1556
+ async function stopDaemon() {
1557
+ const info = getDaemonInfo();
1558
+ if (!info) return { ok: false, error: "No daemon is running." };
1559
+ let daemonErrors = [];
1560
+ try {
1561
+ const { IPCClient: IPCClient2 } = await import("./ipcClient-LZQCCNMR.js");
1562
+ const client = new IPCClient2(info.port);
1563
+ const result = await client.shutdown();
1564
+ logger.debug("Sent shutdown command to daemon", { result });
1565
+ if (Array.isArray(result?.errors)) daemonErrors = result.errors;
1566
+ } catch (e) {
1567
+ return { ok: false, error: `Failed to reach daemon: ${errorMessage(e)}` };
1568
+ }
1569
+ const exited = await waitForExit(info.pid, 5e3);
1570
+ if (!exited) {
1571
+ const detail = daemonErrors.length > 0 ? ` Daemon reported: ${daemonErrors.join("; ")}` : "";
1572
+ return { ok: false, error: `Daemon PID ${info.pid} did not exit within 5s.${detail}` };
1573
+ }
1574
+ if (daemonErrors.length > 0) {
1575
+ return { ok: false, error: `Daemon exited but reported errors: ${daemonErrors.join("; ")}` };
1576
+ }
1577
+ return { ok: true };
1578
+ }
1579
+ async function waitForExit(pid, timeoutMs) {
1580
+ const deadline = Date.now() + timeoutMs;
1581
+ while (Date.now() < deadline) {
1582
+ if (!isProcessAlive(pid)) return true;
1583
+ await new Promise((r) => setTimeout(r, 100));
1584
+ }
1585
+ return !isProcessAlive(pid);
1586
+ }
1587
+
1588
+ // src/daemon/ws/wsStream.ts
1589
+ import { WebSocket as WebSocket2 } from "ws";
1590
+ var WS_NORMAL_CLOSE = 1e3;
1591
+ var WsStream = class {
1592
+ constructor(getWsUrl) {
1593
+ this.getWsUrl = getWsUrl;
1594
+ this.ws = null;
1595
+ this.wsReady = null;
1596
+ this.wsResolve = null;
1597
+ this.subscribedTunnels = /* @__PURE__ */ new Map();
1598
+ this.callbacks = {
1599
+ stats: [],
1600
+ disconnect: [],
1601
+ reconnecting: [],
1602
+ reconnected: [],
1603
+ reconnection_failed: [],
1604
+ error: [],
1605
+ url_ready: [],
1606
+ worker_error: [],
1607
+ will_reconnect: [],
1608
+ stopped: []
1609
+ };
1610
+ this.openListeners = [];
1611
+ this.closeListeners = [];
1612
+ }
1613
+ // Lifecycle
1614
+ async ensureOpen() {
1615
+ if (this.ws && this.ws.readyState === WebSocket2.OPEN) return;
1616
+ this.wsReady = new Promise((resolve) => {
1617
+ this.wsResolve = resolve;
1618
+ });
1619
+ this.ws = new WebSocket2(this.getWsUrl());
1620
+ this.ws.on("open", () => {
1621
+ if (this.wsResolve) {
1622
+ this.wsResolve();
1623
+ this.wsResolve = null;
1624
+ }
1625
+ for (const cb of this.openListeners) {
1626
+ try {
1627
+ cb();
1628
+ } catch (err) {
1629
+ logger.debug("WsStream open listener threw", { error: errorMessage(err) });
1630
+ }
1631
+ }
1632
+ });
1633
+ this.ws.on("message", (data) => this.handleMessage(data.toString()));
1634
+ this.ws.on("close", (code) => {
1635
+ this.ws = null;
1636
+ this.wsReady = null;
1637
+ this.wsResolve = null;
1638
+ for (const cb of this.closeListeners) {
1639
+ try {
1640
+ cb(code);
1641
+ } catch (err) {
1642
+ logger.debug("WsStream close listener threw", { error: errorMessage(err) });
1643
+ }
1644
+ }
1645
+ });
1646
+ this.ws.on("error", (err) => {
1647
+ logger.debug("WsStream WS error", { error: errorMessage(err) });
1648
+ });
1649
+ await this.wsReady;
1650
+ }
1651
+ /** Send a normal close (1000). Use for user-initiated shutdown. */
1652
+ closeNormally() {
1653
+ if (!this.ws) return;
1654
+ try {
1655
+ this.ws.close(WS_NORMAL_CLOSE, "Client closing");
1656
+ } catch {
1657
+ }
1658
+ this.ws = null;
1659
+ this.wsReady = null;
1660
+ this.wsResolve = null;
1661
+ this.subscribedTunnels.clear();
1662
+ }
1663
+ /** Hard kill the socket. Use when daemon is known dead. */
1664
+ terminate() {
1665
+ if (!this.ws) return;
1666
+ try {
1667
+ this.ws.terminate();
1668
+ } catch {
1669
+ }
1670
+ this.ws = null;
1671
+ this.wsReady = null;
1672
+ this.wsResolve = null;
1673
+ this.subscribedTunnels.clear();
1674
+ }
1675
+ isOpen() {
1676
+ return !!this.ws && this.ws.readyState === WebSocket2.OPEN;
1677
+ }
1678
+ // Subscriptions
1679
+ async subscribe(tunnelId, mode = SessionMode.Foreground) {
1680
+ await this.ensureOpen();
1681
+ if (this.subscribedTunnels.has(tunnelId)) return;
1682
+ const msg = { type: "subscribe", tunnelId, mode };
1683
+ this.ws.send(JSON.stringify(msg));
1684
+ this.subscribedTunnels.set(tunnelId, { mode });
1685
+ }
1686
+ /**
1687
+ * Remove the local subscription. Sends an unsubscribe frame if the socket
1688
+ * is still open; if not, the daemon cleans up server-side when the session
1689
+ * closes. Returns true if the caller had this subscription.
1690
+ */
1691
+ unsubscribe(tunnelId) {
1692
+ const wasSubscribed = this.subscribedTunnels.delete(tunnelId);
1693
+ if (!wasSubscribed) return false;
1694
+ if (this.ws && this.ws.readyState === WebSocket2.OPEN) {
1695
+ try {
1696
+ const msg = { type: "unsubscribe", tunnelId };
1697
+ this.ws.send(JSON.stringify(msg));
1698
+ } catch {
1699
+ }
1700
+ }
1701
+ return true;
1702
+ }
1703
+ hasSubscriptions() {
1704
+ return this.subscribedTunnels.size > 0;
1705
+ }
1706
+ subscriptionCount() {
1707
+ return this.subscribedTunnels.size;
1708
+ }
1709
+ /** Snapshot of current subscriptions, for the reconnect path to replay. */
1710
+ snapshotSubscriptions() {
1711
+ return Array.from(this.subscribedTunnels.entries());
1712
+ }
1713
+ /**
1714
+ * Re-open the WS (fresh socket) and re-subscribe a previously captured set.
1715
+ * Clears existing state first so ensureOpen builds a new connection.
1716
+ */
1717
+ async restoreSubscriptions(snapshot) {
1718
+ this.subscribedTunnels.clear();
1719
+ this.ws = null;
1720
+ this.wsReady = null;
1721
+ this.wsResolve = null;
1722
+ await this.ensureOpen();
1723
+ for (const [tunnelId, info] of snapshot) {
1724
+ const msg = { type: "subscribe", tunnelId, mode: info.mode };
1725
+ this.ws.send(JSON.stringify(msg));
1726
+ this.subscribedTunnels.set(tunnelId, info);
1727
+ }
1728
+ }
1729
+ // Event registration
1730
+ onOpen(cb) {
1731
+ this.openListeners.push(cb);
1732
+ }
1733
+ onClose(cb) {
1734
+ this.closeListeners.push(cb);
1735
+ }
1736
+ onStats(cb) {
1737
+ this.callbacks.stats.push(cb);
1738
+ }
1739
+ onDisconnect(cb) {
1740
+ this.callbacks.disconnect.push(cb);
1741
+ }
1742
+ onReconnecting(cb) {
1743
+ this.callbacks.reconnecting.push(cb);
1744
+ }
1745
+ onReconnected(cb) {
1746
+ this.callbacks.reconnected.push(cb);
1747
+ }
1748
+ onReconnectionFailed(cb) {
1749
+ this.callbacks.reconnection_failed.push(cb);
1750
+ }
1751
+ onError(cb) {
1752
+ this.callbacks.error.push(cb);
1753
+ }
1754
+ onUrlReady(cb) {
1755
+ this.callbacks.url_ready.push(cb);
1756
+ }
1757
+ onWorkerError(cb) {
1758
+ this.callbacks.worker_error.push(cb);
1759
+ }
1760
+ onWillReconnect(cb) {
1761
+ this.callbacks.will_reconnect.push(cb);
1762
+ }
1763
+ onStopped(cb) {
1764
+ this.callbacks.stopped.push(cb);
1765
+ }
1766
+ // Private
1767
+ handleMessage(raw) {
1768
+ let msg;
1769
+ try {
1770
+ msg = JSON.parse(raw);
1771
+ } catch {
1772
+ return;
1773
+ }
1774
+ if (msg.type !== "tunnel_event") return;
1775
+ const { tunnelId, event, payload } = msg;
1776
+ switch (event) {
1777
+ case "stats":
1778
+ for (const cb of this.callbacks.stats) cb(tunnelId, payload.stats);
1779
+ break;
1780
+ case "disconnect": {
1781
+ const p = payload;
1782
+ for (const cb of this.callbacks.disconnect) cb(tunnelId, p.error, p.messages);
1783
+ break;
1784
+ }
1785
+ case "reconnecting":
1786
+ for (const cb of this.callbacks.reconnecting) cb(tunnelId, payload.retryCnt);
1787
+ break;
1788
+ case "reconnected":
1789
+ for (const cb of this.callbacks.reconnected) cb(tunnelId, payload.urls);
1790
+ break;
1791
+ case "reconnection_failed":
1792
+ for (const cb of this.callbacks.reconnection_failed) cb(tunnelId, payload.retryCnt);
1793
+ break;
1794
+ case "error": {
1795
+ const p = payload;
1796
+ for (const cb of this.callbacks.error) cb(tunnelId, p.message, p.isFatal);
1797
+ break;
1798
+ }
1799
+ case "url_ready":
1800
+ for (const cb of this.callbacks.url_ready) cb(tunnelId, payload.urls);
1801
+ break;
1802
+ case "worker_error":
1803
+ for (const cb of this.callbacks.worker_error) cb(tunnelId, payload.message);
1804
+ break;
1805
+ case "will_reconnect": {
1806
+ const p = payload;
1807
+ for (const cb of this.callbacks.will_reconnect) cb(tunnelId, p.error, p.messages);
1808
+ break;
1809
+ }
1810
+ case "stopped":
1811
+ for (const cb of this.callbacks.stopped) cb(tunnelId);
1812
+ break;
1813
+ }
1814
+ }
1815
+ };
1816
+
1817
+ // src/daemon/daemonHealth.ts
1818
+ var RECONNECT_ATTEMPTS = 3;
1819
+ var RECONNECT_INTERVAL_MS = 1e3;
1820
+ var HEARTBEAT_INTERVAL_MS = 5e3;
1821
+ var HEARTBEAT_TIMEOUT_MS = 2e3;
1822
+ var HEARTBEAT_FAILURE_THRESHOLD = 2;
1823
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
1824
+ var DaemonHealth = class {
1825
+ constructor(getIpc, stream) {
1826
+ this.getIpc = getIpc;
1827
+ this.stream = stream;
1828
+ this.originalPid = null;
1829
+ this.lost = false;
1830
+ this.reconnecting = false;
1831
+ this.heartbeatTimer = null;
1832
+ this.lostCallbacks = [];
1833
+ this.reconnectingCallbacks = [];
1834
+ this.reconnectedCallbacks = [];
1835
+ this.stream.onOpen(() => this.startHeartbeat());
1836
+ this.stream.onClose((code) => {
1837
+ this.stopHeartbeat();
1838
+ if (code === WS_NORMAL_CLOSE) return;
1839
+ if (this.lost || this.reconnecting) return;
1840
+ if (!this.stream.hasSubscriptions()) return;
1841
+ this.reconnecting = true;
1842
+ this.attemptReconnect().catch((err) => logger.debug("Reconnect loop threw", { error: err?.message })).finally(() => {
1843
+ this.reconnecting = false;
1844
+ });
1845
+ });
1846
+ }
1847
+ bindPid(pid) {
1848
+ this.originalPid = pid;
1849
+ }
1850
+ isLost() {
1851
+ return this.lost;
1852
+ }
1853
+ onLost(cb) {
1854
+ this.lostCallbacks.push(cb);
1855
+ }
1856
+ onReconnecting(cb) {
1857
+ this.reconnectingCallbacks.push(cb);
1858
+ }
1859
+ onReconnected(cb) {
1860
+ this.reconnectedCallbacks.push(cb);
1861
+ }
1862
+ startHeartbeat() {
1863
+ this.stopHeartbeat();
1864
+ let consecutiveFailures = 0;
1865
+ this.heartbeatTimer = setInterval(async () => {
1866
+ if (this.lost || this.reconnecting) return;
1867
+ const ipc = this.getIpc();
1868
+ if (!ipc) return;
1869
+ try {
1870
+ await ipc.ping(HEARTBEAT_TIMEOUT_MS);
1871
+ consecutiveFailures = 0;
1872
+ } catch (err) {
1873
+ consecutiveFailures += 1;
1874
+ if (consecutiveFailures >= HEARTBEAT_FAILURE_THRESHOLD) {
1875
+ this.triggerLost("heartbeat", errorMessage(err));
1876
+ }
1877
+ }
1878
+ }, HEARTBEAT_INTERVAL_MS);
1879
+ }
1880
+ stopHeartbeat() {
1881
+ if (this.heartbeatTimer) {
1882
+ clearInterval(this.heartbeatTimer);
1883
+ this.heartbeatTimer = null;
1884
+ }
1885
+ }
1886
+ async attemptReconnect() {
1887
+ const snapshot = this.stream.snapshotSubscriptions();
1888
+ for (let attempt = 1; attempt <= RECONNECT_ATTEMPTS; attempt++) {
1889
+ for (const cb of this.reconnectingCallbacks) {
1890
+ try {
1891
+ cb(attempt, RECONNECT_ATTEMPTS);
1892
+ } catch {
1893
+ }
1894
+ }
1895
+ await sleep2(RECONNECT_INTERVAL_MS);
1896
+ if (this.lost) return;
1897
+ const info = getDaemonInfo();
1898
+ if (!info) {
1899
+ this.triggerLost("dead");
1900
+ return;
1901
+ }
1902
+ if (info.pid !== this.originalPid) {
1903
+ this.triggerLost("respawned", `was pid ${this.originalPid}, now pid ${info.pid}`);
1904
+ return;
1905
+ }
1906
+ try {
1907
+ const ipc = this.getIpc();
1908
+ if (!ipc) throw new Error("IPC client missing");
1909
+ await ipc.ping(HEARTBEAT_TIMEOUT_MS);
1910
+ await this.stream.restoreSubscriptions(snapshot);
1911
+ for (const cb of this.reconnectedCallbacks) {
1912
+ try {
1913
+ cb();
1914
+ } catch {
1915
+ }
1916
+ }
1917
+ return;
1918
+ } catch (err) {
1919
+ logger.debug("Reconnect attempt failed", { attempt, error: errorMessage(err) });
1920
+ }
1921
+ }
1922
+ this.triggerLost("hung");
1923
+ }
1924
+ triggerLost(reason, detail) {
1925
+ if (this.lost) return;
1926
+ this.lost = true;
1927
+ this.stopHeartbeat();
1928
+ this.stream.terminate();
1929
+ for (const cb of this.lostCallbacks) {
1930
+ try {
1931
+ cb(reason, detail);
1932
+ } catch (err) {
1933
+ logger.debug("daemon-lost callback threw", { error: errorMessage(err) });
1934
+ }
1935
+ }
1936
+ }
1937
+ };
1938
+
1939
+ // src/daemon/tunnelClient.ts
1940
+ var TunnelClient = class _TunnelClient {
1941
+ constructor(options = {}) {
1942
+ this.ipc = null;
1943
+ this.origin = options.origin ?? "cli";
1944
+ this.stream = new WsStream(() => {
1945
+ if (!this.ipc) throw new Error("TunnelClient not initialized. Call ensureDaemon() first.");
1946
+ return this.ipc.getWsUrl();
1947
+ });
1948
+ this.health = new DaemonHealth(() => this.ipc, this.stream);
1949
+ }
1950
+ async ensureDaemon() {
1951
+ const info = await ensureDaemonRunning();
1952
+ this.ipc = new IPCClient(info.port, this.origin);
1953
+ this.health.bindPid(info.pid);
1954
+ }
1955
+ static async forRemoteManagement() {
1956
+ const client = new _TunnelClient({ origin: "remote" });
1957
+ await client.ensureDaemon();
1958
+ client.health.startHeartbeat();
1959
+ return client;
1960
+ }
1961
+ /**
1962
+ * Close the WebSocket connection and cleanup.
1963
+ */
1964
+ close() {
1965
+ this.health.stopHeartbeat();
1966
+ this.stream.closeNormally();
1967
+ }
1968
+ async ping() {
1969
+ this.assertClient();
1970
+ return this.ipc.ping();
1971
+ }
1972
+ // Tunnel Operations (HTTP)
1973
+ // Callers construct config from SDK shapes (FinalConfig) that are
1974
+ // structurally compatible with the zod-derived wire type but not nominally
1975
+ // identical. Accept the SDK shape and cast at the IPC boundary.
1976
+ async handleStartV2(config, noWait, mode) {
1977
+ this.assertClient();
1978
+ return this.ipc.startTunnelWithConfig(config, mode ?? SessionMode.Detached, noWait);
1979
+ }
1980
+ async handleStart(config, noWait, mode) {
1981
+ this.assertClient();
1982
+ return this.ipc.startTunnelV1(config, mode ?? SessionMode.Detached, noWait);
1983
+ }
1984
+ async handleUpdateConfig(config, noWait) {
1985
+ this.assertClient();
1986
+ return this.ipc.updateConfig(config, noWait);
1987
+ }
1988
+ async handleUpdateConfigV2(config, noWait) {
1989
+ this.assertClient();
1990
+ return this.ipc.updateConfigV2(config, noWait);
1991
+ }
1992
+ async handleStop(tunnelId) {
1993
+ this.assertClient();
1994
+ return this.ipc.stopTunnel(tunnelId);
1995
+ }
1996
+ async handleListV2() {
1997
+ this.assertClient();
1998
+ return this.ipc.listTunnels();
1999
+ }
2000
+ async handleList() {
2001
+ this.assertClient();
2002
+ return this.ipc.listTunnelsV1();
2003
+ }
2004
+ handleRemoveStoppedTunnelByTunnelId(tunnelId) {
2005
+ this.assertClient();
2006
+ void this.ipc.removeStoppedTunnel({ tunnelid: tunnelId });
2007
+ return true;
2008
+ }
2009
+ handleRemoveStoppedTunnelByConfigId(configId) {
2010
+ this.assertClient();
2011
+ void this.ipc.removeStoppedTunnel({ configId });
2012
+ return true;
2013
+ }
2014
+ async handleGet(tunnelId) {
2015
+ this.assertClient();
2016
+ return this.ipc.getTunnel(tunnelId);
2017
+ }
2018
+ async handleRestart(tunnelId) {
2019
+ this.assertClient();
2020
+ return this.ipc.restartTunnel(tunnelId);
2021
+ }
2022
+ async shutdown() {
2023
+ this.assertClient();
2024
+ await this.ipc.shutdown();
2025
+ this.close();
2026
+ }
2027
+ async getLogLevel() {
2028
+ this.assertClient();
2029
+ const res = await this.ipc.getLogLevel();
2030
+ return res.level;
2031
+ }
2032
+ async setLogLevel(level) {
2033
+ this.assertClient();
2034
+ await this.ipc.setLogLevel(level);
2035
+ }
2036
+ async getTunnelLogging() {
2037
+ this.assertClient();
2038
+ const res = await this.ipc.getTunnelLogging();
2039
+ return res.enabled;
2040
+ }
2041
+ async setTunnelLogging(enabled) {
2042
+ this.assertClient();
2043
+ await this.ipc.setTunnelLogging(enabled);
2044
+ }
2045
+ async getLogPaths() {
2046
+ this.assertClient();
2047
+ return await this.ipc.getLogPaths();
2048
+ }
2049
+ async resolveLogPath(q) {
2050
+ this.assertClient();
2051
+ return await this.ipc.resolveLogPath(q);
2052
+ }
2053
+ async restart(tunnelId) {
2054
+ this.assertClient();
2055
+ await this.ipc.restartTunnel(tunnelId);
2056
+ }
2057
+ // Streaming — delegate to WsStream, guarded by daemon-lost
2058
+ async attach(tunnelId, mode = SessionMode.Foreground) {
2059
+ if (this.health.isLost()) return;
2060
+ await this.stream.subscribe(tunnelId, mode);
2061
+ }
2062
+ detach(tunnelId) {
2063
+ const wasSubscribed = this.stream.unsubscribe(tunnelId);
2064
+ if (!wasSubscribed) return;
2065
+ if (!this.stream.hasSubscriptions()) this.close();
2066
+ }
2067
+ // Event registration — delegate to WsStream
2068
+ onStats(cb) {
2069
+ this.stream.onStats(cb);
2070
+ }
2071
+ onDisconnect(cb) {
2072
+ this.stream.onDisconnect(cb);
2073
+ }
2074
+ onReconnecting(cb) {
2075
+ this.stream.onReconnecting(cb);
2076
+ }
2077
+ onReconnected(cb) {
2078
+ this.stream.onReconnected(cb);
2079
+ }
2080
+ onReconnectionFailed(cb) {
2081
+ this.stream.onReconnectionFailed(cb);
2082
+ }
2083
+ onError(cb) {
2084
+ this.stream.onError(cb);
2085
+ }
2086
+ onUrlReady(cb) {
2087
+ this.stream.onUrlReady(cb);
2088
+ }
2089
+ onWorkerError(cb) {
2090
+ this.stream.onWorkerError(cb);
2091
+ }
2092
+ onWillReconnect(cb) {
2093
+ this.stream.onWillReconnect(cb);
2094
+ }
2095
+ onStopped(cb) {
2096
+ this.stream.onStopped(cb);
2097
+ }
2098
+ // Daemon-loss events — delegate to DaemonHealth
2099
+ onDaemonLost(cb) {
2100
+ this.health.onLost(cb);
2101
+ }
2102
+ onDaemonReconnecting(cb) {
2103
+ this.health.onReconnecting(cb);
2104
+ }
2105
+ onDaemonReconnected(cb) {
2106
+ this.health.onReconnected(cb);
2107
+ }
2108
+ isDaemonLost() {
2109
+ return this.health.isLost();
2110
+ }
2111
+ // App-compat shims (register listener + auto-attach in detached mode)
2112
+ handleRegisterStatsListener(tunnelId, listener) {
2113
+ this.onStats(listener);
2114
+ this.attach(tunnelId, "detached").catch(() => {
2115
+ });
2116
+ }
2117
+ handleRegisterDisconnectListener(tunnelId, listener) {
2118
+ this.onDisconnect(listener);
2119
+ this.attach(tunnelId, "detached").catch(() => {
2120
+ });
2121
+ }
2122
+ handleUnregisterStatsListener(_tunnelId, _listenerId) {
2123
+ }
2124
+ async handleGetTunnelStats(tunnelId) {
2125
+ this.assertClient();
2126
+ return this.ipc.getTunnelStats(tunnelId);
2127
+ }
2128
+ // Private
2129
+ assertClient() {
2130
+ if (!this.ipc) {
2131
+ throw new Error("TunnelClient not initialized. Call ensureDaemon() first.");
2132
+ }
2133
+ }
2134
+ };
2135
+
2136
+ export {
2137
+ TunnelStateType,
2138
+ TunnelErrorCodeType,
2139
+ TunnelWarningCode,
2140
+ ErrorCode,
2141
+ isErrorResponse,
2142
+ TunnelOperations,
2143
+ RemoteManagementUnauthorizedError,
2144
+ buildRemoteManagementWsUrl,
2145
+ initiateRemoteManagement,
2146
+ closeRemoteManagement,
2147
+ startRemoteManagement,
2148
+ getRemoteManagementState,
2149
+ getDaemonInfo,
2150
+ isDaemonRunning,
2151
+ startDaemon,
2152
+ ensureDaemonRunning,
2153
+ getInProcessDaemonHandle,
2154
+ getActiveTunnelSummaries,
2155
+ stopDaemon,
2156
+ TunnelClient
2157
+ };