217 lines
4.9 KiB
JavaScript
217 lines
4.9 KiB
JavaScript
/**
|
|
* 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
|
|
});
|