@hamedb89/localghost 0.1.8 → 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/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
- writeLocalghostActivity({ version: LOCALGHOST_ACTIVITY_VERSION, runs: [...current, record] }, path);
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
 
@@ -291,7 +328,8 @@ async function trustCaddy(path) {
291
328
  }
292
329
 
293
330
  // src/context.ts
294
- import { existsSync as existsSync3 } from "fs";
331
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
332
+ import { join as join4 } from "path";
295
333
  import { pathToFileURL } from "url";
296
334
 
297
335
  // src/port.ts
@@ -320,6 +358,339 @@ async function findAvailablePort(startPort, options = {}) {
320
358
  throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}.`);
321
359
  }
322
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
+
323
694
  // src/context.ts
324
695
  var LOCALGHOST_PROJECT_CONFIG_FILES = [
325
696
  "localghost.config.mjs",
@@ -344,6 +715,25 @@ function envHttps() {
344
715
  if (!value) return void 0;
345
716
  return ["1", "true", "yes", "on"].includes(value.toLowerCase());
346
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
+ }
347
737
  function readOptionsFromContext(options) {
348
738
  return {
349
739
  cwd: options.cwd ?? process.cwd(),
@@ -382,11 +772,12 @@ function addDefaultWwwAliases(entries) {
382
772
  function defined(input) {
383
773
  return Object.fromEntries(Object.entries(input).filter(([, value]) => typeof value !== "undefined"));
384
774
  }
385
- async function readProjectConfig(cwd, configFile) {
386
- if (configFile === false) return {};
387
- const candidates = configFile ? [configFile] : LOCALGHOST_PROJECT_CONFIG_FILES;
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;
388
779
  const path = candidates.map((candidate) => resolveDevHostsPath({ cwd, fileName: candidate }).path).find((candidate) => existsSync3(candidate));
389
- if (!path) return {};
780
+ if (!path) return { config: {} };
390
781
  const imported = await import(`${pathToFileURL(path).href}?localghost=${Date.now()}`);
391
782
  const config = imported.default ?? imported;
392
783
  return { config, path };
@@ -396,7 +787,10 @@ function defineLocalghostConfig(config) {
396
787
  }
397
788
  async function resolveLocalghostContext(options = {}) {
398
789
  const cwd = options.cwd ?? process.cwd();
399
- const projectConfig = await readProjectConfig(cwd, options.localghostConfig);
790
+ const projectConfig = await readLocalghostProjectConfig({
791
+ cwd,
792
+ ...typeof options.localghostConfig !== "undefined" ? { configFile: options.localghostConfig } : {}
793
+ });
400
794
  const merged = {
401
795
  ...projectConfig.config,
402
796
  ...defined(options)
@@ -405,7 +799,7 @@ async function resolveLocalghostContext(options = {}) {
405
799
  const resolvedPath = resolveDevHostsPath(readOptions);
406
800
  const configEntries = readDevHosts(readOptions);
407
801
  const requestedPort = merged.port ?? envPort() ?? configEntries[0]?.port ?? 5173;
408
- const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? false;
802
+ const dynamicPort = merged.dynamicPort ?? envDynamicPort() ?? true;
409
803
  const bindHost = merged.bindHost ?? "127.0.0.1";
410
804
  const probeHost = typeof bindHost === "string" ? bindHost : "127.0.0.1";
411
805
  const port = dynamicPort ? await findAvailablePort(requestedPort, { host: probeHost }) : requestedPort;
@@ -413,9 +807,15 @@ async function resolveLocalghostContext(options = {}) {
413
807
  const entries = wwwAlias ? addDefaultWwwAliases(withRuntimePort(configEntries, requestedPort, port)) : withRuntimePort(configEntries, requestedPort, port);
414
808
  const hosts = uniqueHosts(entries);
415
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
+ });
416
816
  return {
417
817
  cwd,
418
- projectName: sanitizeProjectName(merged.project ?? getProjectName(cwd)),
818
+ projectName,
419
819
  readOptions,
420
820
  configPath: resolvedPath.path,
421
821
  configFileName: resolvedPath.fileName,
@@ -429,6 +829,7 @@ async function resolveLocalghostContext(options = {}) {
429
829
  primaryHost,
430
830
  https: merged.https ?? envHttps() ?? false,
431
831
  wwwAlias,
832
+ ghostTunnel,
432
833
  ...projectConfig.path ? { projectConfigPath: projectConfig.path } : {}
433
834
  };
434
835
  }
@@ -486,7 +887,7 @@ function getProductionEnvKeys() {
486
887
  // src/hosts-file.ts
487
888
  import { writeFileSync as writeFileSync3 } from "fs";
488
889
  import { tmpdir } from "os";
489
- import { join as join4 } from "path";
890
+ import { join as join5 } from "path";
490
891
  import { execa as execa3 } from "execa";
491
892
  function escapeRegExp(value) {
492
893
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -528,7 +929,7 @@ function removeManagedBlock(existing, projectName) {
528
929
  }
529
930
  async function writeSystemHostsFile(hostsPath, next, projectName) {
530
931
  const sanitizedProjectName = sanitizeProjectName(projectName);
531
- const tempPath = join4(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
932
+ const tempPath = join5(tmpdir(), `localghost-${sanitizedProjectName}-hosts`);
532
933
  writeFileSync3(tempPath, next, "utf8");
533
934
  if (process.platform === "win32") {
534
935
  throw new Error(`Windows support: run as administrator and copy ${tempPath} to ${hostsPath}.`);
@@ -561,11 +962,11 @@ async function removeSystemHosts(projectName) {
561
962
  }
562
963
 
563
964
  // src/init.ts
564
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
565
- import { join as join5 } from "path";
965
+ import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
966
+ import { join as join6 } from "path";
566
967
  function detectPackageManager(cwd = process.cwd()) {
567
- if (existsSync4(join5(cwd, "pnpm-lock.yaml"))) return "pnpm";
568
- if (existsSync4(join5(cwd, "yarn.lock"))) return "yarn";
968
+ if (existsSync4(join6(cwd, "pnpm-lock.yaml"))) return "pnpm";
969
+ if (existsSync4(join6(cwd, "yarn.lock"))) return "yarn";
569
970
  return "npm";
570
971
  }
571
972
  function packageRunCommand(packageManager, script) {
@@ -590,7 +991,7 @@ function renderConfig(options) {
590
991
  }
591
992
  function readPackageJson(path) {
592
993
  try {
593
- return JSON.parse(readFileSync4(path, "utf8"));
994
+ return JSON.parse(readFileSync5(path, "utf8"));
594
995
  } catch {
595
996
  return null;
596
997
  }
@@ -642,7 +1043,7 @@ function initLocalghost(options = {}) {
642
1043
  const apiPort = options.apiPort ?? 8787;
643
1044
  const packageManager = options.packageManager ?? detectPackageManager(cwd);
644
1045
  const configFile = options.configFile ?? LOCALGHOST_CONFIG_FILE;
645
- const configPath = join5(cwd, configFile);
1046
+ const configPath = join6(cwd, configFile);
646
1047
  const configExists = existsSync4(configPath);
647
1048
  if (configExists && !options.force) {
648
1049
  return {
@@ -659,7 +1060,7 @@ function initLocalghost(options = {}) {
659
1060
  };
660
1061
  }
661
1062
  writeTextFile(configPath, renderConfig({ host, port, apiHost, apiPort }));
662
- const packageJsonPath = join5(cwd, "package.json");
1063
+ const packageJsonPath = join6(cwd, "package.json");
663
1064
  const packageJsonChanged = options.writeScripts ? updatePackageScripts(packageJsonPath, configFile) : false;
664
1065
  return {
665
1066
  configPath,
@@ -676,7 +1077,277 @@ function initLocalghost(options = {}) {
676
1077
  };
677
1078
  }
678
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
+
679
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
+ }
680
1351
  function getDomainRoutes(entries, options = {}) {
681
1352
  const protocol = options.https === true ? "https" : "http";
682
1353
  return [...entries].sort((left, right) => left.host.localeCompare(right.host) || left.port - right.port).map((entry) => ({
@@ -696,13 +1367,40 @@ function formatDomainRoutes(entries, options = {}) {
696
1367
  ...routes.map((route) => ` ${route.url} -> ${route.upstream}`)
697
1368
  ].join("\n");
698
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
+ }
699
1397
 
700
1398
  // src/state.ts
701
1399
  import { existsSync as existsSync5 } from "fs";
702
- import { join as join6 } from "path";
1400
+ import { join as join7 } from "path";
703
1401
  var LOCALGHOST_STATE_FILE = "ops/local/localghost-state.json";
704
1402
  function getLocalghostStatePath(cwd = process.cwd()) {
705
- return join6(cwd, LOCALGHOST_STATE_FILE);
1403
+ return join7(cwd, LOCALGHOST_STATE_FILE);
706
1404
  }
707
1405
  function readLocalghostState(cwd = process.cwd()) {
708
1406
  const path = getLocalghostStatePath(cwd);
@@ -722,11 +1420,11 @@ function patchLocalghostState(cwd, patch) {
722
1420
  }
723
1421
 
724
1422
  // src/update-check.ts
725
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
1423
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
726
1424
  import { homedir as homedir2 } from "os";
727
- import { dirname as dirname4, join as join7 } from "path";
1425
+ import { dirname as dirname4, join as join8 } from "path";
728
1426
  var LOCALGHOST_PACKAGE_NAME = "@hamedb89/localghost";
729
- var LOCALGHOST_VERSION = "0.1.8";
1427
+ var LOCALGHOST_VERSION = "0.1.9";
730
1428
  var UPDATE_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
731
1429
  var UPDATE_CHECK_NOTIFY_TTL_MS = 24 * 60 * 60 * 1e3;
732
1430
  var UPDATE_CHECK_TIMEOUT_MS = 900;
@@ -738,13 +1436,13 @@ function isUpdateCheckDisabled(env = process.env) {
738
1436
  }
739
1437
  function getUpdateCheckCachePath(env = process.env) {
740
1438
  if (env.LOCALGHOST_UPDATE_CHECK_CACHE) return env.LOCALGHOST_UPDATE_CHECK_CACHE;
741
- const cacheRoot = env.XDG_CACHE_HOME || join7(homedir2(), ".cache");
742
- return join7(cacheRoot, "localghost", "update-check.json");
1439
+ const cacheRoot = env.XDG_CACHE_HOME || join8(homedir2(), ".cache");
1440
+ return join8(cacheRoot, "localghost", "update-check.json");
743
1441
  }
744
1442
  function readCache(path = getUpdateCheckCachePath()) {
745
1443
  if (!existsSync6(path)) return null;
746
1444
  try {
747
- return JSON.parse(readFileSync5(path, "utf8"));
1445
+ return JSON.parse(readFileSync6(path, "utf8"));
748
1446
  } catch {
749
1447
  return null;
750
1448
  }
@@ -888,6 +1586,10 @@ ${message}`);
888
1586
  markUpdateNotified(result, cachePath);
889
1587
  }
890
1588
  export {
1589
+ DEFAULT_RELAY_ALLOWED_TARGET_HOSTS,
1590
+ DEFAULT_RELAY_BLOCKED_PORTS,
1591
+ DEFAULT_RELAY_LIMITS,
1592
+ DEFAULT_RELAY_TARGET_POLICY,
891
1593
  LOCALGHOST_ACTIVITY_VERSION,
892
1594
  LOCALGHOST_CONFIG_FILE,
893
1595
  LOCALGHOST_PACKAGE_NAME,
@@ -896,20 +1598,35 @@ export {
896
1598
  UPDATE_CHECK_CACHE_TTL_MS,
897
1599
  UPDATE_CHECK_NOTIFY_TTL_MS,
898
1600
  UPDATE_CHECK_TIMEOUT_MS,
1601
+ assertExactRelayHost,
899
1602
  assertLocalDevelopment,
1603
+ assertRelayLocalTarget,
1604
+ assertSecureGhostTunnelRequest,
1605
+ authenticateRelayAgentToken,
900
1606
  checkCaddy,
901
1607
  checkForUpdate,
902
1608
  compareVersions,
1609
+ constructGhostTunnelHost,
1610
+ constructGhostTunnelURL,
1611
+ constructGhostTunnelUrl,
1612
+ createRelayRouteRegistration,
903
1613
  defineLocalghostConfig,
904
1614
  detectPackageManager,
905
1615
  findAvailablePort,
906
1616
  findLocalMdnsHosts,
907
1617
  formatDomainRoutes,
1618
+ formatGhostTunnel,
908
1619
  formatUpdateMessage,
909
1620
  getCaddyfilePath,
910
1621
  getConfigFileCandidates,
911
1622
  getDevHostsPath,
912
1623
  getDomainRoutes,
1624
+ getGhostTunnelDefaultDisplayUrl,
1625
+ getGhostTunnelDisplayUrl,
1626
+ getGhostTunnelDisplayUrls,
1627
+ getGhostTunnelEntryHost,
1628
+ getGhostTunnelPreviewUrl,
1629
+ getGhostTunnelWildcardHost,
913
1630
  getLocalghostActivityPath,
914
1631
  getLocalghostStatePath,
915
1632
  getProductionEnvKeys,
@@ -922,35 +1639,48 @@ export {
922
1639
  isPortAvailable,
923
1640
  isProcessRunning,
924
1641
  isProductionLike,
1642
+ isRelayRouteActive,
925
1643
  isUpdateCheckDisabled,
926
1644
  listLocalghostRuns,
1645
+ listLocalghostSetups,
927
1646
  markUpdateNotified,
928
1647
  maybeNotifyAboutUpdate,
929
1648
  packageAddCommand,
930
1649
  packageRunCommand,
931
1650
  parseDevHosts,
1651
+ parseGhostTunnelHost,
932
1652
  patchLocalghostState,
933
1653
  pruneLocalghostActivity,
934
1654
  readDevHosts,
935
1655
  readLocalghostActivity,
1656
+ readLocalghostProjectConfig,
936
1657
  readLocalghostState,
1658
+ redactRelayHeaders,
1659
+ redactRelayLogUrl,
937
1660
  registerLocalghostRun,
1661
+ registerLocalghostSetup,
938
1662
  removeManagedBlock,
939
1663
  removeSystemHosts,
940
1664
  renderCaddyfile,
941
1665
  renderHostsBlock,
1666
+ renderRelayOfflineResponse,
942
1667
  resolveDevHostsPath,
1668
+ resolveGhostTunnelConfig,
943
1669
  resolveLocalghostContext,
944
1670
  runCaddy,
945
1671
  runDoctor,
946
1672
  sanitizeProjectName,
947
1673
  shouldNotifyAboutUpdate,
1674
+ signRelayRouteClaim,
948
1675
  startCaddy,
1676
+ stripRelayForwardHeaders,
949
1677
  trustCaddy,
950
1678
  unregisterLocalghostRun,
1679
+ unregisterLocalghostSetup,
951
1680
  updateSystemHosts,
952
1681
  upsertManagedBlock,
953
1682
  validateCaddyfile,
1683
+ verifyRelayRouteClaim,
954
1684
  writeCaddyfile,
955
1685
  writeLocalghostActivity,
956
1686
  writeLocalghostState