@hamedb89/localghost 0.1.6 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -22
- package/apps/macos-widget/LocalghostWidget.entitlements +12 -0
- package/apps/macos-widget/LocalghostWidget.swift +871 -0
- package/apps/macos-widget/Resources/localghost-logo-source.png +0 -0
- package/apps/macos-widget/Resources/localghost-widget-ui-reference.png +0 -0
- package/apps/macos-widget/Shared/LocalghostWidgetSnapshot.swift +50 -0
- package/apps/macos-widget/WidgetExtension/LocalghostDesktopWidget.swift +176 -0
- package/apps/macos-widget/WidgetExtension/LocalghostDesktopWidgetExtension.entitlements +12 -0
- package/apps/macos-widget/build.sh +60 -0
- package/apps/macos-widget/project.yml +40 -0
- package/assets/ghost-tunnel-app-icon.png +0 -0
- package/assets/ghost-tunnel-portal.png +0 -0
- package/assets/ghost-tunnel-wordmark.png +0 -0
- package/dist/cli.js +731 -91
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +138 -5
- package/dist/index.js +833 -37
- package/dist/index.js.map +1 -1
- package/dist/tunnel-DzfLXZ8O.d.ts +126 -0
- package/dist/vite.d.ts +5 -1
- package/dist/vite.js +717 -43
- package/dist/vite.js.map +1 -1
- package/docs/flows.md +19 -0
- package/docs/ghost-tunnel.md +249 -0
- package/docs/github.md +6 -6
- package/docs/localghost.1.md +28 -7
- package/docs/macos-widget.md +108 -0
- package/package.json +8 -2
- package/dist/config-Cde1Bich.d.ts +0 -31
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ function isProcessRunning(pid) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
function emptyActivity() {
|
|
22
|
-
return { version: LOCALGHOST_ACTIVITY_VERSION, runs: [] };
|
|
22
|
+
return { version: LOCALGHOST_ACTIVITY_VERSION, runs: [], setups: [] };
|
|
23
23
|
}
|
|
24
24
|
function readLocalghostActivity(path = getLocalghostActivityPath()) {
|
|
25
25
|
if (!existsSync(path)) return emptyActivity();
|
|
@@ -27,7 +27,8 @@ function readLocalghostActivity(path = getLocalghostActivityPath()) {
|
|
|
27
27
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
28
28
|
return {
|
|
29
29
|
version: LOCALGHOST_ACTIVITY_VERSION,
|
|
30
|
-
runs: Array.isArray(parsed.runs) ? parsed.runs : []
|
|
30
|
+
runs: Array.isArray(parsed.runs) ? parsed.runs : [],
|
|
31
|
+
setups: Array.isArray(parsed.setups) ? parsed.setups : []
|
|
31
32
|
};
|
|
32
33
|
} catch {
|
|
33
34
|
return emptyActivity();
|
|
@@ -42,22 +43,29 @@ function writeLocalghostActivity(activity, path = getLocalghostActivityPath()) {
|
|
|
42
43
|
function createRunId(input, pid) {
|
|
43
44
|
return `${input.projectName}:${input.mode}:${pid}:${Date.now()}`;
|
|
44
45
|
}
|
|
46
|
+
function createSetupId(input) {
|
|
47
|
+
return `${input.projectName}:${input.cwd}:${input.configPath ?? ""}`;
|
|
48
|
+
}
|
|
45
49
|
function pruneLocalghostActivity(path = getLocalghostActivityPath()) {
|
|
46
50
|
const activity = readLocalghostActivity(path);
|
|
47
51
|
const activeRuns = activity.runs.filter((run) => isProcessRunning(run.pid));
|
|
48
52
|
const pruned = activeRuns.length !== activity.runs.length;
|
|
49
53
|
if (pruned) {
|
|
50
|
-
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: activeRuns }, path);
|
|
54
|
+
writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, runs: activeRuns }, path);
|
|
51
55
|
}
|
|
52
56
|
return {
|
|
53
57
|
path,
|
|
54
58
|
pruned,
|
|
55
|
-
runs: activeRuns
|
|
59
|
+
runs: activeRuns,
|
|
60
|
+
setups: activity.setups
|
|
56
61
|
};
|
|
57
62
|
}
|
|
58
63
|
function listLocalghostRuns(path = getLocalghostActivityPath()) {
|
|
59
64
|
return pruneLocalghostActivity(path).runs;
|
|
60
65
|
}
|
|
66
|
+
function listLocalghostSetups(path = getLocalghostActivityPath()) {
|
|
67
|
+
return pruneLocalghostActivity(path).setups;
|
|
68
|
+
}
|
|
61
69
|
function registerLocalghostRun(input, path = getLocalghostActivityPath()) {
|
|
62
70
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
63
71
|
const pid = input.pid ?? process.pid;
|
|
@@ -81,14 +89,43 @@ function registerLocalghostRun(input, path = getLocalghostActivityPath()) {
|
|
|
81
89
|
entries: input.entries
|
|
82
90
|
};
|
|
83
91
|
const current = pruneLocalghostActivity(path).runs.filter((run) => run.id !== record.id);
|
|
84
|
-
|
|
92
|
+
const activity = readLocalghostActivity(path);
|
|
93
|
+
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: [...current, record], setups: activity.setups }, path);
|
|
94
|
+
return record;
|
|
95
|
+
}
|
|
96
|
+
function registerLocalghostSetup(input, path = getLocalghostActivityPath()) {
|
|
97
|
+
const record = {
|
|
98
|
+
id: input.id ?? createSetupId(input),
|
|
99
|
+
cwd: input.cwd,
|
|
100
|
+
projectName: input.projectName,
|
|
101
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
102
|
+
...input.configPath ? { configPath: input.configPath } : {},
|
|
103
|
+
...input.caddyfilePath ? { caddyfilePath: input.caddyfilePath } : {},
|
|
104
|
+
...typeof input.https === "boolean" ? { https: input.https } : {},
|
|
105
|
+
entries: input.entries
|
|
106
|
+
};
|
|
107
|
+
const activity = pruneLocalghostActivity(path);
|
|
108
|
+
const setups = activity.setups.filter((setup) => setup.id !== record.id);
|
|
109
|
+
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: activity.runs, setups: [...setups, record] }, path);
|
|
85
110
|
return record;
|
|
86
111
|
}
|
|
87
112
|
function unregisterLocalghostRun(id, path = getLocalghostActivityPath()) {
|
|
88
113
|
const activity = readLocalghostActivity(path);
|
|
89
114
|
const runs = activity.runs.filter((run) => run.id !== id);
|
|
90
115
|
if (runs.length !== activity.runs.length) {
|
|
91
|
-
writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs }, path);
|
|
116
|
+
writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, runs }, path);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function unregisterLocalghostSetup(options, path = getLocalghostActivityPath()) {
|
|
120
|
+
const activity = readLocalghostActivity(path);
|
|
121
|
+
const setups = activity.setups.filter((setup) => {
|
|
122
|
+
if (setup.cwd !== options.cwd) return true;
|
|
123
|
+
if (options.projectName && setup.projectName !== options.projectName) return true;
|
|
124
|
+
if (options.configPath && setup.configPath !== options.configPath) return true;
|
|
125
|
+
return false;
|
|
126
|
+
});
|
|
127
|
+
if (setups.length !== activity.setups.length) {
|
|
128
|
+
writeLocalghostActivity({ ...activity, version: LOCALGHOST_ACTIVITY_VERSION, setups }, path);
|
|
92
129
|
}
|
|
93
130
|
}
|
|
94
131
|
|
|
@@ -283,6 +320,17 @@ function startCaddy(path) {
|
|
|
283
320
|
stdio: "inherit"
|
|
284
321
|
});
|
|
285
322
|
}
|
|
323
|
+
async function trustCaddy(path) {
|
|
324
|
+
await execa("caddy", ["trust", "--config", path], {
|
|
325
|
+
cwd: dirname3(path),
|
|
326
|
+
stdio: "inherit"
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/context.ts
|
|
331
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
332
|
+
import { join as join4 } from "path";
|
|
333
|
+
import { pathToFileURL } from "url";
|
|
286
334
|
|
|
287
335
|
// src/port.ts
|
|
288
336
|
import { createServer } from "net";
|
|
@@ -310,7 +358,345 @@ async function findAvailablePort(startPort, options = {}) {
|
|
|
310
358
|
throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
|
|
311
359
|
}
|
|
312
360
|
|
|
361
|
+
// src/tunnel.ts
|
|
362
|
+
import { domainToASCII } from "url";
|
|
363
|
+
var DEFAULT_GHOST_TUNNEL_SUBDOMAIN = "ghost";
|
|
364
|
+
var DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS = ["route", "project", "owner"];
|
|
365
|
+
var DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR = "-";
|
|
366
|
+
var DEFAULT_GHOST_TUNNEL_MODE = "manual";
|
|
367
|
+
function isResolvedGhostTunnelConfig(value) {
|
|
368
|
+
return typeof value === "object" && value !== null && "enabled" in value;
|
|
369
|
+
}
|
|
370
|
+
function toGhostTunnelConfig(options) {
|
|
371
|
+
return isResolvedGhostTunnelConfig(options) ? options : resolveGhostTunnelConfig(options);
|
|
372
|
+
}
|
|
373
|
+
function stripHostPort(value) {
|
|
374
|
+
const trimmed = value.trim().toLowerCase();
|
|
375
|
+
if (trimmed.startsWith("[") || trimmed.includes("/")) return "";
|
|
376
|
+
const portSeparator = trimmed.lastIndexOf(":");
|
|
377
|
+
if (portSeparator === -1) return trimmed;
|
|
378
|
+
const port = trimmed.slice(portSeparator + 1);
|
|
379
|
+
return /^\d+$/.test(port) ? trimmed.slice(0, portSeparator) : trimmed;
|
|
380
|
+
}
|
|
381
|
+
function normalizeDomain(value) {
|
|
382
|
+
const host = stripHostPort(value.replace(/^\*\./, ""));
|
|
383
|
+
const ascii = domainToASCII(host);
|
|
384
|
+
if (!ascii || ascii.length > 253 || ascii.includes("..")) return null;
|
|
385
|
+
if (ascii.startsWith(".") || ascii.endsWith(".")) return null;
|
|
386
|
+
if (ascii.includes("*")) return null;
|
|
387
|
+
if (!ascii.split(".").every(isValidHostLabel)) return null;
|
|
388
|
+
return ascii;
|
|
389
|
+
}
|
|
390
|
+
function isValidHostLabel(value) {
|
|
391
|
+
return value.length > 0 && value.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(value);
|
|
392
|
+
}
|
|
393
|
+
function isValidNamespaceTag(value) {
|
|
394
|
+
return /^[a-z][a-z0-9_]*$/i.test(value);
|
|
395
|
+
}
|
|
396
|
+
function isNamespaceTagList(options) {
|
|
397
|
+
return Array.isArray(options);
|
|
398
|
+
}
|
|
399
|
+
function assertValidSubdomain(value) {
|
|
400
|
+
if (!isValidHostLabel(value)) {
|
|
401
|
+
throw new Error(`Invalid ghost tunnel subdomain: ${value}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function normalizeDomains(domains) {
|
|
405
|
+
const values = typeof domains === "string" ? [domains] : [...domains ?? []];
|
|
406
|
+
const normalized = values.map((value) => value.trim()).filter(Boolean).map((value) => {
|
|
407
|
+
const domain = normalizeDomain(value);
|
|
408
|
+
if (!domain) throw new Error(`Invalid ghost tunnel domain: ${value}`);
|
|
409
|
+
return domain;
|
|
410
|
+
});
|
|
411
|
+
return [...new Set(normalized)];
|
|
412
|
+
}
|
|
413
|
+
function parseGhostTunnelMode(value) {
|
|
414
|
+
return value ?? DEFAULT_GHOST_TUNNEL_MODE;
|
|
415
|
+
}
|
|
416
|
+
function resolveNamespaceConfig(options) {
|
|
417
|
+
const tags = isNamespaceTagList(options) ? [...options] : [...options?.tags ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_TAGS];
|
|
418
|
+
let separator = DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
|
|
419
|
+
let spreadTag = tags.includes("project") ? "project" : void 0;
|
|
420
|
+
if (options && !isNamespaceTagList(options)) {
|
|
421
|
+
separator = options.separator ?? DEFAULT_GHOST_TUNNEL_NAMESPACE_SEPARATOR;
|
|
422
|
+
spreadTag = options.spreadTag === false ? false : options.spreadTag ?? spreadTag;
|
|
423
|
+
}
|
|
424
|
+
if (tags.length === 0) {
|
|
425
|
+
throw new Error("Ghost tunnel namespace must include at least one tag.");
|
|
426
|
+
}
|
|
427
|
+
for (const tag of tags) {
|
|
428
|
+
if (!isValidNamespaceTag(tag)) {
|
|
429
|
+
throw new Error(`Invalid ghost tunnel namespace tag: ${tag}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (spreadTag && !tags.includes(spreadTag)) {
|
|
433
|
+
throw new Error(`Ghost tunnel namespace spreadTag must be listed in tags: ${spreadTag}`);
|
|
434
|
+
}
|
|
435
|
+
if (!separator || separator.length > 8 || !/^[a-z0-9-]+$/.test(separator)) {
|
|
436
|
+
throw new Error(`Invalid ghost tunnel namespace separator: ${separator}`);
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
tags,
|
|
440
|
+
separator,
|
|
441
|
+
...spreadTag ? { spreadTag } : {}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function normalizeNamespaceValue(tag, value, separator, options = {}) {
|
|
445
|
+
const normalized = normalizeDomain(value);
|
|
446
|
+
if (!normalized || normalized.includes(".")) {
|
|
447
|
+
throw new Error(`Invalid ghost tunnel namespace value for ${tag}: ${value}`);
|
|
448
|
+
}
|
|
449
|
+
if (!options.allowSeparator && normalized.includes(separator)) {
|
|
450
|
+
throw new Error(`Ghost tunnel namespace value for ${tag} cannot include separator "${separator}": ${value}`);
|
|
451
|
+
}
|
|
452
|
+
return normalized;
|
|
453
|
+
}
|
|
454
|
+
function createNamespaceSlug(config, values) {
|
|
455
|
+
const parts = config.tags.map((tag) => {
|
|
456
|
+
const value = values[tag];
|
|
457
|
+
if (!value) {
|
|
458
|
+
throw new Error(`Missing ghost tunnel namespace value: ${tag}`);
|
|
459
|
+
}
|
|
460
|
+
return normalizeNamespaceValue(tag, value, config.separator, { allowSeparator: tag === config.spreadTag });
|
|
461
|
+
});
|
|
462
|
+
const slug = parts.join(config.separator);
|
|
463
|
+
if (!isValidHostLabel(slug)) {
|
|
464
|
+
throw new Error(`Ghost tunnel namespace is too long for a DNS label: ${slug}`);
|
|
465
|
+
}
|
|
466
|
+
return slug;
|
|
467
|
+
}
|
|
468
|
+
function createNamespaceDisplaySlug(config, values = {}) {
|
|
469
|
+
return config.tags.map((tag) => {
|
|
470
|
+
const value = values[tag];
|
|
471
|
+
if (!value) return `<${tag}>`;
|
|
472
|
+
try {
|
|
473
|
+
return normalizeNamespaceValue(tag, value, config.separator, { allowSeparator: tag === config.spreadTag });
|
|
474
|
+
} catch {
|
|
475
|
+
return `<${tag}>`;
|
|
476
|
+
}
|
|
477
|
+
}).join(config.separator);
|
|
478
|
+
}
|
|
479
|
+
function getPreviewDefaults(preview, defaults) {
|
|
480
|
+
return {
|
|
481
|
+
domain: preview?.domain ?? defaults?.domain,
|
|
482
|
+
route: preview?.route ?? defaults?.route,
|
|
483
|
+
project: preview?.project ?? defaults?.project,
|
|
484
|
+
owner: preview?.owner ?? defaults?.owner,
|
|
485
|
+
values: {
|
|
486
|
+
...defaults?.values ?? {},
|
|
487
|
+
...preview?.values ?? {}
|
|
488
|
+
},
|
|
489
|
+
path: preview?.path,
|
|
490
|
+
protocol: preview?.protocol
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function getDisplayValues(input) {
|
|
494
|
+
return {
|
|
495
|
+
...input.route ? { route: input.route } : {},
|
|
496
|
+
...input.project ? { project: input.project } : {},
|
|
497
|
+
...input.owner ? { owner: input.owner } : {},
|
|
498
|
+
...input.values
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function createDisplayUrl(config, defaults, domain) {
|
|
502
|
+
const input = getPreviewDefaults(config.preview, defaults);
|
|
503
|
+
const protocol = input.protocol ?? "https";
|
|
504
|
+
const slug = createNamespaceDisplaySlug(config.namespace, getDisplayValues(input));
|
|
505
|
+
const entryHost = domain ? getGhostTunnelEntryHost(domain, config) : input.domain ? getGhostTunnelEntryHost(input.domain, config) : `${config.subdomain}.*`;
|
|
506
|
+
const url = `${protocol}://${slug}.${entryHost}/`;
|
|
507
|
+
if (!input.path) return url;
|
|
508
|
+
return `${url}${input.path.replace(/^\/+/, "")}`;
|
|
509
|
+
}
|
|
510
|
+
function createDisplayUrls(config, defaults) {
|
|
511
|
+
const domains = config.domains.length > 0 ? config.domains : defaults?.domain ? [defaults.domain] : [];
|
|
512
|
+
const urls = domains.length > 0 ? domains.map((domain) => createDisplayUrl(config, defaults, domain)) : [createDisplayUrl(config, defaults)];
|
|
513
|
+
return [...new Set(urls)];
|
|
514
|
+
}
|
|
515
|
+
function maybeConstructPreviewUrl(config, defaults) {
|
|
516
|
+
if (!config.preview) return void 0;
|
|
517
|
+
const input = getPreviewDefaults(config.preview, defaults);
|
|
518
|
+
if (!input.domain || !input.route || !input.project || !input.owner) return void 0;
|
|
519
|
+
return constructGhostTunnelUrl({
|
|
520
|
+
domain: input.domain,
|
|
521
|
+
route: input.route,
|
|
522
|
+
project: input.project,
|
|
523
|
+
owner: input.owner,
|
|
524
|
+
values: input.values,
|
|
525
|
+
...input.path ? { path: input.path } : {},
|
|
526
|
+
...input.protocol ? { protocol: input.protocol } : {},
|
|
527
|
+
ghostTunnel: config
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
function parseNamespaceSlug(slug, config) {
|
|
531
|
+
const parts = slug.split(config.separator);
|
|
532
|
+
if (parts.length < config.tags.length) return null;
|
|
533
|
+
if (parts.length !== config.tags.length && !config.spreadTag) return null;
|
|
534
|
+
const namespace = {};
|
|
535
|
+
const spreadIndex = config.spreadTag ? config.tags.indexOf(config.spreadTag) : -1;
|
|
536
|
+
const spreadWidth = spreadIndex >= 0 ? parts.length - config.tags.length + 1 : 1;
|
|
537
|
+
let partIndex = 0;
|
|
538
|
+
for (const [tagIndex, tag] of config.tags.entries()) {
|
|
539
|
+
const value = tagIndex === spreadIndex ? parts.slice(partIndex, partIndex + spreadWidth).join(config.separator) : parts[partIndex];
|
|
540
|
+
if (!value || !isValidHostLabel(value)) return null;
|
|
541
|
+
if (tagIndex !== spreadIndex && value.includes(config.separator)) return null;
|
|
542
|
+
namespace[tag] = value;
|
|
543
|
+
partIndex += tagIndex === spreadIndex ? spreadWidth : 1;
|
|
544
|
+
}
|
|
545
|
+
return namespace;
|
|
546
|
+
}
|
|
547
|
+
function resolveGhostTunnelConfig(options, defaults) {
|
|
548
|
+
if (options === false || typeof options === "undefined") {
|
|
549
|
+
return {
|
|
550
|
+
enabled: false,
|
|
551
|
+
mode: DEFAULT_GHOST_TUNNEL_MODE,
|
|
552
|
+
domains: [],
|
|
553
|
+
subdomain: DEFAULT_GHOST_TUNNEL_SUBDOMAIN,
|
|
554
|
+
namespace: resolveNamespaceConfig(void 0),
|
|
555
|
+
displayUrls: [],
|
|
556
|
+
requireHttps: true,
|
|
557
|
+
requireAuth: true
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
const config = typeof options === "string" ? { mode: options } : options;
|
|
561
|
+
const subdomain = config.subdomain ?? DEFAULT_GHOST_TUNNEL_SUBDOMAIN;
|
|
562
|
+
assertValidSubdomain(subdomain);
|
|
563
|
+
const domains = normalizeDomains(config.domains);
|
|
564
|
+
const enabled = config.enabled ?? true;
|
|
565
|
+
const resolved = {
|
|
566
|
+
enabled,
|
|
567
|
+
mode: parseGhostTunnelMode(config.mode),
|
|
568
|
+
domains,
|
|
569
|
+
subdomain,
|
|
570
|
+
namespace: resolveNamespaceConfig(config.namespace),
|
|
571
|
+
...config.preview ? { preview: config.preview } : {},
|
|
572
|
+
displayUrls: [],
|
|
573
|
+
requireHttps: config.requireHttps ?? true,
|
|
574
|
+
requireAuth: config.requireAuth ?? true
|
|
575
|
+
};
|
|
576
|
+
if (!enabled) {
|
|
577
|
+
return resolved;
|
|
578
|
+
}
|
|
579
|
+
const previewUrl = maybeConstructPreviewUrl(resolved, defaults);
|
|
580
|
+
const displayUrls = previewUrl ? [previewUrl] : createDisplayUrls(resolved, defaults);
|
|
581
|
+
return {
|
|
582
|
+
...resolved,
|
|
583
|
+
displayUrls,
|
|
584
|
+
...displayUrls[0] ? { displayUrl: displayUrls[0] } : {},
|
|
585
|
+
...previewUrl ? { previewUrl } : {}
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function getGhostTunnelEntryHost(domain, options = {}) {
|
|
589
|
+
const config = toGhostTunnelConfig(options);
|
|
590
|
+
const normalizedDomain = normalizeDomain(domain);
|
|
591
|
+
if (!normalizedDomain) {
|
|
592
|
+
throw new Error(`Invalid ghost tunnel domain: ${domain}`);
|
|
593
|
+
}
|
|
594
|
+
return `${config.subdomain}.${normalizedDomain}`;
|
|
595
|
+
}
|
|
596
|
+
function getGhostTunnelWildcardHost(domain, options = {}) {
|
|
597
|
+
return `*.${getGhostTunnelEntryHost(domain, options)}`;
|
|
598
|
+
}
|
|
599
|
+
function constructGhostTunnelHost(input) {
|
|
600
|
+
const config = toGhostTunnelConfig(input.ghostTunnel ?? {});
|
|
601
|
+
if (!config.enabled) {
|
|
602
|
+
throw new Error("Ghost tunnel is not enabled.");
|
|
603
|
+
}
|
|
604
|
+
const namespaceValues = {
|
|
605
|
+
route: input.route,
|
|
606
|
+
project: input.project,
|
|
607
|
+
owner: input.owner,
|
|
608
|
+
...input.values ?? {}
|
|
609
|
+
};
|
|
610
|
+
const slug = createNamespaceSlug(config.namespace, namespaceValues);
|
|
611
|
+
return `${slug}.${getGhostTunnelEntryHost(input.domain, config)}`;
|
|
612
|
+
}
|
|
613
|
+
function constructGhostTunnelUrl(input) {
|
|
614
|
+
const protocol = input.protocol ?? "https";
|
|
615
|
+
const host = constructGhostTunnelHost(input);
|
|
616
|
+
const url = new URL(`${protocol}://${host}/`);
|
|
617
|
+
if (input.path) {
|
|
618
|
+
url.pathname = `/${input.path.replace(/^\/+/, "")}`;
|
|
619
|
+
}
|
|
620
|
+
if (input.searchParams instanceof URLSearchParams) {
|
|
621
|
+
url.search = input.searchParams.toString();
|
|
622
|
+
} else if (input.searchParams) {
|
|
623
|
+
for (const [key, value] of Object.entries(input.searchParams)) {
|
|
624
|
+
if (typeof value !== "undefined" && value !== null) {
|
|
625
|
+
url.searchParams.set(key, String(value));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return url.toString();
|
|
630
|
+
}
|
|
631
|
+
var constructGhostTunnelURL = constructGhostTunnelUrl;
|
|
632
|
+
function getGhostTunnelDefaultDisplayUrl(options = {}, defaults) {
|
|
633
|
+
const config = toGhostTunnelConfig(options);
|
|
634
|
+
if (!config.enabled) return null;
|
|
635
|
+
return createDisplayUrl(config, defaults);
|
|
636
|
+
}
|
|
637
|
+
function getGhostTunnelDisplayUrl(options, defaults) {
|
|
638
|
+
const config = toGhostTunnelConfig(options);
|
|
639
|
+
if (!config.enabled) return null;
|
|
640
|
+
return config.displayUrl ?? config.previewUrl ?? getGhostTunnelDefaultDisplayUrl(config, defaults);
|
|
641
|
+
}
|
|
642
|
+
function getGhostTunnelDisplayUrls(options, defaults) {
|
|
643
|
+
const config = toGhostTunnelConfig(options);
|
|
644
|
+
if (!config.enabled) return [];
|
|
645
|
+
if (config.displayUrls.length > 0) return config.displayUrls;
|
|
646
|
+
const displayUrl = getGhostTunnelDisplayUrl(config, defaults);
|
|
647
|
+
return displayUrl ? [displayUrl] : [];
|
|
648
|
+
}
|
|
649
|
+
function getGhostTunnelPreviewUrl(options) {
|
|
650
|
+
const config = toGhostTunnelConfig(options);
|
|
651
|
+
if (!config.enabled) return null;
|
|
652
|
+
return config.previewUrl ?? maybeConstructPreviewUrl(config) ?? null;
|
|
653
|
+
}
|
|
654
|
+
function parseGhostTunnelHost(host, domain, options = {}) {
|
|
655
|
+
const config = toGhostTunnelConfig(options);
|
|
656
|
+
if (!config.enabled) return null;
|
|
657
|
+
const normalizedHost = normalizeDomain(host);
|
|
658
|
+
const normalizedDomain = normalizeDomain(domain);
|
|
659
|
+
if (!normalizedHost || !normalizedDomain) return null;
|
|
660
|
+
const entryHost = getGhostTunnelEntryHost(normalizedDomain, config);
|
|
661
|
+
const suffix = `.${entryHost}`;
|
|
662
|
+
if (!normalizedHost.endsWith(suffix)) return null;
|
|
663
|
+
const slug = normalizedHost.slice(0, -suffix.length);
|
|
664
|
+
if (!isValidHostLabel(slug)) return null;
|
|
665
|
+
const namespace = parseNamespaceSlug(slug, config.namespace);
|
|
666
|
+
if (!namespace) return null;
|
|
667
|
+
return {
|
|
668
|
+
host: normalizedHost,
|
|
669
|
+
slug,
|
|
670
|
+
namespace,
|
|
671
|
+
entryHost,
|
|
672
|
+
wildcardHost: `*.${entryHost}`,
|
|
673
|
+
domain: normalizedDomain
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function assertSecureGhostTunnelRequest(input) {
|
|
677
|
+
const config = toGhostTunnelConfig(input.ghostTunnel);
|
|
678
|
+
if (!config.enabled) {
|
|
679
|
+
throw new Error("Ghost tunnel is not enabled.");
|
|
680
|
+
}
|
|
681
|
+
if (config.requireHttps && input.protocol !== "https") {
|
|
682
|
+
throw new Error("Ghost tunnel requests must use HTTPS.");
|
|
683
|
+
}
|
|
684
|
+
if (config.requireAuth && input.authenticated !== true) {
|
|
685
|
+
throw new Error("Ghost tunnel requests must be authenticated.");
|
|
686
|
+
}
|
|
687
|
+
const route = parseGhostTunnelHost(input.host, input.domain, config);
|
|
688
|
+
if (!route) {
|
|
689
|
+
throw new Error(`Host is not a valid ghost tunnel host for ${input.domain}.`);
|
|
690
|
+
}
|
|
691
|
+
return route;
|
|
692
|
+
}
|
|
693
|
+
|
|
313
694
|
// src/context.ts
|
|
695
|
+
var LOCALGHOST_PROJECT_CONFIG_FILES = [
|
|
696
|
+
"localghost.config.mjs",
|
|
697
|
+
"localghost.config.js",
|
|
698
|
+
"localghost.config.cjs"
|
|
699
|
+
];
|
|
314
700
|
function parsePort(value) {
|
|
315
701
|
if (!value) return void 0;
|
|
316
702
|
const port = Number.parseInt(value, 10);
|
|
@@ -324,6 +710,30 @@ function envDynamicPort() {
|
|
|
324
710
|
if (!value) return void 0;
|
|
325
711
|
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
326
712
|
}
|
|
713
|
+
function envHttps() {
|
|
714
|
+
const value = process.env.LOCALGHOST_HTTPS;
|
|
715
|
+
if (!value) return void 0;
|
|
716
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
717
|
+
}
|
|
718
|
+
function getPackageName(cwd) {
|
|
719
|
+
try {
|
|
720
|
+
const pkg = JSON.parse(readFileSync4(join4(cwd, "package.json"), "utf8"));
|
|
721
|
+
return typeof pkg.name === "string" ? pkg.name : void 0;
|
|
722
|
+
} catch {
|
|
723
|
+
return void 0;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function getPackageOwner(cwd) {
|
|
727
|
+
const packageName = getPackageName(cwd);
|
|
728
|
+
if (!packageName?.startsWith("@")) return void 0;
|
|
729
|
+
return packageName.slice(1).split("/")[0];
|
|
730
|
+
}
|
|
731
|
+
function getLocalOwner(cwd) {
|
|
732
|
+
return sanitizeProjectName(process.env.LOCALGHOST_OWNER ?? getPackageOwner(cwd) ?? process.env.USER ?? process.env.USERNAME ?? "local");
|
|
733
|
+
}
|
|
734
|
+
function getRouteName(primaryHost, fallback) {
|
|
735
|
+
return sanitizeProjectName(primaryHost.split(".")[0] ?? fallback);
|
|
736
|
+
}
|
|
327
737
|
function readOptionsFromContext(options) {
|
|
328
738
|
return {
|
|
329
739
|
cwd: options.cwd ?? process.cwd(),
|
|
@@ -341,25 +751,71 @@ function withRuntimePort(entries, requestedPort, port) {
|
|
|
341
751
|
function uniqueHosts(entries) {
|
|
342
752
|
return [...new Set(entries.map((entry) => entry.host))];
|
|
343
753
|
}
|
|
754
|
+
function isAliasableHost(host) {
|
|
755
|
+
return host.includes(".") && !host.startsWith("www.") && !host.includes(":");
|
|
756
|
+
}
|
|
757
|
+
function getDefaultWwwAlias(host) {
|
|
758
|
+
return isAliasableHost(host) ? `www.${host}` : null;
|
|
759
|
+
}
|
|
760
|
+
function addDefaultWwwAliases(entries) {
|
|
761
|
+
const seen = new Set(entries.map((entry) => entry.host));
|
|
762
|
+
const aliases = [];
|
|
763
|
+
for (const entry of entries) {
|
|
764
|
+
const alias = getDefaultWwwAlias(entry.host);
|
|
765
|
+
if (alias && !seen.has(alias)) {
|
|
766
|
+
aliases.push({ host: alias, port: entry.port, target: `127.0.0.1:${entry.port}` });
|
|
767
|
+
seen.add(alias);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return [...entries, ...aliases];
|
|
771
|
+
}
|
|
772
|
+
function defined(input) {
|
|
773
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => typeof value !== "undefined"));
|
|
774
|
+
}
|
|
775
|
+
async function readLocalghostProjectConfig(options = {}) {
|
|
776
|
+
const cwd = options.cwd ?? process.cwd();
|
|
777
|
+
if (options.configFile === false) return { config: {} };
|
|
778
|
+
const candidates = options.configFile ? [options.configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
|
|
779
|
+
const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync3(candidate));
|
|
780
|
+
if (!path) return { config: {} };
|
|
781
|
+
const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
|
|
782
|
+
const config = imported.default ?? imported;
|
|
783
|
+
return { config, path };
|
|
784
|
+
}
|
|
344
785
|
function defineLocalghostConfig(config) {
|
|
345
786
|
return config;
|
|
346
787
|
}
|
|
347
788
|
async function resolveLocalghostContext(options = {}) {
|
|
348
789
|
const cwd = options.cwd ?? process.cwd();
|
|
349
|
-
const
|
|
790
|
+
const projectConfig = await readLocalghostProjectConfig({
|
|
791
|
+
cwd,
|
|
792
|
+
...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
|
|
793
|
+
});
|
|
794
|
+
const merged = {
|
|
795
|
+
...projectConfig.config,
|
|
796
|
+
...defined(options)
|
|
797
|
+
};
|
|
798
|
+
const readOptions = readOptionsFromContext({ ...merged, cwd });
|
|
350
799
|
const resolvedPath = resolveDevHostsPath(readOptions);
|
|
351
800
|
const configEntries = readDevHosts(readOptions);
|
|
352
|
-
const requestedPort =
|
|
353
|
-
const dynamicPort =
|
|
354
|
-
const bindHost =
|
|
801
|
+
const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
|
|
802
|
+
const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
|
|
803
|
+
const bindHost = merged.bindHost ?? "127.0.0.1";
|
|
355
804
|
const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
|
|
356
805
|
const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
|
|
357
|
-
const
|
|
806
|
+
const wwwAlias = merged.wwwAlias ?? true;
|
|
807
|
+
const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
|
|
358
808
|
const hosts = uniqueHosts(entries);
|
|
359
|
-
const primaryHost =
|
|
809
|
+
const primaryHost = merged.primaryHost ?? entries.find((entry) => entry.port === port)?.host ?? hosts[0] ?? `${sanitizeProjectName(getProjectName(cwd))}.localhost`;
|
|
810
|
+
const projectName = sanitizeProjectName(merged.project ?? getProjectName(cwd));
|
|
811
|
+
const ghostTunnel = resolveGhostTunnelConfig(merged.ghostTunnel, {
|
|
812
|
+
route: getRouteName(primaryHost, projectName),
|
|
813
|
+
project: projectName,
|
|
814
|
+
owner: getLocalOwner(cwd)
|
|
815
|
+
});
|
|
360
816
|
return {
|
|
361
817
|
cwd,
|
|
362
|
-
projectName
|
|
818
|
+
projectName,
|
|
363
819
|
readOptions,
|
|
364
820
|
configPath: resolvedPath.path,
|
|
365
821
|
configFileName: resolvedPath.fileName,
|
|
@@ -371,7 +827,10 @@ async function resolveLocalghostContext(options = {}) {
|
|
|
371
827
|
dynamicPort,
|
|
372
828
|
bindHost,
|
|
373
829
|
primaryHost,
|
|
374
|
-
https:
|
|
830
|
+
https: merged.https ?? envHttps() ?? false,
|
|
831
|
+
wwwAlias,
|
|
832
|
+
ghostTunnel,
|
|
833
|
+
...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
|
|
375
834
|
};
|
|
376
835
|
}
|
|
377
836
|
|
|
@@ -428,7 +887,7 @@ function getProductionEnvKeys() {
|
|
|
428
887
|
// src/hosts-file.ts
|
|
429
888
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
430
889
|
import { tmpdir } from "os";
|
|
431
|
-
import { join as
|
|
890
|
+
import { join as join5 } from "path";
|
|
432
891
|
import { execa as execa3 } from "execa";
|
|
433
892
|
function escapeRegExp(value) {
|
|
434
893
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -470,7 +929,7 @@ function removeManagedBlock(existing, projectName) {
|
|
|
470
929
|
}
|
|
471
930
|
async function writeSystemHostsFile(hostsPath, next, projectName) {
|
|
472
931
|
const sanitizedProjectName = sanitizeProjectName(projectName);
|
|
473
|
-
const tempPath =
|
|
932
|
+
const tempPath = join5(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
|
|
474
933
|
writeFileSync3(tempPath, next, "utf8");
|
|
475
934
|
if (process.platform === "win32") {
|
|
476
935
|
throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
|
|
@@ -503,11 +962,11 @@ async function removeSystemHosts(projectName) {
|
|
|
503
962
|
}
|
|
504
963
|
|
|
505
964
|
// src/init.ts
|
|
506
|
-
import { existsSync as
|
|
507
|
-
import { join as
|
|
965
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
966
|
+
import { join as join6 } from "path";
|
|
508
967
|
function detectPackageManager(cwd = process.cwd()) {
|
|
509
|
-
if (
|
|
510
|
-
if (
|
|
968
|
+
if (existsSync4(join6(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
969
|
+
if (existsSync4(join6(cwd, "yarn.lock"))) return "yarn";
|
|
511
970
|
return "npm";
|
|
512
971
|
}
|
|
513
972
|
function packageRunCommand(packageManager, script) {
|
|
@@ -532,7 +991,7 @@ function renderConfig(options) {
|
|
|
532
991
|
}
|
|
533
992
|
function readPackageJson(path) {
|
|
534
993
|
try {
|
|
535
|
-
return JSON.parse(
|
|
994
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
536
995
|
} catch {
|
|
537
996
|
return null;
|
|
538
997
|
}
|
|
@@ -556,6 +1015,7 @@ function updatePackageScripts(packageJsonPath, configFile) {
|
|
|
556
1015
|
"localghost:proxy:https": scripts["localghost:proxy:https"] ?? `localghost dev${configFlag} --https`,
|
|
557
1016
|
"localghost:run": scripts["localghost:run"] ?? `localghost run${configFlag} --`,
|
|
558
1017
|
"localghost:ready": scripts["localghost:ready"] ?? `localghost status${configFlag} --ready`,
|
|
1018
|
+
"localghost:trust": scripts["localghost:trust"] ?? `localghost trust${configFlag}`,
|
|
559
1019
|
"localghost:ps": scripts["localghost:ps"] ?? "localghost ps",
|
|
560
1020
|
"localghost:print": scripts["localghost:print"] ?? `localghost print${configFlag}`,
|
|
561
1021
|
"localghost:routes": scripts["localghost:routes"] ?? `localghost routes${configFlag}`,
|
|
@@ -583,8 +1043,8 @@ function initLocalghost(options = {}) {
|
|
|
583
1043
|
const apiPort = options.apiPort ?? 8787;
|
|
584
1044
|
const packageManager = options.packageManager ?? detectPackageManager(cwd);
|
|
585
1045
|
const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
|
|
586
|
-
const configPath =
|
|
587
|
-
const configExists =
|
|
1046
|
+
const configPath = join6(cwd, configFile);
|
|
1047
|
+
const configExists = existsSync4(configPath);
|
|
588
1048
|
if (configExists && !options.force) {
|
|
589
1049
|
return {
|
|
590
1050
|
configPath,
|
|
@@ -600,12 +1060,12 @@ function initLocalghost(options = {}) {
|
|
|
600
1060
|
};
|
|
601
1061
|
}
|
|
602
1062
|
writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
|
|
603
|
-
const packageJsonPath =
|
|
1063
|
+
const packageJsonPath = join6(cwd, "package.json");
|
|
604
1064
|
const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
|
|
605
1065
|
return {
|
|
606
1066
|
configPath,
|
|
607
1067
|
configCreated: true,
|
|
608
|
-
...
|
|
1068
|
+
...existsSync4(packageJsonPath) ? { packageJsonPath } : {},
|
|
609
1069
|
packageJsonChanged,
|
|
610
1070
|
packageManager,
|
|
611
1071
|
nextSteps: [
|
|
@@ -617,7 +1077,277 @@ function initLocalghost(options = {}) {
|
|
|
617
1077
|
};
|
|
618
1078
|
}
|
|
619
1079
|
|
|
1080
|
+
// src/relay.ts
|
|
1081
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
1082
|
+
import { domainToASCII as domainToASCII2 } from "url";
|
|
1083
|
+
var DEFAULT_RELAY_ALLOWED_TARGET_HOSTS = ["localhost", "127.0.0.1", "::1"];
|
|
1084
|
+
var DEFAULT_RELAY_BLOCKED_PORTS = [22, 2375, 2376, 5432, 6379, 9200, 9229, 27017];
|
|
1085
|
+
var DEFAULT_RELAY_LIMITS = {
|
|
1086
|
+
requestBodyBytes: 5 * 1024 * 1024,
|
|
1087
|
+
responseBytes: 25 * 1024 * 1024,
|
|
1088
|
+
timeoutMs: 3e4,
|
|
1089
|
+
maxConcurrentRequests: 20,
|
|
1090
|
+
perRouteRequestsPerMinute: 120,
|
|
1091
|
+
perIpRequestsPerMinute: 60
|
|
1092
|
+
};
|
|
1093
|
+
var DEFAULT_RELAY_TARGET_POLICY = {
|
|
1094
|
+
allowedHosts: [...DEFAULT_RELAY_ALLOWED_TARGET_HOSTS],
|
|
1095
|
+
blockedPorts: [...DEFAULT_RELAY_BLOCKED_PORTS],
|
|
1096
|
+
allowPrivateNetworkTargets: false
|
|
1097
|
+
};
|
|
1098
|
+
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
1099
|
+
"connection",
|
|
1100
|
+
"keep-alive",
|
|
1101
|
+
"proxy-authenticate",
|
|
1102
|
+
"proxy-authorization",
|
|
1103
|
+
"te",
|
|
1104
|
+
"trailer",
|
|
1105
|
+
"transfer-encoding",
|
|
1106
|
+
"upgrade"
|
|
1107
|
+
]);
|
|
1108
|
+
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie"]);
|
|
1109
|
+
var TOKEN_QUERY_PATTERN = /(token|secret|key|password|session|jwt|auth)/i;
|
|
1110
|
+
var HOST_PATTERN2 = /^(?=.{1,253}$)(?!-)[a-z0-9-]+(\.[a-z0-9-]+)*$/i;
|
|
1111
|
+
var IPV4_PATTERN = /^\d{1,3}(?:\.\d{1,3}){3}$/;
|
|
1112
|
+
function base64UrlEncode(value) {
|
|
1113
|
+
return Buffer.from(value).toString("base64url");
|
|
1114
|
+
}
|
|
1115
|
+
function base64UrlDecode(value) {
|
|
1116
|
+
return Buffer.from(value, "base64url").toString("utf8");
|
|
1117
|
+
}
|
|
1118
|
+
function signPayload(payload, secret) {
|
|
1119
|
+
return createHmac("sha256", secret).update(payload).digest("base64url");
|
|
1120
|
+
}
|
|
1121
|
+
function secureEqual(left, right) {
|
|
1122
|
+
const leftBuffer = Buffer.from(left);
|
|
1123
|
+
const rightBuffer = Buffer.from(right);
|
|
1124
|
+
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
|
1125
|
+
}
|
|
1126
|
+
function normalizeHost(host) {
|
|
1127
|
+
const trimmed = host.trim().toLowerCase().replace(/\.$/, "");
|
|
1128
|
+
if (!trimmed || trimmed.includes("*") || trimmed.includes("/") || trimmed.includes(":")) return null;
|
|
1129
|
+
const ascii = domainToASCII2(trimmed);
|
|
1130
|
+
if (!ascii || ascii.includes("..")) return null;
|
|
1131
|
+
return HOST_PATTERN2.test(ascii) ? ascii : null;
|
|
1132
|
+
}
|
|
1133
|
+
function normalizeTargetHost(host) {
|
|
1134
|
+
const trimmed = host.trim().toLowerCase();
|
|
1135
|
+
if (trimmed === "::1" || trimmed === "[::1]") return "::1";
|
|
1136
|
+
if (trimmed.includes("/") || trimmed.includes("*")) return null;
|
|
1137
|
+
if (IPV4_PATTERN.test(trimmed)) return isValidIpv4(trimmed) ? trimmed : null;
|
|
1138
|
+
return normalizeHost(trimmed);
|
|
1139
|
+
}
|
|
1140
|
+
function isValidIpv4(value) {
|
|
1141
|
+
return value.split(".").every((part) => {
|
|
1142
|
+
const octet = Number(part);
|
|
1143
|
+
return Number.isInteger(octet) && octet >= 0 && octet <= 255 && String(octet) === part;
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
function isPrivateIpv4(value) {
|
|
1147
|
+
if (!isValidIpv4(value)) return false;
|
|
1148
|
+
const [first = 0, second = 0] = value.split(".").map((part) => Number(part));
|
|
1149
|
+
return first === 10 || first === 127 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 169 && second === 254;
|
|
1150
|
+
}
|
|
1151
|
+
function isLocalTargetHost(host) {
|
|
1152
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
1153
|
+
}
|
|
1154
|
+
function mergeTargetPolicy(policy) {
|
|
1155
|
+
return {
|
|
1156
|
+
allowedHosts: policy?.allowedHosts ?? DEFAULT_RELAY_TARGET_POLICY.allowedHosts,
|
|
1157
|
+
blockedPorts: policy?.blockedPorts ?? DEFAULT_RELAY_TARGET_POLICY.blockedPorts,
|
|
1158
|
+
allowPrivateNetworkTargets: policy?.allowPrivateNetworkTargets ?? DEFAULT_RELAY_TARGET_POLICY.allowPrivateNetworkTargets
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
function mergeLimits(limits) {
|
|
1162
|
+
const merged = {
|
|
1163
|
+
...DEFAULT_RELAY_LIMITS,
|
|
1164
|
+
...limits ?? {}
|
|
1165
|
+
};
|
|
1166
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
1167
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
1168
|
+
throw new Error(`Invalid relay limit ${key}: ${value}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return merged;
|
|
1172
|
+
}
|
|
1173
|
+
function assertExactRelayHost(host) {
|
|
1174
|
+
const normalized = normalizeHost(host);
|
|
1175
|
+
if (!normalized) {
|
|
1176
|
+
throw new Error(`Relay route claims must use an exact hostname: ${host}`);
|
|
1177
|
+
}
|
|
1178
|
+
return normalized;
|
|
1179
|
+
}
|
|
1180
|
+
function assertRelayLocalTarget(target, policyInput) {
|
|
1181
|
+
if (!target || typeof target !== "object") {
|
|
1182
|
+
throw new Error("Relay target must be an explicit local target object.");
|
|
1183
|
+
}
|
|
1184
|
+
const policy = mergeTargetPolicy(policyInput);
|
|
1185
|
+
const host = normalizeTargetHost(target.host);
|
|
1186
|
+
if (!host) {
|
|
1187
|
+
throw new Error(`Invalid relay target host: ${target.host}`);
|
|
1188
|
+
}
|
|
1189
|
+
if (!Number.isInteger(target.port) || target.port < 1 || target.port > 65535) {
|
|
1190
|
+
throw new Error(`Invalid relay target port: ${target.port}`);
|
|
1191
|
+
}
|
|
1192
|
+
const protocol = target.protocol ?? "http";
|
|
1193
|
+
if (protocol !== "http" && protocol !== "https") {
|
|
1194
|
+
throw new Error(`Invalid relay target protocol: ${String(protocol)}`);
|
|
1195
|
+
}
|
|
1196
|
+
if (policy.blockedPorts.includes(target.port)) {
|
|
1197
|
+
throw new Error(`Relay target port is blocked: ${target.port}`);
|
|
1198
|
+
}
|
|
1199
|
+
const allowedHosts = new Set(policy.allowedHosts.map((allowedHost) => normalizeTargetHost(allowedHost)).filter((value) => Boolean(value)));
|
|
1200
|
+
if (!allowedHosts.has(host)) {
|
|
1201
|
+
throw new Error(`Relay target host is not explicitly allowed: ${host}`);
|
|
1202
|
+
}
|
|
1203
|
+
if (!policy.allowPrivateNetworkTargets && !isLocalTargetHost(host) && (host === "::1" || isPrivateIpv4(host))) {
|
|
1204
|
+
throw new Error(`Private-network relay target requires explicit opt-in: ${host}`);
|
|
1205
|
+
}
|
|
1206
|
+
return {
|
|
1207
|
+
protocol,
|
|
1208
|
+
host,
|
|
1209
|
+
port: target.port
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
function authenticateRelayAgentToken(input) {
|
|
1213
|
+
const expected = `Bearer ${input.agentToken}`;
|
|
1214
|
+
return typeof input.authorizationHeader === "string" && secureEqual(input.authorizationHeader, expected);
|
|
1215
|
+
}
|
|
1216
|
+
function signRelayRouteClaim(claim, signingSecret) {
|
|
1217
|
+
const payload = {
|
|
1218
|
+
...claim,
|
|
1219
|
+
host: assertExactRelayHost(claim.host)
|
|
1220
|
+
};
|
|
1221
|
+
if (!payload.scope) throw new Error("Relay route claim requires a scope.");
|
|
1222
|
+
if (!payload.agentId) throw new Error("Relay route claim requires an agentId.");
|
|
1223
|
+
if (Number.isNaN(Date.parse(payload.expiresAt))) throw new Error("Relay route claim requires a valid expiresAt.");
|
|
1224
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
1225
|
+
const signature = signPayload(encodedPayload, signingSecret);
|
|
1226
|
+
return {
|
|
1227
|
+
payload,
|
|
1228
|
+
token: `${encodedPayload}.${signature}`
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
function verifyRelayRouteClaim(token, signingSecret, options) {
|
|
1232
|
+
const [encodedPayload, signature] = token.split(".");
|
|
1233
|
+
if (!encodedPayload || !signature || token.split(".").length !== 2) {
|
|
1234
|
+
throw new Error("Invalid relay route claim token.");
|
|
1235
|
+
}
|
|
1236
|
+
const expectedSignature = signPayload(encodedPayload, signingSecret);
|
|
1237
|
+
if (!secureEqual(signature, expectedSignature)) {
|
|
1238
|
+
throw new Error("Invalid relay route claim signature.");
|
|
1239
|
+
}
|
|
1240
|
+
const parsed = JSON.parse(base64UrlDecode(encodedPayload));
|
|
1241
|
+
const host = assertExactRelayHost(parsed.host);
|
|
1242
|
+
if (parsed.scope !== options.expectedScope) {
|
|
1243
|
+
throw new Error("Relay route claim scope mismatch.");
|
|
1244
|
+
}
|
|
1245
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1246
|
+
if (Date.parse(parsed.expiresAt) <= now.getTime()) {
|
|
1247
|
+
throw new Error("Relay route claim has expired.");
|
|
1248
|
+
}
|
|
1249
|
+
if (!parsed.agentId) {
|
|
1250
|
+
throw new Error("Relay route claim requires an agentId.");
|
|
1251
|
+
}
|
|
1252
|
+
return { ...parsed, host };
|
|
1253
|
+
}
|
|
1254
|
+
function createRelayRouteRegistration(input) {
|
|
1255
|
+
if (!authenticateRelayAgentToken({
|
|
1256
|
+
agentToken: input.agentToken,
|
|
1257
|
+
...typeof input.authorizationHeader !== "undefined" ? { authorizationHeader: input.authorizationHeader } : {}
|
|
1258
|
+
})) {
|
|
1259
|
+
throw new Error("Relay route registration requires an authenticated local agent.");
|
|
1260
|
+
}
|
|
1261
|
+
const claim = verifyRelayRouteClaim(input.claimToken, input.signingSecret, {
|
|
1262
|
+
expectedScope: input.expectedScope,
|
|
1263
|
+
...input.now ? { now: input.now } : {}
|
|
1264
|
+
});
|
|
1265
|
+
const target = assertRelayLocalTarget(input.target, input.targetPolicy);
|
|
1266
|
+
const access = input.publicMode === true ? "public" : input.access ?? "private";
|
|
1267
|
+
const passwordProtected = input.passwordProtected ?? false;
|
|
1268
|
+
const authRequired = input.authRequired ?? false;
|
|
1269
|
+
if (access === "public" && input.publicMode !== true) {
|
|
1270
|
+
throw new Error("Relay public mode must be explicitly enabled.");
|
|
1271
|
+
}
|
|
1272
|
+
if (access === "private" && !passwordProtected && !authRequired) {
|
|
1273
|
+
throw new Error("Private relay previews require password or auth.");
|
|
1274
|
+
}
|
|
1275
|
+
return {
|
|
1276
|
+
host: claim.host,
|
|
1277
|
+
scope: claim.scope,
|
|
1278
|
+
agentId: claim.agentId,
|
|
1279
|
+
expiresAt: claim.expiresAt,
|
|
1280
|
+
target,
|
|
1281
|
+
access,
|
|
1282
|
+
passwordProtected,
|
|
1283
|
+
authRequired,
|
|
1284
|
+
limits: mergeLimits(input.limits)
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
function isRelayRouteActive(route, options) {
|
|
1288
|
+
if (!options.agentConnected) return false;
|
|
1289
|
+
return Date.parse(route.expiresAt) > (options.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
1290
|
+
}
|
|
1291
|
+
function stripRelayForwardHeaders(headers) {
|
|
1292
|
+
const stripped = {};
|
|
1293
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1294
|
+
if (typeof value === "undefined") continue;
|
|
1295
|
+
const lowerName = name.toLowerCase();
|
|
1296
|
+
if (HOP_BY_HOP_HEADERS.has(lowerName)) continue;
|
|
1297
|
+
if (lowerName.startsWith("x-localghost-")) continue;
|
|
1298
|
+
stripped[name] = value;
|
|
1299
|
+
}
|
|
1300
|
+
return stripped;
|
|
1301
|
+
}
|
|
1302
|
+
function redactRelayHeaders(headers) {
|
|
1303
|
+
const redacted = {};
|
|
1304
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1305
|
+
if (typeof value === "undefined") continue;
|
|
1306
|
+
redacted[name] = SENSITIVE_HEADERS.has(name.toLowerCase()) ? "[redacted]" : value;
|
|
1307
|
+
}
|
|
1308
|
+
return redacted;
|
|
1309
|
+
}
|
|
1310
|
+
function redactRelayLogUrl(input) {
|
|
1311
|
+
const url = new URL(input, "http://localghost.invalid");
|
|
1312
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
1313
|
+
if (TOKEN_QUERY_PATTERN.test(key)) {
|
|
1314
|
+
url.searchParams.set(key, "[redacted]");
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return input.startsWith("http://") || input.startsWith("https://") ? url.toString() : `${url.pathname}${url.search}`;
|
|
1318
|
+
}
|
|
1319
|
+
function renderRelayOfflineResponse() {
|
|
1320
|
+
return {
|
|
1321
|
+
status: 503,
|
|
1322
|
+
headers: {
|
|
1323
|
+
"content-type": "text/html; charset=utf-8",
|
|
1324
|
+
"cache-control": "no-store"
|
|
1325
|
+
},
|
|
1326
|
+
body: [
|
|
1327
|
+
"<!doctype html>",
|
|
1328
|
+
"<html>",
|
|
1329
|
+
'<head><meta charset="utf-8"><title>Preview offline</title></head>',
|
|
1330
|
+
"<body><h1>Preview offline</h1><p>The local agent is not connected. Try again later.</p></body>",
|
|
1331
|
+
"</html>"
|
|
1332
|
+
].join("")
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
|
|
620
1336
|
// src/routes.ts
|
|
1337
|
+
var ansi = {
|
|
1338
|
+
cyan: "\x1B[36m",
|
|
1339
|
+
dim: "\x1B[2m",
|
|
1340
|
+
green: "\x1B[32m",
|
|
1341
|
+
reset: "\x1B[0m",
|
|
1342
|
+
yellow: "\x1B[33m"
|
|
1343
|
+
};
|
|
1344
|
+
function colorize(value, color, enabled) {
|
|
1345
|
+
return enabled ? `${color}${value}${ansi.reset}` : value;
|
|
1346
|
+
}
|
|
1347
|
+
function colorizeUrl(value, enabled) {
|
|
1348
|
+
if (!enabled) return value;
|
|
1349
|
+
return colorize(value.replace(/\*/g, `${ansi.yellow}*${ansi.cyan}`), ansi.cyan, enabled);
|
|
1350
|
+
}
|
|
621
1351
|
function getDomainRoutes(entries, options = {}) {
|
|
622
1352
|
const protocol = options.https === true ? "https" : "http";
|
|
623
1353
|
return [...entries].sort((left, right) => left.host.localeCompare(right.host) || left.port - right.port).map((entry) => ({
|
|
@@ -637,32 +1367,64 @@ function formatDomainRoutes(entries, options = {}) {
|
|
|
637
1367
|
...routes.map((route) => ` ${route.url} -> ${route.upstream}`)
|
|
638
1368
|
].join("\n");
|
|
639
1369
|
}
|
|
1370
|
+
function formatGhostTunnel(config, options = {}) {
|
|
1371
|
+
if (!config.enabled) return null;
|
|
1372
|
+
const color = options.color === true;
|
|
1373
|
+
const label = options.label ?? "expected";
|
|
1374
|
+
const labelColor = label === "running" ? ansi.green : ansi.dim;
|
|
1375
|
+
const lines = [
|
|
1376
|
+
"localghost ghost tunnel",
|
|
1377
|
+
` mode: ${config.mode}`
|
|
1378
|
+
];
|
|
1379
|
+
const urls = config.displayUrls.length > 0 ? config.displayUrls : config.displayUrl ? [config.displayUrl] : [];
|
|
1380
|
+
if (urls.length === 0) {
|
|
1381
|
+
lines.push(` ${label}: unavailable`);
|
|
1382
|
+
} else if (urls.length === 1) {
|
|
1383
|
+
lines.push(` ${colorize(label, labelColor, color)}: ${colorizeUrl(urls[0], color)}`);
|
|
1384
|
+
} else {
|
|
1385
|
+
lines.push(` ${colorize(label, labelColor, color)}:`);
|
|
1386
|
+
for (const url of urls) {
|
|
1387
|
+
lines.push(` ${colorizeUrl(url, color)}`);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
if (options.verbose) {
|
|
1391
|
+
lines.push(` domains: ${config.domains.length > 0 ? config.domains.join(", ") : "*"}`);
|
|
1392
|
+
lines.push(` access: ${config.requireAuth ? "auth required" : "app decides"}`);
|
|
1393
|
+
lines.push(` transport: ${config.requireHttps ? "https required" : "http allowed"}`);
|
|
1394
|
+
}
|
|
1395
|
+
return lines.join("\n");
|
|
1396
|
+
}
|
|
640
1397
|
|
|
641
1398
|
// src/state.ts
|
|
642
|
-
import { existsSync as
|
|
643
|
-
import { join as
|
|
1399
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1400
|
+
import { join as join7 } from "path";
|
|
644
1401
|
var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
|
|
645
1402
|
function getLocalghostStatePath(cwd = process.cwd()) {
|
|
646
|
-
return
|
|
1403
|
+
return join7(cwd, LOCALGHOST_STATE_FILE);
|
|
647
1404
|
}
|
|
648
1405
|
function readLocalghostState(cwd = process.cwd()) {
|
|
649
1406
|
const path = getLocalghostStatePath(cwd);
|
|
650
|
-
if (!
|
|
1407
|
+
if (!existsSync5(path)) return null;
|
|
651
1408
|
return JSON.parse(readTextFile(path));
|
|
652
1409
|
}
|
|
653
1410
|
function writeLocalghostState(cwd, state) {
|
|
654
1411
|
const path = getLocalghostStatePath(cwd);
|
|
655
|
-
writeTextFile(path, `${JSON.stringify({ version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1412
|
+
writeTextFile(path, `${JSON.stringify({ ...state, version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
656
1413
|
`);
|
|
657
1414
|
return path;
|
|
658
1415
|
}
|
|
1416
|
+
function patchLocalghostState(cwd, patch) {
|
|
1417
|
+
const current = readLocalghostState(cwd);
|
|
1418
|
+
if (!current) return null;
|
|
1419
|
+
return writeLocalghostState(cwd, { ...current, ...patch });
|
|
1420
|
+
}
|
|
659
1421
|
|
|
660
1422
|
// src/update-check.ts
|
|
661
|
-
import { existsSync as
|
|
1423
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
662
1424
|
import { homedir as homedir2 } from "os";
|
|
663
|
-
import { dirname as dirname4, join as
|
|
1425
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
664
1426
|
var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
|
|
665
|
-
var LOCALGHOST_VERSION = "0.1.
|
|
1427
|
+
var LOCALGHOST_VERSION = "0.1.9";
|
|
666
1428
|
var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
667
1429
|
var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
668
1430
|
var UPDATE_CHECK_TIMEOUT_MS = 900;
|
|
@@ -674,13 +1436,13 @@ function isUpdateCheckDisabled(env = process.env) {
|
|
|
674
1436
|
}
|
|
675
1437
|
function getUpdateCheckCachePath(env = process.env) {
|
|
676
1438
|
if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
|
|
677
|
-
const cacheRoot = env.XDG_CACHE_HOME ||
|
|
678
|
-
return
|
|
1439
|
+
const cacheRoot = env.XDG_CACHE_HOME || join8(homedir2(), ".cache");
|
|
1440
|
+
return join8(cacheRoot, "localghost", "update-check.json");
|
|
679
1441
|
}
|
|
680
1442
|
function readCache(path = getUpdateCheckCachePath()) {
|
|
681
|
-
if (!
|
|
1443
|
+
if (!existsSync6(path)) return null;
|
|
682
1444
|
try {
|
|
683
|
-
return JSON.parse(
|
|
1445
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
684
1446
|
} catch {
|
|
685
1447
|
return null;
|
|
686
1448
|
}
|
|
@@ -824,6 +1586,10 @@ ${message}`);
|
|
|
824
1586
|
markUpdateNotified(result, cachePath);
|
|
825
1587
|
}
|
|
826
1588
|
export {
|
|
1589
|
+
DEFAULT_RELAY_ALLOWED_TARGET_HOSTS,
|
|
1590
|
+
DEFAULT_RELAY_BLOCKED_PORTS,
|
|
1591
|
+
DEFAULT_RELAY_LIMITS,
|
|
1592
|
+
DEFAULT_RELAY_TARGET_POLICY,
|
|
827
1593
|
LOCALGHOST_ACTIVITY_VERSION,
|
|
828
1594
|
LOCALGHOST_CONFIG_FILE,
|
|
829
1595
|
LOCALGHOST_PACKAGE_NAME,
|
|
@@ -832,20 +1598,35 @@ export {
|
|
|
832
1598
|
UPDATE_CHECK_CACHE_TTL_MS,
|
|
833
1599
|
UPDATE_CHECK_NOTIFY_TTL_MS,
|
|
834
1600
|
UPDATE_CHECK_TIMEOUT_MS,
|
|
1601
|
+
assertExactRelayHost,
|
|
835
1602
|
assertLocalDevelopment,
|
|
1603
|
+
assertRelayLocalTarget,
|
|
1604
|
+
assertSecureGhostTunnelRequest,
|
|
1605
|
+
authenticateRelayAgentToken,
|
|
836
1606
|
checkCaddy,
|
|
837
1607
|
checkForUpdate,
|
|
838
1608
|
compareVersions,
|
|
1609
|
+
constructGhostTunnelHost,
|
|
1610
|
+
constructGhostTunnelURL,
|
|
1611
|
+
constructGhostTunnelUrl,
|
|
1612
|
+
createRelayRouteRegistration,
|
|
839
1613
|
defineLocalghostConfig,
|
|
840
1614
|
detectPackageManager,
|
|
841
1615
|
findAvailablePort,
|
|
842
1616
|
findLocalMdnsHosts,
|
|
843
1617
|
formatDomainRoutes,
|
|
1618
|
+
formatGhostTunnel,
|
|
844
1619
|
formatUpdateMessage,
|
|
845
1620
|
getCaddyfilePath,
|
|
846
1621
|
getConfigFileCandidates,
|
|
847
1622
|
getDevHostsPath,
|
|
848
1623
|
getDomainRoutes,
|
|
1624
|
+
getGhostTunnelDefaultDisplayUrl,
|
|
1625
|
+
getGhostTunnelDisplayUrl,
|
|
1626
|
+
getGhostTunnelDisplayUrls,
|
|
1627
|
+
getGhostTunnelEntryHost,
|
|
1628
|
+
getGhostTunnelPreviewUrl,
|
|
1629
|
+
getGhostTunnelWildcardHost,
|
|
849
1630
|
getLocalghostActivityPath,
|
|
850
1631
|
getLocalghostStatePath,
|
|
851
1632
|
getProductionEnvKeys,
|
|
@@ -858,33 +1639,48 @@ export {
|
|
|
858
1639
|
isPortAvailable,
|
|
859
1640
|
isProcessRunning,
|
|
860
1641
|
isProductionLike,
|
|
1642
|
+
isRelayRouteActive,
|
|
861
1643
|
isUpdateCheckDisabled,
|
|
862
1644
|
listLocalghostRuns,
|
|
1645
|
+
listLocalghostSetups,
|
|
863
1646
|
markUpdateNotified,
|
|
864
1647
|
maybeNotifyAboutUpdate,
|
|
865
1648
|
packageAddCommand,
|
|
866
1649
|
packageRunCommand,
|
|
867
1650
|
parseDevHosts,
|
|
1651
|
+
parseGhostTunnelHost,
|
|
1652
|
+
patchLocalghostState,
|
|
868
1653
|
pruneLocalghostActivity,
|
|
869
1654
|
readDevHosts,
|
|
870
1655
|
readLocalghostActivity,
|
|
1656
|
+
readLocalghostProjectConfig,
|
|
871
1657
|
readLocalghostState,
|
|
1658
|
+
redactRelayHeaders,
|
|
1659
|
+
redactRelayLogUrl,
|
|
872
1660
|
registerLocalghostRun,
|
|
1661
|
+
registerLocalghostSetup,
|
|
873
1662
|
removeManagedBlock,
|
|
874
1663
|
removeSystemHosts,
|
|
875
1664
|
renderCaddyfile,
|
|
876
1665
|
renderHostsBlock,
|
|
1666
|
+
renderRelayOfflineResponse,
|
|
877
1667
|
resolveDevHostsPath,
|
|
1668
|
+
resolveGhostTunnelConfig,
|
|
878
1669
|
resolveLocalghostContext,
|
|
879
1670
|
runCaddy,
|
|
880
1671
|
runDoctor,
|
|
881
1672
|
sanitizeProjectName,
|
|
882
1673
|
shouldNotifyAboutUpdate,
|
|
1674
|
+
signRelayRouteClaim,
|
|
883
1675
|
startCaddy,
|
|
1676
|
+
stripRelayForwardHeaders,
|
|
1677
|
+
trustCaddy,
|
|
884
1678
|
unregisterLocalghostRun,
|
|
1679
|
+
unregisterLocalghostSetup,
|
|
885
1680
|
updateSystemHosts,
|
|
886
1681
|
upsertManagedBlock,
|
|
887
1682
|
validateCaddyfile,
|
|
1683
|
+
verifyRelayRouteClaim,
|
|
888
1684
|
writeCaddyfile,
|
|
889
1685
|
writeLocalghostActivity,
|
|
890
1686
|
writeLocalghostState
|