import type { BrowserCookie } from "@umt/contracts"; // BR-2 凭据统一:站点账号里存的是 HTTP `Cookie:` 请求头串("a=1; b=2"),浏览器 profile 里是带域名/路径/期限的 Cookie 对象。 // 这里只做两者互转与比较;值是凭据,不打印、不进错误信息。 /** 注入时 Cookie 头串里没有期限信息:给 30 天,站点响应的 Set-Cookie 会按站点自己的期限覆盖 */ export const INJECTED_COOKIE_TTL_SEC = 30 * 24 * 3600; /** "a=1; b=2" → [[a, 1], [b, 2]](同名后者覆盖;去掉空段与首尾空白) */ export function parseCookieHeader(header: string | null | undefined): Map { const out = new Map(); for (const part of (header ?? "").split(";")) { const i = part.indexOf("="); if (i <= 0) continue; const name = part.slice(0, i).trim(); if (!name || /[\s,;]/.test(name)) continue; out.set(name, part.slice(i + 1).trim()); } return out; } /** Map → "a=1; b=2"(按名字排序,比较稳定) */ export function formatCookieHeader(jar: Map): string { return [...jar.entries()] .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) .map(([k, v]) => `${k}=${v}`) .join("; "); } /** 多个 Cookie 头串合并,靠后的优先(用户填的 + 自动登录得到的会话 Cookie → 会话 Cookie 覆盖同名项) */ export function mergeCookieHeaders(...headers: (string | null | undefined)[]): string | null { const jar = new Map(); for (const h of headers) for (const [k, v] of parseCookieHeader(h)) jar.set(k, v); return jar.size ? formatCookieHeader(jar) : null; } /** 两个 Cookie 头串是否等价(忽略顺序与空白) */ export function sameCookieHeader(a: string | null | undefined, b: string | null | undefined): boolean { const x = parseCookieHeader(a); const y = parseCookieHeader(b); if (x.size !== y.size) return false; for (const [k, v] of x) if (y.get(k) !== v) return false; return true; } /** 站点地址 → 注入用 Cookie 对象:域名 = 站点主机(host-only),路径 /,https 站点标 secure */ export function cookiesFromHeader( header: string, siteUrl: string, nowSec = Math.floor(Date.now() / 1000), ): BrowserCookie[] { const u = new URL(siteUrl); const secure = u.protocol === "https:"; return [...parseCookieHeader(header)].map(([name, value]) => ({ name, value, domain: u.hostname, path: "/", expires: nowSec + INJECTED_COOKIE_TTL_SEC, httpOnly: false, secure, sameSite: "Lax" as const, })); } /** Cookie 的 domain 是否作用于该主机(host-only 精确匹配;.根域 匹配子域) */ export function cookieDomainMatches(cookieDomain: string, host: string): boolean { const d = cookieDomain.toLowerCase().replace(/^\./, ""); const h = host.toLowerCase(); return h === d || h.endsWith(`.${d}`); } /** * 浏览器读回的 Cookie → 站点 Cookie 头串。只取作用于站点主机、路径为根(或站点路径前缀)且未过期的, * 其它域名(统计、CDN、第三方)的 Cookie 一律丢弃(回写只对该站域名)。 */ export function headerFromCookies( cookies: readonly BrowserCookie[], siteUrl: string, nowSec = Math.floor(Date.now() / 1000), ): string | null { const u = new URL(siteUrl); const jar = new Map(); for (const c of cookies) { if (!cookieDomainMatches(c.domain, u.hostname)) continue; if (c.expires !== undefined && c.expires > 0 && c.expires <= nowSec) continue; const path = c.path || "/"; if (!u.pathname.startsWith(path)) continue; // 同名多条:更具体的域名 / 路径优先(与浏览器发送顺序一致) const specific = c.domain.replace(/^\./, "").length * 1000 + path.length; const prev = jar.get(c.name); if (!prev || specific >= prev.specific) jar.set(c.name, { v: c.value, specific }); } if (!jar.size) return null; return formatCookieHeader(new Map([...jar].map(([k, x]) => [k, x.v]))); }