UNPKG

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
"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: {