@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.
Files changed (82) hide show
  1. package/README.md +216 -70
  2. package/dist/zest.de.js +776 -286
  3. package/dist/zest.de.js.map +1 -1
  4. package/dist/zest.de.min.js +1 -1
  5. package/dist/zest.en.js +776 -286
  6. package/dist/zest.en.js.map +1 -1
  7. package/dist/zest.en.min.js +1 -1
  8. package/dist/zest.es.js +776 -286
  9. package/dist/zest.es.js.map +1 -1
  10. package/dist/zest.es.min.js +1 -1
  11. package/dist/zest.esm.js +776 -286
  12. package/dist/zest.esm.js.map +1 -1
  13. package/dist/zest.esm.min.js +1 -1
  14. package/dist/zest.fr.js +776 -286
  15. package/dist/zest.fr.js.map +1 -1
  16. package/dist/zest.fr.min.js +1 -1
  17. package/dist/zest.headless.esm.js +2299 -0
  18. package/dist/zest.headless.esm.js.map +1 -0
  19. package/dist/zest.headless.esm.min.js +1 -0
  20. package/dist/zest.it.js +776 -286
  21. package/dist/zest.it.js.map +1 -1
  22. package/dist/zest.it.min.js +1 -1
  23. package/dist/zest.ja.js +776 -286
  24. package/dist/zest.ja.js.map +1 -1
  25. package/dist/zest.ja.min.js +1 -1
  26. package/dist/zest.js +776 -286
  27. package/dist/zest.js.map +1 -1
  28. package/dist/zest.min.js +1 -1
  29. package/dist/zest.nl.js +776 -286
  30. package/dist/zest.nl.js.map +1 -1
  31. package/dist/zest.nl.min.js +1 -1
  32. package/dist/zest.pl.js +776 -286
  33. package/dist/zest.pl.js.map +1 -1
  34. package/dist/zest.pl.min.js +1 -1
  35. package/dist/zest.pt.js +776 -286
  36. package/dist/zest.pt.js.map +1 -1
  37. package/dist/zest.pt.min.js +1 -1
  38. package/dist/zest.ru.js +776 -286
  39. package/dist/zest.ru.js.map +1 -1
  40. package/dist/zest.ru.min.js +1 -1
  41. package/dist/zest.uk.js +776 -286
  42. package/dist/zest.uk.js.map +1 -1
  43. package/dist/zest.uk.min.js +1 -1
  44. package/dist/zest.zh.js +776 -286
  45. package/dist/zest.zh.js.map +1 -1
  46. package/dist/zest.zh.min.js +1 -1
  47. package/package.json +17 -4
  48. package/src/api/public-api.js +97 -0
  49. package/src/config/defaults.js +150 -0
  50. package/src/config/parser.js +104 -0
  51. package/src/core/categories.js +52 -0
  52. package/src/core/cookie-interceptor.js +131 -0
  53. package/src/core/dnt.js +56 -0
  54. package/src/core/known-trackers.js +195 -0
  55. package/src/core/pattern-matcher.js +111 -0
  56. package/src/core/script-blocker.js +314 -0
  57. package/src/core/security.js +204 -0
  58. package/src/core/storage-interceptor.js +173 -0
  59. package/src/core-lifecycle.js +192 -0
  60. package/src/headless.js +133 -0
  61. package/src/i18n/lang-en.js +54 -0
  62. package/src/i18n/single/lang-de.js +55 -0
  63. package/src/i18n/single/lang-en.js +55 -0
  64. package/src/i18n/single/lang-es.js +55 -0
  65. package/src/i18n/single/lang-fr.js +55 -0
  66. package/src/i18n/single/lang-it.js +55 -0
  67. package/src/i18n/single/lang-ja.js +55 -0
  68. package/src/i18n/single/lang-nl.js +55 -0
  69. package/src/i18n/single/lang-pl.js +55 -0
  70. package/src/i18n/single/lang-pt.js +55 -0
  71. package/src/i18n/single/lang-ru.js +55 -0
  72. package/src/i18n/single/lang-uk.js +55 -0
  73. package/src/i18n/single/lang-zh.js +55 -0
  74. package/src/i18n/translations.js +546 -0
  75. package/src/index.js +266 -0
  76. package/src/integrations/consent-signals.js +71 -0
  77. package/src/storage/consent-store.js +201 -0
  78. package/src/storage/events.js +84 -0
  79. package/src/ui/banner.js +134 -0
  80. package/src/ui/modal.js +215 -0
  81. package/src/ui/styles.js +519 -0
  82. 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
+ }