@taxum/cookie
Version:
Cookie jar for Taxum
174 lines • 5.53 kB
JavaScript
/**
* Decodes a percent-encoded cookie component.
*
* A component that fails to decode (an invalid percent sequence) is returned
* unchanged rather than throwing, so a single malformed cookie does not fail the
* whole request. This is more conservative than a per-escape lossy decode: the
* entire component is left raw when any escape is invalid, not just the bad
* bytes.
*/
const decodeComponent = (value) => {
try {
return decodeURIComponent(value);
}
catch {
return value;
}
};
/**
* Rejects a `Path` or `Domain` attribute value that could break out of the
* `Set-Cookie` line.
*
* These attributes are written verbatim, so a control character or a ";"
* would allow header or attribute injection. Name and value are safe because
* they are percent-encoded on {@link Cookie.encode}.
*/
const assertValidAttribute = (field, value) => {
for (const character of value) {
const code = character.charCodeAt(0);
if (code <= 0x1f || code === 0x7f || character === ";") {
throw new TypeError(`Cookie ${field} must not contain control characters or a ";" separator`);
}
}
};
/**
* Represents an HTTP cookie.
*
* @example
* ```ts
* import { Cookie } from "@taxum/cookie";
*
* const cookie = new Cookie("my-cookie", "my-value");
* ```
*/
export class Cookie {
name;
value;
expires;
maxAge;
domain;
path;
secure;
httpOnly;
sameSite;
partitioned;
/**
* Creates a new {@link Cookie} with a given name and value.
*
* You can additionally supply any of the standard cookie options.
*/
constructor(name, value = "", options) {
if (options?.path !== undefined) {
assertValidAttribute("path", options.path);
}
if (options?.domain !== undefined) {
assertValidAttribute("domain", options.domain);
}
this.name = name;
this.value = value;
this.expires = options?.expires;
this.maxAge = options?.maxAge;
this.domain = options?.domain;
this.path = options?.path;
this.secure = options?.secure;
this.httpOnly = options?.httpOnly;
this.sameSite = options?.sameSite;
this.partitioned = options?.partitioned;
}
/**
* Parses a cookie string into a `Cookie` header.
*/
static parse(cookie) {
const separatorIndex = cookie.indexOf("=");
if (separatorIndex === -1) {
return null;
}
const name = cookie.slice(0, separatorIndex).trim();
const value = cookie.slice(separatorIndex + 1).trim();
if (name.length === 0) {
return null;
}
return new Cookie(decodeComponent(name), decodeComponent(value));
}
/**
* Converts the cookie to a removal cookie.
*/
asRemoval() {
return new Cookie(this.name, "", {
expires: new Date(0),
maxAge: 0,
domain: this.domain,
path: this.path,
secure: this.secure,
httpOnly: this.httpOnly,
sameSite: this.sameSite,
partitioned: this.partitioned,
});
}
/**
* Returns a new cookie with the specified value.
*/
withValue(value) {
return new Cookie(this.name, value, {
expires: this.expires,
maxAge: this.maxAge,
domain: this.domain,
path: this.path,
secure: this.secure,
httpOnly: this.httpOnly,
sameSite: this.sameSite,
partitioned: this.partitioned,
});
}
/**
* Encodes the cookie for `Set-Cookie` headers.
*
* biome-ignore lint/complexity/noExcessiveCognitiveComplexity: simple enough
*/
encode() {
const parameters = [`${encodeURIComponent(this.name)}=${encodeURIComponent(this.value)}`];
if (this.httpOnly) {
parameters.push("HttpOnly");
}
if (this.sameSite) {
parameters.push(`SameSite=${this.sameSite}`);
}
if (this.partitioned) {
parameters.push("Partitioned");
}
if (this.secure ||
this.partitioned ||
(this.secure === undefined && this.sameSite === "None")) {
parameters.push("Secure");
}
if (this.path !== undefined) {
parameters.push(`Path=${this.path}`);
}
if (this.domain !== undefined) {
parameters.push(`Domain=${this.domain}`);
}
if (this.maxAge !== undefined) {
if (typeof this.maxAge === "number") {
parameters.push(`Max-Age=${Math.max(0, Math.floor(this.maxAge))}`);
}
else {
const totalOf = {
unit: "seconds",
relativeTo: Temporal.Now.zonedDateTimeISO(),
};
parameters.push(`Max-Age=${Math.max(0, Math.floor(this.maxAge.total(totalOf)))}`);
}
}
if (this.expires) {
if (this.expires instanceof Date) {
parameters.push(`Expires=${this.expires.toUTCString()}`);
}
else {
const instant = "toInstant" in this.expires ? this.expires.toInstant() : this.expires;
parameters.push(`Expires=${new Date(instant.epochMilliseconds).toUTCString()}`);
}
}
return parameters.join("; ");
}
}
//# sourceMappingURL=cookie.js.map