1import { createAdvertisedEndpoint } from "@t3tools/shared/advertisedEndpoint";
2import type { AdvertisedEndpoint, AdvertisedEndpointProvider } from "@t3tools/contracts";
3import {
4 buildTailscaleHttpsBaseUrl,
5 isTailscaleIpv4Address,
6 parseTailscaleMagicDnsName,
7 probeTailscaleHttpsEndpoint,
8 readTailscaleStatus,
9} from "@t3tools/tailscale";
10import * as Effect from "effect/Effect";
11import * as Option from "effect/Option";
12import * as HttpClient from "effect/http/HttpClient";
13import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
14
15import type { NetworkInterfaces } from "./DesktopNetworkInterfaces.ts";
16
17export { parseTailscaleMagicDnsName } from "@t3tools/tailscale";
18
19const TAILSCALE_ENDPOINT_PROVIDER: AdvertisedEndpointProvider = {
20 id: "tailscale",
21 label: "Tailscale",
22 kind: "private-network",
23 isAddon: true,
24};
25
26function resolveTailscaleIpAdvertisedEndpoints(input: {
27 readonly port: number;
28 readonly networkInterfaces: NetworkInterfaces;
29}): readonly AdvertisedEndpoint[] {
30 const seen = new Set<string>();
31 const endpoints: AdvertisedEndpoint[] = [];
32
33 for (const interfaceAddresses of Object.values(input.networkInterfaces)) {
34 if (!interfaceAddresses) continue;
35
36 for (const address of interfaceAddresses) {
37 if (address.internal) continue;
38 if (address.family !== "IPv4") continue;
39 if (!isTailscaleIpv4Address(address.address)) continue;
40 if (seen.has(address.address)) continue;
41 seen.add(address.address);
42
43 endpoints.push(
44 createAdvertisedEndpoint({
45 provider: TAILSCALE_ENDPOINT_PROVIDER,
46 source: "desktop-addon",
47 id: `tailscale-ip:http://${address.address}:${input.port}`,
48 label: "Tailscale IP",
49 httpBaseUrl: `http://${address.address}:${input.port}`,
50 reachability: "private-network",
51 status: "available",
52 description: "Reachable from devices on the same Tailnet.",
53 }),
54 );
55 }
56 }
57
58 return endpoints;
59}
60
61const resolveTailscaleMagicDnsAdvertisedEndpoint = Effect.fn(
62 "resolveTailscaleMagicDnsAdvertisedEndpoint",
63)(function* (input: {
64 readonly dnsName: string | null;
65 readonly serveEnabled: boolean;
66 readonly servePort?: number;
67 readonly probe?: (baseUrl: string) => Effect.Effect<boolean, never, HttpClient.HttpClient>;
68}): Effect.fn.Return<Option.Option<AdvertisedEndpoint>, never, HttpClient.HttpClient> {
69 if (!input.dnsName) {
70 return Option.none();
71 }
72
73 const httpBaseUrl = buildTailscaleHttpsBaseUrl({
74 magicDnsName: input.dnsName,
75 ...(input.servePort === undefined ? {} : { servePort: input.servePort }),
76 });
77 const probe =
78 input.probe?.(httpBaseUrl) ??
79 probeTailscaleHttpsEndpoint({
80 baseUrl: httpBaseUrl,
81 });
82 const isReachable = input.serveEnabled ? yield* probe : false;
83
84 return Option.some(
85 createAdvertisedEndpoint({
86 provider: TAILSCALE_ENDPOINT_PROVIDER,
87 source: "desktop-addon",
88 id: `tailscale-magicdns:${httpBaseUrl}`,
89 label: "Tailscale HTTPS",
90 httpBaseUrl,
91 reachability: "private-network",
92 hostedHttpsCompatibility: isReachable ? "compatible" : "requires-configuration",
93 status: isReachable ? "available" : "unavailable",
94 description: isReachable
95 ? "HTTPS endpoint served by Tailscale Serve."
96 : "MagicDNS hostname. Configure Tailscale Serve for HTTPS access.",
97 }),
98 );
99});
100
101export const resolveTailscaleAdvertisedEndpoints = Effect.fn("resolveTailscaleAdvertisedEndpoints")(
102 function* (input: {
103 readonly port: number;
104 readonly serveEnabled?: boolean;
105 readonly servePort?: number;
106 readonly networkInterfaces: NetworkInterfaces;
107 readonly statusJson?: string | null;
108 readonly readMagicDnsName?: Effect.Effect<
109 string | null,
110 never,
111 ChildProcessSpawner.ChildProcessSpawner
112 >;
113 readonly probe?: (baseUrl: string) => Effect.Effect<boolean, never, HttpClient.HttpClient>;
114 }): Effect.fn.Return<
115 readonly AdvertisedEndpoint[],
116 never,
117 ChildProcessSpawner.ChildProcessSpawner | HttpClient.HttpClient
118 > {
119 const ipEndpoints = resolveTailscaleIpAdvertisedEndpoints(input);
120 const readDnsName =
121 input.readMagicDnsName ??
122 readTailscaleStatus.pipe(
123 Effect.map((status) => status.magicDnsName),
124 Effect.orElseSucceed((): string | null => null),
125 );
126 const dnsName =
127 input.statusJson === undefined
128 ? yield* readDnsName
129 : input.statusJson
130 ? yield* parseTailscaleMagicDnsName(input.statusJson).pipe(
131 Effect.orElseSucceed(() => null),
132 )
133 : null;
134 const magicDnsEndpoint = yield* resolveTailscaleMagicDnsAdvertisedEndpoint({
135 dnsName,
136 serveEnabled: input.serveEnabled === true,
137 ...(input.servePort === undefined ? {} : { servePort: input.servePort }),
138 ...(input.probe === undefined ? {} : { probe: input.probe }),
139 });
140
141 return Option.match(magicDnsEndpoint, {
142 onNone: () => ipEndpoints,
143 onSome: (endpoint) => [...ipEndpoints, endpoint],
144 });
145 },
146);