| import type { BindMode, DeploymentExposure, DeploymentMode } from "./constants.js"; |
|
|
| export const LOOPBACK_BIND_HOST = "127.0.0.1"; |
| export const ALL_INTERFACES_BIND_HOST = "0.0.0.0"; |
|
|
| function normalizeHost(host: string | null | undefined): string | undefined { |
| const trimmed = host?.trim(); |
| return trimmed ? trimmed : undefined; |
| } |
|
|
| export function isLoopbackHost(host: string | null | undefined): boolean { |
| const normalized = normalizeHost(host)?.toLowerCase(); |
| return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1"; |
| } |
|
|
| export function isAllInterfacesHost(host: string | null | undefined): boolean { |
| const normalized = normalizeHost(host)?.toLowerCase(); |
| return normalized === "0.0.0.0" || normalized === "::"; |
| } |
|
|
| export function inferBindModeFromHost( |
| host: string | null | undefined, |
| opts?: { tailnetBindHost?: string | null | undefined }, |
| ): BindMode { |
| const normalized = normalizeHost(host); |
| const tailnetBindHost = normalizeHost(opts?.tailnetBindHost); |
|
|
| if (!normalized || isLoopbackHost(normalized)) return "loopback"; |
| if (isAllInterfacesHost(normalized)) return "lan"; |
| if (tailnetBindHost && normalized === tailnetBindHost) return "tailnet"; |
| return "custom"; |
| } |
|
|
| export function validateConfiguredBindMode(input: { |
| deploymentMode: DeploymentMode; |
| deploymentExposure: DeploymentExposure; |
| bind?: BindMode | null | undefined; |
| host?: string | null | undefined; |
| customBindHost?: string | null | undefined; |
| }): string[] { |
| const bind = input.bind ?? inferBindModeFromHost(input.host); |
| const customBindHost = normalizeHost(input.customBindHost); |
| const errors: string[] = []; |
|
|
| if (input.deploymentMode === "local_trusted" && bind !== "loopback") { |
| errors.push("local_trusted requires server.bind=loopback"); |
| } |
|
|
| if (bind === "custom" && !customBindHost) { |
| const legacyHost = normalizeHost(input.host); |
| if (!legacyHost || isLoopbackHost(legacyHost) || isAllInterfacesHost(legacyHost)) { |
| errors.push("server.customBindHost is required when server.bind=custom"); |
| } |
| } |
|
|
| if (input.deploymentMode === "authenticated" && input.deploymentExposure === "public" && bind === "tailnet") { |
| errors.push("server.bind=tailnet is only supported for authenticated/private deployments"); |
| } |
|
|
| return errors; |
| } |
|
|
| export function resolveRuntimeBind(input: { |
| bind?: BindMode | null | undefined; |
| host?: string | null | undefined; |
| customBindHost?: string | null | undefined; |
| tailnetBindHost?: string | null | undefined; |
| }): { |
| bind: BindMode; |
| host: string; |
| customBindHost?: string; |
| errors: string[]; |
| } { |
| const bind = input.bind ?? inferBindModeFromHost(input.host, { tailnetBindHost: input.tailnetBindHost }); |
| const legacyHost = normalizeHost(input.host); |
| const customBindHost = |
| normalizeHost(input.customBindHost) ?? |
| (bind === "custom" && legacyHost && !isLoopbackHost(legacyHost) && !isAllInterfacesHost(legacyHost) |
| ? legacyHost |
| : undefined); |
|
|
| switch (bind) { |
| case "loopback": |
| return { bind, host: LOOPBACK_BIND_HOST, customBindHost, errors: [] }; |
| case "lan": |
| return { bind, host: ALL_INTERFACES_BIND_HOST, customBindHost, errors: [] }; |
| case "custom": |
| return customBindHost |
| ? { bind, host: customBindHost, customBindHost, errors: [] } |
| : { bind, host: legacyHost ?? LOOPBACK_BIND_HOST, errors: ["server.customBindHost is required when server.bind=custom"] }; |
| case "tailnet": { |
| const tailnetBindHost = normalizeHost(input.tailnetBindHost); |
| return tailnetBindHost |
| ? { bind, host: tailnetBindHost, customBindHost, errors: [] } |
| : { |
| bind, |
| host: legacyHost ?? LOOPBACK_BIND_HOST, |
| customBindHost, |
| errors: [ |
| "server.bind=tailnet requires a detected Tailscale address or PAPERCLIP_TAILNET_BIND_HOST", |
| ], |
| }; |
| } |
| } |
| } |
|
|