paperclip/server/src/url-utils.ts

58 lines
2.1 KiB
TypeScript

/**
* Shared base-URL helpers used by both server startup (index.ts) and worktree
* config materialization (worktree-config.ts). Keeping a single source of truth
* for host classification and port rewriting prevents the two paths from drifting.
*/
export function isLoopbackHost(host: string): boolean {
// Strip surrounding brackets so a URL hostname form ("[::1]") matches too.
const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
}
/**
* Rewrite the port of any explicit-port base URL to `port`, leaving portless and
* unparseable URLs untouched.
*
* Used by the worktree path: a worktree is a distinct instance on its own server
* port, so its advertised base URL must follow that port even for a non-loopback
* host (worktrees are reachable at the same host on their own port).
*/
export function rewriteUrlPort(rawUrl: string | undefined, port: number): string | undefined {
if (!rawUrl) return undefined;
try {
const parsed = new URL(rawUrl);
// The URL API normalizes default ports like :80/:443 to "", so treat them as stable URLs.
if (!parsed.port) return rawUrl;
parsed.port = String(port);
return parsed.toString();
} catch {
return rawUrl;
}
}
/**
* Like {@link rewriteUrlPort}, but only for loopback hosts.
*
* Used at server startup for the main instance's `authPublicBaseUrl`. An explicit
* *external* base URL (e.g. a Tailscale Serve listener on a non-default port like
* :8443) must survive untouched: rewriting its port to the internal listen port
* yields an unreachable URL (scheme/port mismatch) that then propagates to spawned
* agents as a dead PAPERCLIP_API_URL. (BRO-1558)
*/
export function rewriteLoopbackUrlPort(
rawUrl: string | undefined,
port: number,
): string | undefined {
if (!rawUrl) return undefined;
try {
const parsed = new URL(rawUrl);
if (!parsed.port) return rawUrl;
if (!isLoopbackHost(parsed.hostname)) return rawUrl;
parsed.port = String(port);
return parsed.toString();
} catch {
return rawUrl;
}
}