1import * as NodeOS from "node:os";
2
3import { QrCode } from "@t3tools/shared/qrCode";
4import * as Effect from "effect/Effect";
5import { HttpServer } from "effect/http";
6
7import * as ServerConfig from "./config.ts";
8import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
9
10export interface HeadlessServeAccessInfo {
11 readonly connectionString: string;
12 readonly token: string;
13 readonly pairingUrl: string;
14}
15
16type NetworkInterfacesMap = ReturnType<typeof NodeOS.networkInterfaces>;
17
18export const isLoopbackHost = (host: string | undefined): boolean => {
19 if (!host || host.length === 0) {
20 return true;
21 }
22
23 return (
24 host === "localhost" ||
25 host === "127.0.0.1" ||
26 host === "::1" ||
27 host === "[::1]" ||
28 host.startsWith("127.")
29 );
30};
31
32export const isWildcardHost = (host: string | undefined): boolean =>
33 host === "0.0.0.0" || host === "::" || host === "[::]";
34
35export const formatHostForUrl = (host: string): string =>
36 host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
37
38const normalizeHost = (host: string): string =>
39 host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
40
41const isIpv4Family = (family: string | number): boolean => family === "IPv4" || family === 4;
42
43const isIpv6Family = (family: string | number): boolean => family === "IPv6" || family === 6;
44
45export const resolveHeadlessConnectionHost = (
46 host: string | undefined,
47 interfaces: NetworkInterfacesMap = NodeOS.networkInterfaces(),
48): string => {
49 if (!host) {
50 return "localhost";
51 }
52
53 if (!isWildcardHost(host)) {
54 return normalizeHost(host);
55 }
56
57 const interfaceEntries = Object.values(interfaces).flatMap((entries) => entries ?? []);
58 const externalIpv4 = interfaceEntries.find(
59 (entry) => !entry.internal && isIpv4Family(entry.family),
60 );
61 if (externalIpv4) {
62 return externalIpv4.address;
63 }
64
65 const externalIpv6 = interfaceEntries.find(
66 (entry) => !entry.internal && isIpv6Family(entry.family),
67 );
68 return externalIpv6 ? normalizeHost(externalIpv6.address) : "localhost";
69};
70
71export const resolveHeadlessConnectionString = (
72 host: string | undefined,
73 port: number,
74 interfaces: NetworkInterfacesMap = NodeOS.networkInterfaces(),
75): string => {
76 const connectionHost = resolveHeadlessConnectionHost(host, interfaces);
77 return `http://${formatHostForUrl(connectionHost)}:${port}`;
78};
79
80export const resolveListeningPort = (address: unknown, fallbackPort: number): number => {
81 if (
82 typeof address === "object" &&
83 address !== null &&
84 "port" in address &&
85 typeof address.port === "number"
86 ) {
87 return address.port;
88 }
89 return fallbackPort;
90};
91
92export const buildPairingUrl = (connectionString: string, token: string): string => {
93 const url = new URL(connectionString);
94 url.pathname = "/pair";
95 url.searchParams.delete("token");
96 url.hash = new URLSearchParams([["token", token]]).toString();
97 return url.toString();
98};
99
100export const renderTerminalQrCode = (value: string, margin = 2): string => {
101 const qrCode = QrCode.encodeText(value, QrCode.Ecc.MEDIUM);
102 const rows: Array<string> = [];
103 const isDark = (x: number, y: number): boolean =>
104 x >= 0 && x < qrCode.size && y >= 0 && y < qrCode.size && qrCode.getModule(x, y);
105
106 for (let y = -margin; y < qrCode.size + margin; y += 2) {
107 let row = "";
108
109 for (let x = -margin; x < qrCode.size + margin; x += 1) {
110 const topDark = isDark(x, y);
111 const bottomDark = isDark(x, y + 1);
112
113 row += topDark ? (bottomDark ? "█" : "▀") : bottomDark ? "▄" : " ";
114 }
115
116 rows.push(row);
117 }
118
119 return rows.join("\n");
120};
121
122export const formatHeadlessServeOutput = (accessInfo: HeadlessServeAccessInfo): string =>
123 [
124 "T3 Code server is ready.",
125 `Connection string: ${accessInfo.connectionString}`,
126 `Token: ${accessInfo.token}`,
127 `Pairing URL: ${accessInfo.pairingUrl}`,
128 "",
129 renderTerminalQrCode(accessInfo.pairingUrl),
130 "",
131 ].join("\n");
132
133export const issueHeadlessServeAccessInfo = Effect.fn("issueHeadlessServeAccessInfo")(function* () {
134 const serverConfig = yield* ServerConfig.ServerConfig;
135 const httpServer = yield* HttpServer.HttpServer;
136 const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;
137 const connectionString = resolveHeadlessConnectionString(
138 serverConfig.host,
139 resolveListeningPort(httpServer.address, serverConfig.port),
140 );
141 const issued = yield* serverAuth.issueStartupPairingCredential();
142
143 return {
144 connectionString,
145 token: issued.credential,
146 pairingUrl: buildPairingUrl(connectionString, issued.credential),
147 } satisfies HeadlessServeAccessInfo;
148});