Files

216 lines
5.5 KiB
JavaScript

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