/** * Cookie Consent Core * storage.js * * Cookie-based persistence for consent state. * This module deliberately contains no consent/business operations. */ const DEFAULT_COOKIE_NAME = "cc_consent"; const DEFAULT_DURATION_DAYS = 180; const DEFAULT_PATH = "/"; const DEFAULT_SAME_SITE = "Lax"; function isBrowser() { return typeof document !== "undefined"; } function encode(value) { return encodeURIComponent(value); } function decode(value) { try { return decodeURIComponent(value); } catch { return null; } } function buildAttributes(options = {}) { const { duration, path = DEFAULT_PATH, domain, sameSite = DEFAULT_SAME_SITE, secure, partitioned = false } = options; const attributes = []; if (typeof duration === "number" && Number.isFinite(duration)) { if (duration <= 0) { attributes.push("Max-Age=0"); } else { const maxAge = Math.floor(duration * 24 * 60 * 60); attributes.push(`Max-Age=${maxAge}`); } } if (path) { attributes.push(`Path=${path}`); } if (domain) { attributes.push(`Domain=${domain}`); } if (sameSite) { attributes.push(`SameSite=${sameSite}`); } const shouldSecure = secure === true || (secure === undefined && typeof location !== "undefined" && location.protocol === "https:"); if (shouldSecure) { attributes.push("Secure"); } if (partitioned) { attributes.push("Partitioned"); } return attributes.join("; "); } export function createCookieStorage(options = {}) { const config = { name: options.name || DEFAULT_COOKIE_NAME, duration: typeof options.duration === "number" ? options.duration : DEFAULT_DURATION_DAYS, path: options.path || DEFAULT_PATH, domain: options.domain, sameSite: options.sameSite || DEFAULT_SAME_SITE, secure: options.secure, partitioned: options.partitioned === true }; return Object.freeze({ name: config.name, getRaw() { if (!isBrowser()) { return null; } const prefix = `${encode(config.name)}=`; const cookies = document.cookie ? document.cookie.split("; ") : []; for (const item of cookies) { if (item.startsWith(prefix)) { return decode(item.slice(prefix.length)); } } return null; }, get() { const raw = this.getRaw(); if (!raw) { return null; } try { return JSON.parse(raw); } catch { return null; } }, set(payload) { if (!isBrowser()) { return false; } let serialized; try { serialized = JSON.stringify(payload); } catch { return false; } const attributes = buildAttributes(config); document.cookie = `${encode(config.name)}=${encode(serialized)}` + (attributes ? `; ${attributes}` : ""); return this.getRaw() !== null; }, remove() { if (!isBrowser()) { return false; } const attributes = buildAttributes({ ...config, duration: 0 }); document.cookie = `${encode(config.name)}=` + (attributes ? `; ${attributes}` : ""); return this.getRaw() === null; }, getConfig() { return { ...config }; } }); } /** * Validates the persisted envelope before the consent engine attempts to * interpret its contents. Category semantics are validated separately. */ export function isStoragePayload(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return false; } if (typeof value.engineVersion !== "string" || !value.engineVersion) { return false; } if (typeof value.policyVersion !== "string" || !value.policyVersion) { return false; } if (!Number.isFinite(value.timestamp)) { return false; } if (!value.categories || typeof value.categories !== "object" || Array.isArray(value.categories)) { return false; } if (typeof value.jurisdiction !== "string" || !value.jurisdiction) { return false; } if (typeof value.gpc !== "boolean") { return false; } return true; } export const STORAGE_DEFAULTS = Object.freeze({ name: DEFAULT_COOKIE_NAME, duration: DEFAULT_DURATION_DAYS, path: DEFAULT_PATH, sameSite: DEFAULT_SAME_SITE });