1import type {
2 BrowserNavigationTarget,
3 EnvironmentId,
4 PreviewUrlResolution,
5} from "@t3tools/contracts";
6import { isLoopbackHost, normalizePreviewUrl } from "@t3tools/shared/preview";
7import { isLocalLoopbackHost, isPrivateNetworkHost } from "@t3tools/shared/hostClassification";
8
9import type { ConnectionTarget } from "@t3tools/client-runtime/connection";
10
11import { isDesktopLocalConnectionTarget } from "~/connection/desktopLocal";
12import { isElectron } from "~/env";
13import { readPreparedConnection } from "~/state/session";
14
15export { normalizeHostname, isLocalLoopbackHost } from "@t3tools/shared/hostClassification";
16
17const readEnvironmentUrl = (environmentId: EnvironmentId): URL => {
18 const connection = readPreparedConnection(environmentId);
19 if (!connection) throw new Error(`Environment ${environmentId} is not connected.`);
20 return new URL(connection.httpBaseUrl);
21};
22
23const resolveEnvironmentPortTarget = (
24 environmentId: EnvironmentId,
25 target: Extract<BrowserNavigationTarget, { readonly kind: "environment-port" }>,
26 environmentUrl: URL,
27 requestedUrl?: string,
28 sourceUrl?: URL,
29): PreviewUrlResolution => {
30 if (!isPrivateNetworkHost(environmentUrl.hostname)) {
31 throw new Error(
32 "This environment port needs the planned authenticated preview gateway; its server address is not directly private-network reachable.",
33 );
34 }
35 const protocol = target.protocol ?? "http";
36 const path = target.path?.startsWith("/") ? target.path : `/${target.path ?? ""}`;
37 const normalizedEnvironmentHost = environmentUrl.hostname.replace(/^\[|\]$/g, "");
38 // localhost lets Chromium reach loopback servers bound to either IPv4 or IPv6.
39 const resolvedHost = isLocalLoopbackHost(normalizedEnvironmentHost)
40 ? "localhost"
41 : normalizedEnvironmentHost.includes(":")
42 ? `[${normalizedEnvironmentHost}]`
43 : normalizedEnvironmentHost;
44 const resolved = sourceUrl
45 ? new URL(sourceUrl)
46 : new URL(path, `${protocol}://${resolvedHost}:${target.port}`);
47 if (sourceUrl) {
48 resolved.hostname = resolvedHost;
49 resolved.port = String(target.port);
50 }
51 return {
52 requestedUrl: requestedUrl ?? `${protocol}://localhost:${target.port}${path}`,
53 resolvedUrl: resolved.toString(),
54 resolutionKind: isLocalLoopbackHost(normalizedEnvironmentHost)
55 ? "direct"
56 : "direct-private-network",
57 environmentId,
58 };
59};
60
61// Desktop renders primary and WSL tabs locally; loopback servers require localhost even for LAN backends.
62const reachesEnvironmentLoopback = (target: ConnectionTarget): boolean =>
63 isElectron &&
64 (target._tag === "PrimaryConnectionTarget" || isDesktopLocalConnectionTarget(target));
65
66export function resolveDiscoveredServerUrl(environmentId: EnvironmentId, rawUrl: string): string {
67 try {
68 const normalizedUrl = normalizePreviewUrl(rawUrl);
69 const parsed = new URL(normalizedUrl);
70 if (!isLoopbackHost(parsed.hostname)) return normalizedUrl;
71 const target = readPreparedConnection(environmentId)?.target;
72 if (target && reachesEnvironmentLoopback(target)) return normalizedUrl;
73 return resolveEnvironmentPortTarget(
74 environmentId,
75 {
76 kind: "environment-port",
77 port: Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80)),
78 protocol: parsed.protocol === "https:" ? "https" : "http",
79 path: `${parsed.pathname}${parsed.search}${parsed.hash}`,
80 },
81 readEnvironmentUrl(environmentId),
82 rawUrl,
83 parsed,
84 ).resolvedUrl;
85 } catch {
86 return rawUrl;
87 }
88}