litenode
Version:
Lightweight and modular web framework
256 lines (221 loc) • 8.2 kB
JavaScript
/**
* Parse cookie string and return an object with all cookies
* @param {string} cookieString - The cookie header string to parse
* @returns {Object} Object containing all cookies as key-value pairs
*/
export function parseCookies(cookieString) {
const cookies = {}
if (!cookieString) return cookies
cookieString.split(";").forEach((cookie) => {
const parts = cookie.split("=")
const name = parts[0].trim()
// Handle cookies without values (flags) and properly decode values
const value = parts.length > 1 ? decodeURIComponent(parts[1].trim()) : ""
if (name) {
cookies[name] = value
}
})
return cookies
}
/**
* Cookie parser middleware for LiteNode
* Adds a cookies object to the request containing all parsed cookies
*/
export function cookieParser() {
return (req, res) => {
req.cookies = parseCookies(req.headers.cookie)
}
}
/**
* Serializes cookie options into a string
* @param {Object} options - Cookie options
* @returns {string} Serialized cookie options string
*/
function serializeCookieOptions(options) {
const optionsArray = []
if (options.maxAge) {
optionsArray.push(`Max-Age=${options.maxAge}`)
} else if (options.expires) {
if (options.expires instanceof Date) {
optionsArray.push(`Expires=${options.expires.toUTCString()}`)
} else {
optionsArray.push(`Expires=${options.expires}`)
}
}
if (options.domain) {
optionsArray.push(`Domain=${options.domain}`)
}
if (options.path) {
optionsArray.push(`Path=${options.path}`)
} else {
// Default path to root if not specified
optionsArray.push("Path=/")
}
if (options.secure) {
optionsArray.push("Secure")
}
if (options.httpOnly) {
optionsArray.push("HttpOnly")
}
if (options.sameSite) {
optionsArray.push(`SameSite=${options.sameSite}`)
}
return optionsArray.join("; ")
}
/**
* Extends the response object with cookie management methods
* @param {Object} nativeRes - The native response object
*/
export function extendResponseWithCookies(nativeRes) {
/**
* Sets a cookie with the given name, value, and options
*
* @param {string} name - The name of the cookie
* @param {string} value - The value of the cookie
* @param {Object} options - Cookie options
* @param {number} options.maxAge - Max age in seconds
* @param {Date|string} options.expires - Expiration date
* @param {string} options.path - Cookie path (defaults to '/')
* @param {string} options.domain - Cookie domain
* @param {boolean} options.secure - Secure flag
* @param {boolean} options.httpOnly - HttpOnly flag
* @param {string} options.sameSite - SameSite policy ('Strict', 'Lax', or 'None')
* @returns {Object} The response object for chaining
*
* @example
* // Set a simple cookie that expires in 1 hour
* res.setCookie('session', 'abc123', { maxAge: 3600 });
*
* // Set a secure, http-only cookie
* res.setCookie('auth', 'token', {
* maxAge: 86400,
* secure: true,
* httpOnly: true,
* sameSite: 'Strict'
* });
*/
nativeRes.setCookie = (name, value, options = {}) => {
if (!name || name.length === 0) {
throw new Error("Cookie name is required")
}
// Handle invalid characters in cookie name
if (/[=,; \t\r\n\013\014]/.test(name)) {
throw new Error("Cookie name contains invalid characters")
}
// Handle null or undefined value as empty string
const stringValue = value != null ? value : ""
// Encode the cookie value to handle special characters
const encodedValue = encodeURIComponent(stringValue)
// Build the cookie string
let cookieString = `${name}=${encodedValue}`
// Add options if any
if (Object.keys(options).length > 0) {
const serializedOptions = serializeCookieOptions(options)
if (serializedOptions) {
cookieString += `; ${serializedOptions}`
}
}
// Set the cookie header
const existingCookies = nativeRes.getHeader("Set-Cookie") || []
const cookies = Array.isArray(existingCookies) ? existingCookies : [existingCookies]
nativeRes.setHeader("Set-Cookie", [...cookies, cookieString])
return nativeRes // Allow chaining
}
/**
* Gets the current value of cookie headers to be set
*
* @returns {Array} Array of cookie headers
*/
nativeRes.getCookies = () => {
return nativeRes.getHeader("Set-Cookie") || []
}
/**
* Clears a cookie by setting its expiration in the past
*
* @param {string} name - The name of the cookie to clear
* @param {Object} options - Cookie options (path and domain must match the original cookie)
* @returns {Object} The response object for chaining
*
* @example
* // Clear a cookie
* res.clearCookie('session');
*
* // Clear a cookie with specific path/domain
* res.clearCookie('auth', { path: '/api', domain: 'example.com' });
*/
nativeRes.clearCookie = (name, options = {}) => {
// To clear a cookie, set maxAge to 0 and expires to a past date
const clearOptions = {
...options,
expires: new Date(0), // Thu, 01 Jan 1970 00:00:00 GMT
maxAge: 0,
}
return nativeRes.setCookie(name, "", clearOptions)
}
}
/**
* A simple signed cookie implementation using HMAC
* @param {string} secret - The secret key used for signing
* @returns {Object} Object with sign and verify methods
*/
export function createSignedCookies(secret) {
if (!secret || typeof secret !== "string" || secret.length < 16) {
throw new Error("A strong secret of at least 16 characters is required for signed cookies")
}
const sign = async (value) => {
const { createHmac } = await import("node:crypto")
const hmac = createHmac("sha256", secret)
hmac.update(value)
const signature = hmac.digest("base64")
return `${value}.${signature}`
}
const verify = async (signedValue) => {
if (!signedValue || !signedValue.includes(".")) return null
const [value, signature] = signedValue.split(".")
const { createHmac } = await import("node:crypto")
const hmac = createHmac("sha256", secret)
hmac.update(value)
const expectedSignature = hmac.digest("base64")
if (signature === expectedSignature) {
return value
}
return null
}
return {
/**
* Signs a cookie value
* @param {string} value - The value to sign
* @returns {Promise<string>} The signed value
*/
sign,
/**
* Verifies a signed cookie value
* @param {string} signedValue - The signed value to verify
* @returns {Promise<string|null>} The original value if signature is valid, null otherwise
*/
verify,
/**
* Sets a signed cookie
* @param {Object} res - The response object
* @param {string} name - The cookie name
* @param {string} value - The cookie value
* @param {Object} options - Cookie options
* @returns {Promise<Object>} The response object for chaining
*/
async setCookie(res, name, value, options = {}) {
const signedValue = await sign(value)
return res.setCookie(name, signedValue, options)
},
/**
* Gets and verifies a signed cookie
* @param {Object} req - The request object
* @param {string} name - The cookie name
* @returns {Promise<string|null>} The original value if signature is valid, null otherwise
*/
async getCookie(req, name) {
const signedValue = req.cookies[name]
if (!signedValue) return null
return await verify(signedValue)
},
}
}