UNPKG

hono

Version:

Web framework built on Web Standards

138 lines (137 loc) 4.73 kB
// src/utils/cookie.ts import { decodeURIComponent_ } from "./url.js"; var algorithm = { name: "HMAC", hash: "SHA-256" }; var getCryptoKey = async (secret) => { const secretBuf = typeof secret === "string" ? new TextEncoder().encode(secret) : secret; return await crypto.subtle.importKey("raw", secretBuf, algorithm, false, ["sign", "verify"]); }; var makeSignature = async (value, secret) => { const key = await getCryptoKey(secret); const signature = await crypto.subtle.sign(algorithm.name, key, new TextEncoder().encode(value)); return btoa(String.fromCharCode(...new Uint8Array(signature))); }; var verifySignature = async (base64Signature, value, secret) => { try { const signatureBinStr = atob(base64Signature); const signature = new Uint8Array(signatureBinStr.length); for (let i = 0, len = signatureBinStr.length; i < len; i++) { signature[i] = signatureBinStr.charCodeAt(i); } return await crypto.subtle.verify(algorithm, secret, signature, new TextEncoder().encode(value)); } catch (e) { return false; } }; var validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/; var validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/; var parse = (cookie, name) => { const pairs = cookie.trim().split(";"); return pairs.reduce((parsedCookie, pairStr) => { pairStr = pairStr.trim(); const valueStartPos = pairStr.indexOf("="); if (valueStartPos === -1) { return parsedCookie; } const cookieName = pairStr.substring(0, valueStartPos).trim(); if (name && name !== cookieName || !validCookieNameRegEx.test(cookieName)) { return parsedCookie; } let cookieValue = pairStr.substring(valueStartPos + 1).trim(); if (cookieValue.startsWith('"') && cookieValue.endsWith('"')) { cookieValue = cookieValue.slice(1, -1); } if (validCookieValueRegEx.test(cookieValue)) { parsedCookie[cookieName] = decodeURIComponent_(cookieValue); } return parsedCookie; }, {}); }; var parseSigned = async (cookie, secret, name) => { const parsedCookie = {}; const secretKey = await getCryptoKey(secret); for (const [key, value] of Object.entries(parse(cookie, name))) { const signatureStartPos = value.lastIndexOf("."); if (signatureStartPos < 1) { continue; } const signedValue = value.substring(0, signatureStartPos); const signature = value.substring(signatureStartPos + 1); if (signature.length !== 44 || !signature.endsWith("=")) { continue; } const isVerified = await verifySignature(signature, signedValue, secretKey); parsedCookie[key] = isVerified ? signedValue : false; } return parsedCookie; }; var _serialize = (name, value, opt = {}) => { let cookie = `${name}=${value}`; if (name.startsWith("__Secure-") && !opt.secure) { throw new Error("__Secure- Cookie must have Secure attributes"); } if (name.startsWith("__Host-")) { if (!opt.secure) { throw new Error("__Host- Cookie must have Secure attributes"); } if (opt.path !== "/") { throw new Error('__Host- Cookie must have Path attributes with "/"'); } if (opt.domain) { throw new Error("__Host- Cookie must not have Domain attributes"); } } if (opt && typeof opt.maxAge === "number" && opt.maxAge >= 0) { if (opt.maxAge > 3456e4) { throw new Error( "Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration." ); } cookie += `; Max-Age=${Math.floor(opt.maxAge)}`; } if (opt.domain && opt.prefix !== "host") { cookie += `; Domain=${opt.domain}`; } if (opt.path) { cookie += `; Path=${opt.path}`; } if (opt.expires) { if (opt.expires.getTime() - Date.now() > 3456e7) { throw new Error( "Cookies Expires SHOULD NOT be greater than 400 days (34560000 seconds) in the future." ); } cookie += `; Expires=${opt.expires.toUTCString()}`; } if (opt.httpOnly) { cookie += "; HttpOnly"; } if (opt.secure) { cookie += "; Secure"; } if (opt.sameSite) { cookie += `; SameSite=${opt.sameSite.charAt(0).toUpperCase() + opt.sameSite.slice(1)}`; } if (opt.partitioned) { if (!opt.secure) { throw new Error("Partitioned Cookie must have Secure attributes"); } cookie += "; Partitioned"; } return cookie; }; var serialize = (name, value, opt) => { value = encodeURIComponent(value); return _serialize(name, value, opt); }; var serializeSigned = async (name, value, secret, opt = {}) => { const signature = await makeSignature(value, secret); value = `${value}.${signature}`; value = encodeURIComponent(value); return _serialize(name, value, opt); }; export { parse, parseSigned, serialize, serializeSigned };