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
+57
View File
@@ -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`
+197
View File
@@ -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"
])
);
}
+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
});
}
+104
View File
@@ -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"
]);
+216
View File
@@ -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
});
+45
View File
@@ -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;
}
+34
View File
@@ -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;
}
});
}
+38
View File
@@ -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";
+35
View File
@@ -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";
+73
View File
@@ -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));
}
+55
View File
@@ -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));
}
+215
View File
@@ -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)
);
}
+1
View File
@@ -0,0 +1 @@
{"type":"module"}
+180
View File
@@ -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);
+136
View File
@@ -0,0 +1,136 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cookie Consent Audit Lab</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; }
button { padding: 10px 16px; cursor: pointer; }
#status { margin: 20px 0; font-size: 1.2rem; font-weight: 700; }
pre { background: #f5f5f5; padding: 16px; overflow: auto; }
</style>
</head>
<body>
<h1>Cookie Consent Audit Lab</h1>
<p>Browser audit for Core + Jurisdiction + Integration invariants.</p>
<button id="run">Run audit</button>
<div id="status">Not run</div>
<pre id="log"></pre>
<script type="module">
import { createIntegratedConsentEngine } from "../../integration/index.js";
const status = document.getElementById("status");
const log = document.getElementById("log");
const button = document.getElementById("run");
function assert(condition, message) {
if (!condition) throw new Error(message);
}
function clearConsentCookie() {
document.cookie = "ucc_consent=; Max-Age=0; Path=/; SameSite=Lax";
}
async function test(name, fn, output) {
try {
await fn();
output.push(`PASS ${name}`);
return true;
} catch (error) {
output.push(`FAIL ${name} — ${error.message}`);
return false;
}
}
async function runAudit() {
clearConsentCookie();
const output = [];
let passed = 0;
if (await test("dynamic jurisdiction is reflected in Core state", () => {
const consent = createIntegratedConsentEngine({
jurisdiction: { mode: "fixed", jurisdiction: "global" }
});
consent.initialize();
assert(consent.setJurisdiction("us-ma").jurisdiction === "us-ma", "jurisdiction mismatch");
}, output)) passed++;
if (await test("GPC state is represented without granting optional consent", () => {
const consent = createIntegratedConsentEngine({
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
});
consent.initialize();
const state = consent.setGpc(true);
assert(state.gpc === true, "GPC was not recorded");
assert(state.hasDecision === false, "GPC unexpectedly created a decision");
assert(state.categories.analytics === "unknown", "analytics was granted unexpectedly");
}, output)) passed++;
if (await test("acceptance persists resolved jurisdiction", () => {
clearConsentCookie();
const consent = createIntegratedConsentEngine({
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
});
consent.initialize();
const state = consent.acceptAll();
assert(state.hasDecision === true, "decision was not saved");
assert(state.jurisdiction === "us-ma", "wrong persisted jurisdiction");
}, output)) passed++;
if (await test("reload recovers the persisted decision", () => {
const consent = createIntegratedConsentEngine({
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
});
consent.initialize();
const state = consent.getState();
assert(state.hasDecision === true, "decision did not survive reinitialization");
assert(state.jurisdiction === "us-ma", "jurisdiction did not survive reinitialization");
}, output)) passed++;
if (await test("reset removes the persisted decision", () => {
const consent = createIntegratedConsentEngine({
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
});
consent.initialize();
const state = consent.reset();
assert(state.hasDecision === false, "reset retained decision");
assert(state.categories.analytics === "unknown", "analytics was not reset");
}, output)) passed++;
if (await test("event sequence reports acceptance once", () => {
clearConsentCookie();
const consent = createIntegratedConsentEngine({
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
});
const events = [];
consent.on("consent:changed", () => events.push("changed"));
consent.on("consent:accepted", () => events.push("accepted"));
consent.initialize();
consent.acceptAll();
assert(events.join(",") === "changed,accepted", "unexpected event sequence: " + events.join(","));
}, output)) passed++;
if (await test("returned state cannot mutate Core internals", () => {
const consent = createIntegratedConsentEngine({
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
});
consent.initialize();
const state = consent.getState();
state.categories.analytics = "granted";
state.jurisdiction = "changed";
const current = consent.getState();
assert(current.categories.analytics === "unknown", "categories leaked");
assert(current.jurisdiction === "us-ma", "jurisdiction leaked");
}, output)) passed++;
log.textContent = output.join("\n");
status.textContent = `${passed}/7 audit tests passed`;
clearConsentCookie();
}
button.addEventListener("click", runAudit);
</script>
</body>
</html>
+256
View File
@@ -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.`);
+173
View File
@@ -0,0 +1,173 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cookie Consent Integration Test Lab</title>
<style>
body {
font-family: system-ui, sans-serif;
max-width: 900px;
margin: 40px auto;
padding: 0 20px;
line-height: 1.5;
}
button {
padding: 10px 16px;
cursor: pointer;
}
#summary {
font-weight: 700;
margin: 20px 0;
}
.pass { color: #176b2c; }
.fail { color: #a11; }
pre {
background: #f5f5f5;
padding: 16px;
overflow: auto;
}
</style>
</head>
<body>
<h1>Cookie Consent Integration Test Lab</h1>
<p>Browser regression suite for the Jurisdiction Engine + Consent Core boundary.</p>
<button id="run">Run tests</button>
<div id="summary">Not run.</div>
<pre id="output"></pre>
<script type="module">
import {
resolveConsentContext,
createIntegratedConsentEngine
} from "../../integration/index.js";
const output = document.getElementById("output");
const summary = document.getElementById("summary");
const runButton = document.getElementById("run");
function equal(actual, expected) {
if (actual !== expected) {
throw new Error(`Expected ${expected}, got ${actual}`);
}
}
function deepEqual(actual, expected) {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error("Values are not deeply equal.");
}
}
function throws(fn, pattern) {
try {
fn();
} catch (error) {
if (pattern.test(error.message)) return;
throw error;
}
throw new Error("Expected function to throw.");
}
function test(name, fn) {
try {
fn();
output.textContent += `PASS: ${name}\n`;
return true;
} catch (error) {
output.textContent += `FAIL: ${name}\n${error.stack || error}\n`;
return false;
}
}
runButton.addEventListener("click", () => {
output.textContent = "";
let passed = 0;
const total = 9;
if (test("fixed us-ma resolves into Core context", () => {
const context = resolveConsentContext({
mode: "fixed", jurisdiction: "us-ma"
});
equal(context.resolution.jurisdiction.id, "us-ma");
equal(context.resolution.policy.id, "us-state-privacy");
})) passed++;
if (test("fixed eu resolves into Core context", () => {
const context = resolveConsentContext({
mode: "fixed", jurisdiction: "eu"
});
equal(context.resolution.jurisdiction.id, "eu");
equal(context.resolution.policy.id, "eu-privacy");
})) passed++;
if (test("global resolves into global baseline", () => {
const context = resolveConsentContext({ mode: "global" });
equal(context.resolution.jurisdiction.id, "global");
equal(context.resolution.policy.id, "global-baseline");
})) passed++;
if (test("integration configures Core jurisdiction", () => {
const consent = createIntegratedConsentEngine({
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
});
const state = consent.initialize();
equal(state.jurisdiction, "us-ma");
equal(state.hasDecision, false);
})) passed++;
if (test("jurisdiction does not grant consent", () => {
const consent = createIntegratedConsentEngine({
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
});
const state = consent.initialize();
equal(state.categories.necessary, "granted");
equal(state.categories.preferences, "unknown");
equal(state.categories.analytics, "unknown");
equal(state.categories.marketing, "unknown");
equal(state.hasDecision, false);
})) passed++;
if (test("resolved context is exposed as frozen data", () => {
const consent = createIntegratedConsentEngine({
jurisdiction: { mode: "fixed", jurisdiction: "eu" }
});
const context = consent.getJurisdictionContext();
equal(context.resolution.jurisdiction.id, "eu");
equal(Object.isFrozen(context), true);
})) passed++;
if (test("Core persistence uses resolved jurisdiction", () => {
const consent = createIntegratedConsentEngine({
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
});
consent.initialize();
consent.acceptAll();
equal(consent.getState().jurisdiction, "us-ma");
equal(consent.getState().hasDecision, true);
})) passed++;
if (test("integration is deterministic", () => {
const input = {
mode: "auto",
request: { jurisdiction: "us-ma", source: "server" },
visitor: { jurisdiction: "eu", source: "browser" }
};
deepEqual(
resolveConsentContext(input),
resolveConsentContext(input)
);
})) passed++;
if (test("invalid jurisdiction remains an Engine error", () => {
throws(() => resolveConsentContext({
mode: "fixed", jurisdiction: "not-a-jurisdiction"
}), /Unknown jurisdiction/);
})) passed++;
summary.textContent = `${passed}/${total} tests passed`;
summary.className = passed === total ? "pass" : "fail";
});
</script>
</body>
</html>
+136
View File
@@ -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.");
+84
View File
@@ -0,0 +1,84 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Jurisdiction Engine Test Lab</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; }
button { padding: 10px 16px; cursor: pointer; }
#summary { margin: 20px 0; font-weight: 700; }
.pass { color: #176b2c; }
.fail { color: #a51d2d; }
pre { white-space: pre-wrap; background: #f5f5f5; padding: 16px; border-radius: 8px; }
</style>
</head>
<body>
<h1>Jurisdiction Engine Test Lab</h1>
<p>Browser regression suite for the pure Jurisdiction Engine.</p>
<button id="run">Run tests</button>
<div id="summary">Not run.</div>
<pre id="log"></pre>
<script type="module">
import {
resolveJurisdiction,
isValidResolution,
getResolutionPriority,
getSourceConfidence,
hasJurisdiction,
hasPolicy,
JURISDICTION_CATALOG,
POLICIES
} from "../jurisdiction/index.js";
const tests = [
["fixed us-ma", () => {
const r = resolveJurisdiction({mode:"fixed", jurisdiction:"us-ma"});
return r.status === "resolved" && r.jurisdiction.id === "us-ma" && r.policy.id === "us-state-privacy" && r.source === "site-config" && r.confidence === "explicit";
}],
["fixed eu", () => resolveJurisdiction({mode:"fixed", jurisdiction:"eu"}).policy.id === "eu-privacy"],
["fixed global", () => resolveJurisdiction({mode:"fixed", jurisdiction:"global"}).policy.id === "global-baseline"],
["global mode", () => resolveJurisdiction({mode:"global"}).jurisdiction.id === "global"],
["auto server", () => resolveJurisdiction({mode:"auto", request:{jurisdiction:"us-ca", source:"server"}}).source === "server"],
["auto application", () => resolveJurisdiction({mode:"auto", visitor:{jurisdiction:"us-ma", source:"application"}}).source === "application"],
["auto browser", () => resolveJurisdiction({mode:"auto", visitor:{jurisdiction:"eu", source:"browser"}}).source === "browser"],
["auto fallback", () => { const r=resolveJurisdiction({mode:"auto"}); return r.status === "fallback" && r.jurisdiction.id === "global"; }],
["unknown signal fallback", () => resolveJurisdiction({mode:"auto", request:{jurisdiction:"xx-unknown", source:"server"}}).status === "fallback"],
["server beats browser", () => resolveJurisdiction({mode:"auto", request:{jurisdiction:"us-ma",source:"server"}, visitor:{jurisdiction:"eu",source:"browser"}}).jurisdiction.id === "us-ma"],
["site config beats server", () => resolveJurisdiction({mode:"auto", site:{jurisdiction:"us-ca",source:"site-config"}, request:{jurisdiction:"eu",source:"server"}}).jurisdiction.id === "us-ca"],
["fixed requires jurisdiction", () => { try { resolveJurisdiction({mode:"fixed"}); return false; } catch { return true; } }],
["fixed rejects unknown", () => { try { resolveJurisdiction({mode:"fixed",jurisdiction:"xx"}); return false; } catch { return true; } }],
["invalid mode", () => { try { resolveJurisdiction({mode:"random"}); return false; } catch { return true; } }],
["invalid input", () => { try { resolveJurisdiction(null); return false; } catch { return true; } }],
["catalog policies exist", () => Object.values(JURISDICTION_CATALOG).every(e => hasPolicy(e.policy))],
["result contract", () => [resolveJurisdiction({mode:"fixed",jurisdiction:"us-ma"}),resolveJurisdiction({mode:"global"}),resolveJurisdiction({mode:"auto"})].every(isValidResolution)],
["source priority", () => getResolutionPriority("site-config") > getResolutionPriority("server") && getResolutionPriority("server") > getResolutionPriority("application") && getResolutionPriority("application") > getResolutionPriority("browser") && getResolutionPriority("browser") > getResolutionPriority("fallback")],
["confidence mapping", () => getSourceConfidence("site-config") === "explicit" && getSourceConfidence("server") === "high" && getSourceConfidence("application") === "medium" && getSourceConfidence("browser") === "low" && getSourceConfidence("fallback") === "default"],
["initial catalog", () => ["global","us","us-ma","us-ca","eu"].every(hasJurisdiction) && POLICIES["global-baseline"] !== undefined]
];
const run = document.querySelector("#run");
const summary = document.querySelector("#summary");
const log = document.querySelector("#log");
run.addEventListener("click", () => {
let passed = 0;
const lines = [];
for (const [name, fn] of tests) {
try {
const ok = Boolean(fn());
if (!ok) throw new Error("Assertion returned false");
passed++;
lines.push(`PASS: ${name}`);
} catch (error) {
lines.push(`FAIL: ${name} — ${error.message}`);
}
}
summary.textContent = `${passed}/${tests.length} tests passed`;
summary.className = passed === tests.length ? "pass" : "fail";
log.textContent = lines.join("\n");
});
</script>
</body>
</html>
+196
View File
@@ -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;
}
+525
View File
@@ -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.`);
+70
View File
@@ -0,0 +1,70 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cookie Consent Core — Hardening Test Lab</title>
<style>
body{font-family:system-ui,sans-serif;margin:0;background:#f5f7fa;color:#1f2937}.wrap{max-width:1000px;margin:32px auto;padding:0 16px}.card{background:#fff;border:1px solid #dfe4ea;border-radius:12px;padding:20px;margin-bottom:18px}button{padding:9px 13px;border:1px solid #cbd2da;border-radius:7px;background:#fff;cursor:pointer}button.primary{background:#1f2937;color:#fff}.actions{display:flex;gap:8px;flex-wrap:wrap}.pass{color:#137333}.fail{color:#b42318}.neutral{color:#697386}.test{display:flex;justify-content:space-between;border-bottom:1px solid #eee;padding:10px 0;gap:16px}.test:last-child{border:0}pre{background:#111827;color:#e5e7eb;padding:16px;border-radius:8px;overflow:auto}code{font-family:ui-monospace,monospace}
</style>
</head>
<body>
<div class="wrap">
<header><h1>Cookie Consent Core — Hardening Test Lab</h1><p>Regression and robustness tests for Core v1.0.1.</p></header>
<section class="card"><h2>Controls</h2><div class="actions"><button id="run" class="primary">Run tests</button><button id="accept">Accept All</button><button id="reject">Reject All</button><button id="revoke">Revoke</button><button id="reset">Reset</button><button id="refresh">Refresh</button></div></section>
<section class="card"><h2>State</h2><pre id="state">—</pre></section>
<section class="card"><h2>Tests</h2><div id="tests"></div><p id="summary" class="neutral">Not run.</p></section>
<section class="card"><h2>Cookie</h2><pre id="cookie">—</pre></section>
<section class="card"><h2>Event log</h2><pre id="log">—</pre></section>
</div>
<script type="module">
import { createConsentEngine, ENGINE_VERSION } from "../core/consent.js";
import { isStoragePayload } from "../core/storage.js";
import { isValidCategoriesObject } from "../core/categories.js";
const config={policyVersion:"2026-01",jurisdiction:"global",storage:{name:"cc_consent",duration:180,path:"/",sameSite:"Lax"}};
const engine=createConsentEngine(config);
const log=[];
const events=["initialized","consent:loaded","consent:accepted","consent:rejected","consent:changed","consent:revoked","consent:expired","consent:reset","storage:error"];
for(const name of events) engine.on(name,payload=>{log.push({time:new Date().toLocaleTimeString(),event:name,payload});render();});
function render(){document.querySelector("#state").textContent=JSON.stringify(engine.getState(),null,2);document.querySelector("#cookie").textContent=document.cookie||"(no cookie)";document.querySelector("#log").textContent=JSON.stringify(log,null,2)}
function assert(c,m){if(!c)throw new Error(m)}
function clean(){engine.reset();document.cookie="cc_consent=; Max-Age=0; Path=/; SameSite=Lax"}
function test(name,fn){try{fn();return [name,true] }catch(e){return [name,false,e.message]}}
function run(){
const results=[];
function setCookiePayload(payload){
document.cookie="cc_consent="+encodeURIComponent(JSON.stringify(payload))+"; Path=/; SameSite=Lax";
}
function clearDirect(){ document.cookie="cc_consent=; Max-Age=0; Path=/; SameSite=Lax"; }
clean(); engine.initialize();
results.push(test("Default / unknown",()=>{const s=engine.getState();assert(s.categories.necessary==="granted","necessary");assert(s.categories.analytics==="unknown","analytics");assert(!s.hasDecision,"decision") }));
results.push(test("Accept All",()=>{engine.acceptAll();const s=engine.getState();assert(s.categories.analytics==="granted","analytics");assert(s.hasDecision,"decision")}));
results.push(test("Accept All idempotent",()=>{const t=engine.getState().timestamp;let n=0;const off=engine.on("consent:changed",()=>n++);engine.acceptAll();off();assert(n===0,"duplicate changed event");assert(engine.getState().timestamp===t,"timestamp changed")}));
results.push(test("Persistence",()=>{const e=createConsentEngine(config);e.initialize();assert(e.getState().source==="storage","source");assert(e.hasConsent("analytics"),"analytics")}));
results.push(test("Reject All",()=>{engine.rejectAll();const s=engine.getState();assert(s.categories.analytics==="denied","analytics");assert(s.categories.necessary==="granted","necessary")}));
results.push(test("Reject All idempotent",()=>{let n=0;const off=engine.on("consent:changed",()=>n++);engine.rejectAll();off();assert(n===0,"duplicate changed event")}));
results.push(test("Selective",()=>{engine.setConsent({preferences:"denied",analytics:"granted",marketing:"denied"});assert(engine.isComplete(),"complete");assert(engine.hasConsent("analytics"),"analytics")}));
results.push(test("Invalid value rejected",()=>{let ok=false;try{engine.setConsent({analytics:"banana"})}catch{ok=true}assert(ok,"must throw")}));
results.push(test("Necessary cannot be denied",()=>{let ok=false;try{engine.setConsent({necessary:"denied"})}catch{ok=true}assert(ok,"must throw")}));
results.push(test("Revoke",()=>{engine.acceptAll();engine.revoke();assert(engine.getState().categories.analytics==="denied","analytics")}));
results.push(test("Revoke idempotent",()=>{let n=0;const off=engine.on("consent:revoked",()=>n++);engine.revoke();off();assert(n===0,"duplicate revoke")}));
results.push(test("Reset",()=>{engine.acceptAll();engine.reset();assert(!engine.isComplete(),"decision");assert(!document.cookie,"cookie remains")}));
results.push(test("Initialize idempotent",()=>{let n=0;const e=createConsentEngine(config);e.on("initialized",()=>n++);e.initialize();e.initialize();assert(n===1,"initialized twice")}));
results.push(test("Strict stored envelope validation",()=>{assert(!isStoragePayload({}),"empty payload accepted");assert(isStoragePayload({engineVersion:ENGINE_VERSION,policyVersion:"2026-01",timestamp:Date.now(),categories:{},jurisdiction:"global",gpc:false}),"valid envelope rejected")}));
results.push(test("Strict category validation",()=>{assert(isValidCategoriesObject({necessary:"granted",preferences:"unknown",analytics:"granted",marketing:"denied"}),"valid categories rejected");assert(!isValidCategoriesObject({necessary:"denied",preferences:"unknown",analytics:"granted",marketing:"denied"}),"necessary denied accepted");assert(!isValidCategoriesObject({necessary:"granted",preferences:"banana",analytics:"granted",marketing:"denied"}),"invalid category accepted")}));
results.push(test("Policy version mismatch",()=>{clean();const e=createConsentEngine(config);e.initialize();e.acceptAll();const fresh=createConsentEngine({...config,policyVersion:"2027-01"});fresh.initialize();assert(!fresh.isComplete(),"old policy accepted");assert(!document.cookie,"old cookie remains")}));
results.push(test("Expired cookie",()=>{clean();const e=createConsentEngine(config);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;setCookiePayload(payload);const fresh=createConsentEngine(config);let expired=0;fresh.on("consent:expired",()=>expired++);fresh.initialize();assert(expired===1,"expired event");assert(!fresh.isComplete(),"expired consent accepted");assert(!document.cookie,"expired cookie remains")}));
results.push(test("Invalid persisted category payload",()=>{clean();const bad={engineVersion:ENGINE_VERSION,policyVersion:"2026-01",timestamp:Date.now(),categories:{necessary:"granted",preferences:"banana",analytics:"granted",marketing:"denied"},jurisdiction:"global",gpc:false};setCookiePayload(bad);const e=createConsentEngine(config);e.initialize();assert(!e.isComplete(),"invalid payload accepted");assert(!document.cookie,"invalid cookie remains")}));
results.push(test("Future timestamp",()=>{clean();const e=createConsentEngine(config);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;setCookiePayload(payload);const fresh=createConsentEngine(config);fresh.initialize();assert(!fresh.isComplete(),"future timestamp accepted");assert(!document.cookie,"future cookie remains")}));
const failed=results.filter(x=>!x[1]);
document.querySelector("#tests").innerHTML=results.map(x=>`<div class="test"><span>${x[0]}${x[2]?` — ${x[2]}`:""}</span><strong class="${x[1]?"pass":"fail"}">${x[1]?"PASS":"FAIL"}</strong></div>`).join("");
document.querySelector("#summary").textContent=`${results.length-failed.length}/${results.length} tests passed`;
document.querySelector("#summary").className=failed.length?"fail":"pass";
render();
}
document.querySelector("#run").onclick=run;document.querySelector("#accept").onclick=()=>{engine.acceptAll();render()};document.querySelector("#reject").onclick=()=>{engine.rejectAll();render()};document.querySelector("#revoke").onclick=()=>{engine.revoke();render()};document.querySelector("#reset").onclick=()=>{engine.reset();render()};document.querySelector("#refresh").onclick=render;engine.initialize();render();
</script>
</body>
</html>