Cookie Serializer And Parser (RFC 6265)

Serialize browser cookies with Max-Age, Expires, Path, Domain, Secure, HttpOnly and SameSite, plus a tolerant Cookie header parser.

JavascriptSecurity Boundaries

Code

snippet
const TOKEN = /^[!#$%&'*+\-.^_\x60|~0-9A-Za-z]+$/;

export function serializeCookie(name, value, options = {}) {
  if (typeof name !== "string" || !TOKEN.test(name)) {
    throw new TypeError("invalid cookie name");
  }
  if (typeof value !== "string") throw new TypeError("cookie value must be a string");
  const encoded = encodeURIComponent(value);
  if (encoded.length > 4096) throw new RangeError("cookie value is too large");
  const parts = [name + "=" + encoded];
  const {
    maxAge,
    expires,
    path,
    domain,
    secure = false,
    httpOnly = false,
    sameSite,
  } = options;

  if (maxAge !== undefined) {
    if (!Number.isInteger(maxAge)) throw new TypeError("maxAge must be an integer");
    parts.push("Max-Age=" + maxAge);
  }
  if (expires !== undefined) {
    if (!(expires instanceof Date) || Number.isNaN(expires.getTime())) {
      throw new TypeError("expires must be a valid Date");
    }
    parts.push("Expires=" + expires.toUTCString());
  }
  if (path !== undefined) {
    if (typeof path !== "string" || !path.startsWith("/")) {
      throw new TypeError("path must start with /");
    }
    parts.push("Path=" + path);
  }
  if (domain !== undefined) {
    if (typeof domain !== "string" || domain.length === 0 || /[;\s]/.test(domain)) {
      throw new TypeError("invalid cookie domain");
    }
    parts.push("Domain=" + domain);
  }
  if (sameSite !== undefined) {
    const normalized = String(sameSite).toLowerCase();
    if (!["lax", "strict", "none"].includes(normalized)) {
      throw new TypeError("sameSite must be lax, strict or none");
    }
    if (normalized === "none" && !secure) {
      throw new TypeError("SameSite=None requires Secure");
    }
    parts.push("SameSite=" + normalized[0].toUpperCase() + normalized.slice(1));
  }
  if (secure) parts.push("Secure");
  if (httpOnly) parts.push("HttpOnly");
  return parts.join("; ");
}

export function parseCookieHeader(header) {
  const cookies = new Map();
  if (typeof header !== "string" || header.trim() === "") return cookies;
  for (const part of header.split(";")) {
    const equals = part.indexOf("=");
    if (equals <= 0) continue;
    const name = part.slice(0, equals).trim();
    const raw = part.slice(equals + 1).trim();
    if (!name || cookies.has(name)) continue;
    try {
      cookies.set(name, decodeURIComponent(raw));
    } catch {
      cookies.set(name, raw);
    }
  }
  return cookies;
}