@taxum/cookie
Version:
Cookie jar for Taxum
161 lines (140 loc) • 4.64 kB
text/typescript
import type { Temporal } from "temporal-spec";
export type CookieOptions = {
expires?: Date | Temporal.ZonedDateTime;
maxAge?: number | Temporal.Duration;
domain?: string;
path?: string;
secure?: boolean;
httpOnly?: boolean;
sameSite?: "Strict" | "Lax" | "None";
partitioned?: boolean;
};
/**
* Represents an HTTP cookie.
*
* @example
* ```ts
* import { Cookie } from "@taxum/cookie";
*
* const cookie = new Cookie("my-cookie", "my-value");
* ```
*/
export class Cookie {
public readonly name: string;
public readonly value: string;
public readonly expires: Date | Temporal.ZonedDateTime | undefined;
public readonly maxAge: number | Temporal.Duration | undefined;
public readonly domain: string | undefined;
public readonly path: string | undefined;
public readonly secure: boolean | undefined;
public readonly httpOnly: boolean | undefined;
public readonly sameSite: "Strict" | "Lax" | "None" | undefined;
public readonly partitioned: boolean | undefined;
/**
* Creates a new {@link Cookie} with a given name and value.
*
* You can additionally supply any of the standard cookie options.
*/
public constructor(name: string, value = "", options?: CookieOptions) {
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.
*/
public static parse(cookie: string): Cookie | null {
const nameValue = cookie.split("=", 2);
if (nameValue.length !== 2) {
return null;
}
const name = nameValue[0].trim();
const value = nameValue[1].trim();
if (name.length === 0) {
return null;
}
return new Cookie(decodeURIComponent(name), decodeURIComponent(value));
}
/**
* Converts the cookie to a removal cookie.
*/
public asRemoval(): Cookie {
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.
*/
public withValue(value: string): Cookie {
return new Cookie(this.name, value, {
expires: this.expires,
maxAge: this.maxAge,
domain: this.domain,
path: this.path,
secure: this.secure,
httpOnly: this.httpOnly,
});
}
/**
* Encodes the cookie for `Set-Cookie` headers.
*
* biome-ignore lint/complexity/noExcessiveCognitiveComplexity: simple enough
*/
public encode(): string {
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 {
parameters.push(`Max-Age=${Math.max(0, this.maxAge.total({ unit: "seconds" }))}`);
}
}
if (this.expires) {
if (this.expires instanceof Date) {
parameters.push(`Expires=${this.expires.toUTCString()}`);
} else {
parameters.push(
`Expires=${new Date(this.expires.toInstant().epochMilliseconds).toUTCString()}`,
);
}
}
return parameters.join("; ");
}
}