c15t
Version:
Headless JavaScript consent management platform for cookie banners, privacy preferences, consent storage, and script gating.
4,127 lines • 174 kB
JavaScript
import { createDeterministicFingerprintSync, createMaterialPolicyFingerprint, isLegalDocumentType, policyDefaults, policyPackPresets, resolvePolicyDecision, validatePolicies, validatePolicyI18nConfig } from "@c15t/schema/types";
import { deepMergeTranslations, detectBrowserLanguage, enTranslations, mergeTranslationConfigs, normalizeI18nConfig, prepareTranslationConfig, resolveTranslationInput, selectLanguage } from "@c15t/translations";
import { createStore } from "zustand/vanilla";
const LEADING_SLASHES_REGEX = /^\/+/;
function createResponseContext(isSuccess, data = null, error = null, response = null) {
return {
data,
error,
ok: isSuccess,
response
};
}
function createErrorResponse(message, status = 500, code = 'ERROR', cause) {
return createResponseContext(false, null, {
message,
status,
code,
cause
}, null);
}
function utils_createErrorResponse(message, status = 500, code = 'HANDLER_ERROR', cause) {
return createErrorResponse(message, status, code, cause);
}
async function executeHandler(endpointHandlers, handlerKey, options) {
const handler = endpointHandlers[handlerKey];
if (!handler) {
const errorResponse = utils_createErrorResponse(`No endpoint handler found for '${String(handlerKey)}'`, 404, 'ENDPOINT_NOT_FOUND');
if (options?.throw) throw new Error(`No endpoint handler found for '${String(handlerKey)}'`);
return errorResponse;
}
try {
const response = await handler(options);
const normalizedResponse = {
data: response.data,
error: response.error,
ok: response.ok ?? !response.error,
response: response.response
};
return normalizedResponse;
} catch (error) {
const errorResponse = utils_createErrorResponse(error instanceof Error ? error.message : String(error), 0, 'HANDLER_ERROR', error);
if (options?.throw) throw error;
return errorResponse;
}
}
async function customFetch(endpointHandlers, dynamicHandlers, path, options) {
const endpointName = path.replace(LEADING_SLASHES_REGEX, '').split('/')[0];
const handler = dynamicHandlers[path];
if (handler) try {
return await handler(options);
} catch (error) {
const errorResponse = utils_createErrorResponse(error instanceof Error ? error.message : String(error), 0, 'HANDLER_ERROR', error);
return errorResponse;
}
if (!endpointName || !(endpointName in endpointHandlers)) {
const errorResponse = utils_createErrorResponse(`No endpoint handler found for '${path}'`, 404, 'ENDPOINT_NOT_FOUND');
return errorResponse;
}
return await executeHandler(endpointHandlers, endpointName, options);
}
async function init_init(endpointHandlers, options) {
const handlerKey = 'init' in endpointHandlers && void 0 !== endpointHandlers.init ? 'init' : 'init';
return await executeHandler(endpointHandlers, handlerKey, options);
}
async function setConsent(endpointHandlers, options) {
return await executeHandler(endpointHandlers, 'setConsent', options);
}
class CustomClient {
endpointHandlers;
dynamicHandlers = {};
constructor(options){
this.endpointHandlers = options.endpointHandlers;
}
async init(options) {
return init_init(this.endpointHandlers, options);
}
async setConsent(options) {
return setConsent(this.endpointHandlers, options);
}
async identifyUser(options) {
if (this.endpointHandlers.identifyUser) return this.endpointHandlers.identifyUser(options);
const subjectId = options.body?.id;
if (!subjectId) return {
ok: false,
data: null,
response: null,
error: {
message: 'Subject ID is required to identify user',
status: 400,
code: 'MISSING_SUBJECT_ID'
}
};
return this.$fetch(`/subjects/${subjectId}`, {
...options,
method: 'PATCH'
});
}
registerHandler(path, handler) {
this.dynamicHandlers[path] = handler;
}
async $fetch(path, options) {
return customFetch(this.endpointHandlers, this.dynamicHandlers, path, options);
}
}
const DEFAULT_RETRY_CONFIG = {
maxRetries: 3,
initialDelayMs: 100,
backoffFactor: 2,
retryableStatusCodes: [
500,
502,
503,
504
],
nonRetryableStatusCodes: [
400,
401,
403,
404
],
retryOnNetworkError: true,
shouldRetry: void 0
};
const ABSOLUTE_URL_REGEX = /^(?:[a-z+]+:)?\/\//i;
const constants_LEADING_SLASHES_REGEX = LEADING_SLASHES_REGEX;
const noop = ()=>{};
function createDebugLogger(enabled) {
if (!enabled) return {
log: noop,
debug: noop
};
return {
log: (...args)=>console.log('[c15t]', ...args),
debug: (...args)=>console.debug('[c15t]', ...args)
};
}
let _debugLogger = createDebugLogger(false);
function getDebugLogger() {
return _debugLogger;
}
function setDebugEnabled(enabled) {
_debugLogger = createDebugLogger(enabled);
}
const C15T_VERSION_HEADER = 'x-c15t-version';
const headers_C15T_VERSION_HEADERS = {
[C15T_VERSION_HEADER]: "2.2.1"
};
const delay = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
function getIdentifySubjectId(submission) {
return submission?.subjectId || submission?.id;
}
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c)=>{
const r = 16 * Math.random() | 0;
const v = 'x' === c ? r : 0x3 & r | 0x8;
return v.toString(16);
});
}
function removeTrailingSlashes(str) {
let i = str.length;
while(i > 0 && '/' === str[i - 1])i--;
return str.slice(0, i);
}
function resolveUrl(backendURL, path) {
if (ABSOLUTE_URL_REGEX.test(backendURL)) {
const backendURLObj = new URL(backendURL);
const basePath = removeTrailingSlashes(backendURLObj.pathname);
const cleanPath = path.replace(constants_LEADING_SLASHES_REGEX, '');
const newPath = `${basePath}/${cleanPath}`;
backendURLObj.pathname = newPath;
return backendURLObj.toString();
}
const cleanBase = removeTrailingSlashes(backendURL);
const cleanPath = path.replace(constants_LEADING_SLASHES_REGEX, '');
return `${cleanBase}/${cleanPath}`;
}
const fetcher_createResponseContext = createResponseContext;
async function fetcher(context, path, options) {
const finalRetryConfig = {
...context.retryConfig,
...options?.retryConfig || {},
retryableStatusCodes: options?.retryConfig?.retryableStatusCodes ?? context.retryConfig.retryableStatusCodes ?? DEFAULT_RETRY_CONFIG.retryableStatusCodes,
nonRetryableStatusCodes: options?.retryConfig?.nonRetryableStatusCodes ?? context.retryConfig.nonRetryableStatusCodes ?? DEFAULT_RETRY_CONFIG.nonRetryableStatusCodes
};
const { maxRetries, initialDelayMs, backoffFactor, retryableStatusCodes, nonRetryableStatusCodes, retryOnNetworkError } = finalRetryConfig;
let attemptsMade = 0;
let currentDelay = initialDelayMs;
let lastErrorResponse = null;
while(attemptsMade <= (maxRetries ?? 0)){
const requestId = generateUUID();
const fetchImpl = context.customFetch || globalThis.fetch;
const resolvedUrl = resolveUrl(context.backendURL, path);
let url;
try {
url = new URL(resolvedUrl);
} catch {
url = new URL(resolvedUrl, window.location.origin);
}
if (options?.query) {
for (const [key, value] of Object.entries(options.query))if (void 0 !== value) url.searchParams.append(key, String(value));
}
const requestOptions = {
method: options?.method || 'GET',
mode: context.corsMode,
credentials: 'include',
headers: {
...headers_C15T_VERSION_HEADERS,
...context.headers,
'X-Request-ID': requestId,
...options?.headers
},
...options?.fetchOptions
};
if (options?.body && 'GET' !== requestOptions.method) requestOptions.body = JSON.stringify(options.body);
try {
const response = await fetchImpl(url.toString(), requestOptions);
let data = null;
let parseError = null;
try {
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json') && 204 !== response.status && '0' !== response.headers.get('content-length')) data = await response.json();
else if (204 === response.status) data = null;
} catch (err) {
parseError = err;
}
if (parseError) {
const errorResponse = fetcher_createResponseContext(false, null, {
message: 'Failed to parse response',
status: response.status,
code: 'PARSE_ERROR',
cause: parseError
}, response);
options?.onError?.(errorResponse, path);
if (options?.throw) throw new Error('Failed to parse response');
return errorResponse;
}
const isSuccess = response.status >= 200 && response.status < 300;
if (isSuccess) {
const successResponse = fetcher_createResponseContext(true, data, null, response);
options?.onSuccess?.(successResponse);
return successResponse;
}
const errorData = data;
const errorResponse = fetcher_createResponseContext(false, null, {
message: errorData?.message || `Request failed with status ${response.status}`,
status: response.status,
code: errorData?.code || 'API_ERROR',
details: errorData?.details || null
}, response);
lastErrorResponse = errorResponse;
let shouldRetryThisRequest = false;
if (nonRetryableStatusCodes?.includes(response.status)) {
getDebugLogger().debug(`Not retrying request to ${path} with status ${response.status} (nonRetryableStatusCodes)`);
shouldRetryThisRequest = false;
} else if ('function' == typeof finalRetryConfig.shouldRetry) try {
shouldRetryThisRequest = finalRetryConfig.shouldRetry(response, {
attemptsMade,
url: url.toString(),
method: requestOptions.method || 'GET'
});
getDebugLogger().debug(`Custom retry strategy for ${path} with status ${response.status}: ${shouldRetryThisRequest}`);
} catch {
shouldRetryThisRequest = retryableStatusCodes?.includes(response.status) ?? false;
getDebugLogger().debug(`Custom retry strategy failed, falling back to status code check: ${shouldRetryThisRequest}`);
}
else {
shouldRetryThisRequest = retryableStatusCodes?.includes(response.status) ?? false;
getDebugLogger().debug(`Standard retry check for ${path} with status ${response.status}: ${shouldRetryThisRequest}`);
}
if (!shouldRetryThisRequest || attemptsMade >= (maxRetries ?? 0)) {
options?.onError?.(errorResponse, path);
if (options?.throw) throw new Error(errorResponse.error?.message || 'Request failed');
return errorResponse;
}
attemptsMade++;
await delay(currentDelay ?? 0);
currentDelay = (currentDelay ?? 0) * (backoffFactor ?? 2);
} catch (fetchError) {
if (fetchError && 'Failed to parse response' === fetchError.message) throw fetchError;
const isNetworkError = !(fetchError instanceof Response);
const errorResponse = fetcher_createResponseContext(false, null, {
message: fetchError instanceof Error ? fetchError.message : String(fetchError),
status: 0,
code: 'NETWORK_ERROR',
cause: fetchError
}, null);
lastErrorResponse = errorResponse;
const shouldRetryThisRequest = isNetworkError && retryOnNetworkError;
if (!shouldRetryThisRequest || attemptsMade >= (maxRetries ?? 0)) {
options?.onError?.(errorResponse, path);
if (options?.throw) throw fetchError;
return errorResponse;
}
attemptsMade++;
await delay(currentDelay ?? 0);
currentDelay = (currentDelay ?? 0) * (backoffFactor ?? 2);
}
}
const maxRetriesErrorResponse = lastErrorResponse || fetcher_createResponseContext(false, null, {
message: `Request failed after ${maxRetries} retries`,
status: 0,
code: 'MAX_RETRIES_EXCEEDED'
}, null);
options?.onError?.(maxRetriesErrorResponse, path);
if (options?.throw) throw new Error(`Request failed after ${maxRetries} retries`);
return maxRetriesErrorResponse;
}
function getDefaultCookieOptions(config) {
return {
expiryDays: config?.defaultExpiryDays ?? 365,
crossSubdomain: config?.crossSubdomain ?? false,
domain: config?.defaultDomain ?? '',
path: '/',
secure: "u" > typeof window && 'https:' === window.location.protocol,
sameSite: 'Lax'
};
}
function getRootDomain() {
if ("u" < typeof window) return '';
const hostname = window.location.hostname;
if ('localhost' === hostname || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) return hostname;
const parts = hostname.split('.');
if (parts.length >= 2) return `.${parts.slice(-2).join('.')}`;
return hostname;
}
const COOKIE_KEY_MAP = {
consents: 'c',
consentInfo: 'i',
timestamp: 'ts',
iabCustomVendorConsents: 'icv',
iabCustomVendorLegitimateInterests: 'icvli',
time: 't',
type: 'y',
id: 'id',
subjectId: 'sid',
externalId: 'eid',
materialPolicyFingerprint: 'mpf',
identityProvider: 'idp'
};
const REVERSE_COOKIE_KEY_MAP = Object.entries(COOKIE_KEY_MAP).reduce((acc, [key, value])=>{
acc[value] = key;
return acc;
}, {});
function shortenFlatKeys(flattened) {
const shortened = {};
for (const [key, value] of Object.entries(flattened)){
const keys = key.split('.');
const shortenedKeys = keys.map((k)=>COOKIE_KEY_MAP[k] || k);
shortened[shortenedKeys.join('.')] = value;
}
return shortened;
}
function expandFlatKeys(shortened) {
const expanded = {};
for (const [key, value] of Object.entries(shortened)){
const keys = key.split('.');
const expandedKeys = keys.map((k)=>REVERSE_COOKIE_KEY_MAP[k] || k);
expanded[expandedKeys.join('.')] = value;
}
return expanded;
}
function flattenObject(obj, prefix = '') {
const flattened = {};
for (const [key, value] of Object.entries(obj)){
const newKey = prefix ? `${prefix}.${key}` : key;
if (null == value) flattened[newKey] = '';
else if ('boolean' == typeof value) {
if (value) flattened[newKey] = '1';
} else if ('object' != typeof value || Array.isArray(value)) flattened[newKey] = String(value);
else Object.assign(flattened, flattenObject(value, newKey));
}
return flattened;
}
function unflattenObject(flattened) {
const result = {};
for (const [key, value] of Object.entries(flattened)){
const keys = key.split('.');
if (0 === keys.length) continue;
let current = result;
for(let i = 0; i < keys.length - 1; i++){
const k = keys[i];
if (void 0 !== k) {
if (!current[k]) current[k] = {};
current = current[k];
}
}
const lastKey = keys[keys.length - 1];
if (void 0 !== lastKey) if ('1' === value) current[lastKey] = true;
else if ('0' === value) current[lastKey] = false;
else if ('' === value) current[lastKey] = null;
else if (Number.isNaN(Number(value)) || '' === value) current[lastKey] = value;
else current[lastKey] = Number(value);
}
return result;
}
function flatToString(flattened) {
return Object.entries(flattened).map(([key, value])=>`${key}:${value}`).join(',');
}
function stringToFlat(str) {
if (!str) return {};
const result = {};
const pairs = str.split(',');
for (const pair of pairs){
const colonIndex = pair.indexOf(':');
if (-1 === colonIndex) continue;
const key = pair.substring(0, colonIndex);
const value = pair.substring(colonIndex + 1);
result[key] = value;
}
return result;
}
function setCookie(name, value, options, config) {
if ("u" < typeof document) return;
const opts = {
...getDefaultCookieOptions(config),
...options
};
if (opts.crossSubdomain && !options?.domain) opts.domain = getRootDomain();
try {
let cookieValue;
if ('string' == typeof value) cookieValue = value;
else {
const flattened = flattenObject(value);
const shortened = shortenFlatKeys(flattened);
cookieValue = flatToString(shortened);
}
const date = new Date();
date.setTime(date.getTime() + 24 * opts.expiryDays * 3600000);
const expires = `expires=${date.toUTCString()}`;
const parts = [
`${name}=${cookieValue}`,
expires,
`path=${opts.path}`
];
if (opts.domain) parts.push(`domain=${opts.domain}`);
if (opts.secure) parts.push('secure');
if (opts.sameSite) parts.push(`SameSite=${opts.sameSite}`);
document.cookie = parts.join('; ');
} catch (error) {
console.warn(`Failed to set cookie "${name}":`, error);
}
}
function getCookie(name) {
if ("u" < typeof document) return null;
try {
const nameEQ = `${name}=`;
const cookies = document.cookie.split(';');
for (const cookie of cookies){
let c = cookie;
while(' ' === c.charAt(0))c = c.substring(1);
if (0 === c.indexOf(nameEQ)) {
const cookieValue = c.substring(nameEQ.length);
if (cookieValue.includes(':')) {
const shortened = stringToFlat(cookieValue);
const expanded = expandFlatKeys(shortened);
const nested = unflattenObject(expanded);
return nested;
}
return cookieValue;
}
}
return null;
} catch (error) {
console.warn(`Failed to get cookie "${name}":`, error);
return null;
}
}
function deleteCookie(name, options, config) {
if ("u" < typeof document) return;
const opts = {
...getDefaultCookieOptions(config),
...options
};
if (opts.crossSubdomain && !options?.domain) opts.domain = getRootDomain();
try {
const parts = [
`${name}=`,
'expires=Thu, 01 Jan 1970 00:00:00 GMT',
`path=${opts.path}`
];
if (opts.domain) parts.push(`domain=${opts.domain}`);
document.cookie = parts.join('; ');
} catch (error) {
console.warn(`Failed to delete cookie "${name}":`, error);
}
}
const defaultTranslationConfig = {
translations: {
en: enTranslations
},
defaultLanguage: 'en',
disableAutoLanguageSwitch: false
};
const consent_types_consentTypes = [
{
defaultValue: true,
description: 'These trackers are used for activities that are strictly necessary to operate or deliver the service you requested from us and, therefore, do not require you to consent.',
disabled: true,
display: true,
gdprType: 1,
name: 'necessary'
},
{
defaultValue: false,
description: 'These trackers enable basic interactions and functionalities that allow you to access selected features of our service and facilitate your communication with us.',
display: false,
gdprType: 2,
name: 'functionality'
},
{
defaultValue: false,
description: 'These trackers help us to measure traffic and analyze your behavior to improve our service.',
display: false,
gdprType: 4,
name: 'measurement'
},
{
defaultValue: false,
description: 'These trackers help us to improve the quality of your user experience and enable interactions with external content, networks, and platforms.',
display: false,
gdprType: 3,
name: 'experience'
},
{
defaultValue: false,
description: 'These trackers help us to deliver personalized ads or marketing content to you, and to measure their performance.',
display: false,
gdprType: 5,
name: 'marketing'
}
];
const allConsentNames = consent_types_consentTypes.map((consent)=>consent.name);
const STORAGE_KEY = 'privacy-consent-storage';
const initial_state_initialState = {
debug: false,
config: {
pkg: 'c15t',
version: "2.2.1",
mode: 'Unknown'
},
consents: consent_types_consentTypes.reduce((acc, consent)=>{
acc[consent.name] = consent.defaultValue;
return acc;
}, {}),
selectedConsents: consent_types_consentTypes.reduce((acc, consent)=>{
acc[consent.name] = consent.defaultValue;
return acc;
}, {}),
consentInfo: null,
branding: 'c15t',
activeUI: 'none',
isLoadingConsentInfo: false,
hasFetchedBanner: false,
lastBannerFetchData: null,
consentCategories: [
'necessary'
],
callbacks: {},
locationInfo: null,
overrides: void 0,
legalLinks: {},
translationConfig: defaultTranslationConfig,
user: void 0,
networkBlocker: void 0,
storageConfig: void 0,
includeNonDisplayedConsents: false,
consentTypes: consent_types_consentTypes,
iframeBlockerConfig: {
disableAutomaticBlocking: false
},
scripts: [],
loadedScripts: {},
scriptIdMap: {},
model: 'opt-in',
policyBanner: {},
policyDialog: {},
policyCategories: null,
policyScopeMode: null,
initDataSource: null,
initDataSourceDetail: null,
iab: null,
reloadOnConsentRevoked: true,
ssrDataUsed: false,
ssrSkippedReason: null
};
function sanitizeIdentifier(value) {
if ('string' != typeof value) return;
const normalized = value.trim();
if ('' === normalized || 'undefined' === normalized || 'null' === normalized) return;
return normalized;
}
function sanitizeSubjectIdentifiers(identifiers) {
const externalId = sanitizeIdentifier(identifiers.externalId);
const identityProvider = sanitizeIdentifier(identifiers.identityProvider);
return {
...externalId ? {
externalId
} : {},
...identityProvider ? {
identityProvider
} : {}
};
}
function sanitizeConsentInfo(consentInfo) {
if (!consentInfo) return consentInfo;
const sanitized = {
...consentInfo
};
const { externalId, identityProvider } = sanitizeSubjectIdentifiers({
externalId: sanitized.externalId,
identityProvider: sanitized.identityProvider
});
if (externalId) sanitized.externalId = externalId;
else delete sanitized.externalId;
if (identityProvider) sanitized.identityProvider = identityProvider;
else delete sanitized.identityProvider;
return sanitized;
}
function isLegacyConsentFormat(data) {
if ('object' != typeof data || null === data) return false;
const record = data;
const consentInfo = record.consentInfo;
if (!consentInfo || 'object' != typeof consentInfo) return false;
const hasLegacyId = 'string' == typeof consentInfo.id;
const hasSubjectId = 'string' == typeof consentInfo.subjectId;
return hasLegacyId && !hasSubjectId;
}
function migrateLegacyStorage(config) {
const newKey = config?.storageKey || "c15t";
const legacyKey = STORAGE_KEY;
if (newKey === legacyKey) return;
try {
if ("u" > typeof window && window.localStorage) {
const existingData = window.localStorage.getItem(newKey);
if (existingData) return void window.localStorage.removeItem(legacyKey);
const legacyData = window.localStorage.getItem(legacyKey);
if (legacyData) {
window.localStorage.setItem(newKey, legacyData);
window.localStorage.removeItem(legacyKey);
getDebugLogger().log(`Migrated consent data from "${legacyKey}" to "${newKey}"`);
}
}
} catch (error) {
console.warn('[c15t] Failed to migrate legacy storage:', error);
}
}
function saveConsentToStorage(data, options, config) {
let localStorageSuccess = false;
let cookieSuccess = false;
const storageKey = config?.storageKey || "c15t";
const existing = getConsentFromStorage(config);
const mergedData = {
...existing,
...data,
consentInfo: sanitizeConsentInfo(data.consentInfo || existing?.consentInfo ? {
...existing?.consentInfo ?? {},
...data.consentInfo ?? {}
} : void 0),
iabCustomVendorConsents: data.iabCustomVendorConsents ?? existing?.iabCustomVendorConsents,
iabCustomVendorLegitimateInterests: data.iabCustomVendorLegitimateInterests ?? existing?.iabCustomVendorLegitimateInterests
};
const cleanedData = {
...mergedData
};
if (!cleanedData.iabCustomVendorConsents || 0 === Object.keys(cleanedData.iabCustomVendorConsents).length) delete cleanedData.iabCustomVendorConsents;
if (!cleanedData.iabCustomVendorLegitimateInterests || 0 === Object.keys(cleanedData.iabCustomVendorLegitimateInterests).length) delete cleanedData.iabCustomVendorLegitimateInterests;
try {
if ("u" > typeof window && window.localStorage) {
window.localStorage.setItem(storageKey, JSON.stringify(cleanedData));
localStorageSuccess = true;
}
} catch (error) {
console.warn('Failed to save consent to localStorage:', error);
}
try {
setCookie(storageKey, cleanedData, options, config);
cookieSuccess = true;
} catch (error) {
console.warn('Failed to save consent to cookie:', error);
}
if (!localStorageSuccess && !cookieSuccess) throw new Error('Failed to save consent to any storage method');
}
function normalizeConsentData(data) {
const consents = data.consents || {};
const normalizedConsents = {
...consents
};
for (const consentName of allConsentNames)normalizedConsents[consentName] = consents[consentName] ?? false;
return {
...data,
consents: normalizedConsents
};
}
function getConsentFromStorage(config) {
migrateLegacyStorage(config);
const storageKey = config?.storageKey || "c15t";
let localStorageData = null;
let cookieData = null;
try {
if ("u" > typeof window && window.localStorage) {
const stored = window.localStorage.getItem(storageKey);
if (stored) localStorageData = JSON.parse(stored);
}
} catch (error) {
console.warn('Failed to read consent from localStorage:', error);
}
try {
cookieData = getCookie(storageKey);
} catch (error) {
console.warn('Failed to read consent from cookie:', error);
}
let chosenData = null;
let chosenSource = null;
if (cookieData) {
chosenData = cookieData;
chosenSource = 'cookie';
} else if (localStorageData) {
chosenData = localStorageData;
chosenSource = 'localStorage';
}
if (chosenData && chosenSource) {
const isCrossSubdomain = config?.crossSubdomain === true || !!config?.defaultDomain;
if ('localStorage' !== chosenSource || cookieData) {
if ('cookie' === chosenSource) try {
if ("u" > typeof window && window.localStorage) {
let normalizedCookieData = chosenData;
if ('object' == typeof normalizedCookieData && null !== normalizedCookieData && 'consents' in normalizedCookieData) normalizedCookieData = normalizeConsentData(normalizedCookieData);
let normalizedLocalStorageData = null;
try {
const stored = window.localStorage.getItem(storageKey);
if (stored) {
const parsed = JSON.parse(stored);
normalizedLocalStorageData = 'object' == typeof parsed && null !== parsed && 'consents' in parsed ? normalizeConsentData(parsed) : parsed;
}
} catch {
normalizedLocalStorageData = null;
}
const cookieJson = JSON.stringify(normalizedCookieData);
const localStorageJson = JSON.stringify(normalizedLocalStorageData);
if (cookieJson !== localStorageJson) {
window.localStorage.setItem(storageKey, cookieJson);
if (normalizedLocalStorageData) if (isCrossSubdomain) getDebugLogger().log('Updated localStorage with consent from cookie (cross-subdomain mode)');
else getDebugLogger().log('Updated localStorage with consent from cookie');
else getDebugLogger().log('Synced consent from cookie to localStorage');
}
}
} catch (error) {
console.warn('[c15t] Failed to sync consent to localStorage:', error);
}
} else try {
setCookie(storageKey, chosenData, void 0, config);
getDebugLogger().log('Synced consent from localStorage to cookie');
} catch (error) {
console.warn('[c15t] Failed to sync consent to cookie:', error);
}
}
if (chosenData && isLegacyConsentFormat(chosenData)) {
getDebugLogger().log('Detected legacy consent format (v1.x). Re-consent required for v2.0.');
deleteConsentFromStorage(void 0, config);
return null;
}
if (chosenData && 'object' == typeof chosenData) {
const normalizedData = normalizeConsentData(chosenData);
if ('object' == typeof normalizedData && null !== normalizedData && 'consentInfo' in normalizedData) {
const dataWithConsentInfo = normalizedData;
dataWithConsentInfo.consentInfo = sanitizeConsentInfo(dataWithConsentInfo.consentInfo);
return dataWithConsentInfo;
}
return normalizedData;
}
return chosenData;
}
function deleteConsentFromStorage(options, config) {
const storageKey = config?.storageKey || "c15t";
try {
if ("u" > typeof window && window.localStorage) {
window.localStorage.removeItem(storageKey);
if (storageKey !== STORAGE_KEY) window.localStorage.removeItem(STORAGE_KEY);
}
} catch (error) {
console.warn('Failed to remove consent from localStorage:', error);
}
try {
deleteCookie(storageKey, options, config);
if (storageKey !== STORAGE_KEY) deleteCookie(STORAGE_KEY, options, config);
} catch (error) {
console.warn('Failed to remove consent cookie:', error);
}
}
const API_ENDPOINTS = {
INIT: '/init',
POST_SUBJECT: '/subjects',
GET_SUBJECT: '/subjects',
PATCH_SUBJECT: '/subjects',
CHECK_CONSENT: '/consents/check',
LIST_SUBJECTS: '/subjects'
};
async function withFallback(context, endpoint, method, options, fallbackFn) {
try {
const response = await fetcher(context, endpoint, {
method,
...options
});
if (response.ok) return response;
console.warn(`API request failed, falling back to offline mode for ${endpoint}`);
return fallbackFn(options);
} catch (error) {
console.warn(`Error calling ${endpoint}, falling back to offline mode:`, error);
return fallbackFn(options);
}
}
async function offlineFallbackForIdentifyUser(options) {
const pendingSubmissionsKey = 'c15t-pending-identify-submissions';
const newSubjectId = getIdentifySubjectId(options?.body);
try {
if ("u" > typeof window && options?.body && window.localStorage) {
let pendingSubmissions = [];
try {
const storedSubmissions = window.localStorage.getItem(pendingSubmissionsKey);
if (storedSubmissions) pendingSubmissions = JSON.parse(storedSubmissions);
} catch (e) {
console.warn('Error parsing pending identify submissions:', e);
pendingSubmissions = [];
}
const newSubmission = options.body;
const isDuplicate = pendingSubmissions.some((submission)=>getIdentifySubjectId(submission) === newSubjectId && submission.externalId === newSubmission.externalId);
if (!isDuplicate) {
pendingSubmissions.push(newSubmission);
window.localStorage.setItem(pendingSubmissionsKey, JSON.stringify(pendingSubmissions));
getDebugLogger().log('Queued identify user submission for retry on next page load');
}
}
} catch (error) {
console.warn('Failed to write to localStorage in identify offline fallback:', error);
}
const response = fetcher_createResponseContext(true, null, null, null);
if (options?.onSuccess) await options.onSuccess(response);
return response;
}
async function identifyUser(context, storageConfig, options) {
const { body, ...restOptions } = options;
const subjectId = getIdentifySubjectId(body);
if (!body || !subjectId) return {
ok: false,
data: null,
response: null,
error: {
message: 'Subject ID is required to identify user',
status: 400,
code: 'MISSING_SUBJECT_ID'
}
};
const existingData = getConsentFromStorage(storageConfig);
saveConsentToStorage({
consents: existingData?.consents || {},
consentInfo: {
...existingData?.consentInfo,
time: existingData?.consentInfo?.time || Date.now(),
subjectId,
externalId: body.externalId,
identityProvider: body.identityProvider
}
}, void 0, storageConfig);
const path = `${API_ENDPOINTS.PATCH_SUBJECT}/${subjectId}`;
const { subjectId: _subjectId, id: _legacySubjectId, ...patchBody } = body;
return withFallback(context, path, 'PATCH', {
...restOptions,
body: patchBody
}, async (fallbackOptions)=>{
const fullBody = {
subjectId,
...fallbackOptions?.body
};
return offlineFallbackForIdentifyUser({
...fallbackOptions,
body: fullBody
});
});
}
function resolveNoPolicyFallback() {
return policyDefaults.offlineNoBanner();
}
function resolveFallbackPolicy(options) {
if (options.explicitPolicy) return options.explicitPolicy;
return policyDefaults.offlineOptInBanner();
}
function buildFallbackInitData(options) {
const data = {
jurisdiction: options.jurisdiction ?? 'NONE',
location: {
countryCode: options.countryCode ?? null,
regionCode: options.regionCode ?? null
},
translations: {
language: options.language ?? 'en',
translations: options.translations ?? enTranslations
},
branding: 'c15t',
gvl: options.gvl ?? null
};
if (options.policy) data.policy = options.policy;
if (options.policyDecision) data.policyDecision = options.policyDecision;
if (options.policySnapshotToken) data.policySnapshotToken = options.policySnapshotToken;
return data;
}
async function createFallbackContext(options, data) {
const response = fetcher_createResponseContext(true, data, null, null);
if (options?.onSuccess) await options.onSuccess(response);
return response;
}
async function offlineFallbackForConsentBanner(options, iabConfig) {
const fallbackPolicy = resolveFallbackPolicy({});
let gvl = null;
if (iabConfig?.enabled && 'iab' === fallbackPolicy.model) try {
const fetchGVL = iabConfig._module?.fetchGVL;
if (fetchGVL) {
const acceptLanguage = options?.headers?.['accept-language'];
gvl = await fetchGVL(iabConfig.vendorIds, acceptLanguage ? {
headers: {
'accept-language': acceptLanguage
}
} : void 0);
}
} catch (error) {
console.warn('Failed to fetch GVL in offline fallback:', error);
}
const fallbackData = buildFallbackInitData({
countryCode: options?.headers?.['x-c15t-country'] ?? null,
regionCode: options?.headers?.['x-c15t-region'] ?? null,
gvl,
policy: fallbackPolicy
});
return createFallbackContext(options, fallbackData);
}
async function hosted_init_init(context, options, iabConfig) {
try {
const response = await fetcher(context, API_ENDPOINTS.INIT, {
method: 'GET',
...options
});
if (response.ok) return response;
console.warn('API request failed, falling back to offline mode for consent banner');
return offlineFallbackForConsentBanner(options, iabConfig);
} catch (error) {
console.warn('Error fetching consent banner info, falling back to offline mode:', error);
return offlineFallbackForConsentBanner(options, iabConfig);
}
}
const PENDING_CONSENT_KEY = 'c15t-pending-consent-submissions';
const PENDING_IDENTIFY_KEY = 'c15t-pending-identify-submissions';
function checkPendingConsentSubmissions(_context, processPendingSubmissions) {
const pendingSubmissionsKey = PENDING_CONSENT_KEY;
if ("u" < typeof window || !window.localStorage) return;
try {
window.localStorage.setItem('c15t-storage-test-key', 'test');
window.localStorage.removeItem('c15t-storage-test-key');
const pendingSubmissionsStr = window.localStorage.getItem(pendingSubmissionsKey);
if (!pendingSubmissionsStr) return;
const pendingSubmissions = JSON.parse(pendingSubmissionsStr);
if (!pendingSubmissions.length) return void window.localStorage.removeItem(pendingSubmissionsKey);
getDebugLogger().log(`Found ${pendingSubmissions.length} pending consent submission(s) to retry`);
setTimeout(()=>{
processPendingSubmissions(pendingSubmissions);
}, 2000);
} catch (error) {
console.warn('Failed to check for pending consent submissions:', error);
}
}
async function processPendingConsentSubmissions(context, submissions) {
const pendingSubmissionsKey = PENDING_CONSENT_KEY;
const maxRetries = 3;
const remainingSubmissions = [
...submissions
];
for(let i = 0; i < maxRetries && remainingSubmissions.length > 0; i++){
const successfulSubmissions = [];
for(let j = 0; j < remainingSubmissions.length; j++){
const submission = remainingSubmissions[j];
if (submission) try {
const { externalId: externalSubjectId, identityProvider } = sanitizeSubjectIdentifiers({
externalId: submission.externalSubjectId,
identityProvider: submission.identityProvider
});
const sanitizedSubmission = {
...submission,
...externalSubjectId ? {
externalSubjectId
} : {},
...identityProvider ? {
identityProvider
} : {}
};
if (!externalSubjectId) delete sanitizedSubmission.externalSubjectId;
if (!identityProvider) delete sanitizedSubmission.identityProvider;
getDebugLogger().log('Retrying consent submission:', submission);
const response = await fetcher(context, API_ENDPOINTS.POST_SUBJECT, {
method: 'POST',
body: sanitizedSubmission
});
if (response.ok) {
getDebugLogger().log('Successfully resubmitted consent');
successfulSubmissions.push(j);
}
} catch (error) {
console.warn('Failed to resend consent submission:', error);
}
}
for(let k = successfulSubmissions.length - 1; k >= 0; k--){
const index = successfulSubmissions[k];
if (void 0 !== index) remainingSubmissions.splice(index, 1);
}
if (0 === remainingSubmissions.length) break;
if (i < maxRetries - 1) await delay(1000 * (i + 1));
}
try {
if ("u" > typeof window && window.localStorage) if (remainingSubmissions.length > 0) {
window.localStorage.setItem(pendingSubmissionsKey, JSON.stringify(remainingSubmissions));
getDebugLogger().log(`${remainingSubmissions.length} consent submissions still pending for future retry`);
} else {
window.localStorage.removeItem(pendingSubmissionsKey);
getDebugLogger().log('All pending consent submissions processed successfully');
}
} catch (error) {
console.warn('Error updating pending submissions storage:', error);
}
}
function checkPendingIdentifySubmissions(_context, processPendingSubmissions) {
if ("u" < typeof window || !window.localStorage) return;
try {
const pendingSubmissionsStr = window.localStorage.getItem(PENDING_IDENTIFY_KEY);
if (!pendingSubmissionsStr) return;
const pendingSubmissions = JSON.parse(pendingSubmissionsStr);
if (!pendingSubmissions.length) return void window.localStorage.removeItem(PENDING_IDENTIFY_KEY);
getDebugLogger().log(`Found ${pendingSubmissions.length} pending identify user submission(s) to retry`);
setTimeout(()=>{
processPendingSubmissions(pendingSubmissions);
}, 2500);
} catch (error) {
console.warn('Failed to check for pending identify submissions:', error);
}
}
async function processPendingIdentifySubmissions(context, submissions) {
const maxRetries = 3;
const remainingSubmissions = [
...submissions
];
for(let i = 0; i < maxRetries && remainingSubmissions.length > 0; i++){
const successfulSubmissions = [];
for(let j = 0; j < remainingSubmissions.length; j++){
const submission = remainingSubmissions[j];
if (!submission) continue;
const subjectId = getIdentifySubjectId(submission);
if (!subjectId) {
console.warn('Dropping pending identify submission without a subject ID');
successfulSubmissions.push(j);
continue;
}
if (!submission.externalId) {
console.warn('Dropping pending identify submission without an externalId');
successfulSubmissions.push(j);
continue;
}
try {
getDebugLogger().log('Retrying identify user submission:', submission);
const path = `${API_ENDPOINTS.PATCH_SUBJECT}/${subjectId}`;
const { subjectId: _subjectId, id: _legacySubjectId, ...patchBody } = submission;
const response = await fetcher(context, path, {
method: 'PATCH',
body: patchBody
});
if (response.ok) {
getDebugLogger().log('Successfully resubmitted identify user');
successfulSubmissions.push(j);
}
} catch (error) {
console.warn('Failed to resend identify user submission:', error);
}
}
for(let k = successfulSubmissions.length - 1; k >= 0; k--){
const index = successfulSubmissions[k];
if (void 0 !== index) remainingSubmissions.splice(index, 1);
}
if (0 === remainingSubmissions.length) break;
if (i < maxRetries - 1) await delay(1000 * (i + 1));
}
try {
if ("u" > typeof window && window.localStorage) if (remainingSubmissions.length > 0) {
window.localStorage.setItem(PENDING_IDENTIFY_KEY, JSON.stringify(remainingSubmissions));
getDebugLogger().log(`${remainingSubmissions.length} identify submissions still pending for future retry`);
} else {
window.localStorage.removeItem(PENDING_IDENTIFY_KEY);
getDebugLogger().log('All pending identify submissions processed successfully');
}
} catch (error) {
console.warn('Error updating pending identify submissions storage:', error);
}
}
async function offlineFallbackForSetConsent(storageConfig, options) {
const pendingSubmissionsKey = 'c15t-pending-consent-submissions';
const subjectId = options?.body?.subjectId;
try {
if ("u" > typeof window) {
saveConsentToStorage({
consents: options?.body?.preferences || {},
consentInfo: {
time: Date.now(),
subjectId,
externalId: options?.body?.externalSubjectId,
identityProvider: options?.body?.identityProvider
}
}, void 0, storageConfig);
if (options?.body && window.localStorage) {
let pendingSubmissions = [];
try {
const storedSubmissions = window.localStorage.getItem(pendingSubmissionsKey);
if (storedSubmissions) pendingSubmissions = JSON.parse(storedSubmissions);
} catch (e) {
console.warn('Error parsing pending submissions:', e);
pendingSubmissions = [];
}
const newSubmission = options.body;
const isDuplicate = pendingSubmissions.some((submission)=>JSON.stringify(submission) === JSON.stringify(newSubmission));
if (!isDuplicate) {
pendingSubmissions.push(newSubmission);
window.localStorage.setItem(pendingSubmissionsKey, JSON.stringify(pendingSubmissions));
getDebugLogger().log('Queued consent submission for retry on next page load');
}
}
}
} catch (error) {
console.warn('Failed to write to localStorage in offline fallback:', error);
}
const response = fetcher_createResponseContext(true, null, null, null);
if (options?.onSuccess) await options.onSuccess(response);
return response;
}
async function set_consent_setConsent(context, storageConfig, options) {
saveConsentToStorage({
consents: options?.body?.preferences || {},
consentInfo: {
time: Date.now(),
subjectId: options?.body?.subjectId,
externalId: options?.body?.externalSubjectId,
identityProvider: options?.body?.identityProvider
}
}, void 0, storageConfig);
const response = await withFallback(context, API_ENDPOINTS.POST_SUBJECT, 'POST', options, async (fallbackOptions)=>offlineFallbackForSetConsent(storageConfig, fallbackOptions));
return response;
}
class C15tClient {
backendURL;
storageConfig;
iabConfig;
headers;
customFetch;
corsMode;
retryConfig;
fetcherContext;
constructor(options){
this.backendURL = options.backendURL.endsWith('/') ? options.backendURL.slice(0, -1) : options.backendURL;
this.headers = {
'Content-Type': 'application/json',
...options.headers
};
this.customFetch = options.customFetch;
this.corsMode = options.corsMode || 'cors';
this.storageConfig = options.storageConfig;
this.iabConfig = options.iabConfig;
this.retryConfig = {
maxRetries: options.retryConfig?.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries ?? 3,
initialDelayMs: options.retryConfig?.initialDelayMs ?? DEFAULT_RETRY_CONFIG.initialDelayMs ?? 100,
backoffFactor: options.retryConfig?.backoffFactor ?? DEFAULT_RETRY_CONFIG.backoffFactor ?? 2,
retryableStatusCodes: options.retryConfig?.retryableStatusCodes ?? DEFAULT_RETRY_CONFIG.retryableStatusCodes,
nonRetryableStatusCodes: options.retryConfig?.nonRetryableStatusCodes ?? DEFAULT_RETRY_CONFIG.nonRetryableStatusCodes,
shouldRetry: options.retryConfig?.shouldRetry ?? DEFAULT_RETRY_CONFIG.shouldRetry,
retryOnNetworkError: options.retryConfig?.retryOnNetworkError ?? DEFAULT_RETRY_CONFIG.retryOnNetworkError
};
this.fetcherContext = {
backendURL: this.backendURL,
headers: this.headers,
customFetch: this.customFetch,
corsMode: this.corsMode,
retryConfig: this.retryConfig
};
this.checkPendingConsentSubmissions();
this.checkPendingIdentifySubmissions();
}
async init(options) {
return hosted_init_init(this.fetcherContext, options, this.iabConfig);
}
async setConsent(options) {
return set_consent_setConsent(this.fetcherContext, this.storageConfig, options);
}
async identifyUser(options) {
return identifyUser(this.fetcherContext, this.storageConfig, options);
}
async $fetch(path, options) {
return fetcher(this.fetcherContext, path, options);
}
checkPendingConsentSubmissions() {
checkPendingConsentSubmissions(this.fetcherContext, (submissions)=>this.processPendingConsentSubmissions(submissions));
}
async processPendingConsentSubmissions(submissions) {
return processPendingConsentSubmissions(this.fetcherContext, submissions);
}
checkPendingIdentifySubmissions() {
checkPendingIdentifySubmissions(this.fetcherContext, (submissions)=>this.processPendingIdentifySubmissions(submissions));
}
async processPendingIdentifySubmissions(submissions) {
return processPendingIdentifySubmissions(this.fetcherContext, submissions);
}
}
function checkJurisdiction(countryCode, regionCode) {
const jurisdictions = {
EU: new Set([
'AT',
'BE',
'BG',
'HR',
'CY',
'CZ',
'DK',
'EE',
'FI',
'FR',
'DE',
'GR',
'HU',
'IE',
'IT',
'LV',
'LT',
'LU',
'MT',
'NL',
'PL',
'PT',
'RO',
'SK',
'SI',
'ES',
'SE'
]),
EEA: new Set([
'IS',
'NO',
'LI'
]),
UK: new Set([
'GB'
]),
CH: new Set([
'CH'
]),
BR: new Set([
'BR'
]),
CA: new Set([
'CA'
]),
AU: new Set([
'AU'
]),
JP: new Set([
'JP'
]),
KR: new Set([
'KR'
]),
CA_QC_REGIONS: new Set([
'QC'
])
};
let jurisdictionCode = 'NONE';
if (countryCode) {
const normalizedCountryCode = countryCode.toUpperCase();
const normalizedRegionCode = regionCode && 'string' == typeof regionCode ? (regionCode.includes('-') ? regionCode.split('-').pop() : regionCode).toUpperCase() : null;
if ('CA' === normalizedCountryCode && normalizedRegionCode && jurisdictions.CA_QC_REGIONS.has(normalizedRegionCode)) return 'QC_LAW25';
const jurisdictionMap = [
{
sets: [
jurisdictions.EU,
jurisdictions.EEA,
jurisdictions.UK
],
code: 'GDPR'
},
{
sets: [
jurisdictions.CH
],
code: 'CH'
},
{
sets: [
jurisdictions.BR
],
code: 'BR'
},
{
sets: [
jurisdictions.CA
],
code: 'PIPEDA'
},
{
sets: [
jurisdictions.AU
],
code: 'AU'
},
{
sets: [
jurisdictions.JP
],
code: 'APPI'
},
{
sets: [
jurisdictions.KR
],
code: 'PIPA'
}
];
for (const { sets, code } of jurisdictionMap)if (sets.some((set)=>set.has(normalizedCountryCode))) {
jurisdictionCode = code;
break;
}
}
return jurisdictionCode;
}
function utils_createResponseContext(data = null) {
return createResponseContext(true, data);
}
async function handleOfflineResponse(options) {
const emptyResponse = utils_createResponseContext();
if (options?.onSuccess) await options.onSuccess(emptyResponse);
return emptyResponse;
}
const DEFAULT_PROFILE = 'default';
function normalizeLanguage(value) {
if (!value) return;
const normalized = value.split(',')[0]?.split(';')[0]?.trim().toLowerCase();
if (!normalized) return;
return normalized.split('-')[0] ?? void 0;
}
function getProfileLanguages(profiles, profile) {
return Object.keys(profiles[profile]?.translations ?? {}).sort();
}
function resolveActiveProfile(input) {
const requestedProfile = input.policyProfile ?? input.defaultProfile;
return input.profiles[requestedProfile] ? requestedProfile : input.defaultProfile;
}
function resolveProfileFallbackLanguage(input) {
const configuredFallbackLanguage = normalizeLanguage(input.profile?.fallbackLanguage) ?? 'en';
const profileLanguages = Object.keys(input.profile?.translations ?? {}).sort();
if (profileLanguages.includes(configuredFallbackLanguage)) return configuredFallbackLanguage;
if (profileLanguages.includes('en')) return 'en';
return profileLanguages[0] ?? configuredFallbackLanguage;
}
function resolveOfflinePolicyTranslations(input) {
const profiles = input.i18n.messages ?? {};
const defaultProfile = input.i18n.defaultProfile ?? DEFAULT_PROFILE;
const profile = resolveActiveProfile({
profiles,
defaultProfile,
policyProfile: input.policyI18n?.messageProfile
});
const profileLanguages = getProfileLanguages(profiles, profile);
const fallbackLanguage = resolveProfileFallbackLanguage({
profile: profiles[profile]
});
const policyLanguage = normalizeLanguage(input.policyI18n?.language);
const requestedLanguage = policyLanguage ?? selectLanguage(profileLanguages, {
header: input.acceptLanguage,
fallback: fallbackLanguage
});
const resolvedLanguage = profiles[profile]?.translations[requestedLanguage] ? requestedLanguage : fallbackLanguage;
const base = enTranslations;
const custom = profiles[profile]?.translations[resolvedLanguage];
return {
language: resolvedLanguage,
translations: custom ? deepMergeTranslations(base, custom) : base
};
}
function resolveConfiguredFallbackLanguage(translations, defaultLanguage) {
const configuredLanguages = Object.keys(translations).sort();
const normalizedDefault = defaultLanguage?.toLowerCase();
if (normalizedDefault && configuredLanguages.includes(normalizedDefault)) return normalizedDefault;
if (configuredLanguages.includes('en')) return 'en';
return configuredLanguages[0] ?? 'en';
}
async function offline_init_init(initialTranslationConfig, options, iabConfig, policyConfig) {
const country = options?.headers?.['x-c15t-country'] ?? 'GB';
const region = options?.headers?.['x-c15t-region'] ?? null;
const headerLanguage = options?.headers?.['accept-language'] ?? null;
const jurisdictionCode = checkJurisdiction(country, region);
const configuredPolicies = policyConfig?.policyPacks;
const hasExplicitPolicies = policyConfig?.policyPacks !== void 0;
const i18nValidation = validatePolicyI18nConfig({
i18n: policyConfig?.i18n,
policies: configuredPolicies
});
for (const warning of i18nValidation.warnings)console.warn(`[c15t] offlinePolicy.i18n: ${warning}`);
if (i18nValidation.errors.length > 0) throw new Error(`Invalid offlinePolicy.i18n configuration:\n${i18nValidation.errors.map((error)=>`- ${error}`).join('\n')}`);
if (configuredPolicies && configuredPolicies.length > 0) validatePolicies(configuredPolicies, {
iabEnabled: iabConfig?.enabled === true
});
const resolvedPolicyDecision = configuredPolicies && configuredPolicies.length > 0 ? await resolvePolicyDecision({
policies: configuredPolicies,
countryCode: country,
regionCode: region,
jurisdiction: jurisdictionCode,
iabEnabled: iabConfig?.enabled === true
}) : void 0;
const shouldUseSyntheticFallbackDefaults = !policyConfig?.policy && !resolvedPolicyDecision && !hasExplicitPolicies;
const resolvedPolicyConfig = {
...policyConfig,
policy: policyConfig?.policy ?? resolvedPolicyDecision?.policy ?? (hasExplicitPolicies ? resolveNoPolicyFallback() : void 0) ?? (shouldUseSyntheticFallbackDefaults ? resolveFallbackPolicy({}) : void 0),
policyDecision: policyConfig?.policyDecision ?? (resolvedPolicyDecision ? {
policyId: resolvedPolicyDecision.policy.id,
fingerprint: resolvedPolicyDecision.fingerprint,
matchedBy: resolvedPolicyDecision.matchedBy,
country,
region,
jurisdiction: jurisdictionCode
} : void 0)
};
let language;
let translationsForLanguage;
if (policyConfig?.i18n?.messages && Object.keys(policyConfig.i18n.messages).length > 0) {
const resolvedTranslations = resolveOfflinePolicyTranslations({
acceptLanguage: headerLanguage,
i18n: policyConfig.i18n,
policyI18n: resolvedPolicyConfig.policy?.i18n
});
language = resolvedTranslations.language;
translationsForLanguage = resolvedTranslations.translations;
} else if (initialTranslationConfig?.translations && Object.keys(initialTranslationConfig.translations).length > 0) {
const customTranslations = initialTranslationConfig.translations;
const availableLanguages = Object.keys(customTranslations);
const fallbackLanguage = resolveConfiguredFallbackLanguage(customTranslations, initialTranslationConfig.defaultLanguage);
language = selectLanguage(availableLanguages, {
header: headerLanguage,
fallback: fallbackLanguage
});
const base = enTranslations;
const customForLanguage = customTranslations[language] ?? {};
translationsForLanguage = deepMergeTranslations(base, customForLanguage);
} else {
const availableLanguages = Object.keys(defaultTranslationConfig.translations);
const fallbackLanguage = defaultTranslationConfig.defaultLanguage ?? 'en';
language = selectLanguage(availableLanguages, {
header: headerLanguage,
fallback: fallbackLanguage
});
translationsForLanguage = defaultTranslationConfig.translations[language];
}
let gvl = null;
if (iabConfig?.enabled && resolvedPolicyConfig.policy?.model === 'iab') if (iabConfig.gvl) gvl = iabConfig.gvl;
else try {
const fetchGVL = iabConfig._module?.fetchGVL;
if (fetchGVL) gvl = await fetchGVL(iabConfig.vendorIds, {
headers: {
'accept-language': language
}
});
} catch (error) {
console.warn('Failed to fetch GVL in offline mode:', error);
}
const responseData = buildFallbackInitData({
jurisdiction: jurisdictionCode,
countryCode: country,
regionCode: region,
language,
translations: translationsForLanguage,
gvl,
policy: resolvedPolicyConfig.policy,
policyDecision: resolvedPolicyConfig.policyDecision,
policySnapshotToken: resolvedPolicyConfig.policySnapshotToken
});
const response = utils_createResponseContext(responseData);
if (options?.onSuccess) await options.onSuccess(response);
return response;
}
async function offline_set_consent_setConsent(storageConfig, options) {
const subjectId = options?.body?.subjectId;
try {
if ("u" > typeof window) saveConsentToStorage({
consentInfo: {
time: Date.now(),
subjectId,
externalId: options?.body?.externalSubjectId,
identityProvider: options?.body?.identityProvider
},
consents: options?.body?.preferences || {}
}, void 0, storageConfig);
} catch (error) {
console.warn('Failed to write to storage:', error);
}
return await handleOfflineResponse(options);
}
class OfflineClient {
storageConfig;
initialTranslationConfig;
iabConfig;
policyConfig;
constructor(storageConfig, initialTranslationConfig, iabConfig, policyConfig){
this.storageConfig = storageConfig;
this.initialTranslationConfig = initialTranslationConfig;
this.iabConfig = iabConfig;
this.policyConfig = policyConfig;
}
async init(options) {
return offline_init_init(this.initialTranslationConfig, options, this.iabConfig, this.policyConfig);
}
async setConsent(options) {
return offline_set_consent_setConsent(this.storageConfig, options);
}
async identifyUser(options) {
console.warn('identifyUser called in offline mode - external ID will not be linked');
return handleOfflineResponse(options);
}
async $fetch(_path, options) {
return await handleOfflineResponse(options);
}
}
const DEFAULT_BACKEND_URL = '/api/c15t';
const DEFAULT_CLIENT_MODE = 'hosted';
let hasWarnedAboutLegacyC15tMode = false;
function normalizeClientMode(mode) {
if ('c15t' === mode) {
const nodeEnv = "u" > typeof globalThis && 'process' in globalThis ? globalThis.process?.env?.NODE_ENV : void 0;
if (!hasWarnedAboutLegacyC15tMode && 'production' !== nodeEnv) {
hasWarnedAboutLegacyC15tMode = true;
console.warn("[c15t] `mode: 'c15t'` is deprecated and will be removed in a future major release. Use `mode: 'hosted'` instead.");
}
return DEFAULT_CLIENT_MODE;
}
if ('offline' === mode || 'custom' === mode) return mode;
return DEFAULT_CLIENT_MODE;
}
function assertUnreachableMode(mode) {
throw new Error(`Unsupported client mode: ${String(mode)}`);
}
function resolveOfflinePolicyOption(options) {
if (void 0 !== options.offlinePolicy) return options.offlinePolicy;
return options.store?.offlinePolicy;
}
const clientRegistry = new Map();
function serializeStorageConfig(storageConfig) {
if (!storageConfig) return '';
const sorted = Object.keys(storageConfig).sort().map((key)=>{
const value = storageConfig[key];
if (null == value) return `${key}:null`;
return `${key}:${String(value)}`;
}).join('|');
return sorted;
}
function getClientCacheKey(options) {
const normalizedMode = normalizeClientMode(options.mode);
const resolvedOfflinePolicy = resolveOfflinePolicyOption(options);
const storageConfigPart = serializeStorageConfig(options.storageConfig);
const storageKey = storageConfigPart ? `:storage:${storageConfigPart}` : '';
if ('offline' === normalizedMode) {
const initialTranslations = options.store?.initialTranslationConfig?.translations;
const initialDefaultLanguage = options.store?.initialTranslationConfig?.defaultLanguage;
let translationPart = '';
translationPart = initialTranslations ? `:translations:${Object.keys(initialTranslations).sort().join(',')}` : '';
let defaultLanguagePart = '';
defaultLanguagePart = initialDefaultLanguage ? `:default-language:${initialDefaultLanguage}` : '';
const iabConfig = options.store?.iab;
let iabPart = '';
iabPart = iabConfig?.enabled ? ':iab:enabled' : '';
let offlinePolicyPart = '';
if (resolvedOfflinePolicy) offlinePolicyPart = `:policy:${createDeterministicFingerprintSync(resolvedOfflinePolicy)}`;
return `offline${storageKey}${translationPart}${defaultLanguagePart}${iabPart}${offlinePolicyPart}`;
}
if ('custom' === normalizedMode) {
const handlers = 'endpointHandlers' in options ? options.endpointHandlers : void 0;
const handlerKeys = Object.keys(handlers || {}).sort().join(',');
return `custom:${handlerKeys}${storageKey}`;
}
let headersPart = '';
if ('headers' in options && options.headers) {
const headerKeys = Object.keys(options.headers).sort();
headersPart = `:headers:${headerKeys.map((k)=>`${k}=${options.headers?.[k]}`).join(',')}`;
}
return `hosted:${options.backendURL || ''}${headersPart}${storageKey}`;
}
function configureConsentManager(options) {
const cacheKey = getClientCacheKey(options);
if (clientRegistry.has(cacheKey)) {
if ('offline' !== options.mode && 'custom' !== options.mode && 'headers' in options && options.headers) {
const existingClient = clientRegistry.get(cacheKey);
if (existingClient instanceof C15tClient) existingClient.headers = {
'Content-Type': 'application/json',
...options.headers
};
}
const existingClient = clientRegistry.get(cacheKey);
if (existingClient) return new Proxy(existingClient, {
get (target, prop) {
return target[prop];
}
});
}
const mode = normalizeClientMode(options.mode);
let client;
switch(mode){
case 'custom':
{
const customOptions = options;
client = new CustomClient({
endpointHandlers: customOptions.endpointHandlers
});
break;
}
case 'offline':
{
const iabConfig = options.store?.iab;
const policyConfig = resolveOfflinePolicyOption(options);
client = new OfflineClient(options.storageConfig, options.store?.initialTranslationConfig, iabConfig ? {
enabled: iabConfig.enabled,
vendorIds: iabConfig.vendors,
gvl: iabConfig.gvl,
_module: iabConfig._module
} : void 0, policyConfig);
break;
}
case 'hosted':
{
const hostedOptions = options;
const iabConfig = options.store?.iab;
client = new C15tClient({
backendURL: hostedOptions.backendURL || DEFAULT_BACKEND_URL,
headers: hostedOptions.headers,
customFetch: hostedOptions.customFetch,
retryConfig: hostedOptions.retryConfig,
storageConfig: options.storageConfig,
iabConfig: iabConfig ? {
enabled: iabConfig.enabled,
vendorIds: iabConfig.vendors,
gvl: iabConfig.gvl,
_module: iabConfig._module
} : void 0
});
break;
}
default:
client = assertUnreachableMode(mode);
break;
}
clientRegistry.set(cacheKey, client);
return client;
}
function clearClientRegistry() {
clientRegistry.clear();
}
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
function base58Encode(bytes) {
const base = BigInt(58);
let num = BigInt(0);
for (const byte of bytes)num = num * BigInt(256) + BigInt(byte);
const chars = [];
while(num > 0){
const remainder = num % base;
chars.unshift(BASE58_ALPHABET.charAt(Number(remainder)));
num /= base;
}
for (const byte of bytes)if (0 === byte) chars.unshift(BASE58_ALPHABET.charAt(0));
else break;
return chars.join('') || BASE58_ALPHABET.charAt(0);
}
const EPOCH_TIMESTAMP = 1700000000000;
function generateSubjectId() {
const buf = crypto.getRandomValues(new Uint8Array(20));
const t = Date.now() - EPOCH_TIMESTAMP;
const high = Math.floor(t / 0x100000000);
const low = t >>> 0;
buf[0] = high >>> 24 & 255;
buf[1] = high >>> 16 & 255;
buf[2] = high >>> 8 & 255;
buf[3] = 255 & high;
buf[4] = low >>> 24 & 255;
buf[5] = low >>> 16 & 255;
buf[6] = low >>> 8 & 255;
buf[7] = 255 & low;
return `sub_${base58Encode(buf)}`;
}
function isValidSubjectId(id) {
if (!id.startsWith('sub_')) return false;
const encoded = id.slice(4);
if (0 === encoded.length) return false;
for (const char of encoded)if (!BASE58_ALPHABET.includes(char)) return false;
return true;
}
function isConsentCategory(value) {
return allConsentNames.includes(value);
}
function flattenLayout(layout) {
if (!layout) return [];
return layout.flatMap((group)=>Array.isArray(group) ? group : [
group
]);
}
function applyPolicyPurposeAllowlist(preferences, allowedPurposeIds) {
if (!allowedPurposeIds || 0 === allowedPurposeIds.length || allowedPurposeIds.includes('*')) return preferences;
const allowed = new Set([
'necessary',
...allowedPurposeIds
]);
const next = {};
for (const [key, value] of Object.entries(preferences))next[key] = allowed.has(key) ? value : false;
return next;
}
function stripDisallowedPreferenceKeys(preferences, allowedPurposeIds) {
if (!allowedPurposeIds || 0 === allowedPurposeIds.length || allowedPurposeIds.includes('*')) return preferences;
const allowed = new Set([
'necessary',
...allowedPurposeIds
]);
const next = {};
for (const [key, value] of Object.entries(preferences))if (allowed.has(key)) next[key] = value;
return next;
}
function filterConsentCategoriesByPolicy(categories, allowedPurposeIds) {
const uniqueCategories = Array.from(new Set(categories));
if (!allowedPurposeIds || 0 === allowedPurposeIds.length || allowedPurposeIds.includes('*')) return uniqueCategories;
const allowedCategories = new Set([
'necessary',
...allowedPurposeIds.filter(isConsentCategory)
]);
const filtered = uniqueCategories.filter((category)=>allowedCategories.has(category));
if (!filtered.includes('necessary')) filtered.unshift('necessary');
return filtered;
}
function shouldEnforcePolicyCategoryScope(allowedPurposeIds, scopeMode = 'permissive') {
return 'strict' === scopeMode && Array.isArray(allowedPurposeIds) && allowedPurposeIds.length > 0 && !allowedPurposeIds.includes('*');
}
function applyPolicyScopeForRuntimeGating(consents, _allowedPurposeIds, _scopeMode = 'permissive') {
return consents;
}
function getEffectivePolicy(initData) {
return initData?.policy;
}
function validateUIAgainstPolicy(params) {
const { policy, state } = params;
if (!policy) return [];
const issues = [];
if (policy.ui?.mode && state.mode !== policy.ui.mode) issues.push({
code: 'mode_mismatch',
message: `UI mode "${state.mode}" does not match policy mode "${policy.ui.mode}".`
});
const surfacePolicy = 'banner' === state.mode ? policy.ui?.banner : 'dialog' === state.mode ? policy.ui?.dialog : void 0;
const allowedActions = surfacePolicy?.allowedActions;
if (allowedActions && allowedActions.length > 0) {
const disallowed = state.actions.filter((action)=>!allowedActions.includes(action));
if (disallowed.length > 0) issues.push({
code: 'action_not_allowed',
message: `UI renders actions not allowed by policy: ${disallowed.join(', ')}`
});
}
const expectedLayout = surfacePolicy?.layout;
if (expectedLayout && expectedLayout.length > 0) {
const expected = flattenLayout(expectedLayout);
const actual = state.actions.filter((action)=>expected.includes(action));
if (expected.join('|') !== actual.join('|')) issues.push({
code: 'group_layout_mismatch',
message: `UI action order "${actual.join(', ')}" does not match policy layout "${expected.join(', ')}".`
});
}
if (surfacePolicy?.direction && state.direction) {
if (surfacePolicy.direction !== state.direction) issues.push({
code: 'direction_mismatch',
message: `UI action direction "${state.direction}" does not match policy action direction "${surfacePolicy.direction}".`
});
}
if (surfacePolicy?.uiProfile && state.uiProfile) {
if (surfacePolicy.uiProfile !== state.uiProfile) issues.push({
code: 'ui_profile_mismatch',
message: `UI profile "${state.uiProfile}" does not match policy UI profile "${surfacePolicy.uiProfile}".`
});
}
if ('boolean' == typeof surfacePolicy?.scrollLock && 'boolean' == typeof state.scrollLock && surfacePolicy.scrollLock !== state.scrollLock) issues.push({
code: 'scroll_lock_mismatch',
message: `UI scroll lock "${state.scrollLock ? 'on' : 'off'}" does not match policy scroll lock "${surfacePolicy.scrollLock ? 'on' : 'off'}".`
});
return issues;
}
function validateNonEmptyConditions(conditions, conditionType) {
if (0 === conditions.length) throw new TypeError(`${conditionType} condition cannot be empty`);
}
function evaluateCategoryCondition(category, consents) {
if (!(category in consents)) throw new Error(`Consent category "${category}" not found in consent state`);
return consents[category] || false;
}
function evaluateAndCondition(andCondition, consents) {
const andConditions = Array.isArray(andCondition) ? andCondition : [
andCondition
];
validateNonEmptyConditions(andConditions, 'AND');
return andConditions.every((subCondition)=>evaluateConditionRecursive(subCondition, consents));
}
function evaluateOrCondition(orCondition, consents) {
const orConditions = Array.isArray(orCondition) ? orCondition : [
orCondition
];
validateNonEmptyConditions(orConditions, 'OR');
return orConditions.some((subCondition)=>evaluateConditionRecursive(subCondition, consents));
}
function evaluateConditionRecursive(condition, consents) {
if ('string' == typeof condition) return evaluateCategoryCondition(condition, consents);
if ('object' == typeof condition && null !== condition) {
if ('and' in condition) return evaluateAndCondition(condition.and, consents);
if ('or' in condition) return evaluateOrCondition(condition.or, consents);
if ('not' in condition) return !evaluateConditionRecursive(condition.not, consents);
}
throw new TypeError(`Invalid condition structure: ${JSON.stringify(condition)}`);
}
function has(condition, consents, options) {
const runtimeConsents = options ? applyPolicyScopeForRuntimeGating(consents, options.policyCategories, options.policyScopeMode) : consents;
return evaluateConditionRecursive(condition, runtimeConsents);
}
function extractConsentNamesFromCondition(condition) {
const categories = new Set();
function recurse(cond) {
if ('string' == typeof cond) return void categories.add(cond);
if ('object' == typeof cond && null !== cond) {
if ('and' in cond) {
const conditions = Array.isArray(cond.and) ? cond.and : [
cond.and
];
conditions.forEach(recurse);
} else if ('or' in cond) {
const conditions = Array.isArray(cond.or) ? cond.or : [
cond.or
];
conditions.forEach(recurse);
} else if ('not' in cond) recurse(cond.not);
}
}
recurse(condition);
return Array.from(categories);
}
function createDefaultConsentState() {
return {
experience: false,
functionality: false,
marketing: false,
measurement: false,
necessary: true
};
}
function determineRequiredConsent(iframe) {
const categoryAttr = iframe.getAttribute('data-category');
if (!categoryAttr) return;
if (!allConsentNames.includes(categoryAttr)) throw new Error(`Invalid category attribute "${categoryAttr}" on iframe. Must be one of: ${allConsentNames.join(', ')}`);
return categoryAttr;
}
function processIframeElement(iframe, consents) {
const dataSrc = iframe.getAttribute('data-src');
const requiredConsent = determineRequiredConsent(iframe);
if (!requiredConsent) return;
const hasConsent = has(requiredConsent, consents);
if (hasConsent) {
if (dataSrc && !iframe.src) {
iframe.src = dataSrc;
iframe.removeAttribute('data-src');
}
} else if (iframe.src) iframe.removeAttribute('src');
}
function createIframeBlocker(config = {}, initialConsents) {
const blockerConfig = {
disableAutomaticBlocking: false,
...config
};
let consents = initialConsents || createDefaultConsentState();
function processIframes() {
const iframes = document.querySelectorAll('iframe');
iframes.forEach((iframe)=>{
processIframeElement(iframe, consents);
});
}
function setupMutationObserver() {
const observer = new MutationObserver((mutations)=>{
mutations.forEach((mutation)=>{
mutation.addedNodes.forEach((node)=>{
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node;
if (element.tagName && 'IFRAME' === element.tagName.toUpperCase()) processIframeElement(element, consents);
const iframes = element.querySelectorAll?.('iframe');
if (iframes) iframes.forEach((iframe)=>{
processIframeElement(iframe, consents);
});
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
return observer;
}
let mutationObserver = null;
if (!blockerConfig.disableAutomaticBlocking) {
processIframes();
mutationObserver = setupMutationObserver();
}
return {
updateConsents: (newConsents)=>{
consents = {
...consents,
...newConsents
};
processIframes();
},
processIframes,
destroy: ()=>{
if (mutationObserver) {
mutationObserver.disconnect();
mutationObserver = null;
}
}
};
}
function getIframeConsentCategories() {
if ("u" < typeof document) return [];
const iframes = document.querySelectorAll('iframe[data-category]');
const categories = new Set();
if (!iframes) return [];
iframes.forEach((iframe)=>{
const categoryAttr = iframe.getAttribute('data-category');
if (!categoryAttr) return;
const category = categoryAttr.trim();
if (allConsentNames.includes(category)) categories.add(category);
});
return Array.from(categories);
}
function processAllIframes(consents) {
if ("u" < typeof document) return;
const iframes = document.querySelectorAll('iframe');
if (!iframes) return;
iframes.forEach((iframe)=>{
processIframeElement(iframe, consents);
});
}
function setupIframeObserver(getConsents, onCategoriesDiscovered) {
const observer = new MutationObserver((mutations)=>{
const currentConsents = getConsents();
let hasNewCategories = false;
mutations.forEach((mutation)=>{
mutation.addedNodes.forEach((node)=>{
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node;
if (element.tagName && 'IFRAME' === element.tagName.toUpperCase()) {
processIframeElement(element, currentConsents);
if (element.hasAttribute('data-category')) hasNewCategories = true;
}
const iframes = element.querySelectorAll?.('iframe');
if (iframes && iframes.length > 0) iframes.forEach((iframe)=>{
processIframeElement(iframe, currentConsents);
if (iframe.hasAttribute('data-category')) hasNewCategories = true;
});
}
});
});
if (hasNewCategories && onCategoriesDiscovered) {
const categories = getIframeConsentCategories();
if (categories.length > 0) onCategoriesDiscovered(categories);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
return observer;
}
const DEFAULT_POLICY_ACTIONS = [
'reject',
'accept',
'customize'
];
function dedupeActions(actions) {
if (!actions || 0 === actions.length) return [];
return [
...new Set(actions)
];
}
function resolvePolicyAllowedActions(params) {
const allowed = dedupeActions(params.allowedActions);
return allowed.length > 0 ? allowed : [
...DEFAULT_POLICY_ACTIONS
];
}
function flattenPolicyActionGroups(layout) {
if (!layout || 0 === layout.length) return [];
return layout.flatMap((group)=>Array.isArray(group) ? group : [
group
]);
}
function resolvePolicyActionGroups(params) {
const allowedActions = dedupeActions(params.allowedActions);
if (0 === allowedActions.length) return [];
if (!params.layout || 0 === params.layout.length) return [
[
...allowedActions
]
];
const allowedSet = new Set(allowedActions);
const groups = [];
const seen = new Set();
for (const group of params.layout){
const actions = dedupeActions(Array.isArray(group) ? group : [
group
]).filter((action)=>{
if (!allowedSet.has(action) || seen.has(action)) return false;
seen.add(action);
return true;
});
if (actions.length > 0) groups.push(actions);
}
return groups.length > 0 ? groups : [
[
...allowedActions
]
];
}
function resolvePolicyOrderedActions(params) {
return flattenPolicyActionGroups(resolvePolicyActionGroups({
allowedActions: params.allowedActions,
layout: params.layout
}));
}
function resolvePolicyPrimaryActions(params) {
const defaultPrimary = params.orderedActions.includes('customize') ? [
'customize'
] : [];
if (!params.primaryActions || 0 === params.primaryActions.length) return defaultPrimary;
const filtered = params.primaryActions.filter((action)=>params.orderedActions.includes(action));
return filtered.length > 0 ? filtered : defaultPrimary;
}
function resolvePolicyDirection(direction) {
if ('column' === direction) return 'column';
return 'row';
}
function resolvePolicyUiProfile(profile) {
if ('balanced' === profile || 'compact' === profile || 'strict' === profile) return profile;
return 'compact';
}
function shouldFillPolicyActions(params) {
const effectiveUiProfile = resolvePolicyUiProfile(params.uiProfile);
const actionCount = new Set(params.actionGroups.flat()).size;
const isSplitLayout = params.actionGroups.length > 1;
const isColumn = 'column' === params.direction;
return 'strict' === effectiveUiProfile || 'balanced' === effectiveUiProfile && (actionCount <= 2 || 3 === actionCount && (isSplitLayout || isColumn));
}
function hasPolicyHints(surface) {
if (!surface) return false;
return Object.values(surface).some((value)=>{
if (Array.isArray(value)) return value.length > 0;
return void 0 !== value;
});
}
function global_privacy_control_hasGlobalPrivacyControlSignal() {
if ("u" < typeof window) return false;
try {
const navigatorWithGPC = window.navigator;
const value = navigatorWithGPC.globalPrivacyControl;
return true === value || '1' === value;
} catch {
return false;
}
}
const request_context_ABSOLUTE_URL_REGEX = /^https?:\/\//;
function trimTrailingSlash(value) {
if ('/' === value) return value;
return value.endsWith('/') ? value.slice(0, -1) : value;
}
function normalizeAbsoluteURL(url) {
const normalized = url.toString();
return normalized.endsWith('/') ? normalized.slice(0, -1) : normalized;
}
function request_context_buildRequestContextHeaders(overrides) {
const headers = {};
if (overrides?.country) headers['x-c15t-country'] = overrides.country;
if (overrides?.region) headers['x-c15t-region'] = overrides.region;
if (overrides?.language) headers['accept-language'] = overrides.language;
return headers;
}
function canonicalizeBrowserBackendURL(backendURL) {
try {
const normalizedBackendURL = trimTrailingSlash(backendURL);
if (request_context_ABSOLUTE_URL_REGEX.test(normalizedBackendURL)) return normalizeAbsoluteURL(new URL(normalizedBackendURL));
if (!normalizedBackendURL.startsWith('/')) return;
return normalizeAbsoluteURL(new URL(normalizedBackendURL, window.location.origin));
} catch {
return;
}
}
function createRuntimeRequestContextMatcher(options) {
const normalizedBackendURL = canonicalizeBrowserBackendURL(options.backendURL);
if (!normalizedBackendURL) return;
const detectedGpc = global_privacy_control_hasGlobalPrivacyControlSignal();
return {
backendURL: normalizedBackendURL,
country: options.overrides?.country,
region: options.overrides?.region,
language: options.overrides?.language,
gpc: options.overrides?.gpc ?? ('boolean' == typeof detectedGpc ? detectedGpc : false),
credentials: options.credentials ?? 'include'
};
}
function matchesStoredRequestContext(stored, matcher) {
if (stored.backendURL !== matcher.backendURL) return false;
if (stored.gpc !== matcher.gpc) return false;
if (void 0 !== matcher.country && stored.country !== matcher.country) return false;
if (void 0 !== matcher.region && stored.region !== matcher.region) return false;
if (void 0 !== matcher.language && stored.language !== matcher.language) return false;
if (void 0 !== stored.credentials && stored.credentials !== matcher.credentials) return false;
return true;
}
const WINDOW_PROMISES_KEY = '__c15tInitialDataPromises';
function getBrowserWindow() {
if ("u" < typeof window) return;
return window;
}
function getMatchingPrefetchEntry(options) {
const browserWindow = getBrowserWindow();
if (!browserWindow) return;
const matcher = createRuntimeRequestContextMatcher({
backendURL: options.backendURL,
overrides: options.overrides,
credentials: options.credentials
});
if (!matcher) return;
const entries = Object.values(browserWindow[WINDOW_PROMISES_KEY] ?? {});
const matches = entries.filter((entry)=>{
const requestContext = entry.requestContext;
return requestContext ? matchesStoredRequestContext(requestContext, matcher) : false;
});
return 1 === matches.length ? matches[0] : void 0;
}
function getMatchingPrefetchedInitialData(options) {
return getMatchingPrefetchEntry(options)?.promise;
}
function buildPrefetchScript(options) {
const payload = {
backendURL: options.backendURL,
credentials: options.credentials ?? 'include',
headers: {
...headers_C15T_VERSION_HEADERS,
...request_context_buildRequestContextHeaders(options.overrides)
},
requestContext: {
country: options.overrides?.country ?? null,
region: options.overrides?.region ?? null,
language: options.overrides?.language ?? null
}
};
const json = JSON.stringify(payload).replace(/</g, '\\u003c');
return `(() => {
const mapKey = '${WINDOW_PROMISES_KEY}';
if (typeof window === 'undefined') {
return;
}
const payload = ${json};
const trimTrailingSlash = (value) => {
if (value === '/') {
return value;
}
return value.endsWith('/') ? value.slice(0, -1) : value;
};
const canonicalizeBackendURL = (backendURL) => {
try {
const normalizedBackendURL = trimTrailingSlash(backendURL);
if (/^https?:\\/\\//.test(normalizedBackendURL)) {
return trimTrailingSlash(new URL(normalizedBackendURL).toString());
}
if (!normalizedBackendURL.startsWith('/')) {
return undefined;
}
return trimTrailingSlash(
new URL(normalizedBackendURL, window.location.origin).toString()
);
} catch {
return undefined;
}
};
const buildCacheKey = (url, credentials, headers, gpc) => {
const sortedHeaders = Object.entries(headers)
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
.map(([key, value]) => key + ':' + value)
.join('|');
return url + '|' + credentials + '|gpc:' + String(gpc) + '|' + sortedHeaders;
};
const detectGpc = () => {
try {
const value = window.navigator.globalPrivacyControl;
return value === true || value === '1';
} catch {
return false;
}
};
const backendURL = canonicalizeBackendURL(payload.backendURL);
if (!backendURL) {
return;
}
const gpc = detectGpc();
const requestContext = {
backendURL,
country: payload.requestContext.country,
region: payload.requestContext.region,
language: payload.requestContext.language,
gpc,
credentials: payload.credentials
};
const url = backendURL + '/init';
const cacheKey = buildCacheKey(url, payload.credentials, payload.headers, gpc);
const promises = (window[mapKey] = window[mapKey] || {});
if (promises[cacheKey]) {
return;
}
const promise = fetch(url, {
method: 'GET',
credentials: payload.credentials,
headers: payload.headers
})
.then((response) => (response.ok ? response.json() : undefined))
.then((init) => (init
? {
init,
gvl: init.gvl,
metadata: {
requestContext
}
}
: undefined))
.catch(() => undefined);
promises[cacheKey] = {
promise,
requestContext
};
})();`;
}
const REGISTRY_KEY = '__c15tScriptDebugListeners';
let fallbackListeners = null;
function getListeners() {
if ("u" < typeof window) {
if (!fallbackListeners) fallbackListeners = new Set();
return fallbackListeners;
}
const host = window;
const existing = host[REGISTRY_KEY];
if (existing) return existing;
const listeners = new Set();
host[REGISTRY_KEY] = listeners;
return listeners;
}
function emitScriptDebugEvent(event) {
const fullEvent = {
...event,
timestamp: Date.now()
};
for (const listener of getListeners())try {
listener(fullEvent);
} catch (error) {
console.error("Failed to handle c15t script debug event listener", error);
}
return fullEvent;
}
function subscribeToScriptDebugEvents(listener) {
const listeners = getListeners();
listeners.add(listener);
return ()=>{
listeners.delete(listener);
};
}
function generateRandomScriptId() {
if ("u" > typeof crypto && crypto.randomUUID) return crypto.randomUUID().replace(/-/g, '').substring(0, 8);
if ("u" > typeof crypto && crypto.getRandomValues) {
const array = new Uint8Array(4);
crypto.getRandomValues(array);
return Array.from(array, (byte)=>byte.toString(36)).join('').padEnd(8, '0').substring(0, 8);
}
const randomStr = Math.random().toString(36).substring(2);
return randomStr.padEnd(8, '0').substring(0, 8);
}
function getScriptElementId(scriptId, anonymizeId, scriptIdMap) {
if (anonymizeId) {
if (scriptIdMap[scriptId]) return scriptIdMap[scriptId];
scriptIdMap[scriptId] = generateRandomScriptId();
return scriptIdMap[scriptId];
}
return `c15t-script-${scriptId}`;
}
const loadedScripts = new Map();
function hasLoadedScript(src) {
return loadedScripts.has(src);
}
function getLoadedScript(src) {
return loadedScripts.get(src);
}
function setLoadedScript(src, element) {
loadedScripts.set(src, element);
}
function deleteLoadedScript(src) {
loadedScripts.delete(src);
}
function utils_getLoadedScriptsSnapshot() {
return loadedScripts;
}
function hasIABConsent(script, iabConsent) {
if (void 0 !== script.vendorId) {
const vendorKey = String(script.vendorId);
if (!iabConsent.vendorConsents[vendorKey]) return false;
}
if (script.iabPurposes && script.iabPurposes.length > 0) {
const hasAllPurposeConsents = script.iabPurposes.every((purposeId)=>true === iabConsent.purposeConsents[purposeId]);
if (!hasAllPurposeConsents) return false;
}
if (script.iabLegIntPurposes && script.iabLegIntPurposes.length > 0) {
const hasAllLegIntConsents = script.iabLegIntPurposes.every((purposeId)=>true === iabConsent.purposeLegitimateInterests[purposeId]);
if (!hasAllLegIntConsents) return false;
}
if (script.iabSpecialFeatures && script.iabSpecialFeatures.length > 0) {
const hasAllSpecialFeatures = script.iabSpecialFeatures.every((featureId)=>true === iabConsent.specialFeatureOptIns[featureId]);
if (!hasAllSpecialFeatures) return false;
}
return true;
}
function scriptHasConsent(script, consents, options) {
if (options?.model === 'iab' && options.iabConsent && (void 0 !== script.vendorId || script.iabPurposes || script.iabLegIntPurposes || script.iabSpecialFeatures)) return hasIABConsent(script, options.iabConsent);
return has(script.category, consents);
}
function emitLifecycleEvent(script, action, message, info, data) {
emitScriptDebugEvent({
source: "script-loader",
scope: 'lifecycle',
action,
message,
scriptId: script.id,
elementId: info?.elementId,
hasConsent: info?.hasConsent,
callback: getCallbackFromAction(action, data),
data
});
}
function getCallbackFromAction(action, data) {
if ('callback_start' === action || 'callback_complete' === action || 'callback_error' === action) return data?.callback;
}
function getErrorMessage(error) {
if (error instanceof Error) return error.message;
if ('string' == typeof error) return error;
return 'Unknown error';
}
function invokeScriptCallback(script, callbackName, callback, info) {
if (!callback) return;
emitLifecycleEvent(script, 'callback_start', `${callbackName} started`, info, {
callback: callbackName
});
try {
callback(info);
emitLifecycleEvent(script, 'callback_complete', `${callbackName} completed`, info, {
callback: callbackName
});
} catch (error) {
emitLifecycleEvent(script, 'callback_error', `${callbackName} failed`, info, {
callback: callbackName,
error: getErrorMessage(error)
});
throw error;
}
}
function loadScripts(scripts, consents, scriptIdMap = {}, options) {
const loadedScriptIds = [];
scripts.forEach((script)=>{
const hasConsent = scriptHasConsent(script, consents, options);
if (!script.alwaysLoad && !hasConsent) return void emitLifecycleEvent(script, 'skipped', 'Script skipped due to missing consent', {
hasConsent
}, {
reason: 'missing_consent'
});
if (hasLoadedScript(script.id)) {
const callbackInfo = {
id: script.id,
elementId: getScriptElementId(script.id, false !== script.anonymizeId, scriptIdMap),
hasConsent,
consents
};
emitLifecycleEvent(script, 'already_loaded', 'Script already loaded; running consent sync', callbackInfo);
invokeScriptCallback(script, 'onConsentChange', script.onConsentChange, callbackInfo);
return;
}
if (script.src && script.textContent) throw new Error(`Script '${script.id}' cannot have both 'src' and 'textContent'. Choose one.`);
if (!script.src && !script.textContent && !script.callbackOnly) throw new Error(`Script '${script.id}' must have either 'src', 'textContent', or 'callbackOnly' set to true.`);
if (true === script.callbackOnly) {
const shouldAnonymize = false !== script.anonymizeId;
const elementId = getScriptElementId(script.id, shouldAnonymize, scriptIdMap);
const callbackInfo = {
id: script.id,
elementId,
consents,
hasConsent
};
invokeScriptCallback(script, 'onBeforeLoad', script.onBeforeLoad, callbackInfo);
invokeScriptCallback(script, 'onLoad', script.onLoad, callbackInfo);
setLoadedScript(script.id, null);
loadedScriptIds.push(script.id);
emitLifecycleEvent(script, 'loaded', "Callback-only script marked as loaded", callbackInfo, {
callbackOnly: true
});
return;
}
const shouldAnonymize = false !== script.anonymizeId;
const elementId = getScriptElementId(script.id, shouldAnonymize, scriptIdMap);
if (true === script.persistAfterConsentRevoked) {
const existingElement = document.getElementById(elementId);
if (existingElement) {
const callbackInfo = {
id: script.id,
hasConsent,
elementId,
consents,
element: existingElement
};
emitLifecycleEvent(script, 'already_loaded', "Persisted script element already exists; reusing it", callbackInfo, {
reason: 'persisted_element'
});
invokeScriptCallback(script, 'onConsentChange', script.onConsentChange, callbackInfo);
invokeScriptCallback(script, 'onLoad', script.onLoad, callbackInfo);
setLoadedScript(script.id, existingElement);
loadedScriptIds.push(script.id);
emitLifecycleEvent(script, 'loaded', "Existing script element marked as loaded", callbackInfo, {
reusedElement: true
});
return;
}
}
const scriptElement = document.createElement("script");
scriptElement.id = elementId;
if (script.src) scriptElement.src = script.src;
else if (script.textContent) scriptElement.textContent = script.textContent;
if (script.fetchPriority) scriptElement.fetchPriority = script.fetchPriority;
if (script.async) scriptElement.async = true;
else if (false === script.async) scriptElement.async = false;
if (script.defer) scriptElement.defer = true;
const nonce = script.nonce ?? options?.nonce;
if (nonce) scriptElement.nonce = nonce;
if (script.attributes) Object.entries(script.attributes).forEach(([key, value])=>{
scriptElement.setAttribute(key, value);
});
const callbackInfo = {
id: script.id,
hasConsent,
elementId,
consents,
element: scriptElement
};
if (script.onLoad) if (script.textContent) setTimeout(()=>{
invokeScriptCallback(script, 'onLoad', script.onLoad, {
...callbackInfo
});
}, 0);
else {
emitLifecycleEvent(script, 'load_listener_attached', 'Attached load listener', callbackInfo);
scriptElement.addEventListener('load', ()=>{
invokeScriptCallback(script, 'onLoad', script.onLoad, {
...callbackInfo
});
});
}
if (script.onError) if (script.textContent) ;
else {
emitLifecycleEvent(script, 'error_listener_attached', 'Attached error listener', callbackInfo);
scriptElement.addEventListener('error', ()=>{
invokeScriptCallback(script, 'onError', script.onError, {
...callbackInfo,
error: new Error(`Failed to load script: ${script.src}`)
});
});
}
invokeScriptCallback(script, 'onBeforeLoad', script.onBeforeLoad, callbackInfo);
const target = script.target ?? 'head';
const targetElement = 'body' === target ? document.body : document.head;
if (!targetElement) throw new Error(`Document ${target} is not available for script injection`);
targetElement.appendChild(scriptElement);
emitLifecycleEvent(script, 'element_appended', `Script element appended to ${target}`, callbackInfo, {
target
});
setLoadedScript(script.id, scriptElement);
loadedScriptIds.push(script.id);
emitLifecycleEvent(script, 'loaded', 'Script marked as loaded', callbackInfo);
});
return loadedScriptIds;
}
function unloadScripts(scripts, consents, scriptIdMap = {}, options) {
const unloadedScriptIds = [];
scripts.forEach((script)=>{
const hasConsent = scriptHasConsent(script, consents, options);
if (!hasLoadedScript(script.id)) return;
if (script.alwaysLoad) return;
if (!hasConsent) {
const scriptElement = getLoadedScript(script.id);
const callbackInfo = {
id: script.id,
elementId: getScriptElementId(script.id, false !== script.anonymizeId, scriptIdMap),
hasConsent,
consents,
element: scriptElement && null !== scriptElement ? scriptElement : void 0
};
if (true === script.callbackOnly || null === scriptElement) {
deleteLoadedScript(script.id);
unloadedScriptIds.push(script.id);
emitLifecycleEvent(script, 'unloaded', "Callback-only script marked as unloaded", callbackInfo, {
callbackOnly: true
});
} else if (scriptElement) if (script.persistAfterConsentRevoked) {
deleteLoadedScript(script.id);
unloadedScriptIds.push(script.id);
emitLifecycleEvent(script, 'unloaded', "Persistent script marked as unloaded without removing element", callbackInfo, {
removedElement: false,
persistAfterConsentRevoked: true
});
} else {
scriptElement.remove();
deleteLoadedScript(script.id);
unloadedScriptIds.push(script.id);
emitLifecycleEvent(script, 'unloaded', 'Script element removed after consent revocation', callbackInfo, {
removedElement: true
});
}
}
});
return unloadedScriptIds;
}
function core_updateScripts(scripts, consents, scriptIdMap = {}, options) {
const unloaded = unloadScripts(scripts, consents, scriptIdMap, options);
const loaded = loadScripts(scripts, consents, scriptIdMap, options);
return {
loaded,
unloaded
};
}
function isScriptLoaded(scriptId) {
return hasLoadedScript(scriptId);
}
function getLoadedScriptIds() {
return Array.from(utils_getLoadedScriptsSnapshot().keys());
}
function reloadScript(scriptId, scripts, consents, scriptIdMap = {}, options) {
const script = scripts.find((s)=>s.id === scriptId);
if (!script) return false;
if (hasLoadedScript(scriptId)) {
const scriptElement = getLoadedScript(scriptId);
if (true === script.callbackOnly || null === scriptElement) deleteLoadedScript(scriptId);
else if (scriptElement) {
if (!script.persistAfterConsentRevoked) scriptElement.remove();
deleteLoadedScript(scriptId);
}
}
if (!script.alwaysLoad && !scriptHasConsent(script, consents, options)) {
emitLifecycleEvent(script, 'skipped', 'Reload skipped due to missing consent', {
hasConsent: false,
elementId: getScriptElementId(script.id, false !== script.anonymizeId, scriptIdMap)
}, {
reason: 'reload_missing_consent'
});
return false;
}
loadScripts([
script
], consents, scriptIdMap, options);
return true;
}
function createScriptManager(getState, setState) {
const updateScriptsFn = ()=>{
const { scripts, consents, scriptIdMap, model, iab, nonce, policyCategories, policyScopeMode } = getState();
const iabConsent = iab?.config.enabled ? {
vendorConsents: iab.vendorConsents,
vendorLegitimateInterests: iab.vendorLegitimateInterests,
purposeConsents: iab.purposeConsents,
purposeLegitimateInterests: iab.purposeLegitimateInterests,
specialFeatureOptIns: iab.specialFeatureOptIns
} : void 0;
const runtimeConsents = applyPolicyScopeForRuntimeGating(consents, policyCategories, policyScopeMode);
const result = core_updateScripts(scripts, runtimeConsents, scriptIdMap, {
model,
iabConsent,
nonce
});
const newLoadedScripts = {
...getState().loadedScripts
};
result.loaded.forEach((id)=>{
newLoadedScripts[id] = true;
});
result.unloaded.forEach((id)=>{
newLoadedScripts[id] = false;
});
setState({
loadedScripts: newLoadedScripts
});
return result;
};
return {
updateScripts: ()=>updateScriptsFn(),
setScripts: (scripts)=>{
const state = getState();
const newScriptIdMap = {
...state.scriptIdMap
};
scripts.forEach((script)=>{
if (false !== script.anonymizeId) newScriptIdMap[script.id] = generateRandomScriptId();
});
const newCategories = scripts.flatMap((script)=>extractConsentNamesFromCondition(script.category));
setState({
scripts: [
...state.scripts,
...scripts
],
scriptIdMap: newScriptIdMap
});
getState().updateConsentCategories(newCategories);
updateScriptsFn();
},
removeScript: (scriptId)=>{
const state = getState();
if (hasLoadedScript(scriptId)) {
const scriptElement = getLoadedScript(scriptId);
if (scriptElement) {
scriptElement.remove();
deleteLoadedScript(scriptId);
}
}
const newScriptIdMap = {
...state.scriptIdMap
};
delete newScriptIdMap[scriptId];
setState({
scripts: state.scripts.filter((script)=>script.id !== scriptId),
loadedScripts: {
...state.loadedScripts,
[scriptId]: false
},
scriptIdMap: newScriptIdMap
});
},
reloadScript: (scriptId)=>{
const state = getState();
const iabConsent = state.iab?.config.enabled ? {
vendorConsents: state.iab.vendorConsents,
vendorLegitimateInterests: state.iab.vendorLegitimateInterests,
purposeConsents: state.iab.purposeConsents,
purposeLegitimateInterests: state.iab.purposeLegitimateInterests,
specialFeatureOptIns: state.iab.specialFeatureOptIns
} : void 0;
const runtimeConsents = applyPolicyScopeForRuntimeGating(state.consents, state.policyCategories, state.policyScopeMode);
return reloadScript(scriptId, state.scripts, runtimeConsents, state.scriptIdMap, {
model: state.model,
iabConsent,
nonce: state.nonce
});
},
isScriptLoaded: (scriptId)=>isScriptLoaded(scriptId),
getLoadedScriptIds: ()=>getLoadedScriptIds()
};
}
function createIframeManager(get, _set) {
let observer = null;
let isInitialized = false;
return {
initializeIframeBlocker: ()=>{
if (isInitialized) return;
if ("u" < typeof document) return;
const state = get();
const runtimeConsents = applyPolicyScopeForRuntimeGating(state.consents, state.policyCategories, state.policyScopeMode);
if (state.iframeBlockerConfig?.disableAutomaticBlocking) return;
const discoverAndRegisterCategories = ()=>{
const iframeCategories = getIframeConsentCategories();
if (iframeCategories.length > 0) get().updateConsentCategories(iframeCategories);
};
if ('loading' === document.readyState) document.addEventListener('DOMContentLoaded', discoverAndRegisterCategories);
else discoverAndRegisterCategories();
setTimeout(discoverAndRegisterCategories, 100);
processAllIframes(runtimeConsents);
observer = setupIframeObserver(()=>{
const nextState = get();
return applyPolicyScopeForRuntimeGating(nextState.consents, nextState.policyCategories, nextState.policyScopeMode);
}, (categories)=>get().updateConsentCategories(categories));
isInitialized = true;
},
updateIframeConsents: ()=>{
if (!isInitialized) return;
if ("u" < typeof document) return;
const state = get();
const { consents, iframeBlockerConfig } = state;
if (iframeBlockerConfig?.disableAutomaticBlocking) return;
processAllIframes(applyPolicyScopeForRuntimeGating(consents, state.policyCategories, state.policyScopeMode));
},
destroyIframeBlocker: ()=>{
if (!isInitialized) return;
if ("u" < typeof document) return;
const state = get();
const { iframeBlockerConfig } = state;
if (iframeBlockerConfig?.disableAutomaticBlocking) return;
if (observer) {
observer.disconnect();
observer = null;
}
isInitialized = false;
}
};
}
const PENDING_CONSENT_SYNC_KEY = 'c15t:pending-consent-sync';
function shouldReloadOnConsentChange(previousConsents, newConsents, previousConsentInfo, reloadOnConsentRevoked, consentTypes) {
if (!reloadOnConsentRevoked) return false;
if (null === previousConsentInfo) return false;
const disabledNames = new Set(consentTypes.filter((t)=>t.disabled).map((t)=>t.name));
const wasAnyConsentRevoked = Object.entries(newConsents).some(([key, value])=>!disabledNames.has(key) && true === previousConsents[key] && false === value);
return wasAnyConsentRevoked;
}
function haveConsentsChanged(previousConsents, nextConsents, consentTypes) {
return consentTypes.some((consentType)=>previousConsents[consentType.name] !== nextConsents[consentType.name]);
}
function getConsentCategoryLists(consents, consentCategories, consentTypes) {
const activeCategories = new Set(consentCategories);
const allowedCategories = [];
const deniedCategories = [];
for (const consentType of consentTypes)if (activeCategories.has(consentType.name)) if (consents[consentType.name]) allowedCategories.push(consentType.name);
else deniedCategories.push(consentType.name);
return {
allowedCategories,
deniedCategories
};
}
async function saveConsents({ manager, type, get, set, options, emitConsentChanged }) {
const { callbacks, selectedConsents, consents, consentTypes, updateScripts, updateIframeConsents, updateNetworkBlockerConsents, consentCategories, locationInfo, model, consentInfo, reloadOnConsentRevoked, lastBannerFetchData } = get();
const previousConsents = {
...consents
};
const previousConsentInfo = consentInfo;
const newConsents = {
...selectedConsents ?? consents ?? {}
};
const givenAt = Date.now();
if ('all' === type) {
for (const consent of consentTypes)if (consentCategories.includes(consent.name)) newConsents[consent.name] = true;
} else if ('necessary' === type) for (const consent of consentTypes)newConsents[consent.name] = true === consent.disabled ? consent.defaultValue : false;
const effectivePolicy = getEffectivePolicy(lastBannerFetchData);
const policyCategories = effectivePolicy?.consent?.categories;
const shouldEnforcePolicyScope = shouldEnforcePolicyCategoryScope(policyCategories, effectivePolicy?.consent?.scopeMode ?? null);
const effectiveConsents = shouldEnforcePolicyScope ? applyPolicyPurposeAllowlist(newConsents, policyCategories) : newConsents;
const requestPreferences = shouldEnforcePolicyScope ? stripDisallowedPreferenceKeys(effectiveConsents, policyCategories) : effectiveConsents;
const didChange = haveConsentsChanged(previousConsents, effectiveConsents, consentTypes);
const nextConsentCategoryLists = getConsentCategoryLists(effectiveConsents, consentCategories, consentTypes);
const previousConsentCategoryLists = getConsentCategoryLists(previousConsents, consentCategories, consentTypes);
const consentChangedPayload = didChange ? {
preferences: effectiveConsents,
previousPreferences: previousConsents,
allowedCategories: nextConsentCategoryLists.allowedCategories,
deniedCategories: nextConsentCategoryLists.deniedCategories,
previousAllowedCategories: previousConsentCategoryLists.allowedCategories,
previousDeniedCategories: previousConsentCategoryLists.deniedCategories
} : null;
const materialPolicyFingerprint = lastBannerFetchData?.policy ? await createMaterialPolicyFingerprint(lastBannerFetchData.policy) : void 0;
let subjectId = consentInfo?.subjectId;
if (!subjectId) subjectId = generateSubjectId();
const storedIdentifiers = sanitizeSubjectIdentifiers({
externalId: get().consentInfo?.externalId,
identityProvider: get().consentInfo?.identityProvider
});
const userIdentifiers = sanitizeSubjectIdentifiers({
externalId: get().user?.id,
identityProvider: get().user?.identityProvider
});
const externalId = storedIdentifiers.externalId ?? userIdentifiers.externalId;
const identityProvider = storedIdentifiers.identityProvider ?? userIdentifiers.identityProvider;
const nextConsentInfo = {
time: givenAt,
subjectId,
materialPolicyFingerprint,
...externalId ? {
externalId
} : {},
...identityProvider ? {
identityProvider
} : {}
};
const needsReload = shouldReloadOnConsentChange(previousConsents, effectiveConsents, previousConsentInfo, reloadOnConsentRevoked, consentTypes);
set({
consents: effectiveConsents,
selectedConsents: effectiveConsents,
activeUI: 'none',
consentInfo: nextConsentInfo
});
saveConsentToStorage({
consents: effectiveConsents,
consentInfo: nextConsentInfo
}, void 0, get().storageConfig);
if (needsReload) {
const pendingSync = {
type,
subjectId,
preferences: requestPreferences,
givenAt,
jurisdiction: locationInfo?.jurisdiction ?? void 0,
jurisdictionModel: model,
domain: window.location.hostname,
uiSource: options?.uiSource ?? 'api',
policySnapshotToken: lastBannerFetchData?.policySnapshotToken,
...externalId ? {
externalId
} : {},
...identityProvider ? {
identityProvider
} : {}
};
try {
localStorage.setItem(PENDING_CONSENT_SYNC_KEY, JSON.stringify(pendingSync));
} catch {}
callbacks.onConsentSet?.({
preferences: effectiveConsents
});
if (consentChangedPayload) emitConsentChanged?.(consentChangedPayload);
callbacks.onBeforeConsentRevocationReload?.({
preferences: effectiveConsents
});
window.location.reload();
return;
}
await new Promise((resolve)=>setTimeout(resolve, 0));
updateIframeConsents();
updateScripts();
updateNetworkBlockerConsents();
callbacks.onConsentSet?.({
preferences: effectiveConsents
});
if (consentChangedPayload) emitConsentChanged?.(consentChangedPayload);
const consent = await manager.setConsent({
body: {
type: 'cookie_banner',
domain: window.location.hostname,
preferences: requestPreferences,
subjectId,
jurisdiction: locationInfo?.jurisdiction ?? void 0,
jurisdictionModel: model ?? void 0,
givenAt,
uiSource: options?.uiSource ?? 'api',
consentAction: type,
policySnapshotToken: lastBannerFetchData?.policySnapshotToken,
...externalId ? {
externalSubjectId: externalId
} : {},
...identityProvider ? {
identityProvider
} : {}
}
});
if (!consent.ok) {
const errorMsg = consent.error?.message ?? 'Failed to save consents';
callbacks.onError?.({
error: errorMsg
});
if (!callbacks.onError) console.error(errorMsg);
}
}
function determineModel(jurisdiction, iabEnabled) {
if (null == jurisdiction || 'NONE' === jurisdiction) return null;
if (iabEnabled && [
'UK_GDPR',
'GDPR'
].includes(jurisdiction)) return 'iab';
if ([
'UK_GDPR',
'GDPR',
'CH',
'BR',
'APPI',
'PIPA',
'QC_LAW25'
].includes(jurisdiction)) return 'opt-in';
if ([
'CCPA',
'AU',
'PIPEDA'
].includes(jurisdiction)) return 'opt-out';
return 'opt-in';
}
function calculateAutoGrantedConsents(shouldAutoGrant, hasGpcSignal) {
if (!shouldAutoGrant) return null;
return {
necessary: true,
functionality: true,
experience: true,
marketing: !hasGpcSignal,
measurement: !hasGpcSignal
};
}
function computeAutoGrantInfo(jurisdiction, iabEnabled, consentInfo, policyModel, gpcOverride, policyGpc) {
const consentModel = 'none' === policyModel ? null : policyModel ?? determineModel(jurisdiction, iabEnabled);
const shouldCheckGpc = void 0 !== policyGpc ? policyGpc : true;
const hasGpcSignal = shouldCheckGpc ? void 0 !== gpcOverride ? gpcOverride : global_privacy_control_hasGlobalPrivacyControlSignal() : false;
const shouldAutoGrantConsents = (null === consentModel || 'opt-out' === consentModel) && null === consentInfo;
const autoGrantedConsents = calculateAutoGrantedConsents(shouldAutoGrantConsents, hasGpcSignal);
return {
consentModel,
autoGrantedConsents
};
}
function buildStoreUpdate(data, config, effectiveIABEnabled, initSourceMetadata) {
const { get, initialTranslationConfig } = config;
const { consentInfo, consentTypes } = get();
const { translations, location } = data;
const { consentModel, autoGrantedConsents } = computeAutoGrantInfo(data.jurisdiction ?? null, effectiveIABEnabled, consentInfo, data.policy?.model, config.get().overrides?.gpc, data.policy?.consent?.gpc);
const update = {
model: consentModel,
isLoadingConsentInfo: false,
branding: data.branding ?? 'c15t',
hasFetchedBanner: true,
lastBannerFetchData: data,
locationInfo: {
countryCode: location?.countryCode ?? null,
regionCode: location?.regionCode ?? null,
jurisdiction: data.jurisdiction ?? null
},
policyBanner: {
allowedActions: data.policy?.ui?.banner?.allowedActions,
primaryActions: data.policy?.ui?.banner?.primaryActions,
layout: data.policy?.ui?.banner?.layout,
direction: data.policy?.ui?.banner?.direction,
uiProfile: data.policy?.ui?.banner?.uiProfile,
scrollLock: data.policy?.ui?.banner?.scrollLock
},
policyDialog: {
allowedActions: data.policy?.ui?.dialog?.allowedActions,
primaryActions: data.policy?.ui?.dialog?.primaryActions,
layout: data.policy?.ui?.dialog?.layout,
direction: data.policy?.ui?.dialog?.direction,
uiProfile: data.policy?.ui?.dialog?.uiProfile,
scrollLock: data.policy?.ui?.dialog?.scrollLock
},
policyCategories: data.policy?.consent?.categories ?? null,
policyScopeMode: data.policy?.consent?.scopeMode ?? null,
initDataSource: initSourceMetadata?.initDataSource ?? null,
initDataSourceDetail: initSourceMetadata?.initDataSourceDetail ?? null
};
if (null === consentInfo) if (data.policy?.ui?.mode) update.activeUI = data.policy.ui.mode;
else update.activeUI = consentModel ? 'banner' : 'none';
if (autoGrantedConsents) {
update.consents = autoGrantedConsents;
update.selectedConsents = autoGrantedConsents;
}
const policyCategories = data.policy?.consent?.categories;
const hasStrictPolicyCategoryAllowlist = shouldEnforcePolicyCategoryScope(policyCategories, data.policy?.consent?.scopeMode ?? null);
if (hasStrictPolicyCategoryAllowlist) {
const uniqueAllowedCategories = filterConsentCategoriesByPolicy(allConsentNames, policyCategories);
update.consentCategories = uniqueAllowedCategories;
update.consents = applyPolicyPurposeAllowlist(update.consents ?? get().consents, uniqueAllowedCategories);
update.selectedConsents = applyPolicyPurposeAllowlist(update.selectedConsents ?? get().selectedConsents, uniqueAllowedCategories);
}
const preselectedCategories = data.policy?.consent?.preselectedCategories;
const shouldApplyPreselectedCategories = null === consentInfo && !autoGrantedConsents && Array.isArray(preselectedCategories) && preselectedCategories.length > 0;
if (shouldApplyPreselectedCategories) {
const displayedConsentNames = update.consentCategories ?? get().consentCategories;
const preselectedScope = hasStrictPolicyCategoryAllowlist ? filterConsentCategoriesByPolicy(displayedConsentNames, policyCategories) : displayedConsentNames;
const allowedPreselectedCategories = filterConsentCategoriesByPolicy(preselectedScope, preselectedCategories);
const preselectedSet = new Set(allowedPreselectedCategories);
const selectedConsentBaseline = update.selectedConsents ?? get().selectedConsents;
update.selectedConsents = consentTypes.length > 0 ? consentTypes.reduce((acc, consent)=>{
acc[consent.name] = true === consent.disabled ? consent.defaultValue : preselectedSet.has(consent.name);
return acc;
}, {}) : Object.fromEntries(Object.keys(selectedConsentBaseline).map((category)=>[
category,
'necessary' === category || preselectedSet.has(category)
]));
}
if (translations?.language && translations?.translations) {
let customMessages;
customMessages = initialTranslationConfig?.translations ? {
translations: initialTranslationConfig.translations
} : void 0;
update.translationConfig = prepareTranslationConfig({
translations: {
[translations.language]: translations.translations
},
disableAutoLanguageSwitch: true,
defaultLanguage: translations.language
}, customMessages);
}
return update;
}
function triggerCallbacks(data, config, autoGrantedConsents) {
const { get } = config;
const { callbacks } = get();
const { translations } = data;
if (autoGrantedConsents) callbacks?.onConsentSet?.({
preferences: autoGrantedConsents
});
if (translations?.language && translations?.translations) callbacks?.onBannerFetched?.({
jurisdiction: data.jurisdiction,
location: data.location,
translations: {
language: translations.language,
translations: translations.translations
}
});
}
function getDefaultConsents(consentTypes) {
return consentTypes.reduce((acc, consent)=>{
acc[consent.name] = consent.defaultValue;
return acc;
}, {});
}
async function updateStore(data, config, _hasLocalStorageAccess, prefetchedGVL, initSourceMetadata) {
const { set, get } = config;
const initialState = get();
const currentPolicyFingerprint = data.policy ? await createMaterialPolicyFingerprint(data.policy) : void 0;
if (initialState.consentInfo && currentPolicyFingerprint) {
const storedPolicyFingerprint = initialState.consentInfo.materialPolicyFingerprint;
if (storedPolicyFingerprint && storedPolicyFingerprint !== currentPolicyFingerprint) {
const resetConsents = getDefaultConsents(initialState.consentTypes);
deleteConsentFromStorage(void 0, initialState.storageConfig);
set({
consents: resetConsents,
selectedConsents: resetConsents,
consentInfo: null
});
} else if (!storedPolicyFingerprint) {
const updatedConsentInfo = {
...initialState.consentInfo,
materialPolicyFingerprint: currentPolicyFingerprint
};
saveConsentToStorage({
consents: initialState.consents,
consentInfo: updatedConsentInfo
}, void 0, initialState.storageConfig);
set({
consentInfo: updatedConsentInfo
});
}
}
const { consentInfo } = get();
let iab = get().iab;
if (config.iabConfig && !iab) {
const iabModule = config.iabConfig._module;
if (iabModule) {
iab = iabModule.createIABManager(config.iabConfig, get, set, config.manager);
set({
iab
});
} else console.error('[c15t] IAB config provided without IAB module. Install @c15t/iab and use the iab() wrapper: `import { iab } from "@c15t/iab"; iab({ cmpId: ... })`');
}
const serverDisabledGVL = iab?.config.enabled && !prefetchedGVL;
const effectiveIABEnabled = iab?.config.enabled && !serverDisabledGVL;
if (serverDisabledGVL) console.warn('IAB mode disabled: Server returned 200 without GVL. Client IAB settings overridden.');
const { consentModel, autoGrantedConsents } = computeAutoGrantInfo(data.jurisdiction ?? null, effectiveIABEnabled, consentInfo, data.policy?.model, get().overrides?.gpc, data.policy?.consent?.gpc);
const storeUpdate = buildStoreUpdate(data, config, effectiveIABEnabled, initSourceMetadata);
if (serverDisabledGVL && iab) storeUpdate.iab = {
...iab,
config: {
...iab.config,
enabled: false
}
};
else if (iab && null != data.cmpId) storeUpdate.iab = {
...iab,
config: {
...iab.config,
cmpId: data.cmpId
}
};
set(storeUpdate);
triggerCallbacks(data, config, autoGrantedConsents);
get().updateScripts();
if (effectiveIABEnabled && 'iab' === consentModel && iab) {
const serverCustomVendors = data.customVendors ?? [];
const clientCustomVendors = iab.config.customVendors ?? [];
const serverVendorIds = new Set(serverCustomVendors.map((v)=>v.id));
const mergedCustomVendors = [
...serverCustomVendors,
...clientCustomVendors.filter((v)=>!serverVendorIds.has(v.id))
];
const mergedConfig = {
...iab.config,
customVendors: mergedCustomVendors,
...null != data.cmpId && {
cmpId: data.cmpId
}
};
const iabModule = config.iabConfig?._module;
if (iabModule) iabModule.initializeIABMode(mergedConfig, {
set,
get
}, prefetchedGVL).catch((err)=>{
console.error('Failed to initialize IAB mode in updateStore:', err);
});
}
}
function checkLocalStorageAccess(set) {
try {
if (window.localStorage) {
window.localStorage.setItem('c15t-storage-test-key', 'test');
window.localStorage.removeItem('c15t-storage-test-key');
return true;
}
} catch (error) {
console.warn('localStorage not available, skipping consent banner:', error);
set({
isLoadingConsentInfo: false,
activeUI: 'none'
});
}
return false;
}
function shouldReuseSSRData(config, data) {
const requestContext = data.metadata?.requestContext;
if (!requestContext || !config.backendURL) return true;
const matcher = createRuntimeRequestContextMatcher({
backendURL: config.backendURL,
overrides: config.get().overrides,
credentials: config.requestCredentials
});
if (!matcher) return true;
return matchesStoredRequestContext(requestContext, matcher);
}
async function initConsentManager(config) {
const { get, set, manager } = config;
const { callbacks } = get();
if ("u" < typeof window) return;
const hasLocalStorageAccess = checkLocalStorageAccess(set);
if (!hasLocalStorageAccess) return;
set({
isLoadingConsentInfo: true
});
processPendingConsentSync(manager, callbacks);
const ssrResult = await tryUseSSRData(config);
if (ssrResult) return ssrResult;
return fetchFromAPI(config, hasLocalStorageAccess, manager, callbacks);
}
async function tryUseSSRData(config) {
const { ssrData, set } = config;
if (!ssrData) return void set({
ssrDataUsed: false,
ssrSkippedReason: 'no_data'
});
const data = await ssrData;
if (data?.init && !shouldReuseSSRData(config, data)) return void set({
ssrDataUsed: false,
ssrSkippedReason: 'context_mismatch'
});
if (data?.init) {
const initSourceMetadata = inferSSRInitSourceMetadata(data);
await updateStore(data.init, config, true, data.gvl, {
initDataSource: initSourceMetadata.initDataSource,
initDataSourceDetail: initSourceMetadata.initDataSourceDetail
});
set({
ssrDataUsed: true,
ssrSkippedReason: null
});
return data.init;
}
set({
ssrDataUsed: false,
ssrSkippedReason: 'fetch_failed'
});
}
async function fetchFromAPI(config, hasLocalStorageAccess, manager, callbacks) {
const { set, get } = config;
try {
const { language, country, region } = config.get().overrides ?? {};
const initContext = await manager.init({
headers: {
...language && {
'accept-language': language
},
...country && {
'x-c15t-country': country
},
...region && {
'x-c15t-region': region
}
},
onError: callbacks.onError ? (context)=>{
callbacks.onError?.({
error: context.error?.message || 'Unknown error'
});
} : void 0
});
const { data, error } = initContext;
if (error || !data) throw new Error(`Failed to fetch consent banner info: ${error?.message}`);
const initSourceMetadata = inferInitSourceMetadata(initContext, get().config.mode);
await updateStore(data, config, hasLocalStorageAccess, data.gvl ?? void 0, initSourceMetadata);
return data;
} catch (error) {
console.error('Error fetching consent banner information:', error);
set({
isLoadingConsentInfo: false,
activeUI: 'none'
});
const errorMessage = error instanceof Error ? error.message : 'Unknown error fetching consent banner information';
callbacks.onError?.({
error: errorMessage
});
return;
}
}
function inferInitSourceMetadata(initContext, mode) {
const response = initContext?.response ?? null;
if (response) {
const cache = inspectBackendCache(response.headers);
if (cache.isCacheHit) return {
initDataSource: 'backend-cache-hit',
initDataSourceDetail: cache.detail
};
return {
initDataSource: 'backend',
initDataSourceDetail: cache.detail
};
}
if ('offline' === mode) return {
initDataSource: 'offline-mode',
initDataSourceDetail: null
};
if ('custom' === mode) return {
initDataSource: 'custom',
initDataSourceDetail: null
};
if ('hosted' === mode || 'c15t' === mode) return {
initDataSource: 'offline-fallback',
initDataSourceDetail: null
};
return {
initDataSource: 'backend',
initDataSourceDetail: null
};
}
function inferSSRInitSourceMetadata(data) {
const cache = data.metadata?.cache;
const requestDurationMs = data.metadata?.requestDurationMs;
const detailParts = [
'via=ssr'
];
if (cache?.detail) detailParts.push(cache.detail);
if ('number' == typeof requestDurationMs && Number.isFinite(requestDurationMs)) detailParts.push(`duration=${Math.max(0, Math.round(requestDurationMs))}ms`);
const detail = detailParts.length > 0 ? detailParts.join(', ') : null;
if (cache?.isHit === true) return {
initDataSource: 'backend-cache-hit',
initDataSourceDetail: detail
};
if (cache) return {
initDataSource: 'backend',
initDataSourceDetail: detail
};
return {
initDataSource: 'ssr',
initDataSourceDetail: detail
};
}
function inspectBackendCache(headers) {
const cacheHeaders = [
'x-vercel-cache',
'cf-cache-status',
'x-cache',
'cache-status'
];
let headerDetail = null;
let headerIndicatesHit = false;
for (const headerName of cacheHeaders){
const headerValue = headers.get(headerName);
if (headerValue) {
headerDetail = `${headerName}=${headerValue}`;
headerIndicatesHit = /\b(hit|stale|revalidated|updating)\b/i.test(headerValue);
break;
}
}
const ageHeader = headers.get('age');
const ageValue = ageHeader ? Number.parseInt(ageHeader, 10) : NaN;
const ageIndicatesCache = Number.isFinite(ageValue) && ageValue > 0;
const ageDetail = ageIndicatesCache ? `age=${ageValue}` : null;
const detail = headerDetail && ageDetail ? `${headerDetail}, ${ageDetail}` : headerDetail ?? ageDetail;
return {
isCacheHit: headerIndicatesHit || ageIndicatesCache,
detail
};
}
function processPendingConsentSync(manager, callbacks) {
try {
const pendingSync = localStorage.getItem(PENDING_CONSENT_SYNC_KEY);
if (!pendingSync) return;
localStorage.removeItem(PENDING_CONSENT_SYNC_KEY);
const data = JSON.parse(pendingSync);
const { externalId: externalSubjectId, identityProvider } = sanitizeSubjectIdentifiers({
externalId: data.externalId,
identityProvider: data.identityProvider
});
manager.setConsent({
body: {
type: 'cookie_banner',
domain: data.domain,
preferences: data.preferences,
subjectId: data.subjectId,
jurisdiction: data.jurisdiction,
jurisdictionModel: data.jurisdictionModel ?? void 0,
givenAt: data.givenAt,
uiSource: data.uiSource ?? 'api',
policySnapshotToken: data.policySnapshotToken,
...externalSubjectId ? {
externalSubjectId
} : {},
...identityProvider ? {
identityProvider
} : {}
}
}).then((result)=>{
if (!result.ok) {
const errorMsg = result.error?.message ?? 'Failed to sync consent after reload';
callbacks.onError?.({
error: errorMsg
});
if (!callbacks.onError) console.error('Failed to sync consent after reload:', errorMsg);
}
}).catch((err)=>{
const errorMsg = err instanceof Error ? err.message : 'Failed to sync consent after reload';
callbacks.onError?.({
error: errorMsg
});
if (!callbacks.onError) console.error('Failed to sync consent after reload:', err);
});
} catch {}
}
function normalizeMethod(method) {
if (!method) return 'GET';
return method.toUpperCase();
}
function createUrl(rawUrl) {
if (!rawUrl) return null;
try {
if ("u" < typeof window) return null;
return new URL(rawUrl, window.location.href);
} catch {
return null;
}
}
function hostnameMatchesRule(hostname, rule) {
if (!hostname) return false;
const ruleDomain = rule.domain.trim().toLowerCase();
const targetHost = hostname.trim().toLowerCase();
if (!ruleDomain || !targetHost) return false;
if (targetHost === ruleDomain) return true;
const suffix = `.${ruleDomain}`;
const hasSuffix = targetHost.endsWith(suffix);
return hasSuffix;
}
function pathMatchesRule(pathname, rule) {
const hasPathFilter = 'string' == typeof rule.pathIncludes;
if (!hasPathFilter) return true;
if (!pathname) return false;
return pathname.includes(rule.pathIncludes);
}
function methodMatchesRule(method, rule) {
if (!rule.methods || 0 === rule.methods.length) return true;
if (!method) return false;
const upperMethod = normalizeMethod(method);
return rule.methods.some((allowedMethod)=>normalizeMethod(allowedMethod) === upperMethod);
}
function shouldApplyRule(url, method, rule) {
if (!hostnameMatchesRule(url.hostname, rule)) return false;
if (!pathMatchesRule(url.pathname, rule)) return false;
if (!methodMatchesRule(method, rule)) return false;
return true;
}
function shouldBlockRequest(request, consents, config) {
if (!config) return {
shouldBlock: false
};
const isEnabled = false !== config.enabled;
if (!isEnabled) return {
shouldBlock: false
};
if (!config.rules || 0 === config.rules.length) return {
shouldBlock: false
};
const url = createUrl(request.url);
if (!url) return {
shouldBlock: false
};
const method = normalizeMethod(request.method);
for (const rule of config.rules){
const applies = shouldApplyRule(url, method, rule);
if (!applies) continue;
const hasRequiredConsent = has(rule.category, consents);
if (!hasRequiredConsent) return {
shouldBlock: true,
rule
};
}
return {
shouldBlock: false
};
}
function createNetworkBlockerManager(get, _set) {
let originalFetch = null;
let originalXhrOpen = null;
let originalXhrSend = null;
let isInitialized = false;
let blockingConsents = null;
const notifyBlockedRequest = (config, info)=>{
if (!config) return;
if (false !== config.logBlockedRequests) {
const ruleId = info.rule?.id ?? 'unknown';
console.warn('[c15t] Network request blocked by consent manager', {
method: info.method,
url: info.url,
ruleId
});
}
if (config.onRequestBlocked) config.onRequestBlocked(info);
};
const getBlockingConsents = ()=>{
if (blockingConsents) return blockingConsents;
return get().consents;
};
const patchFetch = ()=>{
if ("u" < typeof window) return;
const hasFetch = 'function' == typeof window.fetch;
if (!hasFetch) return;
if (originalFetch) return;
originalFetch = window.fetch;
window.fetch = (input, init)=>{
const state = get();
const config = state.networkBlocker;
if (!originalFetch) throw new Error('Network blocker fetch wrapper not initialized.');
const hasRules = config?.enabled && config?.rules && config?.rules.length > 0;
if (!hasRules) return originalFetch.call(window, input, init);
let method = 'GET';
if (init?.method) method = init.method;
else if (input instanceof Request) method = input.method;
let url;
url = 'string' == typeof input || input instanceof URL ? input.toString() : input.url;
const consents = getBlockingConsents();
const { shouldBlock, rule } = shouldBlockRequest({
url,
method
}, consents, config);
if (shouldBlock) {
notifyBlockedRequest(config, {
method,
url,
rule
});
const blockedResponse = new Response(null, {
status: 451,
statusText: 'Request blocked by consent manager'
});
return Promise.resolve(blockedResponse);
}
return originalFetch.call(window, input, init);
};
};
const patchXmlHttpRequest = ()=>{
if ("u" < typeof window) return;
const hasXhr = void 0 !== window.XMLHttpRequest && 'function' == typeof window.XMLHttpRequest.prototype.open && 'function' == typeof window.XMLHttpRequest.prototype.send;
if (!hasXhr) return;
if (originalXhrOpen || originalXhrSend) return;
originalXhrOpen = window.XMLHttpRequest.prototype.open;
originalXhrSend = window.XMLHttpRequest.prototype.send;
window.XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
const internal = this;
internal.__c15tMethod = method;
internal.__c15tUrl = url;
if (!originalXhrOpen) throw new Error('Network blocker XHR open wrapper not initialized.');
return originalXhrOpen.call(this, method, url, async ?? true, user, password);
};
window.XMLHttpRequest.prototype.send = function(body) {
const state = get();
const config = state.networkBlocker;
const isEnabled = config?.enabled !== false;
const hasRules = isEnabled && config?.rules && config?.rules.length > 0;
if (hasRules) {
const internal = this;
const method = internal.__c15tMethod || 'GET';
const url = internal.__c15tUrl || '';
const consents = getBlockingConsents();
const { shouldBlock, rule } = shouldBlockRequest({
url,
method
}, consents, config);
if (shouldBlock) {
notifyBlockedRequest(config, {
method,
url,
rule
});
try {
this.abort();
} catch {}
const progressEvent = new ProgressEvent('error');
if ('function' == typeof this.onerror) this.onerror(progressEvent);
this.dispatchEvent(progressEvent);
return;
}
}
if (!originalXhrSend) throw new Error('Network blocker XHR send wrapper not initialized.');
return originalXhrSend.call(this, body);
};
};
return {
initializeNetworkBlocker: ()=>{
if (isInitialized) return;
if ("u" < typeof window) return;
const state = get();
const config = state.networkBlocker;
const hasRules = config?.enabled && config?.rules && config?.rules.length > 0;
if (!hasRules) return;
blockingConsents = state.consents;
patchFetch();
patchXmlHttpRequest();
isInitialized = true;
},
updateNetworkBlockerConsents: ()=>{
if (!isInitialized) return;
blockingConsents = get().consents;
},
setNetworkBlocker: (config)=>{
const isEnabled = config?.enabled !== false;
const shouldEnable = isEnabled && config?.rules && config?.rules.length > 0;
const partial = {
networkBlocker: config
};
_set(partial);
if (!shouldEnable) {
if (!isInitialized) return;
if ("u" < typeof window) return;
if (originalFetch) {
window.fetch = originalFetch;
originalFetch = null;
}
if (originalXhrOpen && originalXhrSend) {
window.XMLHttpRequest.prototype.open = originalXhrOpen;
window.XMLHttpRequest.prototype.send = originalXhrSend;
originalXhrOpen = null;
originalXhrSend = null;
}
blockingConsents = null;
isInitialized = false;
return;
}
if (!isInitialized) {
blockingConsents = get().consents;
patchFetch();
patchXmlHttpRequest();
isInitialized = true;
}
},
destroyNetworkBlocker: ()=>{
if (!isInitialized) return;
if ("u" < typeof window) return;
if (originalFetch) {
window.fetch = originalFetch;
originalFetch = null;
}
if (originalXhrOpen && originalXhrSend) {
window.XMLHttpRequest.prototype.open = originalXhrOpen;
window.XMLHttpRequest.prototype.send = originalXhrSend;
originalXhrOpen = null;
originalXhrSend = null;
}
blockingConsents = null;
isInitialized = false;
}
};
}
function coalesceInFlight(requests, key, createRequest) {
const existingRequest = requests.get(key);
if (existingRequest) return existingRequest;
const request = createRequest().finally(()=>{
if (requests.get(key) === request) requests.delete(key);
});
requests.set(key, request);
return request;
}
function isLegalDocumentConsentInput(input) {
return isLegalDocumentType(input.type);
}
const getStoredConsent = (config)=>{
if ("u" < typeof window) return null;
try {
return getConsentFromStorage(config);
} catch (e) {
console.error('Failed to retrieve stored consent:', e);
return null;
}
};
const createConsentManagerStore = (manager, options = {})=>{
const internalOptions = options;
const { namespace = 'c15tStore', iab, ssrData: _unusedSsrData, initialConsentCategories, initialTranslationConfig: legacyInitialTranslationConfig, initialI18nConfig, enabled = true, debug: _unusedDebug, ...storeConfigOptions } = options;
const hasInitialTranslationInput = Boolean(legacyInitialTranslationConfig || initialI18nConfig);
const normalizedInitialTranslationConfig = hasInitialTranslationInput ? resolveTranslationInput(legacyInitialTranslationConfig, initialI18nConfig) : void 0;
setDebugEnabled(true === options.debug);
const storedConsent = getStoredConsent(options.storageConfig);
const getInitialConsentState = ()=>{
if (!enabled) {
const grantedConsents = consent_types_consentTypes.reduce((acc, consent)=>{
acc[consent.name] = true;
return acc;
}, {});
return {
consents: grantedConsents,
selectedConsents: grantedConsents,
consentInfo: {
time: Date.now()
},
activeUI: 'none',
isLoadingConsentInfo: false
};
}
if (storedConsent) return {
consents: storedConsent.consents,
selectedConsents: storedConsent.consents,
consentInfo: storedConsent.consentInfo,
user: storedConsent.consentInfo?.externalId ? {
id: storedConsent.consentInfo.externalId,
identityProvider: storedConsent.consentInfo.identityProvider
} : void 0,
activeUI: 'none',
isLoadingConsentInfo: false
};
return {
activeUI: 'none',
isLoadingConsentInfo: true
};
};
const consentChangeListeners = new Set();
const inFlightConsentSaves = new Map();
const inFlightPolicyConsents = new Map();
const store = createStore((set, get)=>({
...initial_state_initialState,
...storeConfigOptions,
namespace,
iab: null,
...initialConsentCategories && {
consentCategories: initialConsentCategories
},
...getInitialConsentState(),
setActiveUI: (ui, options = {})=>{
if ('none' === ui || 'dialog' === ui) return void set({
activeUI: ui
});
if (options.force) return void set({
activeUI: 'banner'
});
const state = get();
const stored = getStoredConsent();
if (!stored && !state.consentInfo && !state.isLoadingConsentInfo) set({
activeUI: 'banner'
});
},
setSelectedConsent: (name, value)=>{
set((state)=>{
const consentType = state.consentTypes.find((type)=>type.name === name);
if (consentType?.disabled) return state;
return {
selectedConsents: {
...state.selectedConsents,
[name]: value
}
};
});
},
saveConsents: (type, options)=>{
const requestKey = JSON.stringify([
type,
options?.uiSource ?? null,
'custom' === type ? get().selectedConsents : null
]);
return coalesceInFlight(inFlightConsentSaves, requestKey, ()=>saveConsents({
manager,
type,
get,
set,
options,
emitConsentChanged: (payload)=>{
get().callbacks.onConsentChanged?.(payload);
for (const listener of consentChangeListeners)listener(payload);
}
}));
},
setConsent: (name, value)=>{
set((state)=>{
const consentType = state.consentTypes.find((type)=>type.name === name);
if (consentType?.disabled) return state;
const newConsents = {
...state.consents,
[name]: value
};
return {
selectedConsents: newConsents
};
});
get().saveConsents('custom');
},
resetConsents: ()=>{
set(()=>{
const consents = consent_types_consentTypes.reduce((acc, consent)=>{
acc[consent.name] = consent.defaultValue;
return acc;
}, {});
const resetState = {
consents,
selectedConsents: consents,
consentInfo: null
};
deleteConsentFromStorage(void 0, options.storageConfig);
return resetState;
});
},
setConsentCategories: (types)=>set(()=>{
const { policyCategories, policyScopeMode } = get();
if (shouldEnforcePolicyCategoryScope(policyCategories, policyScopeMode)) return {
consentCategories: filterConsentCategoriesByPolicy(types, policyCategories)
};
return {
consentCategories: Array.from(new Set(types))
};
}),
setCallback: (name, callback)=>{
const currentState = get();
set((state)=>({
callbacks: {
...state.callbacks,
[name]: callback
}
}));
if ('onConsentSet' === name && callback && 'function' == typeof callback) callback?.({
preferences: currentState.consents
});
if ('onBannerFetched' === name && currentState.hasFetchedBanner && currentState.lastBannerFetchData && callback && 'function' == typeof callback) {
const { lastBannerFetchData } = currentState;
const jurisdictionCode = lastBannerFetchData.jurisdiction ?? 'NONE';
callback?.({
jurisdiction: {
code: jurisdictionCode,
message: ''
},
location: {
countryCode: lastBannerFetchData.location.countryCode ?? null,
regionCode: lastBannerFetchData.location.regionCode ?? null
},
translations: {
language: lastBannerFetchData.translations.language,
translations: lastBannerFetchData.translations.translations
}
});
}
},
subscribeToConsentChanges: (listener)=>{
consentChangeListeners.add(listener);
return ()=>{
consentChangeListeners.delete(listener);
};
},
setLocationInfo: (location)=>set({
locationInfo: location
}),
initConsentManager: ()=>{
if (!enabled) return Promise.resolve(void 0);
return initConsentManager({
manager,
ssrData: options.ssrData,
backendURL: internalOptions.__internal?.backendURL,
requestCredentials: internalOptions.__internal?.requestCredentials,
initialTranslationConfig: normalizedInitialTranslationConfig,
iabConfig: iab,
get,
set
});
},
getDisplayedConsents: ()=>{
const { consentCategories, consentTypes } = get();
return consentTypes.filter((consent)=>consentCategories.includes(consent.name));
},
hasConsented: ()=>{
const { consentInfo } = get();
return null != consentInfo;
},
has: (condition)=>{
const { consents, policyCategories, policyScopeMode } = get();
return has(condition, consents, {
policyCategories,
policyScopeMode
});
},
setTranslationConfig: (config)=>{
set({
translationConfig: config
});
},
updateConsentCategories: (newCategories)=>{
const { consentCategories: currentConsentCategories, policyCategories, policyScopeMode } = get();
const allCategoriesSet = new Set([
...currentConsentCategories,
...newCategories
]);
let consentCategories;
consentCategories = shouldEnforcePolicyCategoryScope(policyCategories, policyScopeMode) ? filterConsentCategoriesByPolicy(Array.from(allCategoriesSet), policyCategories) : Array.from(allCategoriesSet);
set({
consentCategories
});
},
identifyUser: async (user)=>{
const currentInfo = get().consentInfo;
const subjectId = currentInfo?.subjectId;
set({
user
});
if (!subjectId) return;
if (String(currentInfo?.externalId) === String(user.id) && currentInfo?.identityProvider === user.identityProvider) return;
await manager.identifyUser({
body: {
subjectId,
externalId: user.id,
identityProvider: user.identityProvider
}
});
set({
consentInfo: {
...currentInfo,
time: currentInfo?.time || Date.now(),
subjectId,
externalId: user.id,
identityProvider: user.identityProvider
}
});
},
unstable_acceptPolicyConsent: (input)=>{
const requestKey = JSON.stringify([
get().consentInfo?.subjectId ?? null,
input
]);
return coalesceInFlight(inFlightPolicyConsents, requestKey, async ()=>{
const currentState = get();
const currentInfo = currentState.consentInfo;
const subjectId = currentInfo?.subjectId ?? generateSubjectId();
const storedIdentifiers = sanitizeSubjectIdentifiers({
externalId: currentInfo?.externalId,
identityProvider: currentInfo?.identityProvider
});
const userIdentifiers = sanitizeSubjectIdentifiers({
externalId: currentState.user?.id,
identityProvider: currentState.user?.identityProvider
});
const inputIdentifiers = sanitizeSubjectIdentifiers({
externalId: input.externalId,
identityProvider: input.identityProvider
});
const externalId = inputIdentifiers.externalId ?? storedIdentifiers.externalId ?? userIdentifiers.externalId;
const identityProvider = inputIdentifiers.identityProvider ?? storedIdentifiers.identityProvider ?? userIdentifiers.identityProvider;
const domain = input.domain ?? ("u" > typeof window ? window.location.hostname : 'localhost');
const legalDocumentConsent = isLegalDocumentConsentInput(input);
let legalDocumentFields = {};
if (legalDocumentConsent) if (input.documentSnapshotToken) legalDocumentFields = {
documentSnapshotToken: input.documentSnapshotToken
};
else if (input.policyHash) legalDocumentFields = {
policyHash: input.policyHash
};
else if (input.policyId) legalDocumentFields = {
policyId: input.policyId
};
else throw new Error('Legal document consent requires documentSnapshotToken, policyHash, or policyId.');
const givenAt = input.givenAt ?? Date.now();
const response = await manager.setConsent({
body: {
type: input.type,
subjectId,
domain,
givenAt,
uiSource: input.uiSource ?? 'api',
...legalDocumentFields,
...input.metadata ? {
metadata: input.metadata
} : {},
...input.preferences ? {
preferences: input.preferences
} : {},
...externalId ? {
externalSubjectId: externalId
} : {},
...identityProvider ? {
identityProvider
} : {}
}
});
if (!response.ok || !response.data) {
const errorMsg = response.error?.message ?? 'Failed to accept policy consent';
get().callbacks.onError?.({
error: errorMsg
});
const error = new Error(errorMsg);
error.code = response.error?.code;
error.details = response.error?.details ?? null;
error.status = response.error?.status;
throw error;
}
const consent = {
...response.data,
givenAt: response.data.givenAt instanceof Date ? response.data.givenAt : new Date(response.data.givenAt)
};
const latestState = get();
const latestInfo = latestState.consentInfo;
const nextConsentInfo = {
...latestInfo,
time: consent.givenAt.getTime(),
subjectId,
...externalId ? {
externalId
} : {},
...identityProvider ? {
identityProvider
} : {}
};
set({
consentInfo: nextConsentInfo,
...externalId ? {
user: {
id: externalId,
identityProvider
}
} : {}
});
saveConsentToStorage({
consents: latestState.consents,
consentInfo: nextConsentInfo
}, void 0, latestState.storageConfig);
return consent;
});
},
setOverrides: async (overrides)=>{
set({
overrides: {
...get().overrides,
...overrides
}
});
if (!enabled) return;
return await initConsentManager({
manager,
backendURL: internalOptions.__internal?.backendURL,
requestCredentials: internalOptions.__internal?.requestCredentials,
initialTranslationConfig: normalizedInitialTranslationConfig,
iabConfig: iab,
get,
set
});
},
setLanguage: async (language)=>await get().setOverrides({
...get().overrides ?? {},
language
}),
...createScriptManager(get, set),
...createIframeManager(get, set),
...createNetworkBlockerManager(get, set)
}));
store.getState().initializeIframeBlocker();
if (options.networkBlocker) {
store.setState({
networkBlocker: options.networkBlocker
});
store.getState().initializeNetworkBlocker();
}
if (options.scripts && options.scripts.length > 0) store.getState().updateConsentCategories(options.scripts.flatMap((script)=>extractConsentNamesFromCondition(script.category)));
if ("u" > typeof window) {
window[namespace] = store;
store.getState().callbacks.onConsentSet?.({
preferences: store.getState().consents
});
if (options.user) store.getState().identifyUser(options.user);
if (enabled) store.getState().initConsentManager();
else store.getState().updateScripts();
}
return store;
};
const runtime_DEFAULT_BACKEND_URL = '/api/c15t';
const managerCache = new Map();
const storeCache = new Map();
function normalizeRuntimeMode(mode) {
if ('offline' === mode || 'custom' === mode) return mode;
return 'hosted';
}
function generateRuntimeCacheKey(options) {
const enabledKey = false === options.enabled ? 'disabled' : 'enabled';
const normalizedMode = normalizeRuntimeMode(options.mode);
const cacheParts = [
normalizedMode,
options.backendURL ?? 'default',
options.endpointHandlers ? 'custom' : 'none',
options.storageConfig?.storageKey ?? 'default',
options.defaultLanguage ?? 'default',
options.languageSetKey ?? 'default',
options.offlinePolicyKey ?? 'default',
options.headersKey ?? 'default',
enabledKey
];
return cacheParts.join(':');
}
function generateHeadersKey(headers) {
if (!headers) return;
const entries = Object.entries(headers).sort(([a], [b])=>a.localeCompare(b));
if (0 === entries.length) return;
return entries.map(([key, value])=>`${key}=${value}`).join(',');
}
function getOrCreateConsentRuntime(options, pkgInfo) {
const optionBag = options;
const { mode, backendURL, store, i18n, translations, storageConfig, enabled, iab, offlinePolicy, consentCategories, debug, headers, nonce, customFetch: _unusedCustomFetch, retryConfig: _unusedRetryConfig, endpointHandlers: _unusedEndpointHandlers, ...storeOptionOverrides } = optionBag;
const { initialI18nConfig: _unusedTopLevelInitialI18nConfig, initialTranslationConfig: _unusedTopLevelInitialTranslationConfig, ssrData: topLevelSSRData, config: topLevelConfig, ...cleanStoreOptionOverrides } = storeOptionOverrides;
const { initialI18nConfig: _unusedStoreInitialI18nConfig, initialTranslationConfig: _unusedStoreInitialTranslationConfig, ssrData: storeSSRData, config: storeConfig, ...storeWithoutTranslationInputs } = store ?? {};
const preferredLegacyTranslationConfig = translations ?? store?.initialTranslationConfig;
const preferredI18nConfig = i18n ?? store?.initialI18nConfig;
const normalizedInitialTranslationConfig = resolveTranslationInput(preferredLegacyTranslationConfig, preferredI18nConfig);
const normalizedI18nConfig = normalizedInitialTranslationConfig ? normalizeI18nConfig(normalizedInitialTranslationConfig) : void 0;
const normalizedLanguageSet = normalizedI18nConfig ? Object.keys(normalizedI18nConfig.messages).sort() : [];
const resolvedIab = iab ?? storeWithoutTranslationInputs.iab;
const resolvedOfflinePolicy = offlinePolicy ?? storeWithoutTranslationInputs.offlinePolicy;
const resolvedStorageConfig = storageConfig ?? storeWithoutTranslationInputs.storageConfig;
const resolvedEnabled = enabled ?? storeWithoutTranslationInputs.enabled;
const resolvedNonce = nonce ?? storeWithoutTranslationInputs.nonce;
const resolvedBackendURL = backendURL || runtime_DEFAULT_BACKEND_URL;
const explicitSSRData = topLevelSSRData ?? storeSSRData;
const cacheKey = generateRuntimeCacheKey({
mode,
backendURL,
endpointHandlers: 'endpointHandlers' in options ? options.endpointHandlers : void 0,
storageConfig: resolvedStorageConfig,
defaultLanguage: normalizedI18nConfig?.locale,
languageSetKey: normalizedLanguageSet.length > 0 ? normalizedLanguageSet.join(',') : void 0,
offlinePolicyKey: resolvedOfflinePolicy ? JSON.stringify(resolvedOfflinePolicy) : void 0,
headersKey: generateHeadersKey(headers),
enabled: resolvedEnabled
});
let consentManager = managerCache.get(cacheKey);
if (!consentManager) {
const normalizedStoreOptions = {
...storeWithoutTranslationInputs,
initialTranslationConfig: normalizedInitialTranslationConfig,
iab: resolvedIab,
offlinePolicy: resolvedOfflinePolicy
};
consentManager = 'offline' === mode ? configureConsentManager({
mode: 'offline',
store: normalizedStoreOptions,
storageConfig: resolvedStorageConfig
}) : 'custom' === mode && 'endpointHandlers' in options ? configureConsentManager({
mode: 'custom',
endpointHandlers: options.endpointHandlers,
store: normalizedStoreOptions,
storageConfig: resolvedStorageConfig
}) : configureConsentManager({
mode: 'c15t' === mode ? 'c15t' : 'hosted',
backendURL: backendURL || runtime_DEFAULT_BACKEND_URL,
headers,
store: normalizedStoreOptions,
storageConfig: resolvedStorageConfig
});
managerCache.set(cacheKey, consentManager);
}
let consentStore = storeCache.get(cacheKey);
if (consentStore) {
if (consentStore.getState().nonce !== resolvedNonce) consentStore.setState({
nonce: resolvedNonce
});
} else {
const normalizedMode = normalizeRuntimeMode(mode);
const userConfig = storeConfig ?? topLevelConfig;
const autoPrefetchedSSRData = 'hosted' !== normalizedMode || "u" < typeof window || explicitSSRData ? void 0 : getMatchingPrefetchedInitialData({
backendURL: resolvedBackendURL,
overrides: options.overrides,
credentials: 'include'
});
const resolvedSSRData = explicitSSRData ?? autoPrefetchedSSRData;
consentStore = createConsentManagerStore(consentManager, {
config: {
...userConfig ?? {},
pkg: pkgInfo?.pkg || 'c15t',
version: pkgInfo?.version || "2.2.1",
mode: normalizedMode,
meta: {
...userConfig?.meta ?? {},
...'hosted' === normalizedMode ? {
backendURL: resolvedBackendURL,
requestCredentials: 'include'
} : {}
}
},
...cleanStoreOptionOverrides,
...storeWithoutTranslationInputs,
iab: resolvedIab,
offlinePolicy: resolvedOfflinePolicy,
storageConfig: resolvedStorageConfig,
enabled: resolvedEnabled,
nonce: resolvedNonce,
initialTranslationConfig: normalizedInitialTranslationConfig,
initialConsentCategories: consentCategories,
ssrData: resolvedSSRData,
debug,
__internal: 'hosted' === normalizedMode ? {
backendURL: resolvedBackendURL,
requestCredentials: 'include'
} : void 0
});
storeCache.set(cacheKey, consentStore);
}
return {
consentManager,
consentStore,
cacheKey
};
}
function clearConsentRuntimeCache() {
managerCache.clear();
storeCache.clear();
clearClientRegistry();
}
export { API_ENDPOINTS, C15tClient, CustomClient, OfflineClient, allConsentNames, applyPolicyPurposeAllowlist, applyPolicyScopeForRuntimeGating, buildPrefetchScript, clearClientRegistry, clearConsentRuntimeCache, configureConsentManager, consent_types_consentTypes as consentTypes, core_updateScripts as updateScripts, createConsentManagerStore, createIframeBlocker, deepMergeTranslations, defaultTranslationConfig, deleteConsentFromStorage, deleteCookie, detectBrowserLanguage, emitScriptDebugEvent, filterConsentCategoriesByPolicy, flattenPolicyActionGroups, generateSubjectId, getConsentFromStorage, getCookie, getEffectivePolicy, getLoadedScriptIds, getOrCreateConsentRuntime, getRootDomain, has, hasPolicyHints, isScriptLoaded, isValidSubjectId, loadScripts, mergeTranslationConfigs, policyPackPresets, prepareTranslationConfig, resolvePolicyActionGroups, resolvePolicyAllowedActions, resolvePolicyDirection, resolvePolicyOrderedActions, resolvePolicyPrimaryActions, resolvePolicyUiProfile, saveConsentToStorage, setCookie, shouldFillPolicyActions, subscribeToScriptDebugEvents, unloadScripts, validateUIAgainstPolicy };