fix(tailscale-https-broker): version floor + install docs (PAP-17079)

Replace the pinned Tailscale minor allowlist with a numeric minimum-version
comparison (floor 1.80) so current releases like 1.102.2 pass --doctor while
1.78 and malformed output are still rejected; add boundary + malformed unit
coverage. Correct broker lifecycle commands to the installed unit name
paperclip-tailscale-https-broker and document copying the built package to
/opt/paperclip/packages/tailscale-https-broker to match ExecStart and doctor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Fable 5 committed 2026-08-16 19:26:07 +00:00
1 parent 90cf40bf0f
commit 78b5ac1ce5
3 files changed
+67 -12

No files matched your search

@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { isSupportedTailscaleVersion, parseTailscaleVersion } from "./tailscale-cli.js";
describe("parseTailscaleVersion", () => {
it("parses the numeric major.minor prefix of CLI output", () => {
expect(parseTailscaleVersion("1.102.2\n tailscale commit: abc\n")).toEqual({ major: 1, minor: 102 });
expect(parseTailscaleVersion(" 1.80.3")).toEqual({ major: 1, minor: 80 });
});
it("returns null for malformed output", () => {
expect(parseTailscaleVersion("")).toBeNull();
expect(parseTailscaleVersion("garbage")).toBeNull();
expect(parseTailscaleVersion("v1.80.1")).toBeNull();
expect(parseTailscaleVersion("1")).toBeNull();
expect(parseTailscaleVersion("tailscale 1.80")).toBeNull();
});
});
describe("isSupportedTailscaleVersion", () => {
it("accepts the minimum version and anything newer", () => {
expect(isSupportedTailscaleVersion("1.80.0")).toBe(true);
expect(isSupportedTailscaleVersion("1.84.1")).toBe(true);
// Numeric, not lexicographic: "1.102" < "1.80" as strings.
expect(isSupportedTailscaleVersion("1.102.2\n tailscale commit: abc\n")).toBe(true);
expect(isSupportedTailscaleVersion("2.0.0")).toBe(true);
});
it("rejects versions below the minimum", () => {
expect(isSupportedTailscaleVersion("1.78")).toBe(false);
expect(isSupportedTailscaleVersion("1.79.9")).toBe(false);
expect(isSupportedTailscaleVersion("0.99.0")).toBe(false);
});
it("rejects malformed version output", () => {
expect(isSupportedTailscaleVersion("")).toBe(false);
expect(isSupportedTailscaleVersion("garbage")).toBe(false);
expect(isSupportedTailscaleVersion("v1.102.2")).toBe(false);
});
});
@@ -10,8 +10,12 @@ import type { CliResult } from "./broker-core.js";
const MAX_OUTPUT_BYTES = 256 * 1024;
const DEFAULT_TIMEOUT_MS = 10_000;
/** Tailscale CLI versions this broker has been validated against (pinned). */
export const SUPPORTED_TAILSCALE_VERSIONS = new Set<string>(["1.80", "1.82", "1.84"]);
/**
* Minimum Tailscale CLI version the broker accepts. Serve subcommand syntax and
* JSON status output are stable from this release onward, so newer releases are
* accepted by numeric comparison rather than a pinned allowlist.
*/
export const MIN_TAILSCALE_VERSION = { major: 1, minor: 80 } as const;
export function createTailscaleRunner(options: { timeoutMs?: number } = {}) {
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -39,13 +43,15 @@ export function createTailscaleRunner(options: { timeoutMs?: number } = {}) {
};
}
/** Parse the `major.minor` prefix of `tailscale version` output. */
export function parseTailscaleMinor(versionOutput: string): string | null {
/** Parse the numeric `major.minor` prefix of `tailscale version` output. */
export function parseTailscaleVersion(versionOutput: string): { major: number; minor: number } | null {
const match = /^\s*(\d+)\.(\d+)/.exec(versionOutput);
return match ? `${match[1]}.${match[2]}` : null;
return match ? { major: Number(match[1]), minor: Number(match[2]) } : null;
}
export function isSupportedTailscaleVersion(versionOutput: string): boolean {
const minor = parseTailscaleMinor(versionOutput);
return minor !== null && SUPPORTED_TAILSCALE_VERSIONS.has(minor);
const version = parseTailscaleVersion(versionOutput);
if (version === null) return false;
if (version.major !== MIN_TAILSCALE_VERSION.major) return version.major > MIN_TAILSCALE_VERSION.major;
return version.minor >= MIN_TAILSCALE_VERSION.minor;
}