import { deleteCookie, getCookies, Keygrip, ms, setCookie } from "./deps.ts";
import type { Cookie, Response, SameSite, ServerRequest } from "./deps.ts";
const FIELD_CONTENT_REGEXP = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
const SAME_SITE: Record<string, SameSite> = { true: "Strict", strict: "Strict", lax: "Lax", none: "None",};
export interface CookieOptions { maxAge?: number | string; expires?: Date | number | string; path?: string; domain?: string; secure?: boolean; httpOnly?: boolean; signed?: boolean; overwrite?: boolean; sameSite?: SameSite | true;}
export interface CookieJarOptions { keys?: string[] | Keygrip; secure?: boolean;}
export class CookieJar extends Map<string, string> { #res: Response; #secure: boolean = false; #keys?: Keygrip;
constructor(req: ServerRequest, res: Response, opts?: CookieJarOptions) { super(); this.#res = res;
if (opts) { const { keys, secure } = opts; this.#secure = !!secure; if (keys) { this.#keys = keys instanceof Keygrip ? keys : new Keygrip(keys); } }
const cookies = getCookies(req); for (const key of Object.keys(cookies)) { super.set(key, cookies[key]); } }
private setCookie( name: string, value: string | null | undefined, opts: CookieOptions, ): boolean { let { secure, maxAge, expires, sameSite, domain, path = "/", httpOnly = true, } = opts; if (!this.#secure && secure) { throw new Error("Cannot send secure cookie over unencrypted connection"); }
if (sameSite) { const key = String(sameSite).toLowerCase(); sameSite = SAME_SITE[key]; if (typeof opts.sameSite != "string") { throw new TypeError("option sameSite is invalid"); } }
if (maxAge && typeof maxAge == "string") { const res = ms(maxAge) as number; if (res == null || isNaN(res)) { throw new TypeError("maxAge string invalid"); } maxAge = (res / 1000) | 0; }
if (expires) { if (typeof expires == "string") { const time = ms(expires); if (typeof time == "number") { expires = Date.now() + time; } }
expires = new Date(expires); }
const cookie = { name, value, secure, domain, path, httpOnly, sameSite, maxAge, expires, } as Cookie; setCookie(this.#res, cookie);
const signed = opts.signed ?? !!this.#keys; if (signed) { if (!this.#keys) { throw new Error(".keys required for signed cookies"); }
cookie.name += ".sig"; cookie.value = this.#keys.sign(`${cookie.name}=${cookie.value}`); setCookie(this.#res, cookie); } return true; }
get(name: string, opts?: Pick<CookieOptions, "signed">): string | undefined { const signed = opts?.signed ?? !!this.#keys; const value = super.get(name);
if (signed) { const signatureName = `${name}.sig`; const signature = super.get(signatureName);
if (signature) { if (!this.#keys) { throw new Error("Keys required for signed cookies"); }
const data = `${name}=${value}`; const idx = this.#keys.index(data, signature); if (idx == -1) { return this.delete(signatureName), void 0; } else if (idx) { this.set(signatureName, this.#keys.sign(data), { signed: false }); } } }
return value; }
set(name: string, value: any, options?: CookieOptions): this { if (!value && !options) { return this.delete(name), this; }
const opts = options ?? {}; value = value && String(value);
if (!opts.overwrite && super.has(name)) { return this; } else if (!FIELD_CONTENT_REGEXP.test(name)) { throw new TypeError("argument name is invalid"); } else if (value && !FIELD_CONTENT_REGEXP.test(value)) { throw new TypeError("argument value is invalid"); } else if (opts.path && !FIELD_CONTENT_REGEXP.test(opts.path)) { throw new TypeError("option path is invalid"); } else if (opts.domain && !FIELD_CONTENT_REGEXP.test(opts.domain)) { throw new TypeError("option domain is invalid"); }
this.setCookie(name, value, opts); if (opts.signed ?? this.#keys) { if (!this.#keys) { throw new Error(".keys required for signed cookies"); }
name += ".sig"; value = this.#keys.sign(`${name}=${value}`); this.setCookie(name, value, opts); }
return this; }
delete( name: string, opts?: Exclude<CookieOptions, "expires" | "maxAge" | "overwrite">, ): boolean { if (!FIELD_CONTENT_REGEXP.test(name)) { throw new TypeError("argument name is invalid"); }
if (opts) { opts.maxAge = ""; opts.expires = new Date(0); return this.setCookie(name, "", opts) && super.delete(name); }
deleteCookie(this.#res, name); return super.delete(name); }
clear(): void { for (const key of this.keys()) { const idx = key.lastIndexOf(".sig"); if (idx != -1 && super.has(key.substring(0, idx))) { continue; }
deleteCookie(this.#res, key); } super.clear(); }}