From 9e1082d47bff4b6ff97677eede90976fedce367e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deyson=20Thom=C3=A9?= Date: Wed, 2 Sep 2026 14:19:05 -0300 Subject: [PATCH] Initial reliable release of consent engine --- README.md | 57 +++ core/categories.js | 197 +++++++++ core/consent.js | 551 ++++++++++++++++++++++++ core/events.js | 104 +++++ core/storage.js | 216 ++++++++++ integration/context.js | 45 ++ integration/index.js | 34 ++ jurisdiction/constants.js | 38 ++ jurisdiction/index.js | 35 ++ jurisdiction/jurisdiction.js | 73 ++++ jurisdiction/policies.js | 55 +++ jurisdiction/resolver.js | 215 +++++++++ package.json | 1 + test-core.mjs | 180 ++++++++ tests/audit/audit-test.html | 136 ++++++ tests/audit/audit-test.mjs | 256 +++++++++++ tests/integration/integration-test.html | 173 ++++++++ tests/integration/integration-test.mjs | 136 ++++++ tests/jurisdiction-test.html | 84 ++++ tests/jurisdiction-test.mjs | 196 +++++++++ tests/reliability/reliability-test.mjs | 525 ++++++++++++++++++++++ tests/test.html | 70 +++ 22 files changed, 3377 insertions(+) create mode 100644 README.md create mode 100644 core/categories.js create mode 100644 core/consent.js create mode 100644 core/events.js create mode 100644 core/storage.js create mode 100644 integration/context.js create mode 100644 integration/index.js create mode 100644 jurisdiction/constants.js create mode 100644 jurisdiction/index.js create mode 100644 jurisdiction/jurisdiction.js create mode 100644 jurisdiction/policies.js create mode 100644 jurisdiction/resolver.js create mode 100644 package.json create mode 100644 test-core.mjs create mode 100644 tests/audit/audit-test.html create mode 100644 tests/audit/audit-test.mjs create mode 100644 tests/integration/integration-test.html create mode 100644 tests/integration/integration-test.mjs create mode 100644 tests/jurisdiction-test.html create mode 100644 tests/jurisdiction-test.mjs create mode 100644 tests/reliability/reliability-test.mjs create mode 100644 tests/test.html diff --git a/README.md b/README.md new file mode 100644 index 0000000..d661c7b --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Universal Cookie Consent — Jurisdiction Engine v1 + +Stage 3.2 introduces a pure, deterministic Jurisdiction Engine. + +## Responsibilities + +The engine resolves a jurisdiction context and maps it to a policy family. +It deliberately does **not**: + +- read IP addresses; +- call geolocation APIs; +- access browser location; +- inspect GPC; +- enforce consent; +- modify the Core; +- integrate GTM; +- provide legal advice. + +## Modes + +- `fixed`: the site explicitly selects a jurisdiction. +- `auto`: the site supports multiple jurisdictions and supplies available signals. +- `global`: the site deliberately selects the global baseline for everyone. + +## Resolution priority + +`site-config` > `server` > `application` > `browser` > `fallback` + +The resolver is deterministic and uses only supplied signals. + +## Initial catalog + +- `global` +- `us` +- `us-ma` +- `us-ca` +- `eu` + +The policy catalog is intentionally a structural placeholder. Detailed legal requirements will be researched and added in a later policy stage rather than being guessed or embedded in the resolver. + +## Tests + +Node regression: + +```bash +node tests/jurisdiction-test.mjs +``` + +Browser Test Lab: + +```bash +python -m http.server 8000 +``` + +Then open: + +`http://localhost:8000/tests/jurisdiction-test.html` diff --git a/core/categories.js b/core/categories.js new file mode 100644 index 0000000..732fbeb --- /dev/null +++ b/core/categories.js @@ -0,0 +1,197 @@ +/** + * Cookie Consent Core + * categories.js + * + * Defines the consent categories and normalizes category state. + * This module has no DOM, storage, network, or GTM dependencies. + */ + +export const CATEGORY_NAMES = Object.freeze([ + "necessary", + "preferences", + "analytics", + "marketing" +]); + +/** + * Legacy/default denied state kept as a public export for compatibility. + * New/undecided visitors should use createUnknownCategories(). + */ +export const DEFAULT_CATEGORIES = Object.freeze({ + necessary: "granted", + preferences: "denied", + analytics: "denied", + marketing: "denied" +}); + +const VALID_STATES = new Set(["unknown", "granted", "denied"]); + +export function createDefaultCategories() { + return { ...DEFAULT_CATEGORIES }; +} + +export function createUnknownCategories() { + return { + necessary: "granted", + preferences: "unknown", + analytics: "unknown", + marketing: "unknown" + }; +} + +export function isValidCategory(category) { + return CATEGORY_NAMES.includes(category); +} + +export function isValidState(state) { + return VALID_STATES.has(state); +} + +export function normalizeCategories(input = {}, fallback = DEFAULT_CATEGORIES) { + const source = input && typeof input === "object" ? input : {}; + const fallbackState = + fallback && typeof fallback === "object" + ? fallback + : DEFAULT_CATEGORIES; + + const normalized = { + necessary: "granted", + preferences: "denied", + analytics: "denied", + marketing: "denied" + }; + + for (const category of CATEGORY_NAMES) { + if (category === "necessary") { + normalized.necessary = "granted"; + continue; + } + + const candidate = source[category]; + + if (isValidState(candidate)) { + normalized[category] = candidate; + continue; + } + + const fallbackValue = fallbackState[category]; + normalized[category] = isValidState(fallbackValue) + ? fallbackValue + : "denied"; + } + + return normalized; +} + +/** + * Strict validation used for persisted or externally supplied category + * objects. Unlike normalizeCategories(), this function never repairs data. + */ +export function isValidCategoriesObject(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return false; + } + + for (const category of CATEGORY_NAMES) { + if (!Object.prototype.hasOwnProperty.call(value, category)) { + return false; + } + } + + if (value.necessary !== "granted") { + return false; + } + + return CATEGORY_NAMES + .filter((category) => category !== "necessary") + .every((category) => isValidState(value[category])); +} + +export function mergeCategories(current, changes) { + const base = normalizeCategories(current, DEFAULT_CATEGORIES); + const incoming = changes && typeof changes === "object" ? changes : {}; + const merged = { ...base }; + + for (const category of CATEGORY_NAMES) { + if (category === "necessary") { + if (Object.prototype.hasOwnProperty.call(incoming, category) && + incoming[category] !== "granted") { + throw new TypeError( + `Invalid consent state for "necessary". ` + + `The necessary category must always be "granted".` + ); + } + + merged.necessary = "granted"; + continue; + } + + if (Object.prototype.hasOwnProperty.call(incoming, category)) { + const value = incoming[category]; + + if (!isValidState(value)) { + throw new TypeError( + `Invalid consent state for "${category}". ` + + `Expected "unknown", "granted", or "denied".` + ); + } + + merged[category] = value; + } + } + + return merged; +} + +export function createAcceptedCategories() { + return { + necessary: "granted", + preferences: "granted", + analytics: "granted", + marketing: "granted" + }; +} + +export function createRejectedCategories() { + return { + necessary: "granted", + preferences: "denied", + analytics: "denied", + marketing: "denied" + }; +} + +export function hasAcceptedAll(categories) { + const state = normalizeCategories(categories, DEFAULT_CATEGORIES); + + return CATEGORY_NAMES + .filter((category) => category !== "necessary") + .every((category) => state[category] === "granted"); +} + +export function hasRejectedAll(categories) { + const state = normalizeCategories(categories, DEFAULT_CATEGORIES); + + return CATEGORY_NAMES + .filter((category) => category !== "necessary") + .every((category) => state[category] === "denied"); +} + +export function isDecisionComplete(categories) { + const state = normalizeCategories(categories, createUnknownCategories()); + + return CATEGORY_NAMES + .filter((category) => category !== "necessary") + .every((category) => state[category] !== "unknown"); +} + +export function toPermissionMap(categories) { + const state = normalizeCategories(categories, DEFAULT_CATEGORIES); + + return Object.fromEntries( + CATEGORY_NAMES.map((category) => [ + category, + state[category] === "granted" + ]) + ); +} diff --git a/core/consent.js b/core/consent.js new file mode 100644 index 0000000..fe35c27 --- /dev/null +++ b/core/consent.js @@ -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 + }); +} diff --git a/core/events.js b/core/events.js new file mode 100644 index 0000000..b128dff --- /dev/null +++ b/core/events.js @@ -0,0 +1,104 @@ +/** + * Cookie Consent Core + * events.js + * + * Tiny dependency-free event bus. + */ + +export function createEventBus() { + const listeners = new Map(); + + function on(eventName, callback) { + if (typeof eventName !== "string" || !eventName.trim()) { + throw new TypeError("eventName must be a non-empty string."); + } + + if (typeof callback !== "function") { + throw new TypeError("callback must be a function."); + } + + if (!listeners.has(eventName)) { + listeners.set(eventName, new Set()); + } + + listeners.get(eventName).add(callback); + return () => off(eventName, callback); + } + + function once(eventName, callback) { + const unsubscribe = on(eventName, (payload) => { + unsubscribe(); + callback(payload); + }); + + return unsubscribe; + } + + function off(eventName, callback) { + const callbacks = listeners.get(eventName); + + if (!callbacks) { + return false; + } + + const removed = callbacks.delete(callback); + + if (callbacks.size === 0) { + listeners.delete(eventName); + } + + return removed; + } + + function emit(eventName, payload) { + const callbacks = listeners.get(eventName); + + if (!callbacks) { + return; + } + + [...callbacks].forEach((callback) => { + try { + callback(payload); + } catch (error) { + queueMicrotask(() => { + throw error; + }); + } + }); + } + + function clear(eventName) { + if (typeof eventName === "string") { + listeners.delete(eventName); + return; + } + + listeners.clear(); + } + + function listenerCount(eventName) { + return listeners.get(eventName)?.size || 0; + } + + return Object.freeze({ + on, + once, + off, + emit, + clear, + listenerCount + }); +} + +export const EVENT_NAMES = Object.freeze([ + "initialized", + "consent:loaded", + "consent:accepted", + "consent:rejected", + "consent:changed", + "consent:revoked", + "consent:expired", + "consent:reset", + "storage:error" +]); diff --git a/core/storage.js b/core/storage.js new file mode 100644 index 0000000..d6fb936 --- /dev/null +++ b/core/storage.js @@ -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 +}); diff --git a/integration/context.js b/integration/context.js new file mode 100644 index 0000000..1a48c45 --- /dev/null +++ b/integration/context.js @@ -0,0 +1,45 @@ +/** + * Universal Cookie Consent + * integration/context.js + * + * Thin integration boundary between the pure Jurisdiction Engine and + * the independent Consent Core. + * + * The jurisdiction result is context only. It never grants or denies + * consent categories. + */ + +import { + resolveJurisdiction, + isValidResolution +} from "../jurisdiction/index.js"; + +function freezeResolution(resolution) { + return Object.freeze({ + status: resolution.status, + jurisdiction: Object.freeze({ ...resolution.jurisdiction }), + policy: Object.freeze({ ...resolution.policy }), + source: resolution.source, + confidence: resolution.confidence + }); +} + +export function resolveConsentContext(input = {}) { + const resolution = resolveJurisdiction(input); + + if (!isValidResolution(resolution)) { + throw new Error("Jurisdiction Engine returned an invalid resolution."); + } + + return Object.freeze({ + resolution: freezeResolution(resolution) + }); +} + +export function getConsentJurisdiction(input = {}) { + return resolveConsentContext(input).resolution.jurisdiction.id; +} + +export function getConsentPolicy(input = {}) { + return resolveConsentContext(input).resolution.policy.id; +} diff --git a/integration/index.js b/integration/index.js new file mode 100644 index 0000000..a7b2ced --- /dev/null +++ b/integration/index.js @@ -0,0 +1,34 @@ +/** + * Universal Cookie Consent + * integration/index.js + * + * Public integration API. + * + * Jurisdiction resolution is performed before the Consent Core is created. + * The resolved jurisdiction becomes Core configuration; it does not become + * a consent decision. + */ + +import { createConsentEngine } from "../core/consent.js"; +import { resolveConsentContext } from "./context.js"; + +export { resolveConsentContext } from "./context.js"; + +export function createIntegratedConsentEngine({ + jurisdiction = {}, + core = {} +} = {}) { + const context = resolveConsentContext(jurisdiction); + + const engine = createConsentEngine({ + ...core, + jurisdiction: context.resolution.jurisdiction.id + }); + + return Object.freeze({ + ...engine, + getJurisdictionContext() { + return context; + } + }); +} diff --git a/jurisdiction/constants.js b/jurisdiction/constants.js new file mode 100644 index 0000000..5c7e4ed --- /dev/null +++ b/jurisdiction/constants.js @@ -0,0 +1,38 @@ +/** + * Universal Cookie Consent + * jurisdiction/constants.js + * + * Pure constants for jurisdiction resolution. No DOM, network, storage, + * geolocation, GTM, or legal enforcement logic belongs here. + */ + +export const JURISDICTION_MODES = Object.freeze([ + "fixed", + "auto", + "global" +]); + +export const RESOLUTION_SOURCES = Object.freeze([ + "site-config", + "server", + "application", + "browser", + "fallback" +]); + +export const CONFIDENCE_LEVELS = Object.freeze([ + "explicit", + "high", + "medium", + "low", + "default" +]); + +export const RESOLUTION_STATUSES = Object.freeze([ + "resolved", + "fallback" +]); + +export const DEFAULT_JURISDICTION_ID = "global"; +export const DEFAULT_POLICY_ID = "global-baseline"; +export const DEFAULT_MODE = "auto"; diff --git a/jurisdiction/index.js b/jurisdiction/index.js new file mode 100644 index 0000000..91ce070 --- /dev/null +++ b/jurisdiction/index.js @@ -0,0 +1,35 @@ +/** + * Universal Cookie Consent + * jurisdiction/index.js + * + * Public API for the Jurisdiction Engine. + */ + +export { + JURISDICTION_MODES, + RESOLUTION_SOURCES, + CONFIDENCE_LEVELS, + RESOLUTION_STATUSES, + DEFAULT_JURISDICTION_ID, + DEFAULT_POLICY_ID, + DEFAULT_MODE +} from "./constants.js"; + +export { + JURISDICTION_CATALOG, + getJurisdiction, + hasJurisdiction +} from "./jurisdiction.js"; + +export { + POLICIES, + getPolicy, + hasPolicy +} from "./policies.js"; + +export { + resolveJurisdiction, + getResolutionPriority, + getSourceConfidence, + isValidResolution +} from "./resolver.js"; diff --git a/jurisdiction/jurisdiction.js b/jurisdiction/jurisdiction.js new file mode 100644 index 0000000..b346e39 --- /dev/null +++ b/jurisdiction/jurisdiction.js @@ -0,0 +1,73 @@ +/** + * Universal Cookie Consent + * jurisdiction/jurisdiction.js + * + * Declarative jurisdiction catalog. This file maps jurisdiction identifiers + * to policy identifiers. It does not detect visitors or enforce consent. + */ + +import { hasPolicy } from "./policies.js"; + +const JURISDICTIONS = { + global: { + id: "global", + country: null, + region: null, + policy: "global-baseline" + }, + + us: { + id: "us", + country: "US", + region: null, + policy: "us-state-privacy" + }, + + "us-ma": { + id: "us-ma", + country: "US", + region: "MA", + policy: "us-state-privacy" + }, + + "us-ca": { + id: "us-ca", + country: "US", + region: "CA", + policy: "us-california-privacy" + }, + + eu: { + id: "eu", + country: "EU", + region: null, + policy: "eu-privacy" + } +}; + +for (const entry of Object.values(JURISDICTIONS)) { + if (!hasPolicy(entry.policy)) { + throw new Error(`Jurisdiction references unknown policy: ${entry.policy}`); + } +} + +export const JURISDICTION_CATALOG = Object.freeze( + Object.fromEntries( + Object.entries(JURISDICTIONS).map(([id, value]) => [ + id, + Object.freeze({ ...value }) + ]) + ) +); + +export function getJurisdiction(id) { + if (typeof id !== "string") { + return null; + } + + return JURISDICTION_CATALOG[id] || null; +} + +export function hasJurisdiction(id) { + return Boolean(getJurisdiction(id)); +} diff --git a/jurisdiction/policies.js b/jurisdiction/policies.js new file mode 100644 index 0000000..62b2817 --- /dev/null +++ b/jurisdiction/policies.js @@ -0,0 +1,55 @@ +/** + * Universal Cookie Consent + * jurisdiction/policies.js + * + * Declarative policy catalog. + * + * IMPORTANT: + * These entries identify policy families only. They are NOT legal advice and + * deliberately do not encode detailed legal requirements yet. Those rules + * will be added after current-source legal research and policy review. + */ + +const freeze = (value) => Object.freeze(value); + +export const POLICIES = Object.freeze({ + "global-baseline": freeze({ + id: "global-baseline", + family: "global", + version: "1.0", + description: "Conservative global baseline pending jurisdiction-specific policy." + }), + + "us-state-privacy": freeze({ + id: "us-state-privacy", + family: "us-state", + version: "1.0", + description: "Policy family for U.S. state privacy regimes." + }), + + "us-california-privacy": freeze({ + id: "us-california-privacy", + family: "us-state", + version: "1.0", + description: "Policy family reserved for California-specific requirements." + }), + + "eu-privacy": freeze({ + id: "eu-privacy", + family: "eu", + version: "1.0", + description: "Policy family reserved for applicable European privacy requirements." + }) +}); + +export function getPolicy(policyId) { + if (typeof policyId !== "string") { + return null; + } + + return POLICIES[policyId] || null; +} + +export function hasPolicy(policyId) { + return Boolean(getPolicy(policyId)); +} diff --git a/jurisdiction/resolver.js b/jurisdiction/resolver.js new file mode 100644 index 0000000..70c027b --- /dev/null +++ b/jurisdiction/resolver.js @@ -0,0 +1,215 @@ +/** + * Universal Cookie Consent + * jurisdiction/resolver.js + * + * Pure deterministic jurisdiction resolver. + * + * It consumes explicitly supplied signals. It never performs network calls, + * reads IP addresses, accesses browser location, or guesses silently. + */ + +import { + JURISDICTION_MODES, + RESOLUTION_SOURCES, + CONFIDENCE_LEVELS, + DEFAULT_JURISDICTION_ID, + DEFAULT_POLICY_ID, + DEFAULT_MODE +} from "./constants.js"; +import { getJurisdiction, hasJurisdiction } from "./jurisdiction.js"; +import { getPolicy, hasPolicy } from "./policies.js"; + +const SOURCE_CONFIDENCE = Object.freeze({ + "site-config": "explicit", + server: "high", + application: "medium", + browser: "low", + fallback: "default" +}); + +const SOURCE_PRIORITY = Object.freeze({ + "site-config": 500, + server: 400, + application: 300, + browser: 200, + fallback: 100 +}); + +function isPlainObject(value) { + return Boolean( + value && + typeof value === "object" && + !Array.isArray(value) + ); +} + +function normalizeString(value) { + return typeof value === "string" ? value.trim() : ""; +} + +function assertMode(mode) { + if (!JURISDICTION_MODES.includes(mode)) { + throw new TypeError( + `Invalid jurisdiction mode. Expected: ${JURISDICTION_MODES.join(", ")}.` + ); + } +} + +function resolveEntry(id, source, status = "resolved") { + const entry = getJurisdiction(id); + + if (!entry) { + throw new Error(`Unknown jurisdiction: ${id}`); + } + + const policy = getPolicy(entry.policy); + + if (!policy) { + throw new Error(`Unknown policy: ${entry.policy}`); + } + + return Object.freeze({ + status, + jurisdiction: Object.freeze({ + id: entry.id, + country: entry.country, + region: entry.region + }), + policy: Object.freeze({ + id: policy.id, + family: policy.family, + version: policy.version + }), + source, + confidence: SOURCE_CONFIDENCE[source] + }); +} + +function normalizeSignal(signal, fallbackSource) { + if (!isPlainObject(signal)) { + return null; + } + + const id = normalizeString(signal.jurisdiction || signal.id); + if (!id || !hasJurisdiction(id)) { + return null; + } + + const source = normalizeString(signal.source) || fallbackSource; + if (!RESOLUTION_SOURCES.includes(source)) { + return null; + } + + return { + id, + source, + priority: SOURCE_PRIORITY[source] + }; +} + +function chooseBestSignal(signals) { + const candidates = []; + + for (const signal of signals) { + const normalized = normalizeSignal(signal, "browser"); + if (normalized) { + candidates.push(normalized); + } + } + + if (!candidates.length) { + return null; + } + + candidates.sort((a, b) => b.priority - a.priority); + return candidates[0]; +} + +function resolveAuto(input) { + const site = isPlainObject(input.site) ? input.site : {}; + const request = isPlainObject(input.request) ? input.request : {}; + const visitor = isPlainObject(input.visitor) ? input.visitor : {}; + + const signal = chooseBestSignal([ + { jurisdiction: request.jurisdiction, source: request.source || "server" }, + { jurisdiction: request.regionJurisdiction, source: request.regionSource || "server" }, + { jurisdiction: visitor.jurisdiction, source: visitor.source || "browser" }, + { jurisdiction: visitor.regionJurisdiction, source: visitor.regionSource || "browser" }, + { jurisdiction: site.jurisdiction, source: site.source || "application" } + ]); + + if (signal) { + return resolveEntry(signal.id, signal.source); + } + + return resolveEntry(DEFAULT_JURISDICTION_ID, "fallback", "fallback"); +} + +export function resolveJurisdiction(input = {}) { + if (!isPlainObject(input)) { + throw new TypeError("Jurisdiction input must be an object."); + } + + const site = isPlainObject(input.site) ? input.site : {}; + const mode = normalizeString(input.mode || site.mode || DEFAULT_MODE) || DEFAULT_MODE; + + assertMode(mode); + + if (mode === "global") { + return resolveEntry(DEFAULT_JURISDICTION_ID, "site-config"); + } + + if (mode === "fixed") { + const id = normalizeString(input.jurisdiction || site.jurisdiction); + + if (!id) { + throw new TypeError( + "A jurisdiction is required when mode is \"fixed\"." + ); + } + + if (!hasJurisdiction(id)) { + throw new RangeError(`Unknown jurisdiction: ${id}`); + } + + return resolveEntry(id, "site-config"); + } + + return resolveAuto(input); +} + +export function getResolutionPriority(source) { + return SOURCE_PRIORITY[source] || 0; +} + +export function getSourceConfidence(source) { + return SOURCE_CONFIDENCE[source] || null; +} + +export function isValidResolution(result) { + if (!isPlainObject(result)) { + return false; + } + + if (!Object.prototype.hasOwnProperty.call(result, "status")) { + return false; + } + + if (!Object.prototype.hasOwnProperty.call(result, "jurisdiction")) { + return false; + } + + if (!Object.prototype.hasOwnProperty.call(result, "policy")) { + return false; + } + + return ( + (result.status === "resolved" || result.status === "fallback") && + typeof result.jurisdiction.id === "string" && + typeof result.policy.id === "string" && + RESOLUTION_SOURCES.includes(result.source) && + CONFIDENCE_LEVELS.includes(result.confidence) && + hasJurisdiction(result.jurisdiction.id) && + hasPolicy(result.policy.id) + ); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..089153b --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{"type":"module"} diff --git a/test-core.mjs b/test-core.mjs new file mode 100644 index 0000000..86cea60 --- /dev/null +++ b/test-core.mjs @@ -0,0 +1,180 @@ +import assert from 'node:assert/strict'; +import { + createConsentEngine, + ENGINE_VERSION +} from './core/consent.js'; +import { isStoragePayload } from './core/storage.js'; +import { isValidCategoriesObject } from './core/categories.js'; + +let jar = new Map(); +let blockWrites = false; + +globalThis.location = { protocol: 'http:' }; +globalThis.document = {}; +Object.defineProperty(globalThis.document, 'cookie', { + configurable: true, + get() { + return [...jar.entries()].map(([k,v]) => `${k}=${v}`).join('; '); + }, + set(value) { + if (blockWrites) return; + const parts = value.split(';').map(x => x.trim()); + const [name, raw = ''] = parts[0].split('='); + const maxAge = parts.find(x => /^Max-Age=/i.test(x)); + if (maxAge && maxAge.toLowerCase() === 'max-age=0') { + jar.delete(name); + return; + } + jar.set(name, raw); + } +}); + +function clearCookie() { jar.clear(); blockWrites = false; } +function make() { + return createConsentEngine({ + policyVersion: '2026-01', + jurisdiction: 'global', + storage: { name: 'cc_consent', duration: 180, path: '/', sameSite: 'Lax' } + }); +} + +let passed = 0; +function test(name, fn) { + try { fn(); passed++; console.log(`PASS ${name}`); } + catch (e) { console.error(`FAIL ${name}\n`, e); process.exitCode = 1; } +} + +clearCookie(); + +test('default state is undecided', () => { + const e = make(); e.initialize(); const s = e.getState(); + assert.equal(s.categories.necessary, 'granted'); + assert.equal(s.categories.analytics, 'unknown'); + assert.equal(s.hasDecision, false); +}); + +test('accept all', () => { + clearCookie(); + const e = make(); e.initialize(); let changed = 0, accepted = 0; + e.on('consent:changed', () => changed++); e.on('consent:accepted', () => accepted++); + e.acceptAll(); + assert.equal(e.getState().hasDecision, true); + assert.equal(e.getState().categories.analytics, 'granted'); + assert.equal(changed, 1); assert.equal(accepted, 1); +}); + +test('accept all is idempotent', () => { + clearCookie(); + const e = make(); e.initialize(); let events = 0; e.on('consent:changed', () => events++); + e.acceptAll(); const before = e.getState().timestamp; e.acceptAll(); + assert.equal(events, 1); assert.equal(e.getState().timestamp, before); +}); + +test('persistence loads from storage', () => { + clearCookie(); + const e = make(); e.initialize(); e.rejectAll(); + const fresh = make(); fresh.initialize(); const s = fresh.getState(); + assert.equal(s.source, 'storage'); assert.equal(s.categories.analytics, 'denied'); +}); + +test('reject all is idempotent', () => { + clearCookie(); const e = make(); e.initialize(); let events = 0; e.on('consent:changed', () => events++); + e.rejectAll(); e.rejectAll(); assert.equal(events, 1); +}); + +test('selective consent', () => { + clearCookie(); const e = make(); e.initialize(); e.setConsent({ analytics: 'granted', marketing: 'denied', preferences: 'denied' }); + assert.equal(e.getState().categories.analytics, 'granted'); assert.equal(e.isComplete(), true); +}); + +test('invalid consent value throws', () => { + clearCookie(); const e = make(); e.initialize(); assert.throws(() => e.setConsent({ analytics: 'banana' }), TypeError); +}); + +test('necessary cannot be denied', () => { + clearCookie(); const e = make(); e.initialize(); + assert.throws(() => e.setConsent({ necessary: 'denied', analytics: 'granted' }), TypeError); + assert.equal(e.getState().categories.necessary, 'granted'); + assert.equal(e.getState().categories.analytics, 'unknown'); +}); + +test('revoke is idempotent', () => { + clearCookie(); const e = make(); e.initialize(); e.acceptAll(); let revoked = 0; e.on('consent:revoked', () => revoked++); + e.revoke(); e.revoke(); assert.equal(revoked, 1); assert.equal(e.getState().categories.analytics, 'denied'); +}); + +test('reset removes persistence', () => { + clearCookie(); const e = make(); e.initialize(); e.acceptAll(); e.reset(); + assert.equal(e.isComplete(), false); assert.equal(document.cookie, ''); + const fresh = make(); fresh.initialize(); assert.equal(fresh.isComplete(), false); assert.equal(fresh.getState().source, 'default'); +}); + +test('reset does not lie when storage removal fails', () => { + clearCookie(); const e = make(); e.initialize(); e.acceptAll(); blockWrites = true; let reset = 0, errors = 0; + e.on('consent:reset', () => reset++); e.on('storage:error', () => errors++); + const before = e.getState(); e.reset(); + assert.equal(reset, 0); assert.equal(errors, 1); assert.equal(e.getState().categories.analytics, before.categories.analytics); +}); + +test('strict stored envelope validation', () => { + assert.equal(isStoragePayload({}), false); + assert.equal(isStoragePayload({ engineVersion: ENGINE_VERSION, policyVersion: '2026-01', timestamp: Date.now(), categories: {}, jurisdiction: 'global', gpc: false }), true); +}); + +test('strict category validation', () => { + assert.equal(isValidCategoriesObject({ necessary:'granted', preferences:'unknown', analytics:'granted', marketing:'denied' }), true); + assert.equal(isValidCategoriesObject({ necessary:'denied', preferences:'unknown', analytics:'granted', marketing:'denied' }), false); + assert.equal(isValidCategoriesObject({ necessary:'granted', preferences:'banana', analytics:'granted', marketing:'denied' }), false); +}); + +test('initialize is idempotent', () => { + clearCookie(); const e = make(); let initialized = 0; e.on('initialized', () => initialized++); e.initialize(); e.initialize(); assert.equal(initialized, 1); +}); + +test('policy mismatch removes valid cookie', () => { + clearCookie(); const e = make(); e.initialize(); e.acceptAll(); + const fresh = createConsentEngine({ policyVersion:'2027-01', jurisdiction:'global', storage:{ name:'cc_consent', duration:180, path:'/', sameSite:'Lax' } }); + fresh.initialize(); assert.equal(fresh.isComplete(), false); assert.equal(document.cookie, ''); +}); + +test('expired cookie is rejected', () => { + clearCookie(); const e = make(); e.initialize(); e.acceptAll(); + const raw = decodeURIComponent(document.cookie.split('=').slice(1).join('=')); const payload = JSON.parse(raw); payload.timestamp = Date.now() - 181*24*60*60*1000; + document.cookie = `cc_consent=${encodeURIComponent(JSON.stringify(payload))}; Path=/; SameSite=Lax`; + const fresh = make(); let expired = 0; fresh.on('consent:expired', () => expired++); fresh.initialize(); + assert.equal(expired, 1); assert.equal(fresh.isComplete(), false); assert.equal(document.cookie, ''); +}); + + +test('necessary denial is explicitly rejected', () => { + clearCookie(); const e = make(); e.initialize(); + assert.throws(() => e.setConsent({ necessary: 'denied' }), TypeError); + assert.equal(e.getState().categories.necessary, 'granted'); +}); + +test('invalid persisted category payload is ignored and removed', () => { + clearCookie(); + const bad = { + engineVersion: ENGINE_VERSION, + policyVersion: '2026-01', + timestamp: Date.now(), + categories: { necessary: 'granted', preferences: 'banana', analytics: 'granted', marketing: 'denied' }, + jurisdiction: 'global', + gpc: false + }; + document.cookie = `cc_consent=${encodeURIComponent(JSON.stringify(bad))}; Path=/; SameSite=Lax`; + const e = make(); e.initialize(); + assert.equal(e.isComplete(), false); assert.equal(document.cookie, ''); +}); + +test('future timestamp is rejected', () => { + clearCookie(); const e = make(); e.initialize(); e.acceptAll(); + const raw = decodeURIComponent(document.cookie.split('=').slice(1).join('=')); + const payload = JSON.parse(raw); payload.timestamp = Date.now() + 60 * 60 * 1000; + document.cookie = `cc_consent=${encodeURIComponent(JSON.stringify(payload))}; Path=/; SameSite=Lax`; + const fresh = make(); fresh.initialize(); + assert.equal(fresh.isComplete(), false); assert.equal(document.cookie, ''); +}); + +console.log(`\n${passed} tests passed.`); +if (process.exitCode) process.exit(1); diff --git a/tests/audit/audit-test.html b/tests/audit/audit-test.html new file mode 100644 index 0000000..76b2b5d --- /dev/null +++ b/tests/audit/audit-test.html @@ -0,0 +1,136 @@ + + + + + + Cookie Consent Audit Lab + + + +

Cookie Consent Audit Lab

+

Browser audit for Core + Jurisdiction + Integration invariants.

+ +
Not run
+

+
+    
+
+
diff --git a/tests/audit/audit-test.mjs b/tests/audit/audit-test.mjs
new file mode 100644
index 0000000..dd167ae
--- /dev/null
+++ b/tests/audit/audit-test.mjs
@@ -0,0 +1,256 @@
+import assert from "node:assert/strict";
+import { createIntegratedConsentEngine } from "../../integration/index.js";
+
+// This suite exercises real persistence (acceptAll/revoke/reset expected to
+// take effect), unlike tests/integration/integration-test.mjs which
+// deliberately asserts the no-op behavior of a bare Node environment.
+// A document.cookie mock is required here for the same reason it is
+// required in test-core.mjs.
+let jar = new Map();
+
+globalThis.location = { protocol: "http:" };
+globalThis.document = {};
+Object.defineProperty(globalThis.document, "cookie", {
+    configurable: true,
+    get() {
+        return [...jar.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
+    },
+    set(value) {
+        const parts = value.split(";").map((x) => x.trim());
+        const [name, raw = ""] = parts[0].split("=");
+        const maxAge = parts.find((x) => /^Max-Age=/i.test(x));
+        if (maxAge && maxAge.toLowerCase() === "max-age=0") {
+            jar.delete(name);
+            return;
+        }
+        jar.set(name, raw);
+    }
+});
+
+let passed = 0;
+
+function test(name, fn) {
+    // Each test starts from a clean cookie jar so persisted consent from
+    // one test cannot leak into the next (they share default jurisdiction
+    // and policyVersion, so a stale cookie would otherwise be reloaded).
+    jar.clear();
+
+    try {
+        fn();
+        console.log(`PASS ${name}`);
+        passed += 1;
+    } catch (error) {
+        console.error(`FAIL ${name}`);
+        throw error;
+    }
+}
+
+function createEngine(jurisdiction = "us-ma") {
+    return createIntegratedConsentEngine({
+        jurisdiction: {
+            mode: "fixed",
+            jurisdiction
+        }
+    });
+}
+
+test("dynamic jurisdiction is reflected in Core state", () => {
+    const consent = createEngine("global");
+    consent.initialize();
+    const state = consent.setJurisdiction("us-ma");
+
+    assert.equal(state.jurisdiction, "us-ma");
+});
+
+test("GPC state is represented without granting optional consent", () => {
+    const consent = createEngine();
+    consent.initialize();
+    const state = consent.setGpc(true);
+
+    assert.equal(state.gpc, true);
+    assert.equal(state.hasDecision, false);
+    assert.equal(state.categories.preferences, "unknown");
+    assert.equal(state.categories.analytics, "unknown");
+    assert.equal(state.categories.marketing, "unknown");
+});
+
+test("revoke produces the Core-defined denied state and is idempotent", () => {
+    const consent = createEngine();
+    consent.initialize();
+
+consent.acceptAll();
+const accepted = consent.getState();
+assert.equal(accepted.hasDecision, true);
+
+    let revokedEvents = 0;
+    consent.on("consent:revoked", () => revokedEvents++);
+
+    const revoked = consent.revoke();
+    const secondRevoke = consent.revoke();
+
+    assert.equal(revoked.categories.necessary, "granted");
+    assert.equal(revoked.categories.preferences, "denied");
+    assert.equal(revoked.categories.analytics, "denied");
+    assert.equal(revoked.categories.marketing, "denied");
+    assert.equal(secondRevoke.categories.analytics, "denied");
+    assert.equal(revokedEvents, 1);
+});
+
+test("reset returns Core to an undecided state", () => {
+    const consent = createEngine();
+    consent.initialize();
+    consent.acceptAll();
+
+    const reset = consent.reset();
+
+    assert.equal(reset.hasDecision, false);
+    assert.equal(reset.categories.necessary, "granted");
+    assert.equal(reset.categories.preferences, "unknown");
+    assert.equal(reset.categories.analytics, "unknown");
+    assert.equal(reset.categories.marketing, "unknown");
+});
+
+test("policy version is part of the Core state", () => {
+    const consent = createEngine();
+    const config = consent.getConfig();
+
+    assert.equal(typeof config.policyVersion, "string");
+    assert.ok(config.policyVersion.length > 0);
+});
+
+test("persistence preserves the resolved jurisdiction after acceptance", () => {
+    const consent = createEngine("us-ma");
+    consent.initialize();
+
+    const saved = consent.acceptAll();
+
+    assert.equal(saved.hasDecision, true);
+    assert.equal(saved.jurisdiction, "us-ma");
+});
+
+test("event sequence reports the consent transition", () => {
+    const consent = createEngine();
+    const events = [];
+
+    consent.on("consent:changed", () => events.push("changed"));
+    consent.on("consent:accepted", () => events.push("accepted"));
+
+    consent.initialize();
+    consent.acceptAll();
+
+    assert.deepEqual(events, ["changed", "accepted"]);
+});
+
+test("Core state remains isolated from returned object mutation", () => {
+    const consent = createEngine();
+    consent.initialize();
+
+    const state = consent.getState();
+    state.categories.analytics = "granted";
+    state.jurisdiction = "changed";
+
+    const current = consent.getState();
+
+    assert.equal(current.categories.analytics, "unknown");
+    assert.equal(current.jurisdiction, "us-ma");
+});
+
+
+test("separate Core instances do not share in-memory state", () => {
+    const first = createEngine("us-ma");
+    const second = createEngine("eu");
+
+    first.initialize();
+    second.initialize();
+
+    first.setConsent({ analytics: "granted" });
+
+    assert.equal(first.getState().categories.analytics, "granted");
+    assert.equal(second.getState().categories.analytics, "unknown");
+    assert.equal(first.getState().jurisdiction, "us-ma");
+    assert.equal(second.getState().jurisdiction, "eu");
+});
+
+test("partial consent remains undecided until all optional categories are resolved", () => {
+    const consent = createEngine();
+    consent.initialize();
+
+    const state = consent.setConsent({ analytics: "granted" });
+
+    assert.equal(state.categories.analytics, "granted");
+    assert.equal(state.categories.preferences, "unknown");
+    assert.equal(state.categories.marketing, "unknown");
+    assert.equal(state.hasDecision, false);
+    assert.equal(consent.isComplete(), false);
+});
+
+test("necessary remains granted through valid consent changes", () => {
+    const consent = createEngine();
+    consent.initialize();
+
+    const state = consent.setConsent({
+        preferences: "denied",
+        analytics: "granted",
+        marketing: "denied"
+    });
+
+    assert.equal(state.categories.necessary, "granted");
+    assert.equal(state.categories.preferences, "denied");
+    assert.equal(state.categories.analytics, "granted");
+    assert.equal(state.categories.marketing, "denied");
+});
+
+test("acceptAll is idempotent through the integration boundary", () => {
+    const consent = createEngine();
+    consent.initialize();
+
+    let changed = 0;
+    let accepted = 0;
+
+    consent.on("consent:changed", () => changed++);
+    consent.on("consent:accepted", () => accepted++);
+
+    consent.acceptAll();
+    const first = consent.getState();
+    consent.acceptAll();
+    const second = consent.getState();
+
+    assert.equal(first.hasDecision, true);
+    assert.equal(second.hasDecision, true);
+    assert.equal(changed, 1);
+    assert.equal(accepted, 1);
+    assert.equal(second.timestamp, first.timestamp);
+});
+
+test("reset is idempotent through the integration boundary", () => {
+    const consent = createEngine();
+    consent.initialize();
+    consent.acceptAll();
+
+    let reset = 0;
+    consent.on("consent:reset", () => reset++);
+
+    consent.reset();
+    const first = consent.getState();
+    consent.reset();
+    const second = consent.getState();
+
+    assert.equal(first.hasDecision, false);
+    assert.equal(second.hasDecision, false);
+    assert.equal(second.categories.analytics, "unknown");
+});
+
+test("GPC never grants optional categories through the integration boundary", () => {
+    const consent = createEngine();
+    consent.initialize();
+
+    const state = consent.setGpc(true);
+
+    assert.equal(state.gpc, true);
+    assert.equal(state.categories.preferences, "unknown");
+    assert.equal(state.categories.analytics, "unknown");
+    assert.equal(state.categories.marketing, "unknown");
+    assert.equal(state.hasDecision, false);
+});
+
+console.log(`${passed} audit tests passed.`);
diff --git a/tests/integration/integration-test.html b/tests/integration/integration-test.html
new file mode 100644
index 0000000..284eaec
--- /dev/null
+++ b/tests/integration/integration-test.html
@@ -0,0 +1,173 @@
+
+
+
+    
+    
+    Cookie Consent Integration Test Lab
+    
+
+
+    

Cookie Consent Integration Test Lab

+

Browser regression suite for the Jurisdiction Engine + Consent Core boundary.

+ + +
Not run.
+

+
+    
+
+
diff --git a/tests/integration/integration-test.mjs b/tests/integration/integration-test.mjs
new file mode 100644
index 0000000..b9ce2a0
--- /dev/null
+++ b/tests/integration/integration-test.mjs
@@ -0,0 +1,136 @@
+import assert from "node:assert/strict";
+
+import {
+    resolveConsentContext,
+    createIntegratedConsentEngine
+} from "../../integration/index.js";
+
+function test(name, fn) {
+    try {
+        fn();
+        console.log(`PASS ${name}`);
+    } catch (error) {
+        console.error(`FAIL ${name}`);
+        throw error;
+    }
+}
+
+test("fixed us-ma resolves into Core context", () => {
+    const context = resolveConsentContext({
+        mode: "fixed",
+        jurisdiction: "us-ma"
+    });
+
+    assert.equal(context.resolution.jurisdiction.id, "us-ma");
+    assert.equal(context.resolution.policy.id, "us-state-privacy");
+});
+
+test("fixed eu resolves into Core context", () => {
+    const context = resolveConsentContext({
+        mode: "fixed",
+        jurisdiction: "eu"
+    });
+
+    assert.equal(context.resolution.jurisdiction.id, "eu");
+    assert.equal(context.resolution.policy.id, "eu-privacy");
+});
+
+test("global resolves into global baseline", () => {
+    const context = resolveConsentContext({ mode: "global" });
+
+    assert.equal(context.resolution.jurisdiction.id, "global");
+    assert.equal(context.resolution.policy.id, "global-baseline");
+});
+
+test("integration configures Core jurisdiction", () => {
+    const consent = createIntegratedConsentEngine({
+        jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
+    });
+
+    const state = consent.initialize();
+
+    assert.equal(state.jurisdiction, "us-ma");
+    assert.equal(state.hasDecision, false);
+});
+
+test("jurisdiction does not grant consent", () => {
+    const consent = createIntegratedConsentEngine({
+        jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
+    });
+
+    const state = consent.initialize();
+
+    assert.equal(state.categories.necessary, "granted");
+    assert.equal(state.categories.preferences, "unknown");
+    assert.equal(state.categories.analytics, "unknown");
+    assert.equal(state.categories.marketing, "unknown");
+    assert.equal(state.hasDecision, false);
+});
+
+test("resolved context is exposed without mutable internals", () => {
+    const consent = createIntegratedConsentEngine({
+        jurisdiction: { mode: "fixed", jurisdiction: "eu" }
+    });
+
+    const context = consent.getJurisdictionContext();
+
+    assert.equal(context.resolution.jurisdiction.id, "eu");
+    assert.equal(Object.isFrozen(context), true);
+    assert.equal(Object.isFrozen(context.resolution), true);
+});
+
+test("Core configuration preserves resolved jurisdiction", () => {
+    const consent = createIntegratedConsentEngine({
+        jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
+    });
+
+    const config = consent.getConfig();
+
+    assert.equal(config.jurisdiction, "us-ma");
+});
+
+test("Node environment does not fake successful persistence", () => {
+    const consent = createIntegratedConsentEngine({
+        jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
+    });
+
+    consent.initialize();
+    const state = consent.acceptAll();
+
+    assert.equal(state.jurisdiction, "us-ma");
+    assert.equal(state.hasDecision, false);
+    assert.equal(state.categories.preferences, "unknown");
+    assert.equal(state.categories.analytics, "unknown");
+    assert.equal(state.categories.marketing, "unknown");
+});
+
+test("integration remains deterministic", () => {
+    const input = {
+        mode: "auto",
+        request: {
+            jurisdiction: "us-ma",
+            source: "server"
+        },
+        visitor: {
+            jurisdiction: "eu",
+            source: "browser"
+        }
+    };
+
+    const first = resolveConsentContext(input);
+    const second = resolveConsentContext(input);
+
+    assert.deepEqual(first, second);
+});
+
+test("invalid jurisdiction remains an Engine error", () => {
+    assert.throws(
+        () => resolveConsentContext({
+            mode: "fixed",
+            jurisdiction: "not-a-jurisdiction"
+        }),
+        /Unknown jurisdiction/
+    );
+});
+
+console.log("10 integration tests passed.");
diff --git a/tests/jurisdiction-test.html b/tests/jurisdiction-test.html
new file mode 100644
index 0000000..11eec65
--- /dev/null
+++ b/tests/jurisdiction-test.html
@@ -0,0 +1,84 @@
+
+
+
+    
+    
+    Jurisdiction Engine Test Lab
+    
+
+
+    

Jurisdiction Engine Test Lab

+

Browser regression suite for the pure Jurisdiction Engine.

+ +
Not run.
+

+
+
+
+
diff --git a/tests/jurisdiction-test.mjs b/tests/jurisdiction-test.mjs
new file mode 100644
index 0000000..5ed9087
--- /dev/null
+++ b/tests/jurisdiction-test.mjs
@@ -0,0 +1,196 @@
+import assert from "node:assert/strict";
+import {
+    resolveJurisdiction,
+    isValidResolution,
+    getResolutionPriority,
+    getSourceConfidence,
+    hasJurisdiction,
+    hasPolicy,
+    JURISDICTION_CATALOG,
+    POLICIES
+} from "../jurisdiction/index.js";
+
+const tests = [];
+function test(name, fn) {
+    tests.push({ name, fn });
+}
+
+test("fixed us-ma resolves from site configuration", () => {
+    const result = resolveJurisdiction({ mode: "fixed", jurisdiction: "us-ma" });
+    assert.equal(result.status, "resolved");
+    assert.equal(result.jurisdiction.id, "us-ma");
+    assert.equal(result.policy.id, "us-state-privacy");
+    assert.equal(result.source, "site-config");
+    assert.equal(result.confidence, "explicit");
+});
+
+test("fixed eu resolves correctly", () => {
+    const result = resolveJurisdiction({ mode: "fixed", jurisdiction: "eu" });
+    assert.equal(result.jurisdiction.id, "eu");
+    assert.equal(result.policy.id, "eu-privacy");
+});
+
+test("fixed global resolves global baseline", () => {
+    const result = resolveJurisdiction({ mode: "fixed", jurisdiction: "global" });
+    assert.equal(result.jurisdiction.id, "global");
+    assert.equal(result.policy.id, "global-baseline");
+});
+
+test("global mode deliberately selects global policy", () => {
+    const result = resolveJurisdiction({ mode: "global" });
+    assert.equal(result.jurisdiction.id, "global");
+    assert.equal(result.source, "site-config");
+});
+
+test("auto resolves server signal", () => {
+    const result = resolveJurisdiction({
+        mode: "auto",
+        request: { jurisdiction: "us-ca", source: "server" }
+    });
+    assert.equal(result.jurisdiction.id, "us-ca");
+    assert.equal(result.source, "server");
+    assert.equal(result.confidence, "high");
+});
+
+test("auto resolves application signal", () => {
+    const result = resolveJurisdiction({
+        mode: "auto",
+        visitor: { jurisdiction: "us-ma", source: "application" }
+    });
+    assert.equal(result.jurisdiction.id, "us-ma");
+    assert.equal(result.source, "application");
+});
+
+test("auto resolves browser signal", () => {
+    const result = resolveJurisdiction({
+        mode: "auto",
+        visitor: { jurisdiction: "eu", source: "browser" }
+    });
+    assert.equal(result.jurisdiction.id, "eu");
+    assert.equal(result.source, "browser");
+    assert.equal(result.confidence, "low");
+});
+
+test("auto falls back conservatively when no signal exists", () => {
+    const result = resolveJurisdiction({ mode: "auto" });
+    assert.equal(result.status, "fallback");
+    assert.equal(result.jurisdiction.id, "global");
+    assert.equal(result.policy.id, "global-baseline");
+    assert.equal(result.source, "fallback");
+    assert.equal(result.confidence, "default");
+});
+
+test("auto ignores unknown jurisdiction signals", () => {
+    const result = resolveJurisdiction({
+        mode: "auto",
+        request: { jurisdiction: "xx-unknown", source: "server" }
+    });
+    assert.equal(result.status, "fallback");
+    assert.equal(result.jurisdiction.id, "global");
+});
+
+test("higher-trust server signal wins over browser signal", () => {
+    const result = resolveJurisdiction({
+        mode: "auto",
+        request: { jurisdiction: "us-ma", source: "server" },
+        visitor: { jurisdiction: "eu", source: "browser" }
+    });
+    assert.equal(result.jurisdiction.id, "us-ma");
+    assert.equal(result.source, "server");
+});
+
+test("explicit site configuration wins over lower-trust signals", () => {
+    const result = resolveJurisdiction({
+        mode: "auto",
+        site: { jurisdiction: "us-ca", source: "site-config" },
+        request: { jurisdiction: "eu", source: "server" }
+    });
+    assert.equal(result.jurisdiction.id, "us-ca");
+    assert.equal(result.source, "site-config");
+});
+
+test("fixed mode requires jurisdiction", () => {
+    assert.throws(
+        () => resolveJurisdiction({ mode: "fixed" }),
+        /jurisdiction is required/i
+    );
+});
+
+test("fixed mode rejects unknown jurisdiction", () => {
+    assert.throws(
+        () => resolveJurisdiction({ mode: "fixed", jurisdiction: "xx" }),
+        /unknown jurisdiction/i
+    );
+});
+
+test("invalid mode is rejected", () => {
+    assert.throws(
+        () => resolveJurisdiction({ mode: "random" }),
+        /invalid jurisdiction mode/i
+    );
+});
+
+test("invalid input type is rejected", () => {
+    assert.throws(
+        () => resolveJurisdiction(null),
+        /input must be an object/i
+    );
+});
+
+test("catalog entries reference existing policies", () => {
+    for (const entry of Object.values(JURISDICTION_CATALOG)) {
+        assert.equal(hasPolicy(entry.policy), true);
+    }
+});
+
+test("resolution result satisfies its contract", () => {
+    const results = [
+        resolveJurisdiction({ mode: "fixed", jurisdiction: "us-ma" }),
+        resolveJurisdiction({ mode: "global" }),
+        resolveJurisdiction({ mode: "auto" })
+    ];
+
+    for (const result of results) {
+        assert.equal(isValidResolution(result), true);
+    }
+});
+
+test("source priority is deterministic", () => {
+    assert.ok(getResolutionPriority("site-config") > getResolutionPriority("server"));
+    assert.ok(getResolutionPriority("server") > getResolutionPriority("application"));
+    assert.ok(getResolutionPriority("application") > getResolutionPriority("browser"));
+    assert.ok(getResolutionPriority("browser") > getResolutionPriority("fallback"));
+});
+
+test("source confidence mapping is deterministic", () => {
+    assert.equal(getSourceConfidence("site-config"), "explicit");
+    assert.equal(getSourceConfidence("server"), "high");
+    assert.equal(getSourceConfidence("application"), "medium");
+    assert.equal(getSourceConfidence("browser"), "low");
+    assert.equal(getSourceConfidence("fallback"), "default");
+});
+
+test("catalog contains the initial architectural jurisdictions", () => {
+    for (const id of ["global", "us", "us-ma", "us-ca", "eu"]) {
+        assert.equal(hasJurisdiction(id), true);
+    }
+    assert.equal(POLICIES["global-baseline"] !== undefined, true);
+});
+
+let passed = 0;
+for (const { name, fn } of tests) {
+    try {
+        fn();
+        passed += 1;
+        console.log(`PASS: ${name}`);
+    } catch (error) {
+        console.error(`FAIL: ${name}`);
+        console.error(error);
+        process.exitCode = 1;
+    }
+}
+
+console.log(`\n${passed}/${tests.length} tests passed`);
+if (passed !== tests.length) {
+    process.exitCode = 1;
+}
diff --git a/tests/reliability/reliability-test.mjs b/tests/reliability/reliability-test.mjs
new file mode 100644
index 0000000..7356a56
--- /dev/null
+++ b/tests/reliability/reliability-test.mjs
@@ -0,0 +1,525 @@
+import assert from "node:assert/strict";
+
+import { createConsentEngine } from "../../core/consent.js";
+import {
+    isStoragePayload
+} from "../../core/storage.js";
+import {
+    isValidCategoriesObject
+} from "../../core/categories.js";
+
+let jar = new Map();
+let blockWrites = false;
+
+globalThis.location = { protocol: "http:" };
+globalThis.document = {};
+
+Object.defineProperty(globalThis.document, "cookie", {
+    configurable: true,
+
+    get() {
+        return [...jar.entries()]
+            .map(([key, value]) => `${key}=${value}`)
+            .join("; ");
+    },
+
+    set(value) {
+        if (blockWrites) {
+            return;
+        }
+
+        const parts = value.split(";").map((item) => item.trim());
+        const [name, raw = ""] = parts[0].split("=");
+
+        const maxAge = parts.find((item) => /^Max-Age=/i.test(item));
+
+        if (maxAge && maxAge.toLowerCase() === "max-age=0") {
+            jar.delete(name);
+            return;
+        }
+
+        jar.set(name, raw);
+    }
+});
+
+function clearEnvironment() {
+    jar.clear();
+    blockWrites = false;
+}
+
+function make(options = {}) {
+    return createConsentEngine({
+        policyVersion: "2026-01",
+        jurisdiction: "global",
+        storage: {
+            name: "cc_reliability",
+            duration: 180,
+            path: "/",
+            sameSite: "Lax"
+        },
+        ...options
+    });
+}
+
+let passed = 0;
+
+function test(name, fn) {
+    clearEnvironment();
+
+    try {
+        fn();
+        passed += 1;
+        console.log(`PASS ${name}`);
+    } catch (error) {
+        console.error(`FAIL ${name}`);
+        throw error;
+    }
+}
+
+
+/* -------------------------------------------------------------------------- */
+/* Input contract                                                              */
+/* -------------------------------------------------------------------------- */
+
+test("engine accepts omitted configuration and applies defaults", () => {
+    const consent = createConsentEngine();
+
+    consent.initialize();
+
+    const state = consent.getState();
+
+    assert.equal(state.hasDecision, false);
+    assert.equal(state.categories.necessary, "granted");
+    assert.equal(state.categories.preferences, "unknown");
+    assert.equal(state.categories.analytics, "unknown");
+    assert.equal(state.categories.marketing, "unknown");
+});
+
+test("rejects invalid consent category value", () => {
+    const consent = make();
+
+    consent.initialize();
+
+    assert.throws(
+        () => consent.setConsent({
+            analytics: "invalid"
+        }),
+        TypeError
+    );
+});
+
+test("rejects denial of necessary category", () => {
+    const consent = make();
+
+    consent.initialize();
+
+    assert.throws(
+        () => consent.setConsent({
+            necessary: "denied"
+        }),
+        TypeError
+    );
+});
+
+
+/* -------------------------------------------------------------------------- */
+/* State isolation                                                             */
+/* -------------------------------------------------------------------------- */
+
+test("separate engines do not share state", () => {
+    const first = make();
+    const second = make();
+
+    first.initialize();
+    second.initialize();
+
+    first.acceptAll();
+
+    assert.equal(first.getState().hasDecision, true);
+    assert.equal(second.getState().hasDecision, false);
+});
+
+test("returned state mutation cannot alter internal categories", () => {
+    const consent = make();
+
+    consent.initialize();
+
+    const state = consent.getState();
+
+    state.categories.analytics = "granted";
+    state.categories.marketing = "granted";
+
+    const current = consent.getState();
+
+    assert.equal(current.categories.analytics, "unknown");
+    assert.equal(current.categories.marketing, "unknown");
+});
+
+test("returned state mutation cannot alter jurisdiction", () => {
+    const consent = make({
+        jurisdiction: "us-ma"
+    });
+
+    consent.initialize();
+
+    const state = consent.getState();
+
+    state.jurisdiction = "eu";
+
+    assert.equal(
+        consent.getState().jurisdiction,
+        "us-ma"
+    );
+});
+
+
+/* -------------------------------------------------------------------------- */
+/* Consent transitions                                                         */
+/* -------------------------------------------------------------------------- */
+
+test("partial consent does not become a complete decision", () => {
+    const consent = make();
+
+    consent.initialize();
+
+    const state = consent.setConsent({
+        analytics: "granted"
+    });
+
+    assert.equal(state.categories.analytics, "granted");
+    assert.equal(state.categories.preferences, "unknown");
+    assert.equal(state.categories.marketing, "unknown");
+    assert.equal(state.hasDecision, false);
+});
+
+test("complete selective consent creates a decision", () => {
+    const consent = make();
+
+    consent.initialize();
+
+    const state = consent.setConsent({
+        preferences: "denied",
+        analytics: "granted",
+        marketing: "denied"
+    });
+
+    assert.equal(state.hasDecision, true);
+    assert.equal(state.categories.necessary, "granted");
+});
+
+test("acceptAll is idempotent", () => {
+    const consent = make();
+
+    consent.initialize();
+
+    let changed = 0;
+    let accepted = 0;
+
+    consent.on("consent:changed", () => changed++);
+    consent.on("consent:accepted", () => accepted++);
+
+    const first = consent.acceptAll();
+    const second = consent.acceptAll();
+
+    assert.equal(first.hasDecision, true);
+    assert.equal(second.hasDecision, true);
+    assert.equal(changed, 1);
+    assert.equal(accepted, 1);
+    assert.equal(second.timestamp, first.timestamp);
+});
+
+test("rejectAll is idempotent", () => {
+    const consent = make();
+
+    consent.initialize();
+
+    let changed = 0;
+    let rejected = 0;
+
+    consent.on("consent:changed", () => changed++);
+    consent.on("consent:rejected", () => rejected++);
+
+    const first = consent.rejectAll();
+    const second = consent.rejectAll();
+
+    assert.equal(first.hasDecision, true);
+    assert.equal(second.hasDecision, true);
+    assert.equal(changed, 1);
+    assert.equal(rejected, 1);
+});
+
+test("revoke after acceptance creates the Core-defined denied state", () => {
+    const consent = make();
+
+    consent.initialize();
+    consent.acceptAll();
+
+    let revoked = 0;
+
+    consent.on("consent:revoked", () => revoked++);
+
+    const first = consent.revoke();
+    const second = consent.revoke();
+
+    assert.equal(first.categories.necessary, "granted");
+    assert.equal(first.categories.preferences, "denied");
+    assert.equal(first.categories.analytics, "denied");
+    assert.equal(first.categories.marketing, "denied");
+
+    assert.equal(second.categories.analytics, "denied");
+    assert.equal(revoked, 1);
+});
+
+test("reset returns the engine to undecided state", () => {
+    const consent = make();
+
+    consent.initialize();
+    consent.acceptAll();
+
+    const state = consent.reset();
+
+    assert.equal(state.hasDecision, false);
+    assert.equal(state.categories.necessary, "granted");
+    assert.equal(state.categories.preferences, "unknown");
+    assert.equal(state.categories.analytics, "unknown");
+    assert.equal(state.categories.marketing, "unknown");
+});
+
+test("reset is idempotent", () => {
+    const consent = make();
+
+    consent.initialize();
+
+    let reset = 0;
+
+    consent.on("consent:reset", () => reset++);
+
+    const first = consent.reset();
+    const second = consent.reset();
+
+    assert.equal(first.hasDecision, false);
+    assert.equal(second.hasDecision, false);
+    assert.equal(reset, 2);
+});
+
+/* -------------------------------------------------------------------------- */
+/* GPC                                                                         */
+/* -------------------------------------------------------------------------- */
+
+test("GPC never grants optional consent", () => {
+    const consent = make();
+
+    consent.initialize();
+
+    const state = consent.setGpc(true);
+
+    assert.equal(state.gpc, true);
+    assert.equal(state.hasDecision, false);
+    assert.equal(state.categories.preferences, "unknown");
+    assert.equal(state.categories.analytics, "unknown");
+    assert.equal(state.categories.marketing, "unknown");
+});
+
+test("GPC can coexist with explicit consent without changing categories", () => {
+    const consent = make();
+
+    consent.initialize();
+
+    consent.acceptAll();
+
+    const state = consent.setGpc(true);
+
+    assert.equal(state.gpc, true);
+    assert.equal(state.categories.analytics, "granted");
+    assert.equal(state.categories.marketing, "granted");
+});
+
+
+/* -------------------------------------------------------------------------- */
+/* Persistence                                                                 */
+/* -------------------------------------------------------------------------- */
+
+test("accepted consent survives a fresh engine instance", () => {
+    const first = make();
+
+    first.initialize();
+    first.acceptAll();
+
+    const second = make();
+
+    second.initialize();
+
+    const state = second.getState();
+
+    assert.equal(state.source, "storage");
+    assert.equal(state.hasDecision, true);
+    assert.equal(state.categories.analytics, "granted");
+});
+
+test("rejected consent survives a fresh engine instance", () => {
+    const first = make();
+
+    first.initialize();
+    first.rejectAll();
+
+    const second = make();
+
+    second.initialize();
+
+    assert.equal(
+        second.getState().categories.analytics,
+        "denied"
+    );
+});
+
+test("policy mismatch invalidates persisted consent", () => {
+    const first = make();
+
+    first.initialize();
+    first.acceptAll();
+
+    const second = make({
+        policyVersion: "2027-01"
+    });
+
+    second.initialize();
+
+    assert.equal(second.isComplete(), false);
+    assert.equal(document.cookie, "");
+});
+
+test("invalid persisted payload is ignored and removed", () => {
+    const bad = {
+        engineVersion: "invalid",
+        policyVersion: "2026-01",
+        timestamp: Date.now(),
+        categories: {
+            necessary: "granted",
+            preferences: "banana",
+            analytics: "granted",
+            marketing: "denied"
+        },
+        jurisdiction: "global",
+        gpc: false
+    };
+
+    document.cookie =
+        `cc_reliability=${encodeURIComponent(JSON.stringify(bad))}; Path=/; SameSite=Lax`;
+
+    const consent = make();
+
+    consent.initialize();
+
+    assert.equal(consent.isComplete(), false);
+    assert.equal(document.cookie, "");
+});
+test("storage removal failure does not falsely report reset", () => {
+    const consent = make();
+
+    consent.initialize();
+    consent.acceptAll();
+
+    blockWrites = true;
+
+    let resetEvents = 0;
+    let storageErrors = 0;
+
+    consent.on("consent:reset", () => resetEvents++);
+    consent.on("storage:error", () => storageErrors++);
+
+    consent.reset();
+
+    assert.equal(resetEvents, 0);
+    assert.equal(storageErrors, 1);
+    assert.equal(consent.getState().hasDecision, true);
+});
+
+
+/* -------------------------------------------------------------------------- */
+/* Data contracts                                                              */
+/* -------------------------------------------------------------------------- */
+
+test("valid categories object satisfies its contract", () => {
+    assert.equal(
+        isValidCategoriesObject({
+            necessary: "granted",
+            preferences: "unknown",
+            analytics: "granted",
+            marketing: "denied"
+        }),
+        true
+    );
+});
+
+test("invalid categories object fails its contract", () => {
+    assert.equal(
+        isValidCategoriesObject({
+            necessary: "granted",
+            preferences: "invalid",
+            analytics: "granted",
+            marketing: "denied"
+        }),
+        false
+    );
+});
+
+test("valid stored envelope satisfies its contract", () => {
+    assert.equal(
+        isStoragePayload({
+            engineVersion: "1.0.0",
+            policyVersion: "2026-01",
+            timestamp: Date.now(),
+            categories: {
+                necessary: "granted",
+                preferences: "unknown",
+                analytics: "granted",
+                marketing: "denied"
+            },
+            jurisdiction: "global",
+            gpc: false
+        }),
+        true
+    );
+});
+
+test("empty stored envelope fails its contract", () => {
+    assert.equal(
+        isStoragePayload({}),
+        false
+    );
+});
+
+
+/* -------------------------------------------------------------------------- */
+/* Lifecycle                                                                    */
+/* -------------------------------------------------------------------------- */
+
+test("initialize is idempotent", () => {
+    const consent = make();
+
+    let initialized = 0;
+
+    consent.on("initialized", () => initialized++);
+
+    consent.initialize();
+    consent.initialize();
+    consent.initialize();
+
+    assert.equal(initialized, 1);
+});
+
+test("initialize without stored consent starts undecided", () => {
+    const consent = make();
+
+    consent.initialize();
+
+    const state = consent.getState();
+
+    assert.equal(state.hasDecision, false);
+    assert.equal(state.source, "default");
+    assert.equal(state.categories.necessary, "granted");
+    assert.equal(state.categories.analytics, "unknown");
+});
+
+
+console.log(`\n${passed} reliability tests passed.`);
diff --git a/tests/test.html b/tests/test.html
new file mode 100644
index 0000000..b108c3c
--- /dev/null
+++ b/tests/test.html
@@ -0,0 +1,70 @@
+
+
+
+
+
+Cookie Consent Core — Hardening Test Lab
+
+
+
+
+

Cookie Consent Core — Hardening Test Lab

Regression and robustness tests for Core v1.0.1.

+

Controls

+

State

—
+

Tests

Not run.

+

Cookie

+

Event log

—
+
+ + +