98 lines · 3.2 KB · TypeScript
RawDownload
/** * Pure URL helpers shared between the preview server, desktop main process, * and web renderer. Centralising these guarantees the four call sites agree * on what counts as "loopback" and how to normalise a free-form URL string. */ import * as Schema from "effect/Schema"; const TAB_ID_PREFIX = "tab_";let nextPreviewTabSequence = 0; /** * Generate a fresh preview tab id. Lives in shared (not contracts) because * the contracts package is schema-only — runtime helpers belong here. */export function newPreviewTabId(): string {  nextPreviewTabSequence += 1;  return `${TAB_ID_PREFIX}${nextPreviewTabSequence.toString(36)}`;} const LOOPBACK_HOSTS: ReadonlySet<string> = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]); /** Internal — used by `lsof` parsing where the host string is wire-formatted. */export const LSOF_LOCAL_HOST_TOKENS: ReadonlySet<string> = new Set([  ...LOOPBACK_HOSTS,  "*",  "[::]",  "[::1]",]); const LOOPBACK_PREFIX_PATTERN = /^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1?\])(?::|\/|$)/i; export function isLoopbackHost(host: string): boolean {  if (LOOPBACK_HOSTS.has(host)) return true;  if (host === "[::1]") return true;  return false;} export class PreviewUrlNormalizationError extends Schema.TaggedError<PreviewUrlNormalizationError>()(  "PreviewUrlNormalizationError",  {    inputLength: Schema.Number,    reason: Schema.Literals(["empty", "parse", "unsupported-protocol"]),    protocol: Schema.optional(Schema.String),    cause: Schema.optional(Schema.Defect()),  },) {  override get message(): string {    const protocol = this.protocol === undefined ? "" : `: ${this.protocol}`;    return `Invalid preview URL (${this.reason}${protocol}; input length ${this.inputLength}).`;  }} export const isPreviewUrlNormalizationError = Schema.is(PreviewUrlNormalizationError); function previewUrlProtocol(rawUrl: string): string | undefined {  return /^([A-Za-z][A-Za-z\d+.-]*):/.exec(rawUrl)?.[1]?.toLowerCase().concat(":");} /** * Normalise a free-form URL string into a fully-qualified `http(s)://` URL. * * - Bare loopback hosts (`localhost`, `localhost:5173`) become `http://...`. * - Bare public hosts (`example.com`) become `https://...`. * - Already-qualified URLs are validated and returned as `URL.href`. * * Throws `PreviewUrlNormalizationError` for empty, unparseable, or * unsupported-protocol inputs. */export function normalizePreviewUrl(rawUrl: string): string {  const trimmed = rawUrl.trim();  if (trimmed.length === 0) {    throw new PreviewUrlNormalizationError({ inputLength: rawUrl.length, reason: "empty" });  }  const useHttp = LOOPBACK_PREFIX_PATTERN.test(trimmed);  const candidate = trimmed.includes("://")    ? trimmed    : `${useHttp ? "http" : "https"}://${trimmed}`;  let parsed: URL;  try {    parsed = new URL(candidate);  } catch (cause) {    throw new PreviewUrlNormalizationError({      inputLength: rawUrl.length,      reason: "parse",      protocol: previewUrlProtocol(candidate),      cause,    });  }  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {    throw new PreviewUrlNormalizationError({      inputLength: rawUrl.length,      reason: "unsupported-protocol",      protocol: parsed.protocol,    });  }  return parsed.href;}