TypeScript 97.5%
SQL 1.4%
Python 0.8%
1/**2 * Small RFC 6265-ish cookie jar used within a single fetch (across redirect hops) and, for sticky3 * sessions, persisted between requests. No dependency; ignores SameSite (irrelevant server-side).4 */5export interface JarCookie {6 name: string;7 value: string;8 domain: string; // without leading dot, lower-case9 hostOnly: boolean;10 path: string;11 secure: boolean;12 expires: number | null; // epoch ms, null = session13}1415export class CookieJar {16 private cookies: JarCookie[] = [];1718 static fromSerialized(list: Array<{ name: string; value: string; domain?: string; path?: string; secure?: boolean; expires?: number | null }> | null | undefined, fallbackHost?: string): CookieJar {19 const jar = new CookieJar();20 for (const c of list ?? []) {21 const domain = (c.domain ?? fallbackHost ?? "").toLowerCase().replace(/^\./, "");22 if (!domain) continue;23 jar.cookies.push({ name: c.name, value: c.value, domain, hostOnly: !c.domain, path: c.path ?? "/", secure: Boolean(c.secure), expires: c.expires ?? null });24 }25 return jar;26 }2728 size(): number {29 return this.cookies.length;30 }3132 serialize(): Array<{ name: string; value: string; domain: string; path: string; secure: boolean; expires: number | null }> {33 const now = Date.now();34 return this.cookies.filter((c) => c.expires === null || c.expires > now).map((c) => ({ name: c.name, value: c.value, domain: c.hostOnly ? c.domain : `.${c.domain}`, path: c.path, secure: c.secure, expires: c.expires }));35 }3637 /** Cookies exposed to the customer: name/value/domain/path only. */38 publicList(): Array<{ name: string; value: string; domain?: string; path?: string }> {39 return this.serialize().map((c) => ({ name: c.name, value: c.value, domain: c.domain.replace(/^\./, ""), path: c.path }));40 }4142 /** Store every Set-Cookie header value received from `url`. Accepts the flattened ", " joined form too. */43 storeFromHeaders(setCookie: string | string[] | undefined, url: URL): void {44 if (!setCookie) return;45 const values = Array.isArray(setCookie) ? setCookie : splitSetCookie(setCookie);46 for (const v of values) this.store(v, url);47 }4849 store(setCookieValue: string, url: URL): void {50 const segs = setCookieValue.split(";").map((s) => s.trim());51 const nv = segs.shift();52 if (!nv) return;53 const eq = nv.indexOf("=");54 if (eq <= 0) return;55 const name = nv.slice(0, eq).trim();56 const value = nv.slice(eq + 1).trim();57 const host = url.hostname.toLowerCase();58 let domain = host;59 let hostOnly = true;60 let path = defaultPath(url.pathname);61 let secure = false;62 let expires: number | null = null;63 for (const a of segs) {64 const [k0, ...rest] = a.split("=");65 const k = (k0 ?? "").trim().toLowerCase();66 const val = rest.join("=").trim();67 if (k === "domain" && val) {68 const d = val.toLowerCase().replace(/^\./, "");69 // reject cookies for unrelated domains (and public suffix-ish single labels)70 if (d && (host === d || host.endsWith(`.${d}`)) && d.includes(".")) {71 domain = d;72 hostOnly = false;73 }74 } else if (k === "path" && val.startsWith("/")) path = val;75 else if (k === "secure") secure = true;76 else if (k === "max-age") {77 const n = Number(val);78 if (Number.isFinite(n)) expires = n <= 0 ? 0 : Date.now() + n * 1000;79 } else if (k === "expires" && expires === null) {80 const t = Date.parse(val);81 if (!Number.isNaN(t)) expires = t;82 }83 }84 // remove existing85 this.cookies = this.cookies.filter((c) => !(c.name === name && c.domain === domain && c.path === path));86 if (expires !== null && expires <= Date.now()) return; // deletion87 this.cookies.push({ name, value, domain, hostOnly, path, secure, expires });88 if (this.cookies.length > 300) this.cookies.splice(0, this.cookies.length - 300);89 }9091 /** Cookie header value for `url` (or null). */92 headerFor(url: URL): string | null {93 const host = url.hostname.toLowerCase();94 const path = url.pathname || "/";95 const secure = url.protocol === "https:";96 const now = Date.now();97 const matches = this.cookies.filter((c) => {98 if (c.expires !== null && c.expires <= now) return false;99 if (c.secure && !secure) return false;100 if (c.hostOnly ? host !== c.domain : !(host === c.domain || host.endsWith(`.${c.domain}`))) return false;101 return path === c.path || (path.startsWith(c.path) && (c.path.endsWith("/") || path[c.path.length] === "/"));102 });103 if (!matches.length) return null;104 matches.sort((a, b) => b.path.length - a.path.length);105 return matches.map((c) => `${c.name}=${c.value}`).join("; ");106 }107108 /** Merge a raw caller-provided Cookie header (name=value; …) as host-only cookies for `url`. */109 addRawCookieHeader(header: string | undefined, url: URL): void {110 if (!header) return;111 for (const part of header.split(";")) {112 const t = part.trim();113 const eq = t.indexOf("=");114 if (eq <= 0) continue;115 this.store(`${t.slice(0, eq)}=${t.slice(eq + 1)}; Path=/`, url);116 }117 }118}119120function defaultPath(p: string): string {121 if (!p || !p.startsWith("/")) return "/";122 const i = p.lastIndexOf("/");123 return i <= 0 ? "/" : p.slice(0, i);124}125126/** Split a flattened Set-Cookie header (joined with ", ") on cookie boundaries (not on Expires dates). */127export function splitSetCookie(raw: string): string[] {128 return raw.split(/,(?=\s*[A-Za-z0-9_\-!#$%&'*+.^`|~]+=)/).map((s) => s.trim()).filter(Boolean);129}130