import type { NavigationBudget } from "@umt/contracts"; import { BrowserError } from "./errors.ts"; import { redactBrowserUrl } from "./redact.ts"; /** 主机是否在白名单内:精确匹配或为白名单项的子域。空白名单 = 全部拒绝。 */ export function hostAllowed(host: string, allowlist: readonly string[]): boolean { const h = host.toLowerCase().replace(/\.$/, ""); return allowlist.some((raw) => { const d = raw.toLowerCase().replace(/^\*\./, "").replace(/\.$/, ""); return d !== "" && (h === d || h.endsWith(`.${d}`)); }); } /** 导航前检查:只允许 http(s) 且主机在白名单内;违规抛 domain_blocked(信息里只含脱敏 URL) */ export function assertNavigable(url: string, budget: NavigationBudget): URL { let u: URL; try { u = new URL(url); } catch { throw new BrowserError("bad_request", "非法 URL"); } if (u.protocol !== "http:" && u.protocol !== "https:") throw new BrowserError("domain_blocked", `不允许的协议 ${u.protocol}`); if (!hostAllowed(u.hostname, budget.domainAllowlist)) throw new BrowserError("domain_blocked", `域名不在白名单:${redactBrowserUrl(u.origin)}`); return u; }