1import type {
2 RelayEnvironmentLinkProofPayload,
3 RelayLinkProofRequest,
4 RelayManagedEndpointOrigin,
5 RelayManagedEndpointRuntimeConfig,
6} from "@t3tools/contracts/relay";
7import type { HttpServerRequest } from "effect/http";
8
9const CLOUD_PROOF_MAX_LIFETIME_SECONDS = 5 * 60;
10const CLOUD_PROOF_CLOCK_SKEW_SECONDS = 60;
11const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);
12
13function normalizeHostname(hostname: string): string {
14 return hostname
15 .trim()
16 .toLowerCase()
17 .replace(/^\[(.*)\]$/, "$1");
18}
19
20function isLoopbackHostname(hostname: string): boolean {
21 return LOOPBACK_HOSTNAMES.has(normalizeHostname(hostname));
22}
23
24function firstForwardedHeaderValue(value: string | undefined): string | undefined {
25 const first = value?.split(",")[0]?.trim();
26 return first && first.length > 0 ? first : undefined;
27}
28
29export function requestAbsoluteUrl(request: HttpServerRequest.HttpServerRequest): string | null {
30 try {
31 return new URL(request.originalUrl).href;
32 } catch {
33 const host = firstForwardedHeaderValue(request.headers.host) ?? "127.0.0.1";
34 try {
35 return new URL(request.originalUrl, `http://${host}`).href;
36 } catch {
37 return null;
38 }
39 }
40}
41
42export function hasForwardedAuthorityHeaders(
43 request: HttpServerRequest.HttpServerRequest,
44): boolean {
45 return (
46 firstForwardedHeaderValue(request.headers["x-forwarded-host"]) !== undefined ||
47 firstForwardedHeaderValue(request.headers["x-forwarded-proto"]) !== undefined
48 );
49}
50
51function endpointRequestPort(url: URL): number {
52 return Number(url.port || (url.protocol === "https:" ? 443 : 80));
53}
54
55export function parseManagedEndpointLocalOrigin(localOrigin: string) {
56 const url = new URL(localOrigin);
57 if (
58 localOrigin !== localOrigin.trim() ||
59 (url.protocol !== "http:" && url.protocol !== "https:") ||
60 url.username !== "" ||
61 url.password !== "" ||
62 url.pathname !== "/" ||
63 url.search !== "" ||
64 url.hash !== "" ||
65 localOrigin.includes("?") ||
66 localOrigin.includes("#")
67 ) {
68 throw new Error("Invalid local origin");
69 }
70 const wsUrl = new URL(url.origin);
71 wsUrl.protocol = url.protocol === "https:" ? "wss:" : "ws:";
72 return {
73 httpBaseUrl: url.origin,
74 wsBaseUrl: wsUrl.origin,
75 origin: {
76 localHttpHost: url.hostname,
77 localHttpPort: endpointRequestPort(url),
78 } satisfies RelayManagedEndpointOrigin,
79 };
80}
81
82/**
83 * Whether clients should offer to turn on T3 Connect over this session: only the desktop app's
84 * own bootstrap session, from any address (WSL answers on a distro IP), never through a proxy.
85 * A loopback address alone does not count, since SSH port forwards look like loopback too.
86 */
87export function canLinkHere(
88 request: HttpServerRequest.HttpServerRequest,
89 subject: string,
90): boolean {
91 return (
92 subject === "desktop-bootstrap" &&
93 requestAbsoluteUrl(request) !== null &&
94 !hasForwardedAuthorityHeaders(request)
95 );
96}
97
98export function isAllowedEndpointOrigin(input: {
99 readonly origin: RelayManagedEndpointOrigin;
100 readonly requestUrl: string;
101 readonly allowNonLoopbackRequestHost?: boolean;
102}): boolean {
103 if (!isLoopbackHostname(input.origin.localHttpHost)) {
104 return false;
105 }
106
107 const url = new URL(input.requestUrl);
108 if (!input.allowNonLoopbackRequestHost && !isLoopbackHostname(url.hostname)) {
109 return false;
110 }
111
112 return input.origin.localHttpPort === endpointRequestPort(url);
113}
114
115export function isSupportedLinkProviderKind(request: RelayLinkProofRequest): boolean {
116 return (
117 request.endpoint.providerKind === "cloudflare_tunnel" ||
118 request.endpoint.providerKind === "manual"
119 );
120}
121
122export function linkProofScopes(
123 request: RelayLinkProofRequest,
124): RelayEnvironmentLinkProofPayload["scopes"] {
125 return request.endpoint.providerKind === "cloudflare_tunnel"
126 ? ["agent_activity_notifications", "managed_tunnels"]
127 : ["agent_activity_notifications"];
128}
129
130export function hasExactScope(input: {
131 readonly scopes: ReadonlyArray<string>;
132 readonly expected: string;
133}): boolean {
134 return input.scopes.length === 1 && input.scopes[0] === input.expected;
135}
136
137export function hasBoundedCloudProofLifetime(input: {
138 readonly iat: number;
139 readonly exp: number;
140 readonly nowSeconds: number;
141}): boolean {
142 return (
143 input.exp > input.iat &&
144 input.exp - input.iat <= CLOUD_PROOF_MAX_LIFETIME_SECONDS &&
145 input.iat <= input.nowSeconds + CLOUD_PROOF_CLOCK_SKEW_SECONDS
146 );
147}
148
149export function managedEndpointRuntimeConfigsMatch(
150 left: RelayManagedEndpointRuntimeConfig,
151 right: RelayManagedEndpointRuntimeConfig,
152): boolean {
153 return (
154 left.providerKind === right.providerKind &&
155 left.connectorToken === right.connectorToken &&
156 left.tunnelId === right.tunnelId &&
157 left.tunnelName === right.tunnelName
158 );
159}