c15t
Version:
Developer-first CMP for JavaScript: cookie banner, consent manager, preferences centre. GDPR ready with minimal setup and rich customization
1,211 lines (1,210 loc) • 61.4 kB
JavaScript
"use strict";
var __webpack_require__ = {};
(()=>{
__webpack_require__.d = (exports1, definition)=>{
for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
enumerable: true,
get: definition[key]
});
};
})();
(()=>{
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
})();
(()=>{
__webpack_require__.r = (exports1)=>{
if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
value: 'Module'
});
Object.defineProperty(exports1, '__esModule', {
value: true
});
};
})();
var __webpack_exports__ = {};
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
CustomClient: ()=>CustomClient,
consentTypes: ()=>gdpr_consentTypes,
C15tClient: ()=>C15tClient,
defaultTranslationConfig: ()=>defaultTranslationConfig,
deepMergeTranslations: ()=>translations_namespaceObject.deepMergeTranslations,
mergeTranslationConfigs: ()=>translations_namespaceObject.mergeTranslationConfigs,
createTrackingBlocker: ()=>createTrackingBlocker,
configureConsentManager: ()=>configureConsentManager,
detectBrowserLanguage: ()=>translations_namespaceObject.detectBrowserLanguage,
prepareTranslationConfig: ()=>translations_namespaceObject.prepareTranslationConfig,
OfflineClient: ()=>OfflineClient,
createConsentManagerStore: ()=>createConsentManagerStore,
API_ENDPOINTS: ()=>API_ENDPOINTS
});
const translations_namespaceObject = require("@c15t/translations");
const API_ENDPOINTS = {
SHOW_CONSENT_BANNER: '/show-consent-banner',
SET_CONSENT: '/consent/set',
VERIFY_CONSENT: '/consent/verify'
};
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 LEADING_SLASHES_REGEX = /^\/+/;
const TRAILING_SLASHES_REGEX = /\/+$/;
const delay = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
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);
});
}
class C15tClient {
backendURL;
headers;
customFetch;
corsMode;
retryConfig;
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.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.checkPendingConsentSubmissions();
}
resolveUrl(backendURL, path) {
if (ABSOLUTE_URL_REGEX.test(backendURL)) {
const backendURLObj = new URL(backendURL);
const basePath = backendURLObj.pathname.replace(TRAILING_SLASHES_REGEX, '');
const cleanPath = path.replace(LEADING_SLASHES_REGEX, '');
const newPath = `${basePath}/${cleanPath}`;
backendURLObj.pathname = newPath;
return backendURLObj.toString();
}
const cleanBase = backendURL.replace(TRAILING_SLASHES_REGEX, '');
const cleanPath = path.replace(LEADING_SLASHES_REGEX, '');
return `${cleanBase}/${cleanPath}`;
}
createResponseContext(isSuccess, data = null, error = null, response = null) {
return {
data,
error,
ok: isSuccess,
response
};
}
async fetcher(path, options) {
const finalRetryConfig = {
...this.retryConfig,
...options?.retryConfig || {},
retryableStatusCodes: options?.retryConfig?.retryableStatusCodes ?? this.retryConfig.retryableStatusCodes ?? DEFAULT_RETRY_CONFIG.retryableStatusCodes,
nonRetryableStatusCodes: options?.retryConfig?.nonRetryableStatusCodes ?? this.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 = this.customFetch || globalThis.fetch;
const resolvedUrl = this.resolveUrl(this.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: this.corsMode,
credentials: 'include',
headers: {
...this.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 = this.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 = this.createResponseContext(true, data, null, response);
options?.onSuccess?.(successResponse);
return successResponse;
}
const errorData = data;
const errorResponse = this.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)) {
console.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'
});
console.debug(`Custom retry strategy for ${path} with status ${response.status}: ${shouldRetryThisRequest}`);
} catch {
shouldRetryThisRequest = retryableStatusCodes?.includes(response.status) ?? false;
console.debug(`Custom retry strategy failed, falling back to status code check: ${shouldRetryThisRequest}`);
}
else {
shouldRetryThisRequest = retryableStatusCodes?.includes(response.status) ?? false;
console.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 = this.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 || this.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;
}
async showConsentBanner(options) {
try {
const response = await this.fetcher(API_ENDPOINTS.SHOW_CONSENT_BANNER, {
method: 'GET',
...options
});
if (response.ok || options?.testing) return response;
console.warn('API request failed, falling back to offline mode for consent banner');
return this.offlineFallbackForConsentBanner(options);
} catch (error) {
if (options?.testing || options?.disableFallback) {
const errorResponse = this.createResponseContext(false, null, {
message: error instanceof Error ? error.message : String(error),
status: 0,
code: 'NETWORK_ERROR',
cause: error
}, null);
if (options?.onError) await options.onError(errorResponse, API_ENDPOINTS.SHOW_CONSENT_BANNER);
return errorResponse;
}
console.warn('Error fetching consent banner info, falling back to offline mode:', error);
return this.offlineFallbackForConsentBanner(options);
}
}
async offlineFallbackForConsentBanner(options) {
let shouldShow = true;
let hasLocalStorageAccess = false;
try {
if ('undefined' != typeof window && window.localStorage) {
window.localStorage.setItem('c15t-storage-test-key', 'test');
window.localStorage.removeItem('c15t-storage-test-key');
hasLocalStorageAccess = true;
const storedConsent = window.localStorage.getItem('c15t-consent');
shouldShow = null === storedConsent;
}
} catch (error) {
console.warn('Failed to access localStorage in offline fallback:', error);
shouldShow = false;
}
const response = this.createResponseContext(true, {
showConsentBanner: shouldShow && hasLocalStorageAccess,
jurisdiction: {
code: 'NONE',
message: 'Unknown (offline mode)'
},
location: {
countryCode: null,
regionCode: null
},
translations: translations_namespaceObject.enTranslations
}, null, null);
if (options?.onSuccess) await options.onSuccess(response);
return response;
}
async setConsent(options) {
try {
const response = await this.fetcher(API_ENDPOINTS.SET_CONSENT, {
method: 'POST',
...options
});
if (response.ok && response.data || options?.testing) return response;
console.warn('API request failed, falling back to offline mode for setting consent');
return this.offlineFallbackForSetConsent(options);
} catch (error) {
if (options?.testing || options?.disableFallback) {
const errorResponse = this.createResponseContext(false, null, {
message: error instanceof Error ? error.message : String(error),
status: 0,
code: 'NETWORK_ERROR',
cause: error
}, null);
if (options?.onError) await options.onError(errorResponse, API_ENDPOINTS.SET_CONSENT);
return errorResponse;
}
console.warn('Error setting consent, falling back to offline mode:', error);
return this.offlineFallbackForSetConsent(options);
}
}
async offlineFallbackForSetConsent(options) {
const pendingSubmissionsKey = 'c15t-pending-consent-submissions';
try {
if ('undefined' != typeof window && window.localStorage) {
window.localStorage.setItem('c15t-storage-test-key', 'test');
window.localStorage.removeItem('c15t-storage-test-key');
window.localStorage.setItem('c15t-consent', JSON.stringify({
timestamp: new Date().toISOString(),
preferences: options?.body?.preferences || {}
}));
if (options?.body) {
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));
console.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 = this.createResponseContext(true, null, null, null);
if (options?.onSuccess) await options.onSuccess(response);
return response;
}
async verifyConsent(options) {
const response = await this.fetcher(API_ENDPOINTS.VERIFY_CONSENT, {
method: 'POST',
...options
});
return response;
}
async $fetch(path, options) {
return await this.fetcher(path, options);
}
checkPendingConsentSubmissions() {
const pendingSubmissionsKey = 'c15t-pending-consent-submissions';
if ('undefined' == 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);
console.log(`Found ${pendingSubmissions.length} pending consent submission(s) to retry`);
setTimeout(()=>{
this.processPendingSubmissions(pendingSubmissions);
}, 2000);
} catch (error) {
console.warn('Failed to check for pending consent submissions:', error);
}
}
async processPendingSubmissions(submissions) {
const pendingSubmissionsKey = 'c15t-pending-consent-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];
try {
console.log('Retrying consent submission:', submission);
const response = await this.fetcher(API_ENDPOINTS.SET_CONSENT, {
method: 'POST',
body: submission
});
if (response.ok) {
console.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 ('undefined' != typeof window && window.localStorage) if (remainingSubmissions.length > 0) {
window.localStorage.setItem(pendingSubmissionsKey, JSON.stringify(remainingSubmissions));
console.log(`${remainingSubmissions.length} consent submissions still pending for future retry`);
} else {
window.localStorage.removeItem(pendingSubmissionsKey);
console.log('All pending consent submissions processed successfully');
}
} catch (error) {
console.warn('Error updating pending submissions storage:', error);
}
}
}
const client_custom_LEADING_SLASHES_REGEX = /^\/+/;
class CustomClient {
endpointHandlers;
dynamicHandlers = {};
constructor(options){
this.endpointHandlers = options.endpointHandlers;
}
createErrorResponse(message, status = 500, code = 'HANDLER_ERROR', cause) {
return {
data: null,
error: {
message,
status,
code,
cause
},
ok: false,
response: null
};
}
async executeHandler(handlerKey, options) {
const handler = this.endpointHandlers[handlerKey];
if (!handler) {
const errorResponse = this.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 = this.createErrorResponse(error instanceof Error ? error.message : String(error), 0, 'HANDLER_ERROR', error);
if (options?.throw) throw error;
return errorResponse;
}
}
async showConsentBanner(options) {
return await this.executeHandler('showConsentBanner', options);
}
async setConsent(options) {
return await this.executeHandler('setConsent', options);
}
async verifyConsent(options) {
return await this.executeHandler('verifyConsent', options);
}
registerHandler(path, handler) {
this.dynamicHandlers[path] = handler;
}
async $fetch(path, options) {
const endpointName = path.replace(client_custom_LEADING_SLASHES_REGEX, '').split('/')[0];
if (path in this.dynamicHandlers) {
const handler = this.dynamicHandlers[path];
try {
return await handler(options);
} catch (error) {
const errorResponse = this.createErrorResponse(error instanceof Error ? error.message : String(error), 0, 'HANDLER_ERROR', error);
return errorResponse;
}
}
if (!endpointName || !(endpointName in this.endpointHandlers)) {
const errorResponse = this.createErrorResponse(`No endpoint handler found for '${path}'`, 404, 'ENDPOINT_NOT_FOUND');
return errorResponse;
}
return await this.executeHandler(endpointName, options);
}
}
const defaultTranslationConfig = {
translations: {
en: translations_namespaceObject.enTranslations
},
defaultLanguage: 'en',
disableAutoLanguageSwitch: false
};
class OfflineClient {
createResponseContext(data = null) {
return {
data,
error: null,
ok: true,
response: null
};
}
async handleOfflineResponse(options) {
const emptyResponse = this.createResponseContext();
if (options?.onSuccess) await options.onSuccess(emptyResponse);
return emptyResponse;
}
async showConsentBanner(options) {
let shouldShow = true;
try {
if ('undefined' != typeof window && window.localStorage) {
window.localStorage.setItem('c15t-storage-test-key', 'test');
window.localStorage.removeItem('c15t-storage-test-key');
const storedConsent = window.localStorage.getItem('c15t-consent');
shouldShow = null === storedConsent;
}
} catch (error) {
console.warn('Failed to access localStorage:', error);
shouldShow = false;
}
const response = this.createResponseContext({
showConsentBanner: shouldShow,
jurisdiction: {
code: 'GDPR',
message: 'EU'
},
branding: 'c15t',
location: {
countryCode: 'GB',
regionCode: null
},
translations: {
language: defaultTranslationConfig.defaultLanguage,
translations: defaultTranslationConfig.translations[defaultTranslationConfig.defaultLanguage ?? 'en']
}
});
if (options?.onSuccess) await options.onSuccess(response);
return response;
}
async setConsent(options) {
try {
if ('undefined' != typeof window && window.localStorage) {
window.localStorage.setItem('c15t-storage-test-key', 'test');
window.localStorage.removeItem('c15t-storage-test-key');
window.localStorage.setItem('c15t-consent', JSON.stringify({
timestamp: new Date().toISOString(),
preferences: options?.body?.preferences || {}
}));
}
} catch (error) {
console.warn('Failed to write to localStorage:', error);
}
return await this.handleOfflineResponse(options);
}
async verifyConsent(options) {
return await this.handleOfflineResponse(options);
}
async $fetch(_path, options) {
return await this.handleOfflineResponse(options);
}
}
const DEFAULT_BACKEND_URL = '/api/c15t';
const DEFAULT_CLIENT_MODE = 'c15t';
const clientRegistry = new Map();
function getClientCacheKey(options) {
if ('offline' === options.mode) return 'offline';
if ('custom' === options.mode) {
const handlerKeys = Object.keys(options.endpointHandlers || {}).sort().join(',');
return `custom:${handlerKeys}`;
}
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 `c15t:${options.backendURL || ''}${headersPart}`;
}
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 = options.mode || DEFAULT_CLIENT_MODE;
let client;
switch(mode){
case 'custom':
{
const customOptions = options;
client = new CustomClient({
endpointHandlers: customOptions.endpointHandlers
});
break;
}
case 'offline':
client = new OfflineClient();
break;
default:
{
const c15tOptions = options;
client = new C15tClient({
backendURL: c15tOptions.backendURL || DEFAULT_BACKEND_URL,
headers: c15tOptions.headers,
customFetch: c15tOptions.customFetch,
retryConfig: c15tOptions.retryConfig
});
break;
}
}
clientRegistry.set(cacheKey, client);
return client;
}
const DEFAULT_DOMAIN_CONSENT_MAP = {
'www.google-analytics.com': 'measurement',
'analytics.google.com': 'measurement',
'www.googletagmanager.com': 'measurement',
'stats.g.doubleclick.net': 'measurement',
'ampcid.google.com': 'measurement',
'analytics.twitter.com': 'measurement',
'analytics.pinterest.com': 'measurement',
'dc.services.visualstudio.com': 'measurement',
'www.clarity.ms': 'measurement',
'www.hotjar.com': 'measurement',
'static.hotjar.com': 'measurement',
"script.hotjar.com": 'measurement',
'insights.hotjar.com': 'measurement',
'mouseflow.com': 'measurement',
'api.mouseflow.com': 'measurement',
'tools.mouseflow.com': 'measurement',
'cdn.heapanalytics.com': 'measurement',
'plausible.io': 'measurement',
'matomo.cloud': 'measurement',
'matomo.org': 'measurement',
'mixpanel.com': 'measurement',
'api.mixpanel.com': 'measurement',
'sentry.io': 'measurement',
'browser.sentry-cdn.com': 'measurement',
'js.monitor.azure.com': 'measurement',
'stats.wp.com': 'measurement',
'pixel.wp.com': 'measurement',
'analytics.amplitude.com': 'measurement',
'api2.amplitude.com': 'measurement',
'cdn.amplitude.com': 'measurement',
'api.segment.io': 'measurement',
'cdn.segment.com': 'measurement',
'api.segment.com': 'measurement',
'pendo.io': 'measurement',
'data.pendo.io': 'measurement',
'cdn.pendo.io': 'measurement',
'connect.facebook.net': 'marketing',
'platform.twitter.com': 'marketing',
'platform.linkedin.com': 'marketing',
'www.googleadservices.com': 'marketing',
'doubleclick.net': 'marketing',
'googleads.g.doubleclick.net': 'marketing',
'ad.doubleclick.net': 'marketing',
'www.facebook.com': 'marketing',
'ads.linkedin.com': 'marketing',
'ads-api.tiktok.com': 'marketing',
'analytics.tiktok.com': 'marketing',
'business.tiktok.com': 'marketing',
'ads.pinterest.com': 'marketing',
'log.pinterest.com': 'marketing',
'ads-twitter.com': 'marketing',
'static.ads-twitter.com': 'marketing',
'advertising.twitter.com': 'marketing',
'ads.yahoo.com': 'marketing',
'sp.analytics.yahoo.com': 'marketing',
'gemini.yahoo.com': 'marketing',
'adroll.com': 'marketing',
'a.adroll.com': 'marketing',
'd.adroll.com': 'marketing',
's.adroll.com': 'marketing',
'adform.net': 'marketing',
'track.adform.net': 'marketing',
'dmp.adform.net': 'marketing',
'criteo.com': 'marketing',
'static.criteo.net': 'marketing',
'bidder.criteo.com': 'marketing',
'dynamic.criteo.com': 'marketing',
'gum.criteo.com': 'marketing',
'taboola.com': 'marketing',
'cdn.taboola.com': 'marketing',
'trc.taboola.com': 'marketing',
'outbrain.com': 'marketing',
'widgets.outbrain.com': 'marketing',
'tr.outbrain.com': 'marketing',
'amplify.outbrain.com': 'marketing',
'bing.com': 'marketing',
'bat.bing.com': 'marketing',
'clarity.ms': 'marketing',
'quantserve.com': 'marketing',
'secure.quantserve.com': 'marketing',
'pixel.quantserve.com': 'marketing',
'exelator.com': 'marketing',
'load.exelator.com': 'marketing',
'api.exelator.com': 'marketing',
'ad.360yield.com': 'marketing',
'match.360yield.com': 'marketing',
'ad.turn.com': 'marketing',
'r.turn.com': 'marketing',
'd.turn.com': 'marketing',
'cdn.jsdelivr.net': 'functionality',
'ajax.googleapis.com': 'functionality',
'fonts.googleapis.com': 'functionality',
'maps.googleapis.com': 'functionality',
'www.recaptcha.net': 'functionality',
'recaptcha.net': 'functionality',
'www.gstatic.com': 'functionality',
'fonts.gstatic.com': 'functionality',
'cdnjs.cloudflare.com': 'functionality',
'unpkg.com': 'functionality',
'code.jquery.com': 'functionality',
'maxcdn.bootstrapcdn.com': 'functionality',
'cdn.datatables.net': 'functionality',
'js.stripe.com': 'functionality',
'api.stripe.com': 'functionality',
'checkout.stripe.com': 'functionality',
'js.braintreegateway.com': 'functionality',
'api.braintreegateway.com': 'functionality',
'cdn.shopify.com': 'functionality',
'js.intercomcdn.com': 'functionality',
'widget.intercom.io': 'functionality',
'cdn.auth0.com': 'functionality',
'js.pusher.com': 'functionality',
'sockjs.pusher.com': 'functionality',
'app.optimizely.com': 'experience',
'cdn.optimizely.com': 'experience',
'logx.optimizely.com': 'experience',
'cdn.mouseflow.com': 'experience',
'fullstory.com': 'experience',
'rs.fullstory.com': 'experience',
'edge.fullstory.com': 'experience',
'vwo.com': 'experience',
'dev.visualwebsiteoptimizer.com': 'experience',
'assets.adobedtm.com': 'experience',
'cdn.tt.omtrdc.net': 'experience',
'demdex.net': 'experience',
'sc.omtrdc.net': 'experience',
'crazyegg.com': 'experience',
"script.crazyegg.com": 'experience',
'tracking.crazyegg.com': 'experience',
'luckyorange.com': 'experience',
'cdn.luckyorange.com': 'experience',
'w1.luckyorange.com': 'experience',
'upload.luckyorange.com': 'experience',
'clicktale.net': 'experience',
'cdn.clicktale.net': 'experience',
'conductor.clicktale.net': 'experience',
'userzoom.com': 'experience',
'cdn.userzoom.com': 'experience',
'api.userzoom.com': 'experience',
'contentsquare.net': 'experience',
't.contentsquare.net': 'experience',
'app.contentsquare.com': 'experience'
};
const tracking_domains = DEFAULT_DOMAIN_CONSENT_MAP;
function createDefaultConsentState() {
return {
experience: false,
functionality: false,
marketing: false,
measurement: false,
necessary: true
};
}
const WWW_PREFIX_REGEX = /^www\./;
const PORT_NUMBER_REGEX = /:\d+$/;
function createTrackingBlocker(config = {}, initialConsents) {
const blockerConfig = {
disableAutomaticBlocking: false,
...config,
domainConsentMap: config.overrideDomainConsentMap ? config.domainConsentMap : {
...tracking_domains,
...config.domainConsentMap
}
};
let consents = initialConsents || createDefaultConsentState();
const originalFetch = window.fetch;
const originalXHR = window.XMLHttpRequest;
function normalizeDomain(domain) {
return domain.toLowerCase().replace(WWW_PREFIX_REGEX, '').replace(PORT_NUMBER_REGEX, '').trim();
}
function findMatchingDomain(domain, domainMap) {
const normalizedDomain = normalizeDomain(domain);
const directMatch = domainMap[normalizedDomain];
if (directMatch) return directMatch;
for (const [mapDomain, consent] of Object.entries(domainMap)){
const normalizedMapDomain = normalizeDomain(mapDomain);
if (normalizedDomain.endsWith(`.${normalizedMapDomain}`) || normalizedDomain === normalizedMapDomain) return consent;
}
}
function isRequestAllowed(url) {
try {
const domain = new URL(url).hostname;
const requiredConsent = findMatchingDomain(domain, blockerConfig.domainConsentMap || {});
if (!requiredConsent) return true;
const isAllowed = true === consents[requiredConsent];
return isAllowed;
} catch {
return true;
}
}
function dispatchConsentBlockedEvent(url) {
document.dispatchEvent(new CustomEvent('ConsentBlockedRequest', {
detail: {
url
}
}));
}
function interceptNetworkRequests() {
if (window.fetch === originalFetch) window.fetch = async (input, init)=>{
const url = input instanceof Request ? input.url : input.toString();
if (!isRequestAllowed(url)) {
dispatchConsentBlockedEvent(url);
return Promise.reject(new Error(`Request to ${url} blocked due to missing consent`));
}
return await originalFetch.call(window, input, init);
};
if (window.XMLHttpRequest === originalXHR) window.XMLHttpRequest = class extends originalXHR {
open(method, url, async = true, username, password) {
if (!isRequestAllowed(url.toString())) {
dispatchConsentBlockedEvent(url.toString());
throw new Error(`Request to ${url} blocked due to missing consent`);
}
super.open(method, url, async, username, password);
}
};
}
function restoreOriginalRequests() {
if (window.fetch !== originalFetch) window.fetch = originalFetch;
if (window.XMLHttpRequest !== originalXHR) window.XMLHttpRequest = originalXHR;
}
if (!blockerConfig.disableAutomaticBlocking) interceptNetworkRequests();
return {
updateConsents: (newConsents)=>{
consents = {
...consents,
...newConsents
};
},
destroy: ()=>{
restoreOriginalRequests();
}
};
}
const vanilla_namespaceObject = require("zustand/vanilla");
function getEffectiveConsents(consents, honorDoNotTrack) {
if (honorDoNotTrack && 'undefined' != typeof window && '1' === window.navigator.doNotTrack) return Object.keys(consents).reduce((acc, key)=>{
if (key in consents) acc[key] = 'necessary' === key;
return acc;
}, {});
return consents;
}
function hasConsentFor(consentType, consents, honorDoNotTrack) {
const effectiveConsents = getEffectiveConsents(consents, honorDoNotTrack);
return effectiveConsents[consentType] || false;
}
function consent_utils_hasConsented(consentInfo) {
return null !== consentInfo;
}
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,
showPopup: false
});
}
return false;
}
function updateStore(data, { set, get, initialTranslationConfig }, hasLocalStorageAccess) {
const { consentInfo, ignoreGeoLocation, callbacks, setDetectedCountry } = get();
const { translations, location, showConsentBanner } = data;
const updatedStore = {
isLoadingConsentInfo: false,
branding: data.branding ?? 'c15t',
...null === consentInfo ? {
showPopup: showConsentBanner && hasLocalStorageAccess || ignoreGeoLocation
} : {},
...data.jurisdiction?.code === 'NONE' && !data.showConsentBanner && {
consents: {
necessary: true,
functionality: true,
experience: true,
marketing: true,
measurement: true
}
},
locationInfo: {
countryCode: location?.countryCode ?? null,
regionCode: location?.regionCode ?? null,
jurisdiction: data.jurisdiction?.code ?? null,
jurisdictionMessage: data.jurisdiction?.message ?? null
},
jurisdictionInfo: data.jurisdiction
};
if (translations?.language && translations?.translations) {
const translationConfig = (0, translations_namespaceObject.prepareTranslationConfig)({
translations: {
[translations.language]: translations.translations
},
disableAutoLanguageSwitch: true,
defaultLanguage: translations.language
}, initialTranslationConfig);
updatedStore.translationConfig = translationConfig;
}
if (data.location?.countryCode) setDetectedCountry(data.location.countryCode);
updatedStore.hasFetchedBanner = true;
updatedStore.lastBannerFetchData = data;
set(updatedStore);
callbacks?.onBannerFetched?.({
showConsentBanner: data.showConsentBanner,
jurisdiction: data.jurisdiction,
location: data.location,
translations: {
language: translations.language,
translations: translations.translations
}
});
}
async function fetchConsentBannerInfo(config) {
const { get, set, manager, initialData } = config;
const { hasConsented, callbacks } = get();
if ('undefined' == typeof window || hasConsented()) return void set({
isLoadingConsentInfo: false
});
const hasLocalStorageAccess = checkLocalStorageAccess(set);
if (!hasLocalStorageAccess) return;
set({
isLoadingConsentInfo: true
});
if (initialData) {
const showConsentBanner = await initialData;
if (showConsentBanner) {
updateStore(showConsentBanner, config, true);
return showConsentBanner;
}
}
try {
const { data, error } = await manager.showConsentBanner({
onError: callbacks.onError ? (context)=>{
if (callbacks.onError) callbacks.onError({
error: context.error?.message || 'Unknown error'
});
} : void 0
});
if (error || !data) throw new Error(`Failed to fetch consent banner info: ${error?.message}`);
updateStore(data, config, hasLocalStorageAccess);
return data;
} catch (error) {
console.error('Error fetching consent banner information:', error);
set({
isLoadingConsentInfo: false
});
const errorMessage = error instanceof Error ? error.message : 'Unknown error fetching consent banner information';
callbacks.onError?.({
error: errorMessage
});
set({
showPopup: false
});
return;
}
}
const DEFAULT_GTM_CONSENT_CONFIG = {
functionality_storage: 'denied',
security_storage: 'denied',
analytics_storage: 'denied',
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
personalization_storage: 'denied'
};
const CONSENT_STATE_TO_GTM_MAPPING = {
necessary: [
'security_storage'
],
functionality: [
'functionality_storage'
],
measurement: [
'analytics_storage'
],
marketing: [
'ad_storage',
'ad_user_data',
'ad_personalization'
],
experience: [
'personalization_storage'
]
};
function mapConsentStateToGTM(consentState) {
const gtmConfig = {
...DEFAULT_GTM_CONSENT_CONFIG
};
for (const consentType of Object.keys(consentState)){
const isGranted = consentState[consentType];
const gtmConsentTypes = CONSENT_STATE_TO_GTM_MAPPING[consentType];
for (const gtmType of gtmConsentTypes)gtmConfig[gtmType] = isGranted ? 'granted' : 'denied';
}
return gtmConfig;
}
function initializeGTMDataLayer(gtm) {
const gtmConsent = gtm.consentState ? mapConsentStateToGTM(gtm.consentState) : DEFAULT_GTM_CONSENT_CONFIG;
const gtmSetupScript = document.createElement("script");
gtmSetupScript.textContent = `
window.dataLayer = window.dataLayer || [];
window.gtag = function gtag() {
window.dataLayer.push(arguments);
};
window.gtag('consent', 'default', {
...${JSON.stringify(gtmConsent)},
});
window.dataLayer.push({
'gtm.start': Date.now(),
event: 'gtm.js',
});
`;
if (!document.head) throw new Error("Document head is not available for script injection");
document.head.appendChild(gtmSetupScript);
}
function createGTMScript(gtm) {
const gtmScript = document.createElement("script");
gtmScript.async = true;
gtmScript.src = gtm.customScriptUrl ? gtm.customScriptUrl : `https://www.googletagmanager.com/gtm.js?id=${gtm.id}`;
if (!document.head) throw new Error("Document head is not available for script injection");
document.head.appendChild(gtmScript);
}
function setupGTM(gtm) {
const id = gtm.id;
if (!id || 0 === id.trim().length) throw new Error('GTM container ID is required and must be a non-empty string');
if ('undefined' == typeof window || 'undefined' == typeof document) return;
initializeGTMDataLayer(gtm);
createGTMScript(gtm);
}
function updateGTMConsent(consentState) {
if (!window.gtag) return;
window.gtag('consent', 'update', mapConsentStateToGTM(consentState));
}
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) {
return evaluateConditionRecursive(condition, consents);
}
const gdpr_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 version = '1.6.0';
const STORAGE_KEY = 'privacy-consent-storage';
const initialState = {
config: {
pkg: 'c15t',
version: version,
mode: 'Unknown'
},
consents: gdpr_consentTypes.reduce((acc, consent)=>{
acc[consent.name] = consent.defaultValue;
return acc;
}, {}),
selectedConsents: gdpr_consentTypes.reduce((acc, consent)=>{
acc[consent.name] = consent.defaultValue;
return acc;
}, {}),
consentInfo: null,
branding: 'c15t',
showPopup: true,
isLoadingConsentInfo: false,
hasFetchedBanner: false,
lastBannerFetchData: null,
gdprTypes: [
'necessary',
'marketing'
],
isPrivacyDialogOpen: false,
isConsentDomain: false,
complianceSettings: {
gdpr: {
enabled: true,
appliesGlobally: true,
applies: true
},
ccpa: {
enabled: true,
appliesGlobally: false,
applies: void 0
},
lgpd: {
enabled: false,
appliesGlobally: false,
applies: void 0
},
usStatePrivacy: {
enabled: true,
appliesGlobally: false,
applies: void 0
}
},
callbacks: {},
detectedCountry: null,
locationInfo: null,
jurisdictionInfo: null,
translationConfig: defaultTranslationConfig,
includeNonDisplayedConsents: false,
consentTypes: gdpr_consentTypes,
ignoreGeoLocation: false,
privacySettings: {