pinggy 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -4
- package/dist/{chunk-HUN2MRZO.js → chunk-3RTRUYNW.js} +3 -1
- package/dist/{chunk-MBN3YBO4.js → chunk-443UO6IY.js} +204 -41
- package/dist/index.cjs +1025 -167
- package/dist/index.d.cts +20 -11
- package/dist/index.d.ts +20 -11
- package/dist/index.js +10 -7
- package/dist/{main-2RDHMQT7.js → main-PUM4SD6B.js} +771 -114
- package/dist/workers/file_serve_worker.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ Create secure, shareable tunnels to your localhost and manage them from the comm
|
|
|
11
11
|
- Remote management via secure WebSocket connection (works with Pinggy Dashboard)
|
|
12
12
|
- Configurable logging to file and/or stdout
|
|
13
13
|
- Save and load configuration files
|
|
14
|
+
- Config store for saving, listing, updating, and starting named tunnel configs
|
|
15
|
+
- Auto-start support for launching saved tunnels automatically
|
|
14
16
|
- Simple file server mode for quickly sharing local files
|
|
15
17
|
- Built-in TUI (Text User Interface) for viewing tunnel statistics, requests, and responses in real time
|
|
16
18
|
|
|
@@ -95,11 +97,11 @@ The CLI supports both SSH-style flags and more descriptive long flags. Below is
|
|
|
95
97
|
| `--logfile` | Path to log file |
|
|
96
98
|
| `--v` | Print logs to stdout |
|
|
97
99
|
| `--vv` | Detailed logs (Node.js SDK + Libpinggy) |
|
|
98
|
-
| `--vvv` | Enable logs from CLI, SDK, and Libpinggy |
|
|
100
|
+
| `--vvv` | Enable logs from CLI, SDK, and Libpinggy |
|
|
99
101
|
|
|
100
102
|
---
|
|
101
103
|
|
|
102
|
-
### **Config**
|
|
104
|
+
### **Config (File-based)**
|
|
103
105
|
| Flag | Description |
|
|
104
106
|
|------|-------------|
|
|
105
107
|
| `--saveconf <file>` | Create configuration file with provided options |
|
|
@@ -199,6 +201,94 @@ pinggy --conf ./myconfig.json -p 8080
|
|
|
199
201
|
```
|
|
200
202
|
|
|
201
203
|
|
|
204
|
+
## Config management
|
|
205
|
+
|
|
206
|
+
The CLI includes a built-in config store for saving, listing, and starting tunnel configurations. Configs are persisted as JSON files in your platform's config directory (`~/.config/pinggy/tunnels/` on Linux/macOS, `%APPDATA%/pinggy/tunnels/` on Windows).
|
|
207
|
+
|
|
208
|
+
### Save a tunnel config
|
|
209
|
+
```bash
|
|
210
|
+
pinggy config save my-tunnel -l 3000 token@pro.pinggy.io
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Save with auto-start enabled
|
|
214
|
+
```bash
|
|
215
|
+
pinggy config save my-tunnel --auto -l 3000
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### List all saved configs
|
|
219
|
+
```bash
|
|
220
|
+
pinggy config list
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### View details of a saved config
|
|
224
|
+
```bash
|
|
225
|
+
pinggy config show my-tunnel
|
|
226
|
+
pinggy config show my-tunnel other-tunnel # View multiple configs
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Update a saved config
|
|
230
|
+
```bash
|
|
231
|
+
pinggy config update my-tunnel -l 4000
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Enable or disable auto-start
|
|
235
|
+
```bash
|
|
236
|
+
pinggy config auto my-tunnel
|
|
237
|
+
pinggy config noauto my-tunnel
|
|
238
|
+
pinggy config auto tunnel1 tunnel2 # Multiple configs at once
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Delete a saved config
|
|
242
|
+
```bash
|
|
243
|
+
pinggy config delete my-tunnel
|
|
244
|
+
pinggy config delete tunnel1 tunnel2 # Delete multiple
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Shorthand: view config details
|
|
248
|
+
```bash
|
|
249
|
+
pinggy config my-tunnel # Same as: pinggy config show my-tunnel
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Configs can be looked up by name (exact match) or by configId prefix (partial match).
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
## Starting saved tunnels
|
|
256
|
+
|
|
257
|
+
### Start a saved tunnel
|
|
258
|
+
```bash
|
|
259
|
+
pinggy start my-tunnel
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Start with runtime overrides
|
|
263
|
+
```bash
|
|
264
|
+
pinggy start my-tunnel -l 4000
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Start multiple tunnels
|
|
268
|
+
```bash
|
|
269
|
+
pinggy start tunnel1 tunnel2
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Start all auto-start tunnels
|
|
273
|
+
```bash
|
|
274
|
+
pinggy start --all
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Start with remote management
|
|
278
|
+
```bash
|
|
279
|
+
pinggy start --all --remote-management <API_KEY>
|
|
280
|
+
pinggy start tunnel1 tunnel2 --remote-management <API_KEY>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Start with logging enabled
|
|
284
|
+
```bash
|
|
285
|
+
pinggy start my-tunnel --vvv
|
|
286
|
+
pinggy start --all --logfile /tmp/pinggy.log --loglevel DEBUG
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
> **Note:** Runtime overrides (`-l`, `--type`, `--token`, etc.) can only be used when starting a single tunnel. For multiple tunnels, update the saved config first with `pinggy config update`.
|
|
290
|
+
|
|
291
|
+
|
|
202
292
|
## File server mode
|
|
203
293
|
Serve a local directory quickly over a tunnel:
|
|
204
294
|
` pinggy --serve /path/to/files`
|
|
@@ -216,5 +306,3 @@ This package follows semantic versioning. See package.json for the current versi
|
|
|
216
306
|
|
|
217
307
|
## License
|
|
218
308
|
Apache License Version 2.0
|
|
219
|
-
|
|
220
|
-
|
|
@@ -83,7 +83,9 @@ function enablePackageLogging(opts) {
|
|
|
83
83
|
return applyLoggingConfig(opts ?? {});
|
|
84
84
|
}
|
|
85
85
|
function enableLoggingByLogLevelInSdk(loglevel, logFilePath) {
|
|
86
|
-
if (!loglevel)
|
|
86
|
+
if (!loglevel) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
87
89
|
const l = loglevel.toUpperCase();
|
|
88
90
|
if (loglevel === "DEBUG") {
|
|
89
91
|
pinggy.setDebugLogging(true, LogLevel.DEBUG, logFilePath);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
logger
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-3RTRUYNW.js";
|
|
4
4
|
|
|
5
5
|
// src/utils/printer.ts
|
|
6
6
|
import pico2 from "picocolors";
|
|
@@ -66,6 +66,11 @@ var _CLIPrinter = class _CLIPrinter {
|
|
|
66
66
|
const def = this.errorDefinitions.find((d) => d.match(err));
|
|
67
67
|
const msg = def.message(err);
|
|
68
68
|
console.error(pico2.red(pico2.bold("\u2716 Error:")), pico2.red(msg));
|
|
69
|
+
}
|
|
70
|
+
static fatal(err) {
|
|
71
|
+
const def = this.errorDefinitions.find((d) => d.match(err));
|
|
72
|
+
const msg = def.message(err);
|
|
73
|
+
console.error(pico2.red(pico2.bold("\u2716 Fatal Error:")), pico2.red(msg));
|
|
69
74
|
process.exit(1);
|
|
70
75
|
}
|
|
71
76
|
static red(message) {
|
|
@@ -259,7 +264,9 @@ var TunnelManager = class _TunnelManager {
|
|
|
259
264
|
*/
|
|
260
265
|
async startTunnel(tunnelId) {
|
|
261
266
|
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
262
|
-
if (!managed)
|
|
267
|
+
if (!managed) {
|
|
268
|
+
throw new Error(`Tunnel with id "${tunnelId}" not found`);
|
|
269
|
+
}
|
|
263
270
|
logger.info("Starting tunnel", { tunnelId });
|
|
264
271
|
let urls;
|
|
265
272
|
try {
|
|
@@ -307,7 +314,9 @@ var TunnelManager = class _TunnelManager {
|
|
|
307
314
|
*/
|
|
308
315
|
stopTunnel(tunnelId) {
|
|
309
316
|
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
310
|
-
if (!managed)
|
|
317
|
+
if (!managed) {
|
|
318
|
+
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
319
|
+
}
|
|
311
320
|
logger.info("Stopping tunnel", { tunnelId, configId: managed.configId });
|
|
312
321
|
try {
|
|
313
322
|
managed.instance.stop();
|
|
@@ -488,12 +497,16 @@ var TunnelManager = class _TunnelManager {
|
|
|
488
497
|
getTunnelInstance(configId, tunnelId) {
|
|
489
498
|
if (configId) {
|
|
490
499
|
const managed = this.tunnelsByConfigId.get(configId);
|
|
491
|
-
if (!managed)
|
|
500
|
+
if (!managed) {
|
|
501
|
+
throw new Error(`Tunnel "${configId}" not found`);
|
|
502
|
+
}
|
|
492
503
|
return managed.instance;
|
|
493
504
|
}
|
|
494
505
|
if (tunnelId) {
|
|
495
506
|
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
496
|
-
if (!managed)
|
|
507
|
+
if (!managed) {
|
|
508
|
+
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
509
|
+
}
|
|
497
510
|
return managed.instance;
|
|
498
511
|
}
|
|
499
512
|
throw new Error(`Either configId or tunnelId must be provided`);
|
|
@@ -672,12 +685,16 @@ var TunnelManager = class _TunnelManager {
|
|
|
672
685
|
getManagedTunnel(configId, tunnelId) {
|
|
673
686
|
if (configId) {
|
|
674
687
|
const managed = this.tunnelsByConfigId.get(configId);
|
|
675
|
-
if (!managed)
|
|
688
|
+
if (!managed) {
|
|
689
|
+
throw new Error(`Tunnel "${configId}" not found`);
|
|
690
|
+
}
|
|
676
691
|
return managed;
|
|
677
692
|
}
|
|
678
693
|
if (tunnelId) {
|
|
679
694
|
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
680
|
-
if (!managed)
|
|
695
|
+
if (!managed) {
|
|
696
|
+
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
697
|
+
}
|
|
681
698
|
return managed;
|
|
682
699
|
}
|
|
683
700
|
throw new Error(`Either configId or tunnelId must be provided`);
|
|
@@ -1066,7 +1083,9 @@ var TunnelManager = class _TunnelManager {
|
|
|
1066
1083
|
notifyPollingErrorListeners(tunnelId, errorMsg) {
|
|
1067
1084
|
try {
|
|
1068
1085
|
const listeners = this.tunnelPollingErrorListeners.get(tunnelId);
|
|
1069
|
-
if (!listeners)
|
|
1086
|
+
if (!listeners) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1070
1089
|
for (const [id, listener] of listeners) {
|
|
1071
1090
|
try {
|
|
1072
1091
|
listener(tunnelId, errorMsg);
|
|
@@ -1081,7 +1100,9 @@ var TunnelManager = class _TunnelManager {
|
|
|
1081
1100
|
notifyErrorListeners(tunnelId, errorMsg, isFatal) {
|
|
1082
1101
|
try {
|
|
1083
1102
|
const listeners = this.tunnelErrorListeners.get(tunnelId);
|
|
1084
|
-
if (!listeners)
|
|
1103
|
+
if (!listeners) {
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1085
1106
|
for (const [id, listener] of listeners) {
|
|
1086
1107
|
try {
|
|
1087
1108
|
listener(tunnelId, errorMsg, isFatal);
|
|
@@ -1130,7 +1151,9 @@ var TunnelManager = class _TunnelManager {
|
|
|
1130
1151
|
managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1131
1152
|
}
|
|
1132
1153
|
const listeners = this.tunnelDisconnectListeners.get(tunnelId);
|
|
1133
|
-
if (!listeners)
|
|
1154
|
+
if (!listeners) {
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1134
1157
|
for (const [id, listener] of listeners) {
|
|
1135
1158
|
try {
|
|
1136
1159
|
listener(tunnelId, error, messages);
|
|
@@ -1158,7 +1181,9 @@ var TunnelManager = class _TunnelManager {
|
|
|
1158
1181
|
try {
|
|
1159
1182
|
logger.info("Tunnel will reconnect", { tunnelId, error, messages });
|
|
1160
1183
|
const listeners = this.tunnelWillReconnectListeners.get(tunnelId);
|
|
1161
|
-
if (!listeners)
|
|
1184
|
+
if (!listeners) {
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1162
1187
|
for (const [id, listener] of listeners) {
|
|
1163
1188
|
try {
|
|
1164
1189
|
listener(tunnelId, error, messages);
|
|
@@ -1186,7 +1211,9 @@ var TunnelManager = class _TunnelManager {
|
|
|
1186
1211
|
try {
|
|
1187
1212
|
logger.info("Tunnel reconnecting", { tunnelId, retryCnt });
|
|
1188
1213
|
const listeners = this.tunnelReconnectingListeners.get(tunnelId);
|
|
1189
|
-
if (!listeners)
|
|
1214
|
+
if (!listeners) {
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1190
1217
|
for (const [id, listener] of listeners) {
|
|
1191
1218
|
try {
|
|
1192
1219
|
listener(tunnelId, retryCnt);
|
|
@@ -1264,7 +1291,9 @@ var TunnelManager = class _TunnelManager {
|
|
|
1264
1291
|
managedTunnel.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1265
1292
|
}
|
|
1266
1293
|
const listeners = this.tunnelReconnectionFailedListeners.get(tunnelId);
|
|
1267
|
-
if (!listeners)
|
|
1294
|
+
if (!listeners) {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1268
1297
|
for (const [id, listener] of listeners) {
|
|
1269
1298
|
try {
|
|
1270
1299
|
listener(tunnelId, retryCnt);
|
|
@@ -1288,7 +1317,9 @@ var TunnelManager = class _TunnelManager {
|
|
|
1288
1317
|
try {
|
|
1289
1318
|
logger.debug("Error in Tunnel Worker", { tunnelId, errorMessage: error.message });
|
|
1290
1319
|
const listeners = this.tunnelWorkerErrorListeners.get(tunnelId);
|
|
1291
|
-
if (!listeners)
|
|
1320
|
+
if (!listeners) {
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1292
1323
|
for (const [id, listener] of listeners) {
|
|
1293
1324
|
try {
|
|
1294
1325
|
listener(tunnelId, error);
|
|
@@ -1771,6 +1802,26 @@ var TunnelOperations = class {
|
|
|
1771
1802
|
}
|
|
1772
1803
|
return status;
|
|
1773
1804
|
}
|
|
1805
|
+
// --- Placeholder response ---
|
|
1806
|
+
buildPendingTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, serve) {
|
|
1807
|
+
return {
|
|
1808
|
+
tunnelid,
|
|
1809
|
+
remoteurls: [],
|
|
1810
|
+
tunnelconfig: pinggyOptionsToTunnelConfig(tunnelConfig, configid, tunnelName, false, void 0, serve),
|
|
1811
|
+
status: this.buildStatus(tunnelid, "starting" /* Starting */, "" /* NoError */),
|
|
1812
|
+
stats: newStats()
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
buildPendingTunnelResponseV2(tunnelid, tunnelConfig, configFromCli, configid, tunnelName, serve) {
|
|
1816
|
+
return {
|
|
1817
|
+
tunnelid,
|
|
1818
|
+
remoteurls: [],
|
|
1819
|
+
tunnelconfig: pinggyOptionsToTunnelConfigV1(tunnelConfig, configFromCli),
|
|
1820
|
+
status: this.buildStatus(tunnelid, "starting" /* Starting */, "" /* NoError */),
|
|
1821
|
+
stats: newStats(),
|
|
1822
|
+
greetmsg: ""
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1774
1825
|
// --- Helper to construct TunnelResponse ---
|
|
1775
1826
|
async buildTunnelResponse(tunnelid, tunnelConfig, configid, tunnelName, serve) {
|
|
1776
1827
|
const [status, stats, tlsInfo, greetMsg, remoteurls] = await Promise.all([
|
|
@@ -1811,10 +1862,10 @@ var TunnelOperations = class {
|
|
|
1811
1862
|
});
|
|
1812
1863
|
}
|
|
1813
1864
|
// --- Operations ---
|
|
1814
|
-
async handleStart(config) {
|
|
1865
|
+
async handleStart(config, noWait = false) {
|
|
1815
1866
|
try {
|
|
1816
1867
|
const opts = tunnelConfigToPinggyOptions(config);
|
|
1817
|
-
const
|
|
1868
|
+
const managed = await this.tunnelManager.createTunnel({
|
|
1818
1869
|
...opts,
|
|
1819
1870
|
configId: config.configid,
|
|
1820
1871
|
name: config.configname,
|
|
@@ -1822,36 +1873,52 @@ var TunnelOperations = class {
|
|
|
1822
1873
|
serve: config.serve
|
|
1823
1874
|
}
|
|
1824
1875
|
});
|
|
1825
|
-
|
|
1876
|
+
const { tunnelid, tunnelName, serve, tunnelConfig } = managed;
|
|
1877
|
+
const startPromise = this.tunnelManager.startTunnel(tunnelid);
|
|
1878
|
+
if (noWait) {
|
|
1879
|
+
startPromise.catch((err) => {
|
|
1880
|
+
logger.error("No-wait startTunnel failed", { tunnelid, err: String(err) });
|
|
1881
|
+
});
|
|
1882
|
+
return this.buildPendingTunnelResponse(tunnelid, tunnelConfig, config.configid, tunnelName, serve);
|
|
1883
|
+
}
|
|
1884
|
+
await startPromise;
|
|
1826
1885
|
const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
|
|
1827
|
-
|
|
1828
|
-
return resp;
|
|
1886
|
+
return this.buildTunnelResponse(tunnelid, tunnelPconfig, config.configid, tunnelName, serve);
|
|
1829
1887
|
} catch (err) {
|
|
1830
1888
|
return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
|
|
1831
1889
|
}
|
|
1832
1890
|
}
|
|
1833
|
-
async handleStartV2(config) {
|
|
1891
|
+
async handleStartV2(config, noWait = false) {
|
|
1834
1892
|
try {
|
|
1835
|
-
const
|
|
1893
|
+
const managed = await this.tunnelManager.createTunnel(config);
|
|
1894
|
+
const { tunnelid, serve, tunnelConfig } = managed;
|
|
1836
1895
|
await this.tunnelManager.startTunnel(tunnelid);
|
|
1837
1896
|
const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
|
|
1838
|
-
|
|
1839
|
-
return resp;
|
|
1897
|
+
return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.serve);
|
|
1840
1898
|
} catch (err) {
|
|
1841
1899
|
return this.error(ErrorCode.ErrorStartingTunnel, err, "Unknown error occurred while starting tunnel");
|
|
1842
1900
|
}
|
|
1843
1901
|
}
|
|
1844
|
-
async handleUpdateConfig(config) {
|
|
1902
|
+
async handleUpdateConfig(config, noWait = false) {
|
|
1845
1903
|
try {
|
|
1846
1904
|
const opts = tunnelConfigToPinggyOptions(config);
|
|
1847
|
-
const
|
|
1905
|
+
const updateOpts = {
|
|
1848
1906
|
...opts,
|
|
1849
1907
|
configId: config.configid,
|
|
1850
1908
|
name: config.configname,
|
|
1851
1909
|
optional: {
|
|
1852
1910
|
serve: config.serve
|
|
1853
1911
|
}
|
|
1854
|
-
}
|
|
1912
|
+
};
|
|
1913
|
+
if (noWait) {
|
|
1914
|
+
const existing = this.tunnelManager.getManagedTunnel(config.configid);
|
|
1915
|
+
if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
|
|
1916
|
+
this.tunnelManager.updateConfig(updateOpts).catch((err) => {
|
|
1917
|
+
logger.error("No-wait updateConfig failed", { configid: config.configid, err: String(err) });
|
|
1918
|
+
});
|
|
1919
|
+
return this.buildPendingTunnelResponse(existing.tunnelid, existing.tunnelConfig, config.configid, existing.tunnelName, existing.serve);
|
|
1920
|
+
}
|
|
1921
|
+
const tunnel = await this.tunnelManager.updateConfig(updateOpts);
|
|
1855
1922
|
if (!tunnel.instance || !tunnel.tunnelConfig)
|
|
1856
1923
|
throw new Error("Invalid tunnel state after configuration update");
|
|
1857
1924
|
return this.buildTunnelResponse(tunnel.tunnelid, tunnel.tunnelConfig, config.configid, tunnel.tunnelName, tunnel.serve);
|
|
@@ -1859,8 +1926,17 @@ var TunnelOperations = class {
|
|
|
1859
1926
|
return this.error(ErrorCode.InternalServerError, err, "Failed to update tunnel configuration");
|
|
1860
1927
|
}
|
|
1861
1928
|
}
|
|
1862
|
-
async handleUpdateConfigV2(config) {
|
|
1929
|
+
async handleUpdateConfigV2(config, noWait = false) {
|
|
1863
1930
|
try {
|
|
1931
|
+
if (noWait) {
|
|
1932
|
+
const existing = this.tunnelManager.getManagedTunnel(config.configId);
|
|
1933
|
+
console.log(existing);
|
|
1934
|
+
if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
|
|
1935
|
+
this.tunnelManager.updateConfig(config).catch((err) => {
|
|
1936
|
+
logger.error("No-wait updateConfigV2 failed", { configId: config.configId, err: String(err) });
|
|
1937
|
+
});
|
|
1938
|
+
return this.buildPendingTunnelResponseV2(existing.tunnelid, existing.tunnelConfig, config, config.configId, existing.tunnelName, existing.serve);
|
|
1939
|
+
}
|
|
1864
1940
|
const tunnel = await this.tunnelManager.updateConfig(config);
|
|
1865
1941
|
if (!tunnel.instance || !tunnel.tunnelConfig)
|
|
1866
1942
|
throw new Error("Invalid tunnel state after configuration update");
|
|
@@ -1947,8 +2023,16 @@ var TunnelOperations = class {
|
|
|
1947
2023
|
return this.error(ErrorCode.TunnelNotFound, err, "Failed to get tunnel information");
|
|
1948
2024
|
}
|
|
1949
2025
|
}
|
|
1950
|
-
async handleRestart(tunnelid) {
|
|
2026
|
+
async handleRestart(tunnelid, noWait = false) {
|
|
1951
2027
|
try {
|
|
2028
|
+
if (noWait) {
|
|
2029
|
+
const managed2 = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
2030
|
+
if (!managed2?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
2031
|
+
this.tunnelManager.restartTunnel(tunnelid).catch((err) => {
|
|
2032
|
+
logger.error("No-wait restartTunnel failed", { tunnelid, err: String(err) });
|
|
2033
|
+
});
|
|
2034
|
+
return this.buildPendingTunnelResponse(tunnelid, managed2.tunnelConfig, managed2.configId, managed2.tunnelName, managed2.serve);
|
|
2035
|
+
}
|
|
1952
2036
|
await this.tunnelManager.restartTunnel(tunnelid);
|
|
1953
2037
|
const managed = this.tunnelManager.getManagedTunnel("", tunnelid);
|
|
1954
2038
|
if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
|
|
@@ -2217,7 +2301,7 @@ var WebSocketCommandHandler = class {
|
|
|
2217
2301
|
const dc = StartSchema.parse(raw);
|
|
2218
2302
|
queuedConfig = dc.tunnelConfig;
|
|
2219
2303
|
remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
|
|
2220
|
-
const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
|
|
2304
|
+
const result = await this.tunnelHandler.handleStart(dc.tunnelConfig, true);
|
|
2221
2305
|
remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
|
|
2222
2306
|
return this.wrapResponse(result, req);
|
|
2223
2307
|
} catch (e) {
|
|
@@ -2238,7 +2322,7 @@ var WebSocketCommandHandler = class {
|
|
|
2238
2322
|
const dc = StartV2Schema.parse(raw);
|
|
2239
2323
|
queuedConfig = dc.tunnelConfig;
|
|
2240
2324
|
remoteManagementWebSocketPrinter.queueStart(dc.tunnelConfig);
|
|
2241
|
-
const result = await this.tunnelHandler.handleStartV2(dc.tunnelConfig);
|
|
2325
|
+
const result = await this.tunnelHandler.handleStartV2(dc.tunnelConfig, true);
|
|
2242
2326
|
remoteManagementWebSocketPrinter.handleStartResult(dc.tunnelConfig, result);
|
|
2243
2327
|
return this.wrapResponse(result, req);
|
|
2244
2328
|
} catch (e) {
|
|
@@ -2287,7 +2371,7 @@ var WebSocketCommandHandler = class {
|
|
|
2287
2371
|
try {
|
|
2288
2372
|
const dc = RestartSchema.parse(raw);
|
|
2289
2373
|
remoteManagementWebSocketPrinter.printRestartRequested(dc.tunnelID);
|
|
2290
|
-
const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
|
|
2374
|
+
const result = await this.tunnelHandler.handleRestart(dc.tunnelID, true);
|
|
2291
2375
|
remoteManagementWebSocketPrinter.handleRestartResult(dc.tunnelID, result);
|
|
2292
2376
|
return this.wrapResponse(result, req);
|
|
2293
2377
|
} catch (e) {
|
|
@@ -2302,7 +2386,7 @@ var WebSocketCommandHandler = class {
|
|
|
2302
2386
|
async handleUpdateConfigReq(req, raw) {
|
|
2303
2387
|
try {
|
|
2304
2388
|
const dc = UpdateConfigSchema.parse(raw);
|
|
2305
|
-
const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
|
|
2389
|
+
const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig, true);
|
|
2306
2390
|
return this.wrapResponse(result, req);
|
|
2307
2391
|
} catch (e) {
|
|
2308
2392
|
if (e instanceof z2.ZodError) {
|
|
@@ -2316,7 +2400,7 @@ var WebSocketCommandHandler = class {
|
|
|
2316
2400
|
async handleUpdateConfigV2Req(req, raw) {
|
|
2317
2401
|
try {
|
|
2318
2402
|
const dc = UpdateConfigV2Schema.parse(raw);
|
|
2319
|
-
const result = await this.tunnelHandler.handleUpdateConfigV2(dc.tunnelConfig);
|
|
2403
|
+
const result = await this.tunnelHandler.handleUpdateConfigV2(dc.tunnelConfig, true);
|
|
2320
2404
|
return this.wrapResponse(result, req);
|
|
2321
2405
|
} catch (e) {
|
|
2322
2406
|
if (e instanceof z2.ZodError) {
|
|
@@ -2482,6 +2566,12 @@ function handleConnectionStatusMessage(firstMessage) {
|
|
|
2482
2566
|
// src/remote_management/remoteManagement.ts
|
|
2483
2567
|
var RECONNECT_SLEEP_MS = 5e3;
|
|
2484
2568
|
var PING_INTERVAL_MS = 3e4;
|
|
2569
|
+
var RemoteManagementUnauthorizedError = class extends Error {
|
|
2570
|
+
constructor() {
|
|
2571
|
+
super("Unauthorized. Please enter a valid token.");
|
|
2572
|
+
this.name = "RemoteManagementUnauthorizedError";
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2485
2575
|
var _remoteManagementState = {
|
|
2486
2576
|
status: "NOT_RUNNING",
|
|
2487
2577
|
errorMessage: ""
|
|
@@ -2546,9 +2636,14 @@ async function initiateRemoteManagement(remoteManagementConfig) {
|
|
|
2546
2636
|
try {
|
|
2547
2637
|
await handleWebSocketConnection(wsUrl, wsHost, remoteManagementConfig.apiKey);
|
|
2548
2638
|
} catch (error) {
|
|
2639
|
+
if (error instanceof RemoteManagementUnauthorizedError) {
|
|
2640
|
+
throw error;
|
|
2641
|
+
}
|
|
2549
2642
|
logger.warn("Remote management connection error", { error: String(error) });
|
|
2550
2643
|
}
|
|
2551
|
-
if (_stopRequested)
|
|
2644
|
+
if (_stopRequested) {
|
|
2645
|
+
break;
|
|
2646
|
+
}
|
|
2552
2647
|
printer_default.warn(`Remote management disconnected. Reconnecting in ${RECONNECT_SLEEP_MS / 1e3} seconds...`);
|
|
2553
2648
|
logger.info("Reconnecting to remote management after disconnect");
|
|
2554
2649
|
await sleep(RECONNECT_SLEEP_MS);
|
|
@@ -2557,22 +2652,34 @@ async function initiateRemoteManagement(remoteManagementConfig) {
|
|
|
2557
2652
|
logger.info("Remote management stopped.");
|
|
2558
2653
|
return getRemoteManagementState();
|
|
2559
2654
|
}
|
|
2560
|
-
async function handleWebSocketConnection(wsUrl, wsHost, token) {
|
|
2561
|
-
return new Promise((resolve) => {
|
|
2655
|
+
async function handleWebSocketConnection(wsUrl, wsHost, token, onOpenCallback) {
|
|
2656
|
+
return new Promise((resolve, reject) => {
|
|
2562
2657
|
const ws = new WebSocket(wsUrl, {
|
|
2563
2658
|
headers: { Authorization: `Bearer ${token}` }
|
|
2564
2659
|
});
|
|
2565
2660
|
currentWs = ws;
|
|
2566
2661
|
let heartbeat = null;
|
|
2567
2662
|
let firstMessage = true;
|
|
2568
|
-
|
|
2569
|
-
|
|
2663
|
+
let settled = false;
|
|
2664
|
+
const cleanup = (err) => {
|
|
2665
|
+
if (settled) {
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
settled = true;
|
|
2669
|
+
if (heartbeat) {
|
|
2670
|
+
clearInterval(heartbeat);
|
|
2671
|
+
}
|
|
2570
2672
|
currentWs = null;
|
|
2571
|
-
|
|
2673
|
+
if (err) {
|
|
2674
|
+
reject(err);
|
|
2675
|
+
} else {
|
|
2676
|
+
resolve();
|
|
2677
|
+
}
|
|
2572
2678
|
};
|
|
2573
2679
|
ws.once("open", () => {
|
|
2574
2680
|
printer_default.success(`Connected to ${wsHost}`);
|
|
2575
2681
|
setRemoteManagementState({ status: RemoteManagementStatus.Running, errorMessage: "" });
|
|
2682
|
+
onOpenCallback?.();
|
|
2576
2683
|
heartbeat = setInterval(() => {
|
|
2577
2684
|
if (ws.readyState === WebSocket.OPEN) ws.ping();
|
|
2578
2685
|
}, PING_INTERVAL_MS);
|
|
@@ -2600,13 +2707,14 @@ async function handleWebSocketConnection(wsUrl, wsHost, token) {
|
|
|
2600
2707
|
ws.on("unexpected-response", (_, res) => {
|
|
2601
2708
|
if (res.statusCode === 401) {
|
|
2602
2709
|
setRemoteManagementState({ status: RemoteManagementStatus.NotRunning, errorMessage: `HTTP ${res.statusCode}` });
|
|
2603
|
-
printer_default.error("Unauthorized. Please enter a valid token.");
|
|
2604
2710
|
logger.error("Unauthorized (401) on remote management connect");
|
|
2711
|
+
cleanup(new RemoteManagementUnauthorizedError());
|
|
2605
2712
|
ws.close();
|
|
2606
2713
|
} else {
|
|
2607
2714
|
logger.warn("Unexpected HTTP response ", { statusCode: res.statusCode });
|
|
2608
2715
|
printer_default.warn(`Unexpected HTTP ${res.statusCode}. Retrying...`);
|
|
2609
2716
|
cleanup();
|
|
2717
|
+
ws.close();
|
|
2610
2718
|
}
|
|
2611
2719
|
});
|
|
2612
2720
|
ws.on("close", (code, reason) => {
|
|
@@ -2618,7 +2726,7 @@ async function handleWebSocketConnection(wsUrl, wsHost, token) {
|
|
|
2618
2726
|
ws.on("error", (err) => {
|
|
2619
2727
|
setRemoteManagementState({ status: RemoteManagementStatus.Error, errorMessage: err.message });
|
|
2620
2728
|
logger.warn("WebSocket error", { error: err.message });
|
|
2621
|
-
printer_default.
|
|
2729
|
+
printer_default.warn(err.message);
|
|
2622
2730
|
cleanup();
|
|
2623
2731
|
});
|
|
2624
2732
|
});
|
|
@@ -2648,6 +2756,58 @@ async function closeRemoteManagement(timeoutMs = 1e4) {
|
|
|
2648
2756
|
return getRemoteManagementState();
|
|
2649
2757
|
}
|
|
2650
2758
|
}
|
|
2759
|
+
function startRemoteManagement(remoteManagementConfig) {
|
|
2760
|
+
if (!remoteManagementConfig.apiKey || remoteManagementConfig.apiKey.trim().length === 0) {
|
|
2761
|
+
return Promise.reject(new Error("Remote management token is required"));
|
|
2762
|
+
}
|
|
2763
|
+
const wsUrl = remoteManagementConfig.serverUrl;
|
|
2764
|
+
const wsHost = extractHostname(wsUrl);
|
|
2765
|
+
logger.info("Remote management mode enabled.");
|
|
2766
|
+
_stopRequested = false;
|
|
2767
|
+
return new Promise((resolve, reject) => {
|
|
2768
|
+
let firstSettled = false;
|
|
2769
|
+
const settleOnce = (err) => {
|
|
2770
|
+
if (firstSettled) {
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
firstSettled = true;
|
|
2774
|
+
if (err) {
|
|
2775
|
+
reject(err);
|
|
2776
|
+
} else {
|
|
2777
|
+
resolve(getRemoteManagementState());
|
|
2778
|
+
}
|
|
2779
|
+
};
|
|
2780
|
+
const runLoop = async () => {
|
|
2781
|
+
const sigintHandler = () => {
|
|
2782
|
+
_stopRequested = true;
|
|
2783
|
+
};
|
|
2784
|
+
process.once("SIGINT", sigintHandler);
|
|
2785
|
+
while (!_stopRequested) {
|
|
2786
|
+
logger.info("Connecting to remote management", { wsUrl });
|
|
2787
|
+
setRemoteManagementState({ status: RemoteManagementStatus.Connecting, errorMessage: "" });
|
|
2788
|
+
try {
|
|
2789
|
+
await handleWebSocketConnection(wsUrl, wsHost, remoteManagementConfig.apiKey, () => settleOnce());
|
|
2790
|
+
} catch (error) {
|
|
2791
|
+
if (error instanceof RemoteManagementUnauthorizedError) {
|
|
2792
|
+
settleOnce(error);
|
|
2793
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
settleOnce();
|
|
2797
|
+
logger.warn("Remote management connection error", { error: String(error) });
|
|
2798
|
+
}
|
|
2799
|
+
if (_stopRequested) {
|
|
2800
|
+
break;
|
|
2801
|
+
}
|
|
2802
|
+
logger.info("Reconnecting to remote management after disconnect");
|
|
2803
|
+
await sleep(RECONNECT_SLEEP_MS);
|
|
2804
|
+
}
|
|
2805
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
2806
|
+
logger.info("Remote management stopped.");
|
|
2807
|
+
};
|
|
2808
|
+
runLoop().catch((err) => settleOnce(err instanceof Error ? err : new Error(String(err))));
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2651
2811
|
function getRemoteManagementState() {
|
|
2652
2812
|
return _remoteManagementState;
|
|
2653
2813
|
}
|
|
@@ -2668,8 +2828,11 @@ export {
|
|
|
2668
2828
|
TunnelErrorCodeType,
|
|
2669
2829
|
TunnelWarningCode,
|
|
2670
2830
|
TunnelOperations,
|
|
2831
|
+
RemoteManagementUnauthorizedError,
|
|
2832
|
+
buildRemoteManagementWsUrl,
|
|
2671
2833
|
parseRemoteManagement,
|
|
2672
2834
|
initiateRemoteManagement,
|
|
2673
2835
|
closeRemoteManagement,
|
|
2836
|
+
startRemoteManagement,
|
|
2674
2837
|
getRemoteManagementState
|
|
2675
2838
|
};
|