@fluvial/cookies
Version:
A cookie management library for Fluvial
203 lines • 6.71 kB
JavaScript
import { parse } from './parse.js';
/**
* Creates an empty CookieCollection
*/
export function create() {
return new CookieCollection();
}
/**
* Creates a CookieCollection based on a string value from the `Cookie` header
* @param cookieHeader
*/
export function fromHeaderValue(cookieHeader) {
return fromObject(parse(cookieHeader));
}
/**
* Creates a CookieCollection based on an object with keys and values
* @param obj
* @returns
*/
export function fromObject(obj) {
return fromEntries(Object.entries(obj));
}
export function fromEntries(entries) {
const cookies = create();
for (const entry of entries) {
let cookieObj;
if (Array.isArray(entry)) {
const [name, value] = entry;
cookieObj = (typeof value == 'object' && value ?
{ name, ...value } :
{ name, value });
}
else if (typeof entry == 'object' && 'name' in entry && 'value' in entry) {
cookieObj = entry;
ensureCookieStringifies(cookieObj);
}
if (!cookieObj) {
// TODO: do something better with this than simply logging it out
console.warn(`Invalid cookie entry:`, entry);
continue;
}
cookies.add(cookieObj);
}
return cookies;
}
export class CookieCollection {
#onChangeHooks = [];
#cookies = {};
/** The current length of the collection */
get length() {
return Object.keys(this.#cookies).length;
}
/** Returns a true if the collection contains the cookie with the given name */
has(name) {
return Boolean(this.#cookies[name]);
}
add(cookie, value) {
if (typeof cookie == 'string') {
cookie = {
name: cookie,
value,
};
}
if (!cookie || typeof cookie != 'object' || !cookie.name || !cookie.value) {
// TODO: replace this with something better
console.log(`Invalid cookie entry:`, cookie);
return;
}
ensureCookieStringifies(cookie);
this.#cookies[cookie.name] = cookie;
for (const hook of this.#onChangeHooks) {
hook();
}
}
/** Returns in a cookie if one is found with the provided name; otherwise, undefined */
get(name) {
return this.#cookies[name];
}
/** Deletes a cookie with the given name if it exists; if none is found, it does nothing */
remove(name) {
const found = name in this.#cookies;
delete this.#cookies[name];
if (found) {
for (const hook of this.#onChangeHooks) {
hook();
}
}
}
/** This registers a hook for when a cookie is set or removed. It was added since the Express adaptation requires the cookie headers set each time there is a mutation or else it won't work */
onChange(hook) {
this.#onChangeHooks.push(hook);
}
/** Returns the generic object-stringified string or, if specified, a string representative of the collection */
toString(as) {
if (!as || !['cookie-header', 'set-cookie'].includes(as)) {
return `[object ${CookieCollection.name}]`;
}
let result = '';
for (const cookie of Object.values(this.#cookies)) {
if (as == 'cookie-header') {
if (result) {
result += ';';
}
result += cookie.toString('key-value');
}
else {
result += `Set-Cookie: ${cookie.toString('set-cookie')}\r\n`;
}
}
return result;
}
/** Returns an iterable for all cookies in the collection */
values() {
return Object.values(this.#cookies).values();
}
/** Returns an iterable with the key-value pairs of the name itself and the full cookie */
entries() {
return Object.entries(this.#cookies).values();
}
/** Returns an iterable with the name of each cookie in the collection */
keys() {
return Object.keys(this.#cookies).values();
}
[Symbol.iterator]() {
return Object.values(this.#cookies).values();
}
static create = create;
static fromHeaderValue = fromHeaderValue;
static fromObject = fromObject;
static fromEntries = fromEntries;
}
const setCookieKeyMap = {
domain: 'Domain',
expires: 'Expires',
path: 'Path',
secure: 'Secure',
sameSite: 'SameSite',
httpOnly: 'HttpOnly',
partitioned: 'Partitioned',
maxAge: 'Max-Age',
};
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
const daysOfWeek = [
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
];
/** this ensures that it has the correct `toString` method instead of the default */
function ensureCookieStringifies(cookie) {
if (cookie.toString.length != 1) {
Reflect.defineProperty(cookie, 'toString', {
enumerable: false,
configurable: false,
value: function (type) {
let result = `[object Cookie]`;
if (type) {
result = `${this.name}=${this.value}`;
}
if (type === 'set-cookie') {
for (const [key, value] of Object.entries(this)) {
if (key == 'name' || key == 'value' || !(key in setCookieKeyMap))
continue;
if ((key == 'partitioned' || key == 'httpOnly' || key == 'secure') && !value) {
continue;
}
result += `; ${setCookieKeyMap[key]}`;
if (key == 'partitioned' || key == 'httpOnly' || key == 'secure') {
continue;
}
result += '=';
if (key == 'expires') {
const date = value;
result += `${daysOfWeek[date.getUTCDay()]}, ${String(date.getUTCDate()).padStart(2, '0')} ${months[date.getUTCMonth()]} ${date.getUTCFullYear()} ${String(date.getUTCHours()).padStart(2, '0')}:${String(date.getUTCMinutes()).padStart(2, '0')}:${String(date.getUTCSeconds()).padStart(2, '0')} GMT`;
}
else {
result += value;
}
}
}
return result;
},
});
}
}
export default CookieCollection;
//# sourceMappingURL=cookie-collection.js.map