import { assert } from "../_util/assert.ts";import { toIMF } from "../datetime/mod.ts";
export interface Cookie { name: string; value: string; expires?: Date; maxAge?: number; domain?: string; path?: string; secure?: boolean; httpOnly?: boolean; sameSite?: "Strict" | "Lax" | "None"; unparsed?: string[];}
const FIELD_CONTENT_REGEXP = /^(?=[\x20-\x7E]*$)[^()@<>,;:\\"\[\]?={}\s]+$/;
function toString(cookie: Cookie): string { if (!cookie.name) { return ""; } const out: string[] = []; validateName(cookie.name); validateValue(cookie.name, cookie.value); out.push(`${cookie.name}=${cookie.value}`);
if (cookie.name.startsWith("__Secure")) { cookie.secure = true; } if (cookie.name.startsWith("__Host")) { cookie.path = "/"; cookie.secure = true; delete cookie.domain; }
if (cookie.secure) { out.push("Secure"); } if (cookie.httpOnly) { out.push("HttpOnly"); } if (typeof cookie.maxAge === "number" && Number.isInteger(cookie.maxAge)) { assert( cookie.maxAge >= 0, "Max-Age must be an integer superior or equal to 0", ); out.push(`Max-Age=${cookie.maxAge}`); } if (cookie.domain) { validateDomain(cookie.domain); out.push(`Domain=${cookie.domain}`); } if (cookie.sameSite) { out.push(`SameSite=${cookie.sameSite}`); } if (cookie.path) { validatePath(cookie.path); out.push(`Path=${cookie.path}`); } if (cookie.expires) { const dateString = toIMF(cookie.expires); out.push(`Expires=${dateString}`); } if (cookie.unparsed) { out.push(cookie.unparsed.join("; ")); } return out.join("; ");}
function validateName(name: string | undefined | null) { if (name && !FIELD_CONTENT_REGEXP.test(name)) { throw new TypeError(`Invalid cookie name: "${name}".`); }}
function validatePath(path: string | null) { if (path == null) { return; } for (let i = 0; i < path.length; i++) { const c = path.charAt(i); if ( c < String.fromCharCode(0x20) || c > String.fromCharCode(0x7E) || c == ";" ) { throw new Error( path + ": Invalid cookie path char '" + c + "'", ); } }}
function validateValue(name: string, value: string | null) { if (value == null || name == null) return; for (let i = 0; i < value.length; i++) { const c = value.charAt(i); if ( c < String.fromCharCode(0x21) || c == String.fromCharCode(0x22) || c == String.fromCharCode(0x2c) || c == String.fromCharCode(0x3b) || c == String.fromCharCode(0x5c) || c == String.fromCharCode(0x7f) ) { throw new Error( "RFC2616 cookie '" + name + "' cannot have '" + c + "' as value", ); } if (c > String.fromCharCode(0x80)) { throw new Error( "RFC2616 cookie '" + name + "' can only have US-ASCII chars as value" + c.charCodeAt(0).toString(16), ); } }}
function validateDomain(domain: string) { if (domain == null) { return; } const char1 = domain.charAt(0); const charN = domain.charAt(domain.length - 1); if (char1 == "-" || charN == "." || charN == "-") { throw new Error( "Invalid first/last char in cookie domain: " + domain, ); }}
export function getCookies(headers: Headers): Record<string, string> { const cookie = headers.get("Cookie"); if (cookie != null) { const out: Record<string, string> = {}; const c = cookie.split(";"); for (const kv of c) { const [cookieKey, ...cookieVal] = kv.split("="); assert(cookieKey != null); const key = cookieKey.trim(); out[key] = cookieVal.join("="); } return out; } return {};}
export function setCookie(headers: Headers, cookie: Cookie) { const v = toString(cookie); if (v) { headers.append("Set-Cookie", v); }}
export function deleteCookie( headers: Headers, name: string, attributes?: { path?: string; domain?: string },) { setCookie(headers, { name: name, value: "", expires: new Date(0), ...attributes, });}
function parseSetCookie(value: string): Cookie | null { const attrs = value .split(";") .map((attr) => attr .trim() .split("=") .map((keyOrValue) => keyOrValue.trim()) ); const cookie: Cookie = { name: attrs[0][0], value: attrs[0][1], };
for (const [key, value] of attrs.slice(1)) { switch (key.toLocaleLowerCase()) { case "expires": cookie.expires = new Date(value); break; case "max-age": cookie.maxAge = Number(value); if (cookie.maxAge < 0) { console.warn( "Max-Age must be an integer superior or equal to 0. Cookie ignored.", ); return null; } break; case "domain": cookie.domain = value; break; case "path": cookie.path = value; break; case "secure": cookie.secure = true; break; case "httponly": cookie.httpOnly = true; break; case "samesite": cookie.sameSite = value as Cookie["sameSite"]; break; default: if (!Array.isArray(cookie.unparsed)) { cookie.unparsed = []; } cookie.unparsed.push([key, value].join("=")); } } if (cookie.name.startsWith("__Secure-")) { if (!cookie.secure) { console.warn( "Cookies with names starting with `__Secure-` must be set with the secure flag. Cookie ignored.", ); return null; } } if (cookie.name.startsWith("__Host-")) { if (!cookie.secure) { console.warn( "Cookies with names starting with `__Host-` must be set with the secure flag. Cookie ignored.", ); return null; } if (cookie.domain !== undefined) { console.warn( "Cookies with names starting with `__Host-` must not have a domain specified. Cookie ignored.", ); return null; } if (cookie.path !== "/") { console.warn( "Cookies with names starting with `__Host-` must have path be `/`. Cookie has been ignored.", ); return null; } } return cookie;}
export function getSetCookies(headers: Headers): Cookie[] { if (!headers.has("set-cookie")) { return []; } return [...headers.entries()] .filter(([key]) => key === "set-cookie") .map(([_, value]) => value) .map(parseSetCookie) .filter(Boolean) as Cookie[];}