UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

216 lines (215 loc) • 6.79 kB
import { parseCookie, stringifySetCookie } from "cookie"; import { AstroError, AstroErrorData } from "../errors/index.js"; const DELETED_EXPIRATION = new Date(0); const DELETED_VALUE = "deleted"; const responseSentSymbol = Symbol.for("astro.responseSent"); const identity = (value) => value; class AstroCookie { value; constructor(value) { this.value = value; } json() { if (this.value === undefined) { throw new Error(`Cannot convert undefined to an object.`); } return JSON.parse(this.value); } number() { return Number(this.value); } boolean() { if (this.value === "false") return false; if (this.value === "0") return false; return Boolean(this.value); } } class AstroCookies { #request; #requestValues; #outgoing; #consumed; #logger; constructor(request, logger) { this.#request = request; this.#requestValues = null; this.#outgoing = null; this.#consumed = false; this.#logger = logger; } /** * Astro.cookies.delete(key) is used to delete a cookie. Using this method will result * in a Set-Cookie header added to the response. * @param key The cookie to delete * @param options Options related to this deletion, such as the path of the cookie. */ delete(key, options) { // Set-Cookie: token=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT this.#ensureOutgoingMap().set(key, [ DELETED_VALUE, stringifySetCookie({ ...options, name: key, value: DELETED_VALUE, expires: DELETED_EXPIRATION, // Unset `maxAge` to ensure that `expires` takes precedence. maxAge: undefined }), false ]); } /** * Astro.cookies.get(key) is used to get a cookie value. The cookie value is read from the * request. If you have set a cookie via Astro.cookies.set(key, value), the value will be taken * from that set call, overriding any values already part of the request. * @param key The cookie to get. * @returns An object containing the cookie value as well as convenience methods for converting its value. */ get(key, options = undefined) { // Check for outgoing Set-Cookie values first if (this.#outgoing?.has(key)) { let [serializedValue, , isSetValue] = this.#outgoing.get(key); if (isSetValue) { return new AstroCookie(serializedValue); } else { return undefined; } } // decodeURIComponent is the default decode function for cookies const decode = options?.decode ?? decodeURIComponent; const values = this.#ensureParsed(); if (key in values) { const value = values[key]; if (value !== undefined) { let decodedValue; try { decodedValue = decode(value); } catch (_error) { // If decoding fails (e.g., invalid URI sequences), use the original value // This aligns with the behavior of the `cookie` package decodedValue = value; } return new AstroCookie(decodedValue); } } } /** * Astro.cookies.has(key) returns a boolean indicating whether this cookie is either * part of the initial request or set via Astro.cookies.set(key) * @param key The cookie to check for. * @param _options This parameter is no longer used. * @returns */ has(key, _options) { if (this.#outgoing?.has(key)) { let [, , isSetValue] = this.#outgoing.get(key); return isSetValue; } const values = this.#ensureParsed(); return values[key] !== undefined; } /** * Astro.cookies.set(key, value) is used to set a cookie's value. If provided * an object it will be stringified via JSON.stringify(value). Additionally you * can provide options customizing how this cookie will be set, such as setting httpOnly * in order to prevent the cookie from being read in client-side JavaScript. * @param key The name of the cookie to set. * @param value A value, either a string or other primitive or an object. * @param options Options for the cookie, such as the path and security settings. */ set(key, value, options) { if (this.#consumed) { this.#logger.warn("SKIP_FORMAT", "Astro.cookies.set() was called after the cookies had already been sent to the browser.\n" + "This may have happened if this method was called in an imported component.\n" + "Please make sure that Astro.cookies.set() is only called in the frontmatter of the main page."); } let serializedValue; if (typeof value === "string") { serializedValue = value; } else { // Support stringifying JSON objects for convenience. First check that this is // a plain object and if it is, stringify. If not, allow support for toString() overrides. let toStringValue = value.toString(); if (toStringValue === Object.prototype.toString.call(value)) { serializedValue = JSON.stringify(value); } else { serializedValue = toStringValue; } } const { encode, ...attributes } = options ?? {}; this.#ensureOutgoingMap().set(key, [ serializedValue, stringifySetCookie({ ...attributes, name: key, value: serializedValue }, { encode }), true ]); if (this.#request[responseSentSymbol]) { throw new AstroError({ ...AstroErrorData.ResponseSentError }); } } /** * Merges a new AstroCookies instance into the current instance. Any new cookies * will be added to the current instance, overwriting any existing cookies with the same name. */ merge(cookies) { const outgoing = cookies.#outgoing; if (outgoing) { for (const [key, value] of outgoing) { this.#ensureOutgoingMap().set(key, value); } } } /** * Astro.cookies.header() returns an iterator for the cookies that have previously * been set by either Astro.cookies.set() or Astro.cookies.delete(). * This method is primarily used by adapters to set the header on outgoing responses. * @returns */ *headers() { if (this.#outgoing == null) return; for (const [, value] of this.#outgoing) { yield value[1]; } } /** * Marks the cookies as consumed and returns the header values. * After consumption, any subsequent `set()` calls will warn. */ consume() { this.#consumed = true; return this.headers(); } /** * @deprecated Use the instance method `cookies.consume()` instead. * Kept for backward compatibility with adapters. */ static consume(cookies) { return cookies.consume(); } #ensureParsed() { if (!this.#requestValues) { this.#parse(); } if (!this.#requestValues) { this.#requestValues = Object.create(null); } return this.#requestValues; } #ensureOutgoingMap() { if (!this.#outgoing) { this.#outgoing = new Map(); } return this.#outgoing; } #parse() { const raw = this.#request.headers.get("cookie"); if (!raw) { return; } // Pass identity function for decoding so it doesn't use the default. // We'll do the actual decoding when we read the value. this.#requestValues = parseCookie(raw, { decode: identity }); } } export { AstroCookies };