Skip to content

Commit bcf664a

Browse files
committed
fix(server): remove cli-common import from tailscale serve inference
1 parent e0c85ef commit bcf664a

File tree

1 file changed

+98
-3
lines changed

1 file changed

+98
-3
lines changed

apps/server/sources/app/integrations/tailscale/tailscaleServePublicUrlInference.ts

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { parseTailscaleServeHttpsBaseUrlForPort, runTailscaleServeStatus as runSharedTailscaleServeStatus } from "@happier-dev/cli-common/tailscale";
1+
import { execFile } from "node:child_process";
2+
import { promisify } from "node:util";
3+
24
import { parseBooleanEnv, parseIntEnv } from "@/config/env";
35

46
type TailscaleServeStatusRunner = (params: Readonly<{
@@ -7,6 +9,99 @@ type TailscaleServeStatusRunner = (params: Readonly<{
79
tailscaleBin?: string;
810
}>) => Promise<string>;
911

12+
const execFileAsync = promisify(execFile);
13+
14+
function stripTrailingSlash(url: string): string {
15+
return url.replace(/\/+$/, "");
16+
}
17+
18+
function normalizeHttpsUrl(raw: string): string | null {
19+
const value = String(raw ?? "").trim();
20+
if (!value) return null;
21+
22+
let parsed: URL;
23+
try {
24+
parsed = new URL(value);
25+
} catch {
26+
return null;
27+
}
28+
29+
if (parsed.protocol !== "https:") return null;
30+
parsed.username = "";
31+
parsed.password = "";
32+
parsed.search = "";
33+
parsed.hash = "";
34+
return stripTrailingSlash(parsed.toString());
35+
}
36+
37+
function tryParseProxyTargetFromLine(line: string): URL | null {
38+
const trimmed = String(line ?? "").trim();
39+
const match = trimmed.match(/\bproxy\s+(\S+)/i);
40+
const raw = match?.[1] ? String(match[1]).trim() : "";
41+
if (!raw) return null;
42+
43+
try {
44+
return new URL(raw);
45+
} catch {
46+
return null;
47+
}
48+
}
49+
50+
function extractTailscaleServeHttpsUrl(serveStatusText: string): string | null {
51+
const line = String(serveStatusText ?? "")
52+
.split(/\r?\n/)
53+
.map((value) => value.trim())
54+
.find((value) => value.toLowerCase().includes("https://"));
55+
if (!line) return null;
56+
57+
const match = line.match(/https:\/\/\S+/i);
58+
if (!match) return null;
59+
return normalizeHttpsUrl(match[0]);
60+
}
61+
62+
function parseTailscaleServeHttpsBaseUrlForPort(statusText: string, port: number): string | null {
63+
const wantedPort = Number.isFinite(port) && port > 0 ? String(Math.trunc(port)) : "";
64+
if (!wantedPort) return null;
65+
66+
let currentBase: string | null = null;
67+
const lines = String(statusText ?? "").split(/\r?\n/);
68+
for (const rawLine of lines) {
69+
const line = String(rawLine ?? "").trim();
70+
if (!line) continue;
71+
72+
const maybeHttps = line.match(/^(https:\/\/\S+)/i)?.[1];
73+
if (maybeHttps && !line.toLowerCase().includes("proxy")) {
74+
currentBase = normalizeHttpsUrl(maybeHttps);
75+
continue;
76+
}
77+
78+
if (!currentBase) continue;
79+
const proxyTarget = tryParseProxyTargetFromLine(line);
80+
if (!proxyTarget) continue;
81+
if (proxyTarget.port === wantedPort) {
82+
return currentBase;
83+
}
84+
}
85+
86+
return null;
87+
}
88+
89+
async function runLocalTailscaleServeStatus(params: Readonly<{
90+
timeoutMs: number;
91+
env: NodeJS.ProcessEnv;
92+
tailscaleBin?: string;
93+
}>): Promise<string> {
94+
const command = String(params.tailscaleBin ?? params.env.HAPPIER_TAILSCALE_BIN ?? "tailscale").trim() || "tailscale";
95+
const timeoutMs = Math.max(1, Math.min(10_000, Math.trunc(params.timeoutMs)));
96+
const mergedEnv = { ...process.env, ...params.env };
97+
const result = await execFileAsync(command, ["serve", "status"], {
98+
env: mergedEnv,
99+
timeout: timeoutMs,
100+
maxBuffer: 2 * 1024 * 1024,
101+
});
102+
return String(result.stdout ?? "");
103+
}
104+
10105
function resolveTailscaleServeStatusTimeoutMs(env: NodeJS.ProcessEnv): number {
11106
const raw = String(env.HAPPIER_TAILSCALE_SERVE_STATUS_TIMEOUT_MS ?? "").trim();
12107
return parseIntEnv(raw, 750, { min: 1, max: 10_000 });
@@ -32,11 +127,11 @@ export async function inferAndApplyTailscaleServePublicServerUrl(
32127
const statusTimeoutMs = resolveTailscaleServeStatusTimeoutMs(env);
33128

34129
try {
35-
const status = await (deps?.runTailscaleServeStatus ?? runSharedTailscaleServeStatus)({
130+
const status = await (deps?.runTailscaleServeStatus ?? runLocalTailscaleServeStatus)({
36131
timeoutMs: statusTimeoutMs,
37132
env,
38133
});
39-
const inferred = parseTailscaleServeHttpsBaseUrlForPort(status, port);
134+
const inferred = parseTailscaleServeHttpsBaseUrlForPort(status, port) ?? extractTailscaleServeHttpsUrl(status);
40135
if (!inferred) return null;
41136
if (String(env.HAPPIER_PUBLIC_SERVER_URL ?? "").trim()) return null;
42137
env.HAPPIER_PUBLIC_SERVER_URL = inferred;

0 commit comments

Comments
 (0)