Initial reliable release of consent engine
This commit is contained in:
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user