@freshjuice/zest 0.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -70
- package/dist/zest.de.js +776 -286
- package/dist/zest.de.js.map +1 -1
- package/dist/zest.de.min.js +1 -1
- package/dist/zest.en.js +776 -286
- package/dist/zest.en.js.map +1 -1
- package/dist/zest.en.min.js +1 -1
- package/dist/zest.es.js +776 -286
- package/dist/zest.es.js.map +1 -1
- package/dist/zest.es.min.js +1 -1
- package/dist/zest.esm.js +776 -286
- package/dist/zest.esm.js.map +1 -1
- package/dist/zest.esm.min.js +1 -1
- package/dist/zest.fr.js +776 -286
- package/dist/zest.fr.js.map +1 -1
- package/dist/zest.fr.min.js +1 -1
- package/dist/zest.headless.esm.js +2299 -0
- package/dist/zest.headless.esm.js.map +1 -0
- package/dist/zest.headless.esm.min.js +1 -0
- package/dist/zest.it.js +776 -286
- package/dist/zest.it.js.map +1 -1
- package/dist/zest.it.min.js +1 -1
- package/dist/zest.ja.js +776 -286
- package/dist/zest.ja.js.map +1 -1
- package/dist/zest.ja.min.js +1 -1
- package/dist/zest.js +776 -286
- package/dist/zest.js.map +1 -1
- package/dist/zest.min.js +1 -1
- package/dist/zest.nl.js +776 -286
- package/dist/zest.nl.js.map +1 -1
- package/dist/zest.nl.min.js +1 -1
- package/dist/zest.pl.js +776 -286
- package/dist/zest.pl.js.map +1 -1
- package/dist/zest.pl.min.js +1 -1
- package/dist/zest.pt.js +776 -286
- package/dist/zest.pt.js.map +1 -1
- package/dist/zest.pt.min.js +1 -1
- package/dist/zest.ru.js +776 -286
- package/dist/zest.ru.js.map +1 -1
- package/dist/zest.ru.min.js +1 -1
- package/dist/zest.uk.js +776 -286
- package/dist/zest.uk.js.map +1 -1
- package/dist/zest.uk.min.js +1 -1
- package/dist/zest.zh.js +776 -286
- package/dist/zest.zh.js.map +1 -1
- package/dist/zest.zh.min.js +1 -1
- package/package.json +17 -4
- package/src/api/public-api.js +97 -0
- package/src/config/defaults.js +150 -0
- package/src/config/parser.js +104 -0
- package/src/core/categories.js +52 -0
- package/src/core/cookie-interceptor.js +131 -0
- package/src/core/dnt.js +56 -0
- package/src/core/known-trackers.js +195 -0
- package/src/core/pattern-matcher.js +111 -0
- package/src/core/script-blocker.js +314 -0
- package/src/core/security.js +204 -0
- package/src/core/storage-interceptor.js +173 -0
- package/src/core-lifecycle.js +192 -0
- package/src/headless.js +133 -0
- package/src/i18n/lang-en.js +54 -0
- package/src/i18n/single/lang-de.js +55 -0
- package/src/i18n/single/lang-en.js +55 -0
- package/src/i18n/single/lang-es.js +55 -0
- package/src/i18n/single/lang-fr.js +55 -0
- package/src/i18n/single/lang-it.js +55 -0
- package/src/i18n/single/lang-ja.js +55 -0
- package/src/i18n/single/lang-nl.js +55 -0
- package/src/i18n/single/lang-pl.js +55 -0
- package/src/i18n/single/lang-pt.js +55 -0
- package/src/i18n/single/lang-ru.js +55 -0
- package/src/i18n/single/lang-uk.js +55 -0
- package/src/i18n/single/lang-zh.js +55 -0
- package/src/i18n/translations.js +546 -0
- package/src/index.js +266 -0
- package/src/integrations/consent-signals.js +71 -0
- package/src/storage/consent-store.js +201 -0
- package/src/storage/events.js +84 -0
- package/src/ui/banner.js +134 -0
- package/src/ui/modal.js +215 -0
- package/src/ui/styles.js +519 -0
- package/src/ui/widget.js +105 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zest - Lightweight Cookie Consent Toolkit
|
|
3
|
+
* Main entry (full build: logic + UI).
|
|
4
|
+
*
|
|
5
|
+
* For a logic-only build without any CSS / Shadow DOM mounting, import
|
|
6
|
+
* from `@freshjuice/zest/headless` instead.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Core lifecycle (UI-agnostic)
|
|
10
|
+
import {
|
|
11
|
+
coreInit,
|
|
12
|
+
coreAcceptAll,
|
|
13
|
+
coreRejectAll,
|
|
14
|
+
coreUpdateConsent,
|
|
15
|
+
coreReset,
|
|
16
|
+
isInitialized,
|
|
17
|
+
getActiveConfig
|
|
18
|
+
} from './core-lifecycle.js';
|
|
19
|
+
|
|
20
|
+
// Consent store + events
|
|
21
|
+
import {
|
|
22
|
+
getConsent,
|
|
23
|
+
hasConsent,
|
|
24
|
+
hasConsentDecision,
|
|
25
|
+
getConsentProof
|
|
26
|
+
} from './storage/consent-store.js';
|
|
27
|
+
import { emitShow, emitHide, EVENTS, on, once } from './storage/events.js';
|
|
28
|
+
|
|
29
|
+
// DNT introspection
|
|
30
|
+
import { isDoNotTrackEnabled, getDNTDetails } from './core/dnt.js';
|
|
31
|
+
|
|
32
|
+
// Config getters
|
|
33
|
+
import { getConfig, getCurrentConfig } from './config/parser.js';
|
|
34
|
+
|
|
35
|
+
// UI
|
|
36
|
+
import { showBanner, hideBanner, isBannerVisible } from './ui/banner.js';
|
|
37
|
+
import { showModal, hideModal, isModalVisible } from './ui/modal.js';
|
|
38
|
+
import { showWidget, hideWidget, removeWidget, isWidgetVisible } from './ui/widget.js';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Handle accept all — delegates consent logic to core, handles UI swap.
|
|
42
|
+
*/
|
|
43
|
+
function handleAcceptAll() {
|
|
44
|
+
coreAcceptAll();
|
|
45
|
+
const config = getActiveConfig();
|
|
46
|
+
|
|
47
|
+
hideBanner();
|
|
48
|
+
hideModal();
|
|
49
|
+
|
|
50
|
+
if (config?.showWidget) {
|
|
51
|
+
showWidget({ onClick: handleShowSettings });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Handle reject all.
|
|
57
|
+
*/
|
|
58
|
+
function handleRejectAll() {
|
|
59
|
+
coreRejectAll();
|
|
60
|
+
const config = getActiveConfig();
|
|
61
|
+
|
|
62
|
+
hideBanner();
|
|
63
|
+
hideModal();
|
|
64
|
+
|
|
65
|
+
if (config?.showWidget) {
|
|
66
|
+
showWidget({ onClick: handleShowSettings });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Handle save preferences from modal.
|
|
72
|
+
*/
|
|
73
|
+
function handleSavePreferences(selections) {
|
|
74
|
+
coreUpdateConsent(selections);
|
|
75
|
+
const config = getActiveConfig();
|
|
76
|
+
|
|
77
|
+
hideModal();
|
|
78
|
+
|
|
79
|
+
if (config?.showWidget) {
|
|
80
|
+
showWidget({ onClick: handleShowSettings });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Open the settings modal.
|
|
86
|
+
*/
|
|
87
|
+
function handleShowSettings() {
|
|
88
|
+
hideBanner();
|
|
89
|
+
hideWidget();
|
|
90
|
+
|
|
91
|
+
showModal(getConsent(), {
|
|
92
|
+
onSave: handleSavePreferences,
|
|
93
|
+
onAcceptAll: handleAcceptAll,
|
|
94
|
+
onRejectAll: handleRejectAll,
|
|
95
|
+
onClose: handleCloseModal
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
emitShow('modal');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Close the modal — either bring the widget back (decision made) or
|
|
103
|
+
* fall back to the banner (no decision yet).
|
|
104
|
+
*/
|
|
105
|
+
function handleCloseModal() {
|
|
106
|
+
hideModal();
|
|
107
|
+
emitHide('modal');
|
|
108
|
+
|
|
109
|
+
const config = getActiveConfig();
|
|
110
|
+
if (hasConsentDecision() && config?.showWidget) {
|
|
111
|
+
showWidget({ onClick: handleShowSettings });
|
|
112
|
+
} else {
|
|
113
|
+
showBanner({
|
|
114
|
+
onAcceptAll: handleAcceptAll,
|
|
115
|
+
onRejectAll: handleRejectAll,
|
|
116
|
+
onSettings: handleShowSettings
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Initialize Zest with UI.
|
|
123
|
+
*/
|
|
124
|
+
function init(userConfig = {}) {
|
|
125
|
+
const { alreadyInitialized, consent, hasDecision, dntApplied } = coreInit(userConfig);
|
|
126
|
+
if (alreadyInitialized) {
|
|
127
|
+
console.warn('[Zest] Already initialized');
|
|
128
|
+
return Zest;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const config = getActiveConfig();
|
|
132
|
+
|
|
133
|
+
if (!hasDecision && !dntApplied) {
|
|
134
|
+
showBanner({
|
|
135
|
+
onAcceptAll: handleAcceptAll,
|
|
136
|
+
onRejectAll: handleRejectAll,
|
|
137
|
+
onSettings: handleShowSettings
|
|
138
|
+
});
|
|
139
|
+
emitShow('banner');
|
|
140
|
+
} else if (config?.showWidget) {
|
|
141
|
+
showWidget({ onClick: handleShowSettings });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Zest;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const Zest = {
|
|
148
|
+
init,
|
|
149
|
+
|
|
150
|
+
// Banner control
|
|
151
|
+
show() {
|
|
152
|
+
if (!isInitialized()) {
|
|
153
|
+
console.warn('[Zest] Not initialized. Call Zest.init() first.');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
hideModal();
|
|
157
|
+
hideWidget();
|
|
158
|
+
showBanner({
|
|
159
|
+
onAcceptAll: handleAcceptAll,
|
|
160
|
+
onRejectAll: handleRejectAll,
|
|
161
|
+
onSettings: handleShowSettings
|
|
162
|
+
});
|
|
163
|
+
emitShow('banner');
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
hide() {
|
|
167
|
+
hideBanner();
|
|
168
|
+
emitHide('banner');
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// Settings modal
|
|
172
|
+
showSettings() {
|
|
173
|
+
if (!isInitialized()) {
|
|
174
|
+
console.warn('[Zest] Not initialized. Call Zest.init() first.');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
handleShowSettings();
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
hideSettings() {
|
|
181
|
+
hideModal();
|
|
182
|
+
emitHide('modal');
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
// Consent state
|
|
186
|
+
getConsent,
|
|
187
|
+
hasConsent,
|
|
188
|
+
hasConsentDecision,
|
|
189
|
+
getConsentProof,
|
|
190
|
+
|
|
191
|
+
// DNT
|
|
192
|
+
isDoNotTrackEnabled,
|
|
193
|
+
getDNTDetails,
|
|
194
|
+
|
|
195
|
+
// Programmatic accept / reject
|
|
196
|
+
acceptAll() {
|
|
197
|
+
if (!isInitialized()) {
|
|
198
|
+
console.warn('[Zest] Not initialized. Call Zest.init() first.');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
handleAcceptAll();
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
rejectAll() {
|
|
205
|
+
if (!isInitialized()) {
|
|
206
|
+
console.warn('[Zest] Not initialized. Call Zest.init() first.');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
handleRejectAll();
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
// Reset everything and reshow the banner
|
|
213
|
+
reset() {
|
|
214
|
+
coreReset();
|
|
215
|
+
hideModal();
|
|
216
|
+
removeWidget();
|
|
217
|
+
if (isInitialized()) {
|
|
218
|
+
showBanner({
|
|
219
|
+
onAcceptAll: handleAcceptAll,
|
|
220
|
+
onRejectAll: handleRejectAll,
|
|
221
|
+
onSettings: handleShowSettings
|
|
222
|
+
});
|
|
223
|
+
emitShow('banner');
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
// Config introspection
|
|
228
|
+
getConfig: getCurrentConfig,
|
|
229
|
+
|
|
230
|
+
// Events
|
|
231
|
+
on,
|
|
232
|
+
once,
|
|
233
|
+
EVENTS
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Auto-init if config present
|
|
237
|
+
if (typeof window !== 'undefined') {
|
|
238
|
+
// Make Zest available globally. defineProperty with writable:false +
|
|
239
|
+
// configurable:false stops a later-loaded script from replacing the
|
|
240
|
+
// global with a trojanned stand-in.
|
|
241
|
+
try {
|
|
242
|
+
Object.defineProperty(window, 'Zest', {
|
|
243
|
+
value: Object.freeze(Zest),
|
|
244
|
+
writable: false,
|
|
245
|
+
configurable: false,
|
|
246
|
+
enumerable: true
|
|
247
|
+
});
|
|
248
|
+
} catch (e) {
|
|
249
|
+
window.Zest = Zest;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const autoInit = () => {
|
|
253
|
+
const cfg = getConfig();
|
|
254
|
+
if (cfg.autoInit !== false) {
|
|
255
|
+
init(window.ZestConfig);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
if (document.readyState === 'loading') {
|
|
260
|
+
document.addEventListener('DOMContentLoaded', autoInit);
|
|
261
|
+
} else {
|
|
262
|
+
autoInit();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export default Zest;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent Signals - Optional vendor consent mode integrations
|
|
3
|
+
*
|
|
4
|
+
* Pushes consent state to Google Consent Mode v2 and/or Microsoft UET
|
|
5
|
+
* Consent Mode when enabled via config.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Map Zest consent state to Google Consent Mode v2 signals
|
|
10
|
+
*/
|
|
11
|
+
function toGoogleSignals(consent) {
|
|
12
|
+
const g = (val) => val ? 'granted' : 'denied';
|
|
13
|
+
return {
|
|
14
|
+
ad_storage: g(consent.marketing),
|
|
15
|
+
ad_user_data: g(consent.marketing),
|
|
16
|
+
ad_personalization: g(consent.marketing),
|
|
17
|
+
analytics_storage: g(consent.analytics),
|
|
18
|
+
functionality_storage: 'granted', // essential is always true
|
|
19
|
+
personalization_storage: g(consent.functional)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Push consent signal to Google via gtag or dataLayer fallback.
|
|
25
|
+
* Uses a local function to preserve the `arguments` object shape
|
|
26
|
+
* that gtag/dataLayer expects (not an array).
|
|
27
|
+
*/
|
|
28
|
+
function pushGoogle(type, signals) {
|
|
29
|
+
window.dataLayer = window.dataLayer || [];
|
|
30
|
+
if (typeof window.gtag === 'function') {
|
|
31
|
+
window.gtag('consent', type, signals);
|
|
32
|
+
} else {
|
|
33
|
+
function gtagFallback() { window.dataLayer.push(arguments); }
|
|
34
|
+
gtagFallback('consent', type, signals);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Map Zest consent state to Microsoft UET signal.
|
|
40
|
+
* Microsoft UET only exposes ad_storage.
|
|
41
|
+
*/
|
|
42
|
+
function toMicrosoftSignals(consent) {
|
|
43
|
+
return { ad_storage: consent.marketing ? 'granted' : 'denied' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Push consent signal to Microsoft UET
|
|
48
|
+
*/
|
|
49
|
+
function pushMicrosoft(type, signals) {
|
|
50
|
+
window.uetq = window.uetq || [];
|
|
51
|
+
window.uetq.push('consent', type, signals);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Apply consent signals to enabled vendor integrations.
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} consent Current Zest consent state
|
|
58
|
+
* @param {Object} config Merged Zest config
|
|
59
|
+
* @param {boolean} isDefault true on first call (pushes 'default'), false for updates
|
|
60
|
+
*/
|
|
61
|
+
export function applyConsentSignals(consent, config, isDefault) {
|
|
62
|
+
const type = isDefault ? 'default' : 'update';
|
|
63
|
+
|
|
64
|
+
if (config.consentModeGoogle) {
|
|
65
|
+
pushGoogle(type, toGoogleSignals(consent));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (config.consentModeMicrosoft) {
|
|
69
|
+
pushMicrosoft(type, toMicrosoftSignals(consent));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent Store - Manages consent state persistence
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getDefaultConsent, getCategoryIds } from '../core/categories.js';
|
|
6
|
+
import { getOriginalCookieDescriptor } from '../core/cookie-interceptor.js';
|
|
7
|
+
import { sanitizeConsentPayload } from '../core/security.js';
|
|
8
|
+
|
|
9
|
+
const COOKIE_NAME = 'zest_consent';
|
|
10
|
+
const CONSENT_VERSION = '1.0';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Return the Secure flag fragment when running over HTTPS, empty otherwise.
|
|
14
|
+
* On HTTPS sites, omitting Secure lets the cookie leak over plain HTTP.
|
|
15
|
+
*/
|
|
16
|
+
function secureAttribute() {
|
|
17
|
+
try {
|
|
18
|
+
return typeof location !== 'undefined' && location.protocol === 'https:'
|
|
19
|
+
? '; Secure'
|
|
20
|
+
: '';
|
|
21
|
+
} catch (_) {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Current consent state
|
|
27
|
+
let consent = null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the original cookie setter (bypasses interception)
|
|
31
|
+
*/
|
|
32
|
+
function setRawCookie(value) {
|
|
33
|
+
const descriptor = getOriginalCookieDescriptor();
|
|
34
|
+
if (descriptor?.set) {
|
|
35
|
+
descriptor.set.call(document, value);
|
|
36
|
+
} else {
|
|
37
|
+
// Fallback if interceptor not initialized yet
|
|
38
|
+
document.cookie = value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the original cookie getter
|
|
44
|
+
*/
|
|
45
|
+
function getRawCookie() {
|
|
46
|
+
const descriptor = getOriginalCookieDescriptor();
|
|
47
|
+
if (descriptor?.get) {
|
|
48
|
+
return descriptor.get.call(document);
|
|
49
|
+
}
|
|
50
|
+
return document.cookie;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Load consent from cookie.
|
|
55
|
+
*
|
|
56
|
+
* The parsed cookie is validated against the expected schema via
|
|
57
|
+
* sanitizeConsentPayload — only known category keys with boolean values
|
|
58
|
+
* survive, so a tampered cookie can't inject prototype-polluting props
|
|
59
|
+
* or unexpected category shapes.
|
|
60
|
+
*/
|
|
61
|
+
export function loadConsent() {
|
|
62
|
+
try {
|
|
63
|
+
const cookies = getRawCookie();
|
|
64
|
+
const match = cookies.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
|
|
65
|
+
|
|
66
|
+
if (match) {
|
|
67
|
+
const raw = JSON.parse(decodeURIComponent(match[1]));
|
|
68
|
+
const clean = sanitizeConsentPayload(raw, getCategoryIds());
|
|
69
|
+
if (clean && clean.categories) {
|
|
70
|
+
consent = { ...getDefaultConsent(), ...clean.categories };
|
|
71
|
+
return { ...consent };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// Invalid or missing cookie
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
consent = getDefaultConsent();
|
|
79
|
+
return { ...consent };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Save consent to cookie
|
|
84
|
+
*/
|
|
85
|
+
export function saveConsent(expirationDays = 365) {
|
|
86
|
+
if (!consent) {
|
|
87
|
+
consent = getDefaultConsent();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const data = {
|
|
91
|
+
version: CONSENT_VERSION,
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
categories: consent
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const expires = new Date(Date.now() + expirationDays * 24 * 60 * 60 * 1000).toUTCString();
|
|
97
|
+
const cookieValue = `${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(data))}; expires=${expires}; path=/; SameSite=Lax${secureAttribute()}`;
|
|
98
|
+
|
|
99
|
+
setRawCookie(cookieValue);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get current consent state
|
|
104
|
+
*/
|
|
105
|
+
export function getConsent() {
|
|
106
|
+
if (!consent) {
|
|
107
|
+
consent = loadConsent();
|
|
108
|
+
}
|
|
109
|
+
return { ...consent };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Update consent state
|
|
114
|
+
*/
|
|
115
|
+
export function updateConsent(newConsent, expirationDays = 365) {
|
|
116
|
+
const previous = consent ? { ...consent } : getDefaultConsent();
|
|
117
|
+
|
|
118
|
+
consent = {
|
|
119
|
+
essential: true, // Always true
|
|
120
|
+
functional: !!newConsent.functional,
|
|
121
|
+
analytics: !!newConsent.analytics,
|
|
122
|
+
marketing: !!newConsent.marketing
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
saveConsent(expirationDays);
|
|
126
|
+
|
|
127
|
+
return { current: { ...consent }, previous };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if specific category is allowed
|
|
132
|
+
*/
|
|
133
|
+
export function hasConsent(category) {
|
|
134
|
+
if (!consent) {
|
|
135
|
+
consent = loadConsent();
|
|
136
|
+
}
|
|
137
|
+
return consent[category] === true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Accept all categories
|
|
142
|
+
*/
|
|
143
|
+
export function acceptAll(expirationDays = 365) {
|
|
144
|
+
return updateConsent({
|
|
145
|
+
essential: true,
|
|
146
|
+
functional: true,
|
|
147
|
+
analytics: true,
|
|
148
|
+
marketing: true
|
|
149
|
+
}, expirationDays);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Reject all (except essential)
|
|
154
|
+
*/
|
|
155
|
+
export function rejectAll(expirationDays = 365) {
|
|
156
|
+
return updateConsent({
|
|
157
|
+
essential: true,
|
|
158
|
+
functional: false,
|
|
159
|
+
analytics: false,
|
|
160
|
+
marketing: false
|
|
161
|
+
}, expirationDays);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Reset consent (clear cookie)
|
|
166
|
+
*/
|
|
167
|
+
export function resetConsent() {
|
|
168
|
+
setRawCookie(`${COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax${secureAttribute()}`);
|
|
169
|
+
consent = null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if consent has been given (any decision made)
|
|
174
|
+
*/
|
|
175
|
+
export function hasConsentDecision() {
|
|
176
|
+
try {
|
|
177
|
+
const cookies = getRawCookie();
|
|
178
|
+
return cookies.includes(COOKIE_NAME);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get consent proof for compliance
|
|
186
|
+
*/
|
|
187
|
+
export function getConsentProof() {
|
|
188
|
+
try {
|
|
189
|
+
const cookies = getRawCookie();
|
|
190
|
+
const match = cookies.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
|
|
191
|
+
|
|
192
|
+
if (match) {
|
|
193
|
+
const raw = JSON.parse(decodeURIComponent(match[1]));
|
|
194
|
+
return sanitizeConsentPayload(raw, getCategoryIds());
|
|
195
|
+
}
|
|
196
|
+
} catch (e) {
|
|
197
|
+
// Invalid cookie
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events - Custom event dispatching for consent changes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Event names
|
|
6
|
+
export const EVENTS = {
|
|
7
|
+
READY: 'zest:ready',
|
|
8
|
+
CONSENT: 'zest:consent',
|
|
9
|
+
REJECT: 'zest:reject',
|
|
10
|
+
CHANGE: 'zest:change',
|
|
11
|
+
SHOW: 'zest:show',
|
|
12
|
+
HIDE: 'zest:hide'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Dispatch a custom event
|
|
17
|
+
*/
|
|
18
|
+
export function emit(eventName, detail = {}) {
|
|
19
|
+
const event = new CustomEvent(eventName, {
|
|
20
|
+
detail,
|
|
21
|
+
bubbles: true,
|
|
22
|
+
cancelable: true
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
document.dispatchEvent(event);
|
|
26
|
+
return event;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Emit ready event
|
|
31
|
+
*/
|
|
32
|
+
export function emitReady(consent) {
|
|
33
|
+
return emit(EVENTS.READY, { consent });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Emit consent event (user accepted)
|
|
38
|
+
*/
|
|
39
|
+
export function emitConsent(consent, previous) {
|
|
40
|
+
return emit(EVENTS.CONSENT, { consent, previous });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Emit reject event (user rejected all)
|
|
45
|
+
*/
|
|
46
|
+
export function emitReject(consent) {
|
|
47
|
+
return emit(EVENTS.REJECT, { consent });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Emit change event (any consent change)
|
|
52
|
+
*/
|
|
53
|
+
export function emitChange(consent, previous) {
|
|
54
|
+
return emit(EVENTS.CHANGE, { consent, previous });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Emit show event (banner/modal shown)
|
|
59
|
+
*/
|
|
60
|
+
export function emitShow(type = 'banner') {
|
|
61
|
+
return emit(EVENTS.SHOW, { type });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Emit hide event (banner/modal hidden)
|
|
66
|
+
*/
|
|
67
|
+
export function emitHide(type = 'banner') {
|
|
68
|
+
return emit(EVENTS.HIDE, { type });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Subscribe to an event
|
|
73
|
+
*/
|
|
74
|
+
export function on(eventName, callback) {
|
|
75
|
+
document.addEventListener(eventName, callback);
|
|
76
|
+
return () => document.removeEventListener(eventName, callback);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Subscribe to an event once
|
|
81
|
+
*/
|
|
82
|
+
export function once(eventName, callback) {
|
|
83
|
+
document.addEventListener(eventName, callback, { once: true });
|
|
84
|
+
}
|