Add configurable service proxy (#1280)
* Add configurable service proxy * Fix service proxy URL used when opening scripts with a public base URL When a public proxy URL was configured, the View link was falling through to direct connection logic and opening the wrong URL. Move the public URL early-return before connection-type checks, and remove the now-dead duplicate check further down. * Add service proxy documentation Covers config options, URL generation format, wildcard DNS setup, and reverse proxy Host header requirements. * Cap combined public service label and document hostname format * Address service proxy review feedback
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildScriptHostname } from "./script-hostname.js";
|
||||
import {
|
||||
buildPublicScriptHostname,
|
||||
buildPublicScriptProxyUrl,
|
||||
buildScriptHostname,
|
||||
} from "./script-hostname.js";
|
||||
|
||||
describe("buildScriptHostname", () => {
|
||||
it("builds default branch hostnames with script and project labels", () => {
|
||||
@@ -69,3 +73,54 @@ describe("buildScriptHostname", () => {
|
||||
).toBe("untitled.untitled.untitled.localhost");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPublicScriptHostname", () => {
|
||||
it("uses one combined service label under the configured public base host", () => {
|
||||
expect(
|
||||
buildPublicScriptHostname({
|
||||
publicBaseUrl: "https://services.example.com",
|
||||
projectSlug: "paseo",
|
||||
branchName: "feature-auth",
|
||||
scriptName: "web",
|
||||
}),
|
||||
).toBe("web--feature-auth--paseo.services.example.com");
|
||||
});
|
||||
|
||||
it("omits default branch names from the public service label", () => {
|
||||
expect(
|
||||
buildPublicScriptHostname({
|
||||
publicBaseUrl: "https://services.example.com",
|
||||
projectSlug: "paseo",
|
||||
branchName: "main",
|
||||
scriptName: "web",
|
||||
}),
|
||||
).toBe("web--paseo.services.example.com");
|
||||
});
|
||||
|
||||
it("caps the public service label to the DNS label length limit", () => {
|
||||
const hostname = buildPublicScriptHostname({
|
||||
publicBaseUrl: "https://services.example.com",
|
||||
projectSlug: "project-".repeat(10),
|
||||
branchName: "branch-".repeat(10),
|
||||
scriptName: "script-".repeat(10),
|
||||
});
|
||||
const [serviceLabel] = hostname.split(".");
|
||||
|
||||
expect(serviceLabel.length).toBeLessThanOrEqual(63);
|
||||
expect(serviceLabel).toMatch(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/);
|
||||
expect(hostname).toBe(`${serviceLabel}.services.example.com`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPublicScriptProxyUrl", () => {
|
||||
it("preserves the configured public base protocol and port", () => {
|
||||
expect(
|
||||
buildPublicScriptProxyUrl({
|
||||
publicBaseUrl: "https://services.example.com:8443/base-is-ignored",
|
||||
projectSlug: "paseo",
|
||||
branchName: "feature-auth",
|
||||
scriptName: "web",
|
||||
}),
|
||||
).toBe("https://web--feature-auth--paseo.services.example.com:8443");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,48 @@
|
||||
import { slugify } from "./worktree.js";
|
||||
|
||||
const MAX_DNS_LABEL_LENGTH = 63;
|
||||
|
||||
interface BuildScriptHostnameOptions {
|
||||
projectSlug: string;
|
||||
branchName: string | null;
|
||||
scriptName: string;
|
||||
}
|
||||
|
||||
interface BuildPublicScriptHostnameOptions extends BuildScriptHostnameOptions {
|
||||
publicBaseUrl: string;
|
||||
}
|
||||
|
||||
function toHostnameLabel(value: string): string {
|
||||
return slugify(value) || "untitled";
|
||||
}
|
||||
|
||||
function capDnsLabel(label: string): string {
|
||||
if (label.length <= MAX_DNS_LABEL_LENGTH) {
|
||||
return label;
|
||||
}
|
||||
return label.slice(0, MAX_DNS_LABEL_LENGTH).replace(/-+$/g, "") || "untitled";
|
||||
}
|
||||
|
||||
function toPublicServiceLabel({
|
||||
projectSlug,
|
||||
branchName,
|
||||
scriptName,
|
||||
}: BuildScriptHostnameOptions): string {
|
||||
const labels = [toHostnameLabel(scriptName)];
|
||||
const isDefaultBranch = branchName === null || branchName === "main" || branchName === "master";
|
||||
if (!isDefaultBranch) {
|
||||
labels.push(toHostnameLabel(branchName));
|
||||
}
|
||||
labels.push(toHostnameLabel(projectSlug));
|
||||
|
||||
// Public URLs must keep script/branch/project in one label. The local
|
||||
// script.branch.project.localhost shape would require multi-level wildcard
|
||||
// DNS/certificates for arbitrary branch names, while a single label works with
|
||||
// normal `*.base-domain` DNS and wildcard TLS. Cap it to the DNS 63-octet
|
||||
// label limit so long branch/project/script names still produce resolvable URLs.
|
||||
return capDnsLabel(labels.join("--"));
|
||||
}
|
||||
|
||||
export function buildScriptHostname({
|
||||
projectSlug,
|
||||
branchName,
|
||||
@@ -25,3 +58,18 @@ export function buildScriptHostname({
|
||||
|
||||
return `${serviceHostnameLabel}.${toHostnameLabel(branchName)}.${projectHostnameLabel}.localhost`;
|
||||
}
|
||||
|
||||
export function buildPublicScriptHostname({
|
||||
publicBaseUrl,
|
||||
...script
|
||||
}: BuildPublicScriptHostnameOptions): string {
|
||||
const base = new URL(publicBaseUrl);
|
||||
return `${toPublicServiceLabel(script)}.${base.hostname}`;
|
||||
}
|
||||
|
||||
export function buildPublicScriptProxyUrl(options: BuildPublicScriptHostnameOptions): string {
|
||||
const base = new URL(options.publicBaseUrl);
|
||||
const hostname = buildPublicScriptHostname(options);
|
||||
const port = base.port ? `:${base.port}` : "";
|
||||
return `${base.protocol}//${hostname}${port}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user