@worker-tools/request-cookie-store
Version:
An implementation of the Cookie Store API for request handlers.
121 lines (97 loc) • 3.79 kB
text/typescript
// deno-lint-ignore-file no-control-regex
import type { CookieInit } from 'cookie-store-interface';
export const attrsToSetCookie = (attrs: string[][]) => attrs.map(att => att.join('=')).join('; ');
/**
* RegExp to match field-content in RFC 7230 sec 3.2
*
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
* field-vchar = VCHAR / obs-text
* obs-text = %x80-FF
*/
const RE_FIELD_CONTENT = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
type Attr = [string] | [string, string];
type Attrs = [[string, string], ...Attr[]];
/**
* Implements <https://wicg.github.io/cookie-store/#set-a-cookie>
* with some additional behaviors taken from Chrome's implementation.
*/
export function setCookie(
options: string | CookieInit,
value?: string,
origin?: URL,
encode = (x?: string) => x?.toString() ?? '',
): [Attrs, Date | null] | null {
const [name, val] = (typeof options === 'string'
? [options, value]
: [options.name, options.value]).map(encode)
const opts = typeof options === 'string' ? <CookieInit>{} : options;
if (name == null || val == null)
throw TypeError("required value(s) missing");
if (!name.length && val.includes('='))
throw TypeError("Cookie value cannot contain '=' if the name is empty");
if (!name.length && !val.length)
throw TypeError("Cookie name and value both cannot be empty");
// Unspecified, emulating Chrome's current behavior
if (!RE_FIELD_CONTENT.test(name + val) || name.includes('=') || val.includes(';'))
return null;
if (val.includes(', ')) {
throw TypeError("The cookie value must not contain sequence: ', '.");
}
const attrs: Attrs = [[name, val]];
const { domain, path, sameSite } = opts;
if (domain) {
// Unspecified, emulating Chrome's current behavior
if (!RE_FIELD_CONTENT.test(domain) || domain.includes(';'))
return null;
if (domain.startsWith('.'))
throw TypeError('Cookie domain cannot start with "."');
const host = origin?.host;
if (host && domain !== host && !domain.endsWith(`.${host}`))
throw TypeError('Cookie domain must match current host');
attrs.push(['Domain', domain]);
}
let expires: Date | null = null;
if (opts.expires) {
expires = opts.expires instanceof Date
? opts.expires
: new Date(opts.expires);
attrs.push(['Expires', expires.toUTCString()]);
}
if (path) {
if (!path.toString().startsWith('/'))
throw TypeError('Cookie path must start with "/"');
// Unspecified, emulating Chrome's current behavior
if (!RE_FIELD_CONTENT.test(path) || path.includes(';'))
return null;
attrs.push(['Path', path]);
}
// Always secure, except for localhost
if (origin && origin.hostname !== 'localhost')
attrs.push(['Secure']);
if (opts.httpOnly)
attrs.push(['HttpOnly']);
switch (sameSite) {
case undefined: break;
case 'none': attrs.push(['SameSite', 'None']); break;
case 'lax': attrs.push(['SameSite', 'Lax']); break;
case 'strict': attrs.push(['SameSite', 'Strict']); break;
default: throw TypeError(`The provided value '${sameSite}' is not a valid enum value of type CookieSameSite.`);
}
return [attrs, expires]
}
/**
* A not-so-strict parser for cookie headers.
* - Allows pretty much everything in the value, including `=`
* - Trims keys and values
* - Ignores when both name and value are empty (but either empty allowed)
*
* For more on the state of allowed cookie characters,
* see <https://stackoverflow.com/a/1969339/870615>.
*/
export function parseCookieHeader(cookie?: string | null) {
return new Map(cookie?.split(/;\s+/)
.map(x => x.split('='))
.map(([n, ...vs]) => <const>[n.trim(), vs.join('=').trim()])
.filter(([n, v]) => !(n === '' && v === ''))
);
}