@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/README.md CHANGED
@@ -1,12 +1,18 @@
1
1
  # Zest 🍋
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@freshjuice/zest)](https://www.npmjs.com/package/@freshjuice/zest)
4
+ [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
5
+ [![GitHub stars](https://img.shields.io/github/stars/freshjuice-dev/zest)](https://github.com/freshjuice-dev/zest/stargazers)
6
+ [![GitHub forks](https://img.shields.io/github/forks/freshjuice-dev/zest)](https://github.com/freshjuice-dev/zest/network/members)
7
+
3
8
  A lightweight cookie consent toolkit for GDPR/CCPA compliance.
4
9
 
5
- - **Lightweight** - ~9KB gzipped (single language) / ~14KB (all 12 languages)
6
- - **Zero dependencies** - Vanilla JavaScript
7
- - **Shadow DOM** - Styles isolated from your site
8
- - **Modern browsers** - No IE11 polyfills needed
9
- - **Privacy-first** - Respects Do Not Track / Global Privacy Control
10
+ - **Lightweight** ~9KB gzipped (single language) / ~16KB (all 12 languages) / ~11KB (headless)
11
+ - **Zero dependencies** Vanilla JavaScript
12
+ - **Shadow DOM** Styles isolated from your site
13
+ - **Headless mode** Bring your own UI & CSS, use only the consent engine
14
+ - **Privacy-first** Respects Do Not Track / Global Privacy Control
15
+ - **Security-hardened** — XSS-safe templating, URL/color/regex validation, locked interceptors
10
16
 
11
17
  ## Quick Start
12
18
 
@@ -18,7 +24,7 @@ A lightweight cookie consent toolkit for GDPR/CCPA compliance.
18
24
  <script src="https://cdn.jsdelivr.net/npm/@freshjuice/zest"></script>
19
25
  ```
20
26
 
21
- Or with configuration:
27
+ With configuration:
22
28
 
23
29
  ```html
24
30
  <script>
@@ -32,6 +38,23 @@ Or with configuration:
32
38
  <script src="https://unpkg.com/@freshjuice/zest"></script>
33
39
  ```
34
40
 
41
+ As an npm dependency:
42
+
43
+ ```js
44
+ import Zest from '@freshjuice/zest';
45
+
46
+ Zest.init({ mode: 'safe', policyUrl: '/privacy' });
47
+ ```
48
+
49
+ ## Two build flavors
50
+
51
+ | Entry | What you get | Min / Gzip |
52
+ |---|---|---|
53
+ | `@freshjuice/zest` | Consent engine **+ Shadow DOM UI** (banner, modal, widget) | ~50 KB / **~16 KB** |
54
+ | `@freshjuice/zest/headless` | Consent engine only, **no UI / no CSS** — you build the UI | ~31 KB / **~11 KB** |
55
+
56
+ Use **headless** when you want full control over markup and styling.
57
+
35
58
  ## Configuration
36
59
 
37
60
  ### Via `window.ZestConfig`
@@ -44,10 +67,10 @@ window.ZestConfig = {
44
67
  // Theme: 'light' | 'dark' | 'auto' (default: 'auto' follows system)
45
68
  theme: 'auto',
46
69
 
47
- // Accent color for buttons
70
+ // Accent color must be a valid CSS color (hex, named, rgb/rgba, hsl/hsla)
48
71
  accentColor: '#0071e3',
49
72
 
50
- // Link to privacy policy
73
+ // Link to privacy policy — only http:/https:/mailto:/tel:/relative allowed
51
74
  policyUrl: '/privacy',
52
75
 
53
76
  // Show floating widget after consent
@@ -56,7 +79,7 @@ window.ZestConfig = {
56
79
  // Consent expiration in days
57
80
  expiration: 365,
58
81
 
59
- // Callbacks
82
+ // Callbacks — wrapped in try/catch internally, safe to throw
60
83
  callbacks: {
61
84
  onAccept: (consent) => {},
62
85
  onReject: () => {},
@@ -81,26 +104,85 @@ window.ZestConfig = {
81
104
  ## API
82
105
 
83
106
  ```javascript
84
- // Show/hide UI
85
- Zest.show() // Show banner
86
- Zest.hide() // Hide banner
87
- Zest.showSettings() // Show settings modal
88
- Zest.reset() // Clear consent, show banner
89
-
90
- // Consent management
91
- Zest.getConsent() // Get current consent state
92
- Zest.hasConsent('analytics') // Check specific category
93
- Zest.acceptAll() // Accept all categories
94
- Zest.rejectAll() // Reject all (except essential)
107
+ // Show/hide UI (full build only)
108
+ Zest.show() // Show banner
109
+ Zest.hide() // Hide banner
110
+ Zest.showSettings() // Show settings modal
111
+ Zest.hideSettings() // Close settings modal
112
+ Zest.reset() // Clear consent + reshow banner
113
+
114
+ // Consent state
115
+ Zest.getConsent() // { essential, functional, analytics, marketing }
116
+ Zest.hasConsent('analytics') // boolean
117
+ Zest.hasConsentDecision() // boolean has the user made a choice yet?
118
+ Zest.getConsentProof() // full consent cookie payload (compliance audit)
119
+
120
+ // Programmatic actions
121
+ Zest.acceptAll()
122
+ Zest.rejectAll()
123
+ Zest.updateConsent({ analytics: true, marketing: false }) // headless only
124
+
125
+ // DNT / GPC
126
+ Zest.isDoNotTrackEnabled()
127
+ Zest.getDNTDetails() // { enabled, source: 'dnt'|'gpc'|null }
128
+
129
+ // Events — subscribe helpers (also work with addEventListener)
130
+ Zest.on('zest:change', (e) => {})
131
+ Zest.once('zest:ready', (e) => {})
132
+ Zest.EVENTS // { READY, CONSENT, REJECT, CHANGE, SHOW, HIDE }
95
133
  ```
96
134
 
135
+ ## Headless mode — bring your own UI
136
+
137
+ Full control over markup and styling, no Shadow DOM, no inline CSS.
138
+
139
+ ```js
140
+ import Zest from '@freshjuice/zest/headless';
141
+
142
+ Zest.init({
143
+ mode: 'safe',
144
+ respectDNT: true,
145
+ consentModeGoogle: true
146
+ });
147
+
148
+ // Decide when to show YOUR banner
149
+ if (!Zest.hasConsentDecision()) {
150
+ document.querySelector('#my-banner').classList.add('open');
151
+ }
152
+
153
+ // Wire your buttons
154
+ document.querySelector('#accept').onclick = () => Zest.acceptAll();
155
+ document.querySelector('#reject').onclick = () => Zest.rejectAll();
156
+
157
+ document.querySelector('#save').onclick = () => {
158
+ Zest.updateConsent({
159
+ analytics: analyticsCheckbox.checked,
160
+ marketing: marketingCheckbox.checked,
161
+ functional: functionalCheckbox.checked
162
+ });
163
+ };
164
+
165
+ // Listen for changes
166
+ Zest.on(Zest.EVENTS.CHANGE, (e) => {
167
+ console.log('consent changed', e.detail.consent);
168
+ });
169
+ ```
170
+
171
+ What headless gives you:
172
+ - All interceptors (cookies, storage, scripts) still work — just skip the built-in UI
173
+ - Same config surface (`mode`, `respectDNT`, `consentModeGoogle`, `blockedDomains`, `patterns`, etc.)
174
+ - **Does NOT auto-init** — you call `Zest.init()` when ready
175
+ - **Does NOT set `window.Zest`** — you import and use the module directly
176
+
177
+ See `examples/headless.html` for a complete working example.
178
+
97
179
  ## Do Not Track (DNT) / Global Privacy Control (GPC)
98
180
 
99
181
  Zest respects browser privacy signals by default:
100
182
 
101
183
  ```javascript
102
184
  window.ZestConfig = {
103
- respectDNT: true, // Respect DNT/GPC signals (default: true)
185
+ respectDNT: true, // Respect DNT/GPC signals (default: true)
104
186
  dntBehavior: 'reject' // What to do when DNT is enabled
105
187
  };
106
188
  ```
@@ -111,10 +193,9 @@ window.ZestConfig = {
111
193
  | `preselect` | Show banner with non-essential options unchecked |
112
194
  | `ignore` | Ignore DNT/GPC signals completely |
113
195
 
114
- **API methods:**
115
196
  ```javascript
116
- Zest.isDoNotTrackEnabled() // Returns true if DNT or GPC is enabled
117
- Zest.getDNTDetails() // Returns { enabled: boolean, source: 'dnt' | 'gpc' | null }
197
+ Zest.isDoNotTrackEnabled() // true if DNT or GPC is enabled
198
+ Zest.getDNTDetails() // { enabled: boolean, source: 'dnt' | 'gpc' | null }
118
199
  ```
119
200
 
120
201
  ## Blocking Modes
@@ -136,8 +217,6 @@ window.ZestConfig = {
136
217
 
137
218
  ### Custom Blocked Domains
138
219
 
139
- Add your own domains to block:
140
-
141
220
  ```javascript
142
221
  window.ZestConfig = {
143
222
  mode: 'safe',
@@ -150,8 +229,6 @@ window.ZestConfig = {
150
229
 
151
230
  ### Manual Script Tagging
152
231
 
153
- For any mode, you can explicitly tag scripts:
154
-
155
232
  ```html
156
233
  <script data-consent-category="analytics" src="https://..."></script>
157
234
  <script data-consent-category="marketing">
@@ -159,9 +236,11 @@ For any mode, you can explicitly tag scripts:
159
236
  </script>
160
237
  ```
161
238
 
162
- ### Allow Specific Scripts
239
+ > **Note:** `data-consent-category="essential"` on third-party scripts is
240
+ > ignored — self-labeling as essential is a known bypass. Only
241
+ > `functional`, `analytics`, and `marketing` self-labels are honored.
163
242
 
164
- Prevent a script from being blocked (useful in strict/doomsday modes):
243
+ ### Allow Specific Scripts
165
244
 
166
245
  ```html
167
246
  <script data-zest-allow src="https://cdn.example.com/library.js"></script>
@@ -185,11 +264,52 @@ document.addEventListener('zest:change', (e) => {
185
264
  document.addEventListener('zest:ready', (e) => {
186
265
  console.log('Zest initialized:', e.detail.consent);
187
266
  });
267
+
268
+ // Or via the helpers
269
+ Zest.on(Zest.EVENTS.CHANGE, (e) => { /* ... */ });
270
+ Zest.once(Zest.EVENTS.READY, (e) => { /* ... */ });
271
+ ```
272
+
273
+ ## Google Consent Mode v2 / Microsoft UET Consent Mode
274
+
275
+ Optional — push consent state to Google and Microsoft advertising APIs.
276
+
277
+ ### Enable via JavaScript
278
+
279
+ ```javascript
280
+ window.ZestConfig = {
281
+ consentModeGoogle: true,
282
+ consentModeMicrosoft: true
283
+ };
188
284
  ```
189
285
 
286
+ ### Enable via data attributes
287
+
288
+ ```html
289
+ <script
290
+ src="zest.min.js"
291
+ data-consent-mode-google="true"
292
+ data-consent-mode-microsoft="true"
293
+ ></script>
294
+ ```
295
+
296
+ When enabled, Zest automatically:
297
+
298
+ 1. Pushes a `'default'` denied state on page load (before any tracking scripts fire)
299
+ 2. Pushes an `'update'` whenever the user makes a choice
300
+
301
+ ### Category mapping
302
+
303
+ | Zest Category | Google Consent Mode v2 Signals | Microsoft UET Signal |
304
+ |---|---|---|
305
+ | `essential` | `functionality_storage: 'granted'` (always) | — |
306
+ | `functional` | `personalization_storage` | — |
307
+ | `analytics` | `analytics_storage` | — |
308
+ | `marketing` | `ad_storage`, `ad_user_data`, `ad_personalization` | `ad_storage` |
309
+
190
310
  ## Localization
191
311
 
192
- Zest has **built-in translations** with auto-detection.
312
+ Built-in translations with auto-detection.
193
313
 
194
314
  **Supported languages:** `en`, `de`, `es`, `fr`, `it`, `pt`, `nl`, `pl`, `uk`, `ru`, `ja`, `zh`
195
315
 
@@ -197,8 +317,9 @@ Zest has **built-in translations** with auto-detection.
197
317
 
198
318
  | Bundle | Size (gzip) | Description |
199
319
  |--------|-------------|-------------|
200
- | `zest.min.js` | ~14KB | All 12 languages, auto-detects |
201
- | `zest.{lang}.min.js` | ~9KB | Single language (e.g., `zest.de.min.js`) |
320
+ | `zest.min.js` | ~16 KB | All 12 languages, auto-detects |
321
+ | `zest.{lang}.min.js` | ~9 KB | Single language (e.g. `zest.de.min.js`) |
322
+ | `zest.headless.esm.min.js` | ~11 KB | Logic only, no UI / no translations (ESM import) |
202
323
 
203
324
  ```html
204
325
  <!-- Full bundle - auto-detects language -->
@@ -211,61 +332,40 @@ Zest has **built-in translations** with auto-detection.
211
332
  ### Language Detection
212
333
 
213
334
  ```javascript
214
- window.ZestConfig = {
215
- lang: 'auto' // Auto-detect (default)
216
- };
335
+ window.ZestConfig = { lang: 'auto' }; // default
217
336
  ```
218
337
 
219
- **Detection priority:**
220
- 1. `lang` config option (if not 'auto')
221
- 2. `<html lang="de">` attribute
222
- 3. Browser language (`navigator.language`)
223
- 4. Fallback to English
338
+ Priority: `lang` config → `<html lang="...">` → `navigator.language` → English.
224
339
 
225
340
  ### Force Specific Language
226
341
 
227
342
  ```javascript
228
- window.ZestConfig = {
229
- lang: 'de' // Force German
230
- };
343
+ window.ZestConfig = { lang: 'de' };
231
344
  ```
232
345
 
233
346
  ### Override Labels
234
347
 
235
348
  ```javascript
236
349
  window.ZestConfig = {
237
- lang: 'de', // Use German as base
350
+ lang: 'de',
238
351
  labels: {
239
352
  banner: {
240
- title: 'Custom German Title' // Override just this
353
+ title: 'Custom German Title'
241
354
  }
242
355
  }
243
356
  };
244
357
  ```
245
358
 
246
- Standalone JSON translation files also available in `/locales/` for external loading.
359
+ Standalone JSON translation files are in `/locales/`.
247
360
 
248
- ## Custom Styling
361
+ ## Styling the UI (full build)
249
362
 
250
- Override default styles by passing custom CSS:
363
+ The UI is rendered inside a Shadow DOM with `mode: 'open'`, so your global
364
+ CSS can't reach inside the component. You have three options:
251
365
 
252
- ```javascript
253
- window.ZestConfig = {
254
- customStyles: `
255
- .zest-banner {
256
- max-width: 600px;
257
- }
258
- .zest-btn--primary {
259
- border-radius: 20px;
260
- }
261
- .zest-modal {
262
- max-width: 600px;
263
- }
264
- `
265
- };
266
- ```
366
+ ### 1. CSS custom properties (inheritable through Shadow DOM)
267
367
 
268
- Available CSS custom properties (can also be set via parent CSS):
368
+ The following custom properties are exposed on the host elements:
269
369
 
270
370
  ```css
271
371
  zest-banner, zest-modal, zest-widget {
@@ -280,6 +380,32 @@ zest-banner, zest-modal, zest-widget {
280
380
  }
281
381
  ```
282
382
 
383
+ ### 2. `customStyles` config option
384
+
385
+ ```javascript
386
+ window.ZestConfig = {
387
+ customStyles: `
388
+ .zest-banner { max-width: 600px; }
389
+ .zest-btn--primary { border-radius: 20px; }
390
+ .zest-modal { max-width: 600px; }
391
+ `
392
+ };
393
+ ```
394
+
395
+ > **Security note:** `customStyles` is sanitized — `@import`, `expression()`,
396
+ > external `url()` values, and selectors targeting the accept/reject
397
+ > buttons are stripped. This prevents clickjacking via invisible-button
398
+ > CSS attacks. Payloads over 20 KB are dropped entirely.
399
+
400
+ ### 3. Style the host elements directly
401
+
402
+ The custom elements `zest-banner`, `zest-modal`, `zest-widget` live in the
403
+ light DOM — you can position, hide, or z-index them from your global CSS.
404
+
405
+ ### Want full CSS control?
406
+
407
+ Use the **headless** entry and style your own markup however you like.
408
+
283
409
  ## Categories
284
410
 
285
411
  | Category | ID | Default | Description |
@@ -289,18 +415,38 @@ zest-banner, zest-modal, zest-widget {
289
415
  | Analytics | `analytics` | OFF | Usage tracking |
290
416
  | Marketing | `marketing` | OFF | Advertising cookies |
291
417
 
418
+ Unknown cookies default to `marketing` (strictest).
419
+
420
+ ## Security
421
+
422
+ Zest takes a defense-in-depth approach to security.
423
+
424
+ Highlights:
425
+
426
+ - All config-driven HTML is escaped via an internal `escapeHTML` pass
427
+ - `policyUrl` is validated against an allowlist (`http:`, `https:`,
428
+ `mailto:`, `tel:`, relative)
429
+ - `accentColor` must pass a strict color validator
430
+ - `customStyles` is sanitized (see above)
431
+ - Consent cookie JSON is schema-validated on read (prototype pollution safe)
432
+ - On HTTPS, the consent cookie is written with the `Secure` flag
433
+ - `window.Zest` is frozen and non-configurable once installed
434
+ - User callbacks are wrapped in try/catch so a throwing handler can't
435
+ break the consent flow
436
+ - Cookie / storage / script queues are size-capped (DoS prevention)
437
+
438
+ To report a vulnerability, open a private security advisory on GitHub.
439
+
292
440
  ## Config Schema
293
441
 
294
- JSON Schema available for IDE autocompletion: `zest.config.schema.json`
442
+ JSON Schema for IDE autocompletion: [`zest.config.schema.json`](zest.config.schema.json)
295
443
 
296
444
  ## Contributing
297
445
 
298
- Contributions are welcome! Please feel free to submit a Pull Request.
299
-
300
446
  1. Fork the repository
301
447
  2. Create your feature branch (`git checkout -b feature/amazing-feature`)
302
- 3. Commit your changes (`git commit -m 'Add amazing feature'`)
303
- 4. Push to the branch (`git push origin feature/amazing-feature`)
448
+ 3. Commit your changes
449
+ 4. Push to the branch
304
450
  5. Open a Pull Request
305
451
 
306
452
  ## Credits