Initial reliable release of consent engine
This commit is contained in:
+216
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* 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
|
||||
});
|
||||
Reference in New Issue
Block a user