c15t
Version:
<div align="center"> <img src="https://c15t.com/logo-icon.png" alt="c15t Logo" width="64" height="64" /> <h1>c15t</h1> <p>Transform privacy consent from a compliance checkbox into a fully observable system</p>
1,170 lines • 65.8 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 API_ENDPOINTS = {
SHOW_CONSENT_BANNER: '/show-consent-banner',
SET_CONSENT: '/consent/set',
VERIFY_CONSENT: '/consent/verify'
};
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 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;
callbacks;
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.callbacks = options.callbacks;
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}`;
}
getCallbacks() {
return this.callbacks;
}
setCallbacks(callbacks) {
this.callbacks = callbacks;
}
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);
this.callbacks?.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);
this.callbacks?.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);
this.callbacks?.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);
this.callbacks?.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) {
if (this.callbacks?.onConsentBannerFetched && response.data) {
const callbackPayload = {
ok: response.ok,
error: response.error,
response: null,
data: {
showConsentBanner: response.data.showConsentBanner,
jurisdiction: response.data.jurisdiction,
location: {
countryCode: response.data.location.countryCode,
regionCode: response.data.location.regionCode
}
}
};
this.callbacks.onConsentBannerFetched(callbackPayload);
}
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: 'XX',
regionCode: null
}
}, null, null);
if (options?.onSuccess) await options.onSuccess(response);
if (this.callbacks?.onConsentBannerFetched && response.data) {
const callbackPayload = {
ok: response.ok,
error: response.error,
response: null,
data: {
showConsentBanner: response.data.showConsentBanner,
jurisdiction: response.data.jurisdiction,
location: {
countryCode: response.data.location.countryCode,
regionCode: response.data.location.regionCode
}
}
};
this.callbacks.onConsentBannerFetched(callbackPayload);
}
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) {
if (this.callbacks?.onConsentSet) {
const callbackPayload = {
ok: response.ok,
error: response.error,
response: null,
data: {
type: options?.body?.type || 'cookie_banner',
preferences: options?.body?.preferences || {},
domain: options?.body?.domain
}
};
this.callbacks.onConsentSet(callbackPayload);
}
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);
if (this.callbacks?.onConsentSet) {
const callbackPayload = {
...response,
data: {
type: options?.body?.type || 'cookie_banner',
preferences: options?.body?.preferences || {},
domain: options?.body?.domain
}
};
this.callbacks.onConsentSet(callbackPayload);
}
return response;
}
async verifyConsent(options) {
const response = await this.fetcher(API_ENDPOINTS.VERIFY_CONSENT, {
method: 'POST',
...options
});
if (response.ok && this.callbacks?.onConsentVerified && response.data) {
const callbackPayload = {
ok: response.ok,
error: response.error,
response: null,
data: {
type: options?.body?.type || 'cookie_banner',
preferences: options?.body?.preferences || [],
valid: response.data.isValid,
domain: options?.body?.domain
}
};
this.callbacks.onConsentVerified(callbackPayload);
}
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 = {};
callbacks;
constructor(options){
this.endpointHandlers = options.endpointHandlers;
this.callbacks = options.callbacks;
}
getCallbacks() {
return this.callbacks;
}
setCallbacks(callbacks) {
this.callbacks = callbacks;
}
createErrorResponse(message, status = 500, code = 'HANDLER_ERROR', cause) {
return {
data: null,
error: {
message,
status,
code,
cause
},
ok: false,
response: null
};
}
async executeHandler(handlerKey, options, callbackKey) {
const handler = this.endpointHandlers[handlerKey];
if (!handler) {
const errorResponse = this.createErrorResponse(`No endpoint handler found for '${String(handlerKey)}'`, 404, 'ENDPOINT_NOT_FOUND');
this.callbacks?.onError?.(errorResponse, String(handlerKey));
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
};
if (normalizedResponse.ok && callbackKey && this.callbacks?.[callbackKey]) {
const callback = this.callbacks[callbackKey];
let callbackPayload;
if ('onConsentBannerFetched' === callbackKey && normalizedResponse.data) {
const data = normalizedResponse.data;
callbackPayload = {
...normalizedResponse,
data: {
showConsentBanner: data.showConsentBanner,
jurisdiction: data.jurisdiction,
location: {
countryCode: data.location.countryCode || 'GB',
regionCode: data.location.regionCode || null
}
}
};
} else callbackPayload = 'onConsentSet' === callbackKey && normalizedResponse.data ? {
...normalizedResponse,
data: {
type: options?.body?.type || 'cookie_banner',
preferences: options?.body?.preferences || {},
domain: options?.body?.domain
}
} : 'onConsentVerified' === callbackKey && normalizedResponse.data ? {
...normalizedResponse,
data: {
type: options?.body?.type || 'cookie_banner',
preferences: options?.body?.preferences || [],
valid: true,
domain: options?.body?.domain
}
} : normalizedResponse;
callback(callbackPayload);
}
return normalizedResponse;
} catch (error) {
const errorResponse = this.createErrorResponse(error instanceof Error ? error.message : String(error), 0, 'HANDLER_ERROR', error);
this.callbacks?.onError?.(errorResponse, String(handlerKey));
if (options?.throw) throw error;
return errorResponse;
}
}
async showConsentBanner(options) {
return await this.executeHandler('showConsentBanner', options, 'onConsentBannerFetched');
}
async setConsent(options) {
return await this.executeHandler('setConsent', options, 'onConsentSet');
}
async verifyConsent(options) {
return await this.executeHandler('verifyConsent', options, 'onConsentVerified');
}
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);
this.callbacks?.onError?.(errorResponse, path);
return errorResponse;
}
}
if (!endpointName || !(endpointName in this.endpointHandlers)) {
const errorResponse = this.createErrorResponse(`No endpoint handler found for '${path}'`, 404, 'ENDPOINT_NOT_FOUND');
this.callbacks?.onError?.(errorResponse, path);
return errorResponse;
}
return await this.executeHandler(endpointName, options);
}
}
const translations_namespaceObject = require("@c15t/translations");
const defaultTranslationConfig = {
translations: {
en: translations_namespaceObject.enTranslations
},
defaultLanguage: 'en',
disableAutoLanguageSwitch: false
};
class OfflineClient {
callbacks;
constructor(options = {}){
this.callbacks = options.callbacks;
}
getCallbacks() {
return this.callbacks;
}
setCallbacks(callbacks) {
this.callbacks = callbacks;
}
createResponseContext(data = null) {
return {
data,
error: null,
ok: true,
response: null
};
}
async handleOfflineResponse(options, callbackKey, callbackPayload) {
const emptyResponse = this.createResponseContext();
if (options?.onSuccess) await options.onSuccess(emptyResponse);
if (callbackKey && this.callbacks?.[callbackKey]) {
const callback = this.callbacks[callbackKey];
const payload = callbackPayload ? this.createResponseContext(callbackPayload) : emptyResponse;
callback(payload);
}
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'
},
location: {
countryCode: 'GB',
regionCode: null
},
translations: {
language: defaultTranslationConfig.defaultLanguage,
translations: defaultTranslationConfig.translations[defaultTranslationConfig.defaultLanguage ?? 'en']
}
});
if (this.callbacks?.onConsentBannerFetched && response.data) {
const callbackPayload = this.createResponseContext({
showConsentBanner: response.data.showConsentBanner,
jurisdiction: response.data.jurisdiction,
location: {
countryCode: response.data.location.countryCode || 'GB',
regionCode: response.data.location.regionCode || null
}
});
this.callbacks.onConsentBannerFetched(callbackPayload);
}
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);
}
const setConsentCallbackPayload = {
type: options?.body?.type || 'cookie_banner',
preferences: options?.body?.preferences || {},
domain: options?.body?.domain || ''
};
return await this.handleOfflineResponse(options, 'onConsentSet', setConsentCallbackPayload);
}
async verifyConsent(options) {
const verifiedCallbackPayload = {
type: options?.body?.type || 'cookie_banner',
preferences: options?.body?.preferences || [],
valid: true,
domain: options?.body?.domain || ''
};
return await this.handleOfflineResponse(options, 'onConsentVerified', verifiedCallbackPayload);
}
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) {
if ('getCallbacks' === prop) return ()=>options.callbacks;
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,
callbacks: customOptions.callbacks
});
break;
}
case 'offline':
client = new OfflineClient({
callbacks: options.callbacks
});
break;
default:
{
const c15tOptions = options;
client = new C15tClient({
backendURL: c15tOptions.backendURL || DEFAULT_BACKEND_URL,
headers: c15tOptions.headers,
customFetch: c15tOptions.customFetch,
callbacks: c15tOptions.callbacks,
retryConfig: c15tOptions.retryConfig
});
break;
}
}
clientRegistry.set(cacheKey, client);
return client;
}
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, setDetectedCountry, callbacks, ignoreGeoLocation } = get();
const { translations, location, jurisdiction, showConsentBanner } = data;
const updatedStore = {
isLoadingConsentInfo: false,
...null === consentInfo ? {
showPopup: showConsentBanner && hasLocalStorageAccess || ignoreGeoLocation
} : {},
...'NONE' === data.jurisdiction.code && !data.showConsentBanner && {
consents: {
necessary: true,
functionality: true,
experience: true,
marketing: true,
measurement: true
}
},
locationInfo: {
countryCode: location?.countryCode ?? '',
regionCode: location?.regionCode ?? ''
},
jurisdictionInfo: jurisdiction
};
if (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);
if (data.location.regionCode) callbacks.onLocationDetected?.({
countryCode: data.location.countryCode,
regionCode: data.location.regionCode
});
}
set(updatedStore);
}
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) {
set({
isLoadingConsentInfo: false
});
updateStore(showConsentBanner, config, true);
return showConsentBanner;
}
}
try {
const { data, error } = await manager.showConsentBanner({
onError: callbacks.onError ? (context)=>{
if (callbacks.onError) callbacks.onError(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?.(errorMessage);
set({
showPopup: false
});
return;
}
}
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