/** * 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) ); }