pinggy 0.5.1 → 0.5.3
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/dist/{chunk-BFARGPGP.js → chunk-5YHD6M35.js} +10 -0
- package/dist/{chunk-Y65A4BL2.js → chunk-BBPR2LJB.js} +76 -25
- package/dist/{chunk-VJ4VSZGX.js → chunk-GK2PQ7KX.js} +61 -11
- package/dist/{chunk-HUP6YWH6.js → chunk-WM7I57AA.js} +119 -20
- package/dist/{configStore-TSGRNOE3.js → configStore-HP3NXPH4.js} +13 -3
- package/dist/{daemonChild-KXERF36J.js → daemonChild-WIRWKBKB.js} +9 -4
- package/dist/index.cjs +309 -52
- package/dist/index.d.cts +197 -4
- package/dist/index.d.ts +197 -4
- package/dist/index.js +56 -5
- package/dist/{ipcClient-LZQCCNMR.js → ipcClient-Z2RXDQUQ.js} +1 -1
- package/dist/{main-KXUDW6W5.js → main-B2EBHFN2.js} +7 -4
- package/package.json +1 -1
|
@@ -21,6 +21,14 @@ var Route = {
|
|
|
21
21
|
SetTunnelLogging: "POST /config/tunnel-logging",
|
|
22
22
|
GetLogPaths: "GET /logs/paths"
|
|
23
23
|
};
|
|
24
|
+
var DaemonHost = {
|
|
25
|
+
CLI: "cli",
|
|
26
|
+
APP: "app"
|
|
27
|
+
};
|
|
28
|
+
var ShutdownStatus = {
|
|
29
|
+
ShuttingDown: "shutting_down",
|
|
30
|
+
RefusedInApp: "refused_in_app"
|
|
31
|
+
};
|
|
24
32
|
var SessionMode = {
|
|
25
33
|
Foreground: "foreground",
|
|
26
34
|
Detached: "detached"
|
|
@@ -159,6 +167,8 @@ var IPCClient = class {
|
|
|
159
167
|
|
|
160
168
|
export {
|
|
161
169
|
Route,
|
|
170
|
+
DaemonHost,
|
|
171
|
+
ShutdownStatus,
|
|
162
172
|
SessionMode,
|
|
163
173
|
IPCClient
|
|
164
174
|
};
|
|
@@ -10,29 +10,33 @@ import {
|
|
|
10
10
|
startDaemon,
|
|
11
11
|
startRemoteManagement,
|
|
12
12
|
stopDaemon
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-GK2PQ7KX.js";
|
|
14
14
|
import {
|
|
15
15
|
readDaemonConfig
|
|
16
16
|
} from "./chunk-MT44NAXX.js";
|
|
17
17
|
import {
|
|
18
|
+
DaemonHost,
|
|
18
19
|
Route,
|
|
19
|
-
SessionMode
|
|
20
|
-
|
|
20
|
+
SessionMode,
|
|
21
|
+
ShutdownStatus
|
|
22
|
+
} from "./chunk-5YHD6M35.js";
|
|
21
23
|
import {
|
|
22
24
|
ConfigVerb,
|
|
23
25
|
SUBCOMMANDS,
|
|
24
26
|
Subcommand,
|
|
25
27
|
deleteConfig,
|
|
26
28
|
findConfig,
|
|
29
|
+
findConfigByName,
|
|
27
30
|
getAutoStartConfigs,
|
|
28
31
|
listSavedConfigs,
|
|
29
32
|
printConfigDetail,
|
|
30
33
|
printConfigList,
|
|
34
|
+
sanitizeName,
|
|
31
35
|
saveConfig,
|
|
32
36
|
updateConfigAutoStart,
|
|
33
37
|
updateTunnelConfig,
|
|
34
38
|
validateName
|
|
35
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-WM7I57AA.js";
|
|
36
40
|
import {
|
|
37
41
|
TunnelManager,
|
|
38
42
|
detachAllTunnelLoggers,
|
|
@@ -757,6 +761,13 @@ function parseToken(finalConfig, explicitToken) {
|
|
|
757
761
|
finalConfig.token = explicitToken;
|
|
758
762
|
}
|
|
759
763
|
}
|
|
764
|
+
function normalizeForwardingToArray(finalConfig, type) {
|
|
765
|
+
const fwd = finalConfig.forwarding;
|
|
766
|
+
if (typeof fwd !== "string") {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
finalConfig.forwarding = fwd.trim() === "" ? [] : [{ type: type || TunnelType.Http, address: fwd.trim() }];
|
|
770
|
+
}
|
|
760
771
|
function parseArgs2(finalConfig, remainingPositionals) {
|
|
761
772
|
let localserverTls = "";
|
|
762
773
|
localserverTls = parseExtendedOptions(remainingPositionals, finalConfig, localserverTls);
|
|
@@ -877,6 +888,7 @@ function buildFinalConfig(values, positionals, baseConfig) {
|
|
|
877
888
|
if (forceFlag || values.force) {
|
|
878
889
|
finalConfig.force = true;
|
|
879
890
|
}
|
|
891
|
+
normalizeForwardingToArray(finalConfig, type);
|
|
880
892
|
parseArgs2(finalConfig, remainingPositionals);
|
|
881
893
|
storeJson(finalConfig, saveconf);
|
|
882
894
|
return finalConfig;
|
|
@@ -1298,7 +1310,7 @@ async function startBackgroundTunnels(configs, values, positionals) {
|
|
|
1298
1310
|
client.close();
|
|
1299
1311
|
}
|
|
1300
1312
|
async function startAutoStartTunnels() {
|
|
1301
|
-
const { getAutoStartConfigs: getAutoStartConfigs2 } = await import("./configStore-
|
|
1313
|
+
const { getAutoStartConfigs: getAutoStartConfigs2 } = await import("./configStore-HP3NXPH4.js");
|
|
1302
1314
|
const configs = getAutoStartConfigs2();
|
|
1303
1315
|
if (configs.length === 0) {
|
|
1304
1316
|
printer_default.warn("No configs marked for auto-start. Use: pinggy config auto <name>");
|
|
@@ -1767,6 +1779,8 @@ async function handleDaemonStop() {
|
|
|
1767
1779
|
const result = await stopDaemon();
|
|
1768
1780
|
if (result.ok) {
|
|
1769
1781
|
printer_default.success("Daemon stopped.");
|
|
1782
|
+
} else if (result.reason === DaemonHost.APP) {
|
|
1783
|
+
printer_default.print(pico2.yellow("The Pinggy daemon is running inside the Pinggy desktop app. Please quit the Pinggy app to stop it."));
|
|
1770
1784
|
} else {
|
|
1771
1785
|
printer_default.error(result.error);
|
|
1772
1786
|
}
|
|
@@ -1787,6 +1801,7 @@ function handleDaemonStatus() {
|
|
|
1787
1801
|
printer_default.print(` Port: ${info.port}`);
|
|
1788
1802
|
printer_default.print(` Started: ${info.startedAt}`);
|
|
1789
1803
|
printer_default.print(` Uptime: ${uptimeStr}`);
|
|
1804
|
+
printer_default.print(` Host: ${info.host === DaemonHost.APP ? "Pinggy app (in-process)" : "standalone (CLI)"}`);
|
|
1790
1805
|
}
|
|
1791
1806
|
|
|
1792
1807
|
// src/cli/subcommand/handlers/psCommand.ts
|
|
@@ -2217,10 +2232,10 @@ function handleConfig(args) {
|
|
|
2217
2232
|
printConfigList();
|
|
2218
2233
|
return;
|
|
2219
2234
|
case ConfigVerb.Show: {
|
|
2220
|
-
const names = requireNames(rest, "config show");
|
|
2235
|
+
const names = clubSpacedName(requireNames(rest, "config show"));
|
|
2221
2236
|
for (const name of names) {
|
|
2222
|
-
const
|
|
2223
|
-
if (
|
|
2237
|
+
const saved = resolveConfig(name, "config show");
|
|
2238
|
+
if (saved) printConfigDetail(saved);
|
|
2224
2239
|
}
|
|
2225
2240
|
return;
|
|
2226
2241
|
}
|
|
@@ -2230,7 +2245,7 @@ function handleConfig(args) {
|
|
|
2230
2245
|
return;
|
|
2231
2246
|
}
|
|
2232
2247
|
case ConfigVerb.Delete: {
|
|
2233
|
-
const names = requireNames(rest, "config delete");
|
|
2248
|
+
const names = clubSpacedName(requireNames(rest, "config delete"));
|
|
2234
2249
|
for (const name of names) {
|
|
2235
2250
|
const deletedName = deleteConfig(name);
|
|
2236
2251
|
if (deletedName) {
|
|
@@ -2247,7 +2262,7 @@ function handleConfig(args) {
|
|
|
2247
2262
|
return;
|
|
2248
2263
|
}
|
|
2249
2264
|
case ConfigVerb.Auto: {
|
|
2250
|
-
const names = requireNames(rest, "config auto");
|
|
2265
|
+
const names = clubSpacedName(requireNames(rest, "config auto"));
|
|
2251
2266
|
for (const name of names) {
|
|
2252
2267
|
const updated = updateConfigAutoStart(name, true);
|
|
2253
2268
|
if (updated) {
|
|
@@ -2259,7 +2274,7 @@ function handleConfig(args) {
|
|
|
2259
2274
|
return;
|
|
2260
2275
|
}
|
|
2261
2276
|
case ConfigVerb.Noauto: {
|
|
2262
|
-
const names = requireNames(rest, "config noauto");
|
|
2277
|
+
const names = clubSpacedName(requireNames(rest, "config noauto"));
|
|
2263
2278
|
for (const name of names) {
|
|
2264
2279
|
const updated = updateConfigAutoStart(name, false);
|
|
2265
2280
|
if (updated) {
|
|
@@ -2270,10 +2285,14 @@ function handleConfig(args) {
|
|
|
2270
2285
|
}
|
|
2271
2286
|
return;
|
|
2272
2287
|
}
|
|
2273
|
-
default:
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2288
|
+
default: {
|
|
2289
|
+
const names = clubSpacedName([verb, ...rest]);
|
|
2290
|
+
for (const name of names) {
|
|
2291
|
+
const saved = resolveConfig(name, "config");
|
|
2292
|
+
if (saved) printConfigDetail(saved);
|
|
2293
|
+
}
|
|
2276
2294
|
return;
|
|
2295
|
+
}
|
|
2277
2296
|
}
|
|
2278
2297
|
}
|
|
2279
2298
|
function handleConfigSave(name, remainingArgs) {
|
|
@@ -2291,7 +2310,7 @@ function handleConfigSave(name, remainingArgs) {
|
|
|
2291
2310
|
printer_default.success(`Config "${name}" saved.`);
|
|
2292
2311
|
}
|
|
2293
2312
|
function handleConfigUpdate(nameOrId, remainingArgs) {
|
|
2294
|
-
const saved = resolveConfig(nameOrId);
|
|
2313
|
+
const saved = resolveConfig(nameOrId, "config update");
|
|
2295
2314
|
if (!saved) return;
|
|
2296
2315
|
const { values, positionals } = parseCliArgs(cliOptions, remainingArgs);
|
|
2297
2316
|
logger.debug("Building updated config", { nameOrId, values, positionals });
|
|
@@ -2325,7 +2344,7 @@ async function handleStart(args) {
|
|
|
2325
2344
|
return;
|
|
2326
2345
|
}
|
|
2327
2346
|
const resolved = [];
|
|
2328
|
-
for (const name of names) {
|
|
2347
|
+
for (const name of clubSpacedName(names)) {
|
|
2329
2348
|
const saved = resolveConfig(name);
|
|
2330
2349
|
if (!saved) return;
|
|
2331
2350
|
resolved.push(saved);
|
|
@@ -2350,13 +2369,34 @@ async function handleStart(args) {
|
|
|
2350
2369
|
await startMultipleForegroundViaDaemon(resolved, values, positionals);
|
|
2351
2370
|
}
|
|
2352
2371
|
}
|
|
2353
|
-
function
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2372
|
+
function clubSpacedName(names) {
|
|
2373
|
+
if (names.length > 1) {
|
|
2374
|
+
const joined = names.join(" ");
|
|
2375
|
+
if (findConfig(joined) ?? findConfigByName(joined)) {
|
|
2376
|
+
return [joined];
|
|
2377
|
+
}
|
|
2358
2378
|
}
|
|
2359
|
-
return
|
|
2379
|
+
return names;
|
|
2380
|
+
}
|
|
2381
|
+
function resolveConfig(nameOrId, command = "start") {
|
|
2382
|
+
const saved = findConfig(nameOrId) ?? findConfigByName(nameOrId);
|
|
2383
|
+
if (saved) return saved;
|
|
2384
|
+
const sanitizedQuery = sanitizeName(nameOrId);
|
|
2385
|
+
const suggestions = listSavedConfigs().filter(
|
|
2386
|
+
(c) => c.name !== nameOrId && sanitizeName(c.name) === sanitizedQuery
|
|
2387
|
+
);
|
|
2388
|
+
printer_default.error(`No config found matching "${nameOrId}".`);
|
|
2389
|
+
if (suggestions.length > 0) {
|
|
2390
|
+
for (const c of suggestions.slice(0, 3)) {
|
|
2391
|
+
printer_default.info(` Did you mean "${c.name}"? \u2192 pinggy ${command} "${c.name}"`);
|
|
2392
|
+
}
|
|
2393
|
+
if (suggestions.length > 3) {
|
|
2394
|
+
printer_default.info(` \u2026and ${suggestions.length - 3} more. Use: pinggy config list`);
|
|
2395
|
+
}
|
|
2396
|
+
} else {
|
|
2397
|
+
printer_default.info(" Use: pinggy config list");
|
|
2398
|
+
}
|
|
2399
|
+
return null;
|
|
2360
2400
|
}
|
|
2361
2401
|
function requireName(args, command) {
|
|
2362
2402
|
if (args.length === 0 || args[0].startsWith("-")) {
|
|
@@ -2662,6 +2702,10 @@ var IPCServer = class {
|
|
|
2662
2702
|
},
|
|
2663
2703
|
[Route.Shutdown]: () => {
|
|
2664
2704
|
logger.info("Daemon shutdown requested via IPC");
|
|
2705
|
+
if (getDaemonHost() === DaemonHost.APP) {
|
|
2706
|
+
logger.info("Shutdown refused: daemon is hosted in-process by the Pinggy app");
|
|
2707
|
+
return { status: ShutdownStatus.RefusedInApp, errors: [] };
|
|
2708
|
+
}
|
|
2665
2709
|
const errors = [];
|
|
2666
2710
|
const step = (label, fn) => {
|
|
2667
2711
|
try {
|
|
@@ -2676,7 +2720,7 @@ var IPCServer = class {
|
|
|
2676
2720
|
step("clearDaemonState", clearDaemonState);
|
|
2677
2721
|
step("stopAllTunnels", () => TunnelManager.getInstance().stopAllTunnels());
|
|
2678
2722
|
setTimeout(() => process.exit(0), 200);
|
|
2679
|
-
return { status:
|
|
2723
|
+
return { status: ShutdownStatus.ShuttingDown, errors };
|
|
2680
2724
|
}
|
|
2681
2725
|
};
|
|
2682
2726
|
}
|
|
@@ -3119,6 +3163,10 @@ var SessionTracker = class {
|
|
|
3119
3163
|
|
|
3120
3164
|
// src/daemon/lifecycle/daemonChild.ts
|
|
3121
3165
|
var daemonState = { tunnels: [], lastUpdated: "" };
|
|
3166
|
+
var daemonHost = DaemonHost.CLI;
|
|
3167
|
+
function getDaemonHost() {
|
|
3168
|
+
return daemonHost;
|
|
3169
|
+
}
|
|
3122
3170
|
var sessionTrackerRef = null;
|
|
3123
3171
|
function setDaemonSessionTracker(st) {
|
|
3124
3172
|
sessionTrackerRef = st;
|
|
@@ -3248,6 +3296,7 @@ async function restoreCrashedTunnels(manager) {
|
|
|
3248
3296
|
async function runDaemonChild(opts = {}) {
|
|
3249
3297
|
const installSignalHandlers = opts.installSignalHandlers ?? true;
|
|
3250
3298
|
const exitOnFailure = opts.exitOnFailure ?? true;
|
|
3299
|
+
daemonHost = opts.host ?? DaemonHost.CLI;
|
|
3251
3300
|
ensurePinggyConfigDir();
|
|
3252
3301
|
const persistedLevel = readDaemonConfig()?.logLevel;
|
|
3253
3302
|
const envLevel = process.env.PINGGY_LOG_LEVEL;
|
|
@@ -3323,7 +3372,8 @@ async function runDaemonChild(opts = {}) {
|
|
|
3323
3372
|
const info = {
|
|
3324
3373
|
pid: process.pid,
|
|
3325
3374
|
port,
|
|
3326
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3375
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3376
|
+
host: daemonHost
|
|
3327
3377
|
};
|
|
3328
3378
|
writeDaemonInfo(info);
|
|
3329
3379
|
logger.info("Daemon info written", info);
|
|
@@ -3364,7 +3414,7 @@ async function main() {
|
|
|
3364
3414
|
const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
|
|
3365
3415
|
configureLogger(values);
|
|
3366
3416
|
if (values["_daemon-child"]) {
|
|
3367
|
-
const { runDaemonChild: runDaemonChild2 } = await import("./daemonChild-
|
|
3417
|
+
const { runDaemonChild: runDaemonChild2 } = await import("./daemonChild-WIRWKBKB.js");
|
|
3368
3418
|
await runDaemonChild2();
|
|
3369
3419
|
return;
|
|
3370
3420
|
}
|
|
@@ -3398,6 +3448,7 @@ if (entryFile && entryFile === currentFile) {
|
|
|
3398
3448
|
}
|
|
3399
3449
|
|
|
3400
3450
|
export {
|
|
3451
|
+
getDaemonHost,
|
|
3401
3452
|
setDaemonSessionTracker,
|
|
3402
3453
|
removeDaemonInfo,
|
|
3403
3454
|
trackTunnelStart,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DaemonHost,
|
|
2
3
|
IPCClient,
|
|
3
|
-
SessionMode
|
|
4
|
-
|
|
4
|
+
SessionMode,
|
|
5
|
+
ShutdownStatus
|
|
6
|
+
} from "./chunk-5YHD6M35.js";
|
|
5
7
|
import {
|
|
6
8
|
TunnelAlreadyRunningError,
|
|
7
9
|
TunnelManager,
|
|
@@ -209,11 +211,23 @@ var ForwardingEntryV2Schema = z.object({
|
|
|
209
211
|
address: z.string(),
|
|
210
212
|
type: z.enum([TunnelType.Http, TunnelType.Tcp, TunnelType.Udp, TunnelType.Tls, TunnelType.TlsTcp])
|
|
211
213
|
});
|
|
214
|
+
var OptionalSchema = z.object({
|
|
215
|
+
sniServerName: z.string().optional(),
|
|
216
|
+
ssl: z.boolean().optional(),
|
|
217
|
+
additionalArguments: z.string().optional(),
|
|
218
|
+
serve: z.string().optional(),
|
|
219
|
+
manage: z.string().optional(),
|
|
220
|
+
remoteManagement: z.object({ serverUrl: z.string(), apiKey: z.string() }).optional(),
|
|
221
|
+
noTui: z.boolean().optional()
|
|
222
|
+
}).catchall(z.unknown());
|
|
212
223
|
var TunnelConfigV1Schema = z.object({
|
|
213
224
|
// Meta Info
|
|
214
225
|
version: z.string(),
|
|
215
226
|
name: z.string(),
|
|
216
227
|
configId: z.string(),
|
|
228
|
+
hostKeyCheck: z.boolean().optional(),
|
|
229
|
+
platformValue: z.string().optional(),
|
|
230
|
+
isQRCode: z.boolean().optional(),
|
|
217
231
|
// General tunnel configurations
|
|
218
232
|
serverAddress: z.string().optional(),
|
|
219
233
|
token: z.string().optional(),
|
|
@@ -239,9 +253,11 @@ var TunnelConfigV1Schema = z.object({
|
|
|
239
253
|
httpsOnly: z.boolean().optional(),
|
|
240
254
|
originalRequestUrl: z.boolean().optional(),
|
|
241
255
|
allowPreflight: z.boolean().optional(),
|
|
242
|
-
|
|
243
|
-
optional: z.record(z.string(), z.unknown()).optional()
|
|
256
|
+
optional: OptionalSchema.optional()
|
|
244
257
|
});
|
|
258
|
+
var TUNNEL_CONFIG_V1_KEYS = Object.keys(
|
|
259
|
+
TunnelConfigV1Schema.shape
|
|
260
|
+
);
|
|
245
261
|
var StartV2Schema = z.object({
|
|
246
262
|
tunnelID: z.string().nullable().optional(),
|
|
247
263
|
tunnelConfig: TunnelConfigV1Schema
|
|
@@ -490,11 +506,11 @@ var TunnelOperations = class {
|
|
|
490
506
|
startPromise.catch((err) => {
|
|
491
507
|
logger.error("No-wait startTunnel failed", { tunnelid, err: String(err) });
|
|
492
508
|
});
|
|
493
|
-
return this.buildPendingTunnelResponseV2(tunnelid, tunnelConfig, config, config.configId, config.name, config.serve);
|
|
509
|
+
return this.buildPendingTunnelResponseV2(tunnelid, tunnelConfig, config, config.configId, config.name, config.optional?.serve);
|
|
494
510
|
}
|
|
495
511
|
await startPromise;
|
|
496
512
|
const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
|
|
497
|
-
return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.serve);
|
|
513
|
+
return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.optional?.serve);
|
|
498
514
|
} catch (err) {
|
|
499
515
|
if (err instanceof TunnelAlreadyRunningError) {
|
|
500
516
|
return this.error(ErrorCode.TunnelAlreadyRunningError, err, err.message);
|
|
@@ -1491,16 +1507,18 @@ async function ensureDaemonRunning() {
|
|
|
1491
1507
|
const existing = getDaemonInfo();
|
|
1492
1508
|
if (existing) return existing;
|
|
1493
1509
|
if (process.versions.electron) {
|
|
1494
|
-
const { runDaemonChild } = await import("./daemonChild-
|
|
1510
|
+
const { runDaemonChild } = await import("./daemonChild-WIRWKBKB.js");
|
|
1495
1511
|
const handle = await runDaemonChild({
|
|
1496
1512
|
installSignalHandlers: false,
|
|
1497
|
-
exitOnFailure: false
|
|
1513
|
+
exitOnFailure: false,
|
|
1514
|
+
host: DaemonHost.APP
|
|
1498
1515
|
});
|
|
1499
1516
|
inProcessHandle = handle;
|
|
1500
1517
|
return getDaemonInfo() ?? {
|
|
1501
1518
|
pid: handle.pid,
|
|
1502
1519
|
port: handle.port,
|
|
1503
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1520
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1521
|
+
host: DaemonHost.APP
|
|
1504
1522
|
};
|
|
1505
1523
|
}
|
|
1506
1524
|
return startDaemon();
|
|
@@ -1511,7 +1529,7 @@ function getInProcessDaemonHandle() {
|
|
|
1511
1529
|
async function getActiveTunnelSummaries(origin = "app") {
|
|
1512
1530
|
const info = getDaemonInfo();
|
|
1513
1531
|
if (!info) return [];
|
|
1514
|
-
const { IPCClient: IPCClient2 } = await import("./ipcClient-
|
|
1532
|
+
const { IPCClient: IPCClient2 } = await import("./ipcClient-Z2RXDQUQ.js");
|
|
1515
1533
|
const client = new IPCClient2(info.port, origin);
|
|
1516
1534
|
let tunnels;
|
|
1517
1535
|
try {
|
|
@@ -1558,10 +1576,17 @@ async function stopDaemon() {
|
|
|
1558
1576
|
if (!info) return { ok: false, error: "No daemon is running." };
|
|
1559
1577
|
let daemonErrors = [];
|
|
1560
1578
|
try {
|
|
1561
|
-
const { IPCClient: IPCClient2 } = await import("./ipcClient-
|
|
1579
|
+
const { IPCClient: IPCClient2 } = await import("./ipcClient-Z2RXDQUQ.js");
|
|
1562
1580
|
const client = new IPCClient2(info.port);
|
|
1563
1581
|
const result = await client.shutdown();
|
|
1564
1582
|
logger.debug("Sent shutdown command to daemon", { result });
|
|
1583
|
+
if (result?.status === ShutdownStatus.RefusedInApp) {
|
|
1584
|
+
return {
|
|
1585
|
+
ok: false,
|
|
1586
|
+
reason: DaemonHost.APP,
|
|
1587
|
+
error: "The daemon is running inside the Pinggy app."
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1565
1590
|
if (Array.isArray(result?.errors)) daemonErrors = result.errors;
|
|
1566
1591
|
} catch (e) {
|
|
1567
1592
|
return { ok: false, error: `Failed to reach daemon: ${errorMessage(e)}` };
|
|
@@ -1847,6 +1872,13 @@ var DaemonHealth = class {
|
|
|
1847
1872
|
bindPid(pid) {
|
|
1848
1873
|
this.originalPid = pid;
|
|
1849
1874
|
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Forget a previous loss verdict. Called when the client re-binds to a
|
|
1877
|
+
* live daemon via ensureDaemon():
|
|
1878
|
+
*/
|
|
1879
|
+
reset() {
|
|
1880
|
+
this.lost = false;
|
|
1881
|
+
}
|
|
1850
1882
|
isLost() {
|
|
1851
1883
|
return this.lost;
|
|
1852
1884
|
}
|
|
@@ -1951,6 +1983,7 @@ var TunnelClient = class _TunnelClient {
|
|
|
1951
1983
|
const info = await ensureDaemonRunning();
|
|
1952
1984
|
this.ipc = new IPCClient(info.port, this.origin);
|
|
1953
1985
|
this.health.bindPid(info.pid);
|
|
1986
|
+
this.health.reset();
|
|
1954
1987
|
}
|
|
1955
1988
|
static async forRemoteManagement() {
|
|
1956
1989
|
const client = new _TunnelClient({ origin: "remote" });
|
|
@@ -2024,6 +2057,21 @@ var TunnelClient = class _TunnelClient {
|
|
|
2024
2057
|
await this.ipc.shutdown();
|
|
2025
2058
|
this.close();
|
|
2026
2059
|
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Tear down the daemon hosted in THIS process, if any. Stops all tunnels
|
|
2062
|
+
* and removes daemon.json + daemon-state.json so the next CLI run doesn't
|
|
2063
|
+
* "crash-recover" tunnels that died with the host application.
|
|
2064
|
+
*/
|
|
2065
|
+
shutdownInProcessDaemon() {
|
|
2066
|
+
const handle = getInProcessDaemonHandle();
|
|
2067
|
+
if (!handle) return false;
|
|
2068
|
+
try {
|
|
2069
|
+
this.close();
|
|
2070
|
+
} catch {
|
|
2071
|
+
}
|
|
2072
|
+
handle.shutdown();
|
|
2073
|
+
return true;
|
|
2074
|
+
}
|
|
2027
2075
|
async getLogLevel() {
|
|
2028
2076
|
this.assertClient();
|
|
2029
2077
|
const res = await this.ipc.getLogLevel();
|
|
@@ -2139,6 +2187,8 @@ export {
|
|
|
2139
2187
|
TunnelWarningCode,
|
|
2140
2188
|
ErrorCode,
|
|
2141
2189
|
isErrorResponse,
|
|
2190
|
+
TunnelConfigV1Schema,
|
|
2191
|
+
TUNNEL_CONFIG_V1_KEYS,
|
|
2142
2192
|
TunnelOperations,
|
|
2143
2193
|
RemoteManagementUnauthorizedError,
|
|
2144
2194
|
buildRemoteManagementWsUrl,
|
|
@@ -10,6 +10,14 @@ import {
|
|
|
10
10
|
import fs from "fs";
|
|
11
11
|
import path from "path";
|
|
12
12
|
import pico from "picocolors";
|
|
13
|
+
var storageLogger = {
|
|
14
|
+
info: (message, ...meta) => logger.info(message, ...meta),
|
|
15
|
+
warn: (message, ...meta) => logger.warn(message, ...meta),
|
|
16
|
+
error: (message, ...meta) => logger.error(message, ...meta)
|
|
17
|
+
};
|
|
18
|
+
function configureStorageLogger(l) {
|
|
19
|
+
storageLogger = l;
|
|
20
|
+
}
|
|
13
21
|
function buildFilename(name, configId) {
|
|
14
22
|
return `${name}_${configId}.json`;
|
|
15
23
|
}
|
|
@@ -41,7 +49,7 @@ var ConfigVerb = {
|
|
|
41
49
|
var SUBCOMMANDS = Object.values(Subcommand);
|
|
42
50
|
var CONFIG_VERBS = Object.values(ConfigVerb);
|
|
43
51
|
var RESERVED_NAMES = /* @__PURE__ */ new Set([...SUBCOMMANDS, ...CONFIG_VERBS]);
|
|
44
|
-
function
|
|
52
|
+
function validateNameStrict(name) {
|
|
45
53
|
if (!name || name.trim().length === 0) {
|
|
46
54
|
return new Error("Tunnel name cannot be empty.");
|
|
47
55
|
}
|
|
@@ -56,17 +64,39 @@ function validateName(name) {
|
|
|
56
64
|
}
|
|
57
65
|
return null;
|
|
58
66
|
}
|
|
67
|
+
function validateNameForStorage(name) {
|
|
68
|
+
if (!name || name.trim().length === 0) {
|
|
69
|
+
return new Error("Tunnel name cannot be empty.");
|
|
70
|
+
}
|
|
71
|
+
if (/[\u0000-\u001F\/\\]/.test(name)) {
|
|
72
|
+
return new Error("Tunnel name cannot contain path separators or control characters.");
|
|
73
|
+
}
|
|
74
|
+
if (RESERVED_NAMES.has(name.toLowerCase())) {
|
|
75
|
+
return new Error(`"${name}" is a reserved subcommand name. Use a different name.`);
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
var validateName = validateNameStrict;
|
|
59
80
|
function readConfigFile(filePath) {
|
|
60
81
|
try {
|
|
61
82
|
const data = fs.readFileSync(filePath, { encoding: "utf-8" });
|
|
62
83
|
return JSON.parse(data);
|
|
63
84
|
} catch (err) {
|
|
64
|
-
|
|
85
|
+
storageLogger.warn(`Failed to read config file ${filePath}: ${String(err)}`);
|
|
65
86
|
return null;
|
|
66
87
|
}
|
|
67
88
|
}
|
|
68
89
|
function writeConfigFile(filePath, config) {
|
|
69
|
-
|
|
90
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
91
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2), { encoding: "utf-8" });
|
|
92
|
+
fs.renameSync(tmp, filePath);
|
|
93
|
+
}
|
|
94
|
+
function nextTimestamp(prev) {
|
|
95
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
96
|
+
if (prev && ts <= prev) {
|
|
97
|
+
return new Date(new Date(prev).getTime() + 1).toISOString();
|
|
98
|
+
}
|
|
99
|
+
return ts;
|
|
70
100
|
}
|
|
71
101
|
function listSavedConfigs() {
|
|
72
102
|
const dir = getTunnelConfigDir();
|
|
@@ -88,15 +118,15 @@ function findConfigFile(nameOrId) {
|
|
|
88
118
|
if (!fs.existsSync(dir)) return null;
|
|
89
119
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
90
120
|
const sanitized = sanitizeName(nameOrId);
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const filePath = path.join(dir,
|
|
121
|
+
const nameCandidates = files.filter((f) => f.startsWith(sanitized + "_"));
|
|
122
|
+
for (const f of nameCandidates) {
|
|
123
|
+
const filePath = path.join(dir, f);
|
|
94
124
|
const config = readConfigFile(filePath);
|
|
95
125
|
if (config && config.name === nameOrId) return { filePath, config };
|
|
96
126
|
}
|
|
97
127
|
const idCandidates = files.filter((f) => {
|
|
98
128
|
const withoutExt = f.replace(/\.json$/, "");
|
|
99
|
-
const lastUnderscore = withoutExt.
|
|
129
|
+
const lastUnderscore = withoutExt.lastIndexOf("_");
|
|
100
130
|
if (lastUnderscore === -1) return false;
|
|
101
131
|
const idPart = withoutExt.slice(lastUnderscore + 1);
|
|
102
132
|
return idPart.startsWith(nameOrId);
|
|
@@ -108,15 +138,33 @@ function findConfigFile(nameOrId) {
|
|
|
108
138
|
}
|
|
109
139
|
return null;
|
|
110
140
|
}
|
|
141
|
+
function findConfigFileByConfigId(configId) {
|
|
142
|
+
const dir = getTunnelConfigDir();
|
|
143
|
+
if (!fs.existsSync(dir)) return null;
|
|
144
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const filePath = path.join(dir, file);
|
|
147
|
+
const config = readConfigFile(filePath);
|
|
148
|
+
if (config && config.configId === configId) {
|
|
149
|
+
return { filePath, config };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
111
154
|
function findConfigByName(name) {
|
|
112
|
-
const
|
|
113
|
-
|
|
155
|
+
const dir = getTunnelConfigDir();
|
|
156
|
+
if (!fs.existsSync(dir)) return null;
|
|
157
|
+
const prefix = `${sanitizeName(name)}_`;
|
|
158
|
+
const matches = fs.readdirSync(dir).filter((f) => f.endsWith(".json") && f.startsWith(prefix)).map((f) => readConfigFile(path.join(dir, f))).filter((c) => c !== null && c.name === name);
|
|
159
|
+
if (matches.length === 0) return null;
|
|
160
|
+
return matches.sort((a, b) => (b.updatedAt || "").localeCompare(a.updatedAt || ""))[0];
|
|
114
161
|
}
|
|
115
162
|
function findConfig(nameOrId) {
|
|
116
163
|
return findConfigFile(nameOrId)?.config ?? null;
|
|
117
164
|
}
|
|
118
|
-
function saveConfig(name, configId, tunnelConfig, autoStart = false) {
|
|
119
|
-
const
|
|
165
|
+
function saveConfig(name, configId, tunnelConfig, autoStart = false, options = {}) {
|
|
166
|
+
const { strict = true } = options;
|
|
167
|
+
const nameErr = strict ? validateNameStrict(name) : validateNameForStorage(name);
|
|
120
168
|
if (nameErr) {
|
|
121
169
|
throw nameErr;
|
|
122
170
|
}
|
|
@@ -136,12 +184,58 @@ function saveConfig(name, configId, tunnelConfig, autoStart = false) {
|
|
|
136
184
|
updatedAt: now,
|
|
137
185
|
tunnelConfig
|
|
138
186
|
};
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
logger.info(`Config "${name}" saved to ${filePath}`);
|
|
187
|
+
const filePath = path.join(dir, buildFilename(sanitizeName(name), configId));
|
|
188
|
+
writeConfigFile(filePath, saved);
|
|
189
|
+
storageLogger.info(`Config "${name}" saved to ${filePath}`);
|
|
143
190
|
return saved;
|
|
144
191
|
}
|
|
192
|
+
function upsertConfig(saved, options = {}) {
|
|
193
|
+
const { strict = true } = options;
|
|
194
|
+
const dir = ensureTunnelConfigDir();
|
|
195
|
+
const targetPath = path.join(dir, buildFilename(sanitizeName(saved.name), saved.configId));
|
|
196
|
+
const existing = findConfigFileByConfigId(saved.configId);
|
|
197
|
+
if (existing) {
|
|
198
|
+
if (path.resolve(existing.filePath) !== path.resolve(targetPath)) {
|
|
199
|
+
try {
|
|
200
|
+
fs.unlinkSync(existing.filePath);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
storageLogger.warn(`upsertConfig: failed to remove old file ${existing.filePath}: ${String(err)}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
writeConfigFile(targetPath, saved);
|
|
206
|
+
storageLogger.info(`Config "${saved.name}" (${saved.configId}) updated at ${targetPath}`);
|
|
207
|
+
return saved;
|
|
208
|
+
}
|
|
209
|
+
const nameErr = strict ? validateNameStrict(saved.name) : validateNameForStorage(saved.name);
|
|
210
|
+
if (nameErr) {
|
|
211
|
+
throw nameErr;
|
|
212
|
+
}
|
|
213
|
+
writeConfigFile(targetPath, saved);
|
|
214
|
+
storageLogger.info(`Config "${saved.name}" (${saved.configId}) saved to ${targetPath}`);
|
|
215
|
+
return saved;
|
|
216
|
+
}
|
|
217
|
+
function bulkReplace(configs, knownConfigIds, options = {}) {
|
|
218
|
+
const written = [];
|
|
219
|
+
const deleted = [];
|
|
220
|
+
const incomingIds = /* @__PURE__ */ new Set();
|
|
221
|
+
for (const cfg of configs) {
|
|
222
|
+
upsertConfig(cfg, options);
|
|
223
|
+
written.push(cfg.configId);
|
|
224
|
+
incomingIds.add(cfg.configId);
|
|
225
|
+
}
|
|
226
|
+
for (const id of knownConfigIds) {
|
|
227
|
+
if (incomingIds.has(id)) continue;
|
|
228
|
+
const existing = findConfigFileByConfigId(id);
|
|
229
|
+
if (!existing) continue;
|
|
230
|
+
try {
|
|
231
|
+
fs.unlinkSync(existing.filePath);
|
|
232
|
+
deleted.push(id);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
storageLogger.warn(`bulkReplace: failed to delete ${existing.filePath}: ${String(err)}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { written, deleted };
|
|
238
|
+
}
|
|
145
239
|
function loadConfigByName(name) {
|
|
146
240
|
return findConfigByName(name);
|
|
147
241
|
}
|
|
@@ -149,25 +243,25 @@ function deleteConfig(nameOrId) {
|
|
|
149
243
|
const resolved = findConfigFile(nameOrId);
|
|
150
244
|
if (!resolved) return null;
|
|
151
245
|
fs.unlinkSync(resolved.filePath);
|
|
152
|
-
|
|
246
|
+
storageLogger.info(`Config "${resolved.config.name}" deleted.`);
|
|
153
247
|
return resolved.config.name;
|
|
154
248
|
}
|
|
155
249
|
function updateConfigAutoStart(nameOrId, autoStart) {
|
|
156
250
|
const resolved = findConfigFile(nameOrId);
|
|
157
251
|
if (!resolved) return null;
|
|
158
252
|
resolved.config.autoStart = autoStart;
|
|
159
|
-
resolved.config.updatedAt = (
|
|
253
|
+
resolved.config.updatedAt = nextTimestamp(resolved.config.updatedAt);
|
|
160
254
|
writeConfigFile(resolved.filePath, resolved.config);
|
|
161
|
-
|
|
255
|
+
storageLogger.info(`Config "${resolved.config.name}" auto-start set to ${autoStart}`);
|
|
162
256
|
return resolved.config;
|
|
163
257
|
}
|
|
164
258
|
function updateTunnelConfig(nameOrId, tunnelConfig) {
|
|
165
259
|
const resolved = findConfigFile(nameOrId);
|
|
166
260
|
if (!resolved) return null;
|
|
167
261
|
resolved.config.tunnelConfig = tunnelConfig;
|
|
168
|
-
resolved.config.updatedAt = (
|
|
262
|
+
resolved.config.updatedAt = nextTimestamp(resolved.config.updatedAt);
|
|
169
263
|
writeConfigFile(resolved.filePath, resolved.config);
|
|
170
|
-
|
|
264
|
+
storageLogger.info(`Config "${resolved.config.name}" tunnel configuration updated`);
|
|
171
265
|
return resolved.config;
|
|
172
266
|
}
|
|
173
267
|
function getAutoStartConfigs() {
|
|
@@ -248,17 +342,22 @@ Tunnel Config: ${pico.cyanBright(config.name)}`));
|
|
|
248
342
|
}
|
|
249
343
|
|
|
250
344
|
export {
|
|
345
|
+
configureStorageLogger,
|
|
251
346
|
sanitizeName,
|
|
252
347
|
Subcommand,
|
|
253
348
|
ConfigVerb,
|
|
254
349
|
SUBCOMMANDS,
|
|
255
350
|
CONFIG_VERBS,
|
|
256
351
|
RESERVED_NAMES,
|
|
352
|
+
validateNameStrict,
|
|
353
|
+
validateNameForStorage,
|
|
257
354
|
validateName,
|
|
258
355
|
listSavedConfigs,
|
|
259
356
|
findConfigByName,
|
|
260
357
|
findConfig,
|
|
261
358
|
saveConfig,
|
|
359
|
+
upsertConfig,
|
|
360
|
+
bulkReplace,
|
|
262
361
|
loadConfigByName,
|
|
263
362
|
deleteConfig,
|
|
264
363
|
updateConfigAutoStart,
|