Initial reliable release of consent engine

This commit is contained in:
Deyson Thomé
2026-09-02 14:19:05 -03:00
commit 9e1082d47b
22 changed files with 3377 additions and 0 deletions
+551
View File
@@ -0,0 +1,551 @@
/**
* Cookie Consent Core
* consent.js
*
* Main consent engine.
*
* Responsibilities:
* - initialize consent state
* - load/save persisted consent
* - manage categories
* - manage policy version
* - expose consent operations
* - emit lifecycle events
*
* Deliberately excluded:
* - DOM/UI
* - GTM
* - geolocation
* - network requests
* - legal/jurisdiction rules
*/
import {
createDefaultCategories,
createUnknownCategories,
createAcceptedCategories,
createRejectedCategories,
normalizeCategories,
mergeCategories,
hasAcceptedAll,
hasRejectedAll,
isDecisionComplete,
isValidCategoriesObject,
toPermissionMap
} from "./categories.js";
import {
createCookieStorage,
isStoragePayload,
STORAGE_DEFAULTS
} from "./storage.js";
import { createEventBus } from "./events.js";
export const ENGINE_VERSION = "1.0.1";
export const DEFAULT_POLICY_VERSION = "2026-01";
export const DEFAULT_JURISDICTION = "global";
const DEFAULT_CONFIG = Object.freeze({
policyVersion: DEFAULT_POLICY_VERSION,
jurisdiction: DEFAULT_JURISDICTION,
requireDecision: true,
storage: {
name: STORAGE_DEFAULTS.name,
duration: STORAGE_DEFAULTS.duration,
path: STORAGE_DEFAULTS.path,
sameSite: STORAGE_DEFAULTS.sameSite
}
});
function isPlainObject(value) {
return Boolean(
value &&
typeof value === "object" &&
!Array.isArray(value)
);
}
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
function mergeConfig(base, overrides) {
const source = isPlainObject(overrides) ? overrides : {};
return {
...base,
...source,
storage: {
...base.storage,
...(isPlainObject(source.storage) ? source.storage : {})
}
};
}
function buildStoredPayload({
categories,
policyVersion,
jurisdiction,
gpc,
timestamp
}) {
return {
engineVersion: ENGINE_VERSION,
policyVersion,
timestamp,
categories: clone(categories),
jurisdiction,
gpc: Boolean(gpc)
};
}
function isExpired(payload, durationDays, currentTime = Date.now()) {
if (!payload || !Number.isFinite(payload.timestamp)) {
return true;
}
if (!Number.isFinite(durationDays) || durationDays <= 0) {
return false;
}
const durationMs = durationDays * 24 * 60 * 60 * 1000;
const age = currentTime - payload.timestamp;
// A materially future timestamp is not accepted as valid persistence.
if (age < 0) {
return true;
}
return age >= durationMs;
}
function getInitialState(config) {
return {
initialized: false,
hasDecision: false,
categories: createUnknownCategories(),
policyVersion: config.policyVersion,
jurisdiction: config.jurisdiction,
gpc: false,
timestamp: null,
source: "default"
};
}
function categoriesEqual(a, b) {
return (
a.necessary === b.necessary &&
a.preferences === b.preferences &&
a.analytics === b.analytics &&
a.marketing === b.marketing
);
}
export function createConsentEngine(options = {}) {
const config = mergeConfig(DEFAULT_CONFIG, options);
const events = createEventBus();
const storage = createCookieStorage(config.storage);
let state = getInitialState(config);
let initialized = false;
function getState() {
return clone(state);
}
function emit(name, extra = {}) {
events.emit(name, {
...getState(),
...extra
});
}
function persist(categories, metadata = {}) {
const timestamp = Date.now();
const payload = buildStoredPayload({
categories,
policyVersion: config.policyVersion,
jurisdiction:
metadata.jurisdiction || config.jurisdiction,
gpc:
typeof metadata.gpc === "boolean"
? metadata.gpc
: state.gpc,
timestamp
});
try {
const saved = storage.set(payload);
if (!saved) {
events.emit("storage:error", {
operation: "set",
payload: getState()
});
return false;
}
state = {
...state,
categories: normalizeCategories(
payload.categories,
createDefaultCategories()
),
hasDecision: isDecisionComplete(payload.categories),
policyVersion: payload.policyVersion,
jurisdiction: payload.jurisdiction,
gpc: Boolean(payload.gpc),
timestamp: payload.timestamp,
source: metadata.source || "user"
};
return true;
} catch (error) {
events.emit("storage:error", {
operation: "set",
error
});
return false;
}
}
function removePersistedCookie(operation) {
try {
const removed = storage.remove();
if (!removed) {
events.emit("storage:error", {
operation
});
return false;
}
return true;
} catch (error) {
events.emit("storage:error", {
operation,
error
});
return false;
}
}
function loadPersistedState() {
let payload;
try {
payload = storage.get();
} catch (error) {
events.emit("storage:error", {
operation: "get",
error
});
return false;
}
if (!isStoragePayload(payload)) {
return false;
}
if (!isValidCategoriesObject(payload.categories)) {
// Invalid persistence is not allowed to influence the live state.
// Attempt to remove it so it cannot repeatedly poison initialization.
removePersistedCookie("remove-invalid");
return false;
}
if (payload.policyVersion !== config.policyVersion) {
removePersistedCookie("remove-policy-mismatch");
return false;
}
if (isExpired(payload, config.storage.duration)) {
const removed = removePersistedCookie("remove-expired");
if (!removed) {
// Do not pretend the stale cookie has been removed.
// The in-memory state remains undecided for this page load.
state = {
...state,
categories: createUnknownCategories(),
hasDecision: false,
timestamp: null,
source: "expired"
};
emit("consent:expired");
return false;
}
state = {
...state,
categories: createUnknownCategories(),
hasDecision: false,
timestamp: null,
source: "expired"
};
emit("consent:expired");
return false;
}
state = {
...state,
categories: normalizeCategories(
payload.categories,
createUnknownCategories()
),
hasDecision: isDecisionComplete(payload.categories),
policyVersion: config.policyVersion,
jurisdiction: payload.jurisdiction,
gpc: payload.gpc,
timestamp: payload.timestamp,
source: "storage"
};
emit("consent:loaded");
return true;
}
function initialize() {
if (initialized) {
return getState();
}
initialized = true;
loadPersistedState();
state = {
...state,
initialized: true
};
emit("initialized");
return getState();
}
function ensureInitialized() {
if (!initialized) {
initialize();
}
}
function setConsent(changes, metadata = {}) {
ensureInitialized();
if (!isPlainObject(changes)) {
throw new TypeError("Consent changes must be an object.");
}
const previous = state.categories;
const next = mergeCategories(previous, changes);
const changed = !categoriesEqual(previous, next);
if (!changed) {
return getState();
}
const saved = persist(next, metadata);
if (!saved) {
return getState();
}
emit("consent:changed", {
previousCategories: previous,
categories: next
});
if (hasAcceptedAll(next)) {
emit("consent:accepted");
} else if (hasRejectedAll(next)) {
emit("consent:rejected");
}
return getState();
}
function acceptAll() {
ensureInitialized();
const previous = state.categories;
const next = createAcceptedCategories();
if (categoriesEqual(previous, next)) {
return getState();
}
const saved = persist(next);
if (!saved) {
return getState();
}
emit("consent:changed", {
previousCategories: previous,
categories: next
});
emit("consent:accepted");
return getState();
}
function rejectAll() {
ensureInitialized();
const previous = state.categories;
const next = createRejectedCategories();
if (categoriesEqual(previous, next)) {
return getState();
}
const saved = persist(next);
if (!saved) {
return getState();
}
emit("consent:changed", {
previousCategories: previous,
categories: next
});
emit("consent:rejected");
return getState();
}
function revoke() {
ensureInitialized();
const previous = state.categories;
const next = createRejectedCategories();
if (categoriesEqual(previous, next)) {
return getState();
}
const saved = persist(next, {
jurisdiction: state.jurisdiction,
gpc: state.gpc,
source: "user"
});
if (!saved) {
return getState();
}
emit("consent:revoked", {
previousCategories: previous,
categories: next
});
emit("consent:changed", {
previousCategories: previous,
categories: next
});
return getState();
}
function reset() {
ensureInitialized();
const removed = removePersistedCookie("remove-reset");
if (!removed) {
// Do not create a false local reset when persisted consent may
// still exist. This guarantees reload consistency.
return getState();
}
state = getInitialState(config);
state.initialized = true;
emit("consent:reset");
return getState();
}
function setGpc(value) {
ensureInitialized();
state = {
...state,
gpc: Boolean(value)
};
return getState();
}
function setJurisdiction(value) {
ensureInitialized();
if (typeof value !== "string" || !value.trim()) {
throw new TypeError(
"jurisdiction must be a non-empty string."
);
}
state = {
...state,
jurisdiction: value.trim()
};
return getState();
}
function hasConsent(category) {
ensureInitialized();
if (
typeof category !== "string" ||
!Object.prototype.hasOwnProperty.call(
state.categories,
category
)
) {
return false;
}
return state.categories[category] === "granted";
}
function isComplete() {
ensureInitialized();
return state.hasDecision;
}
function isInitialized() {
return initialized;
}
return Object.freeze({
initialize,
getState,
getConsent: getState,
setConsent,
acceptAll,
rejectAll,
revoke,
reset,
setGpc,
setJurisdiction,
hasConsent,
isComplete,
isInitialized,
toPermissionMap() {
ensureInitialized();
return toPermissionMap(state.categories);
},
getConfig() {
return clone(config);
},
on: events.on,
once: events.once,
off: events.off
});
}