import {Request} from '../lifecycle/Request' import {uninfer, infer, uuid4} from '../../util' /** * Base type representing a parsed cookie. */ export interface HTTPCookie { key: string, originalValue: string, value: any, exists: boolean, options?: HTTPCookieOptions, } /** * Type alias for something that is either an HTTP cookie, or undefined. */ export type MaybeHTTPCookie = HTTPCookie | undefined; /** * Interface describing the available cookie options. */ export interface HTTPCookieOptions { domain?: string, expires?: Date, // encodeURIComponent httpOnly?: boolean, maxAge?: number, path?: string, secure?: boolean, signed?: boolean, sameSite?: 'strict' | 'lax' | 'none-secure', } /** * Class for accessing and managing cookies in the associated request. */ export class HTTPCookieJar { /** The cookies parsed from the request. */ protected parsed: {[key: string]: HTTPCookie} = {} constructor( /** The request whose cookies should be loaded. */ protected request: Request, ) { this.parseCookies() } /** * Gets the HTTPCookie by name, if it exists. * @param name */ get(name: string): MaybeHTTPCookie { if ( name in this.parsed ) { return this.parsed[name] } } /** * Set a new cookie using the specified options. * @param name * @param value * @param options */ set(name: string, value: unknown, options?: HTTPCookieOptions): this { this.parsed[name] = { key: name, value, originalValue: uninfer(value), exists: false, options, } return this } /** * Returns true if a cookie exists with the given name. * @param name */ has(name: string): boolean { return Boolean(this.parsed[name]) } /** * Clears the given cookie. * * Important: if the cookie was set with any `options`, the SAME options * must be provided here in order for the cookie to be cleared on the client. * * @param name * @param options */ clear(name: string, options?: HTTPCookieOptions): this { if ( !options ) { options = {} } options.expires = new Date(0) this.parsed[name] = { key: name, value: undefined, originalValue: uuid4(), exists: false, options, } return this } /** * Get an array of `Set-Cookie` headers to include in the response. */ getSetCookieHeaders(): string[] { const headers: string[] = [] for ( const key in this.parsed ) { if ( !Object.prototype.hasOwnProperty.call(this.parsed, key) ) { continue } const cookie = this.parsed[key] if ( cookie.exists ) { continue } const parts = [] parts.push(`${key}=${encodeURIComponent(cookie.originalValue)}`) if ( cookie.options?.expires ) { parts.push(`Expires=${cookie.options.expires.toUTCString()}`) } if ( cookie.options?.maxAge ) { parts.push(`Max-Age=${Math.floor(cookie.options.maxAge)}`) } if ( cookie.options?.domain ) { parts.push(`Domain=${cookie.options.domain}`) } if ( cookie.options?.path ) { parts.push(`Path=${cookie.options.path}`) } if ( cookie.options?.secure ) { parts.push('Secure') } if ( cookie.options?.httpOnly ) { parts.push('HttpOnly') } if ( cookie.options?.sameSite ) { const map = { strict: 'Strict', lax: 'Lax', 'none-secure': 'None; Secure', } parts.push(map[cookie.options.sameSite]) } headers.push(parts.join('; ')) } return headers } /** Parse the cookies from the request. */ private parseCookies() { const cookies = String(this.request.getHeader('cookie')) cookies.split(';').forEach(cookie => { const parts = cookie.split('=') const key = parts.shift()?.trim() if ( !key ) { return } const value = decodeURI(parts.join('=')) this.parsed[key] = { key, originalValue: value, value: infer(value), exists: true, } }) } }