@freshjuice/zest 1.0.0 → 2.1.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 (65) hide show
  1. package/README.md +178 -78
  2. package/dist/zest.d.ts +214 -0
  3. package/dist/zest.de.js +692 -305
  4. package/dist/zest.de.js.map +1 -1
  5. package/dist/zest.de.min.js +1 -1
  6. package/dist/zest.en.js +692 -305
  7. package/dist/zest.en.js.map +1 -1
  8. package/dist/zest.en.min.js +1 -1
  9. package/dist/zest.es.js +692 -305
  10. package/dist/zest.es.js.map +1 -1
  11. package/dist/zest.es.min.js +1 -1
  12. package/dist/zest.esm.js +692 -305
  13. package/dist/zest.esm.js.map +1 -1
  14. package/dist/zest.esm.min.js +1 -1
  15. package/dist/zest.fr.js +692 -305
  16. package/dist/zest.fr.js.map +1 -1
  17. package/dist/zest.fr.min.js +1 -1
  18. package/dist/zest.headless.d.ts +178 -0
  19. package/dist/zest.headless.esm.js +2299 -0
  20. package/dist/zest.headless.esm.js.map +1 -0
  21. package/dist/zest.headless.esm.min.js +1 -0
  22. package/dist/zest.it.js +692 -305
  23. package/dist/zest.it.js.map +1 -1
  24. package/dist/zest.it.min.js +1 -1
  25. package/dist/zest.ja.js +692 -305
  26. package/dist/zest.ja.js.map +1 -1
  27. package/dist/zest.ja.min.js +1 -1
  28. package/dist/zest.js +692 -305
  29. package/dist/zest.js.map +1 -1
  30. package/dist/zest.min.js +1 -1
  31. package/dist/zest.nl.js +692 -305
  32. package/dist/zest.nl.js.map +1 -1
  33. package/dist/zest.nl.min.js +1 -1
  34. package/dist/zest.pl.js +692 -305
  35. package/dist/zest.pl.js.map +1 -1
  36. package/dist/zest.pl.min.js +1 -1
  37. package/dist/zest.pt.js +692 -305
  38. package/dist/zest.pt.js.map +1 -1
  39. package/dist/zest.pt.min.js +1 -1
  40. package/dist/zest.ru.js +692 -305
  41. package/dist/zest.ru.js.map +1 -1
  42. package/dist/zest.ru.min.js +1 -1
  43. package/dist/zest.uk.js +692 -305
  44. package/dist/zest.uk.js.map +1 -1
  45. package/dist/zest.uk.min.js +1 -1
  46. package/dist/zest.zh.js +692 -305
  47. package/dist/zest.zh.js.map +1 -1
  48. package/dist/zest.zh.min.js +1 -1
  49. package/package.json +23 -4
  50. package/src/core/cookie-interceptor.js +20 -5
  51. package/src/core/known-trackers.js +41 -14
  52. package/src/core/pattern-matcher.js +20 -5
  53. package/src/core/script-blocker.js +85 -79
  54. package/src/core/security.js +204 -0
  55. package/src/core/storage-interceptor.js +5 -1
  56. package/src/core-lifecycle.js +192 -0
  57. package/src/headless.js +133 -0
  58. package/src/index.js +73 -184
  59. package/src/storage/consent-store.js +32 -8
  60. package/src/types/zest.d.ts +214 -0
  61. package/src/types/zest.headless.d.ts +178 -0
  62. package/src/ui/banner.js +11 -7
  63. package/src/ui/modal.js +16 -12
  64. package/src/ui/styles.js +25 -4
  65. package/src/ui/widget.js +3 -1
package/src/index.js CHANGED
@@ -1,145 +1,88 @@
1
1
  /**
2
2
  * Zest - Lightweight Cookie Consent Toolkit
3
- * Main entry point
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.
4
7
  */
5
8
 
6
- // Core
7
- import { interceptCookies, setConsentChecker as setCookieChecker, replayCookies, getOriginalCookieDescriptor } from './core/cookie-interceptor.js';
8
- import { interceptStorage, setConsentChecker as setStorageChecker, replayStorage } from './core/storage-interceptor.js';
9
- import { startScriptBlocking, setConsentChecker as setScriptChecker, replayScripts } from './core/script-blocker.js';
10
- import { setPatterns } from './core/pattern-matcher.js';
11
- import { getCategoryIds } from './core/categories.js';
12
- import { isDoNotTrackEnabled, getDNTDetails } from './core/dnt.js';
13
-
14
- // Integrations
15
- import { applyConsentSignals } from './integrations/consent-signals.js';
16
-
17
- // Config
18
- import { getConfig, setConfig, getCurrentConfig } from './config/parser.js';
19
-
20
- // Storage
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
21
  import {
22
- loadConsent,
23
22
  getConsent,
24
23
  hasConsent,
25
- updateConsent,
26
- acceptAll as storeAcceptAll,
27
- rejectAll as storeRejectAll,
28
- resetConsent,
29
24
  hasConsentDecision,
30
25
  getConsentProof
31
26
  } from './storage/consent-store.js';
32
- import { emitReady, emitConsent, emitReject, emitChange, emitShow, emitHide, EVENTS } from './storage/events.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';
33
34
 
34
35
  // UI
35
36
  import { showBanner, hideBanner, isBannerVisible } from './ui/banner.js';
36
37
  import { showModal, hideModal, isModalVisible } from './ui/modal.js';
37
38
  import { showWidget, hideWidget, removeWidget, isWidgetVisible } from './ui/widget.js';
38
39
 
39
- // State
40
- let initialized = false;
41
- let config = null;
42
-
43
- /**
44
- * Consent checker function shared across interceptors
45
- */
46
- function checkConsent(category) {
47
- return hasConsent(category);
48
- }
49
-
50
- /**
51
- * Replay all queued items for newly allowed categories
52
- */
53
- function replayAll(allowedCategories) {
54
- replayCookies(allowedCategories);
55
- replayStorage(allowedCategories);
56
- replayScripts(allowedCategories);
57
- }
58
-
59
40
  /**
60
- * Handle accept all
41
+ * Handle accept all — delegates consent logic to core, handles UI swap.
61
42
  */
62
43
  function handleAcceptAll() {
63
- const result = storeAcceptAll(config.expiration);
64
- const categories = getCategoryIds();
65
-
66
- applyConsentSignals(result.current, config, false);
44
+ coreAcceptAll();
45
+ const config = getActiveConfig();
67
46
 
68
47
  hideBanner();
69
48
  hideModal();
70
49
 
71
- replayAll(categories);
72
-
73
- if (config.showWidget) {
50
+ if (config?.showWidget) {
74
51
  showWidget({ onClick: handleShowSettings });
75
52
  }
76
-
77
- emitConsent(result.current, result.previous);
78
- emitChange(result.current, result.previous);
79
- config.callbacks?.onAccept?.(result.current);
80
- config.callbacks?.onChange?.(result.current);
81
53
  }
82
54
 
83
55
  /**
84
- * Handle reject all
56
+ * Handle reject all.
85
57
  */
86
58
  function handleRejectAll() {
87
- const result = storeRejectAll(config.expiration);
88
-
89
- applyConsentSignals(result.current, config, false);
59
+ coreRejectAll();
60
+ const config = getActiveConfig();
90
61
 
91
62
  hideBanner();
92
63
  hideModal();
93
64
 
94
- if (config.showWidget) {
65
+ if (config?.showWidget) {
95
66
  showWidget({ onClick: handleShowSettings });
96
67
  }
97
-
98
- emitReject(result.current);
99
- emitChange(result.current, result.previous);
100
- config.callbacks?.onReject?.();
101
- config.callbacks?.onChange?.(result.current);
102
68
  }
103
69
 
104
70
  /**
105
- * Handle save preferences from modal
71
+ * Handle save preferences from modal.
106
72
  */
107
73
  function handleSavePreferences(selections) {
108
- const result = updateConsent(selections, config.expiration);
109
-
110
- applyConsentSignals(result.current, config, false);
111
-
112
- // Find newly allowed categories
113
- const newlyAllowed = Object.keys(result.current).filter(
114
- cat => result.current[cat] && !result.previous[cat]
115
- );
116
-
117
- if (newlyAllowed.length > 0) {
118
- replayAll(newlyAllowed);
119
- }
74
+ coreUpdateConsent(selections);
75
+ const config = getActiveConfig();
120
76
 
121
77
  hideModal();
122
78
 
123
- if (config.showWidget) {
79
+ if (config?.showWidget) {
124
80
  showWidget({ onClick: handleShowSettings });
125
81
  }
126
-
127
- // Determine if this was acceptance or rejection based on selections
128
- const hasNonEssential = Object.entries(selections)
129
- .some(([cat, val]) => cat !== 'essential' && val);
130
-
131
- if (hasNonEssential) {
132
- emitConsent(result.current, result.previous);
133
- } else {
134
- emitReject(result.current);
135
- }
136
-
137
- emitChange(result.current, result.previous);
138
- config.callbacks?.onChange?.(result.current);
139
82
  }
140
83
 
141
84
  /**
142
- * Handle show settings
85
+ * Open the settings modal.
143
86
  */
144
87
  function handleShowSettings() {
145
88
  hideBanner();
@@ -156,17 +99,17 @@ function handleShowSettings() {
156
99
  }
157
100
 
158
101
  /**
159
- * Handle close modal
102
+ * Close the modal — either bring the widget back (decision made) or
103
+ * fall back to the banner (no decision yet).
160
104
  */
161
105
  function handleCloseModal() {
162
106
  hideModal();
163
107
  emitHide('modal');
164
108
 
165
- // Show widget if consent was already given
166
- if (hasConsentDecision() && config.showWidget) {
109
+ const config = getActiveConfig();
110
+ if (hasConsentDecision() && config?.showWidget) {
167
111
  showWidget({ onClick: handleShowSettings });
168
112
  } else {
169
- // Show banner again if no decision made
170
113
  showBanner({
171
114
  onAcceptAll: handleAcceptAll,
172
115
  onRejectAll: handleRejectAll,
@@ -176,103 +119,37 @@ function handleCloseModal() {
176
119
  }
177
120
 
178
121
  /**
179
- * Initialize Zest
122
+ * Initialize Zest with UI.
180
123
  */
181
124
  function init(userConfig = {}) {
182
- if (initialized) {
125
+ const { alreadyInitialized, consent, hasDecision, dntApplied } = coreInit(userConfig);
126
+ if (alreadyInitialized) {
183
127
  console.warn('[Zest] Already initialized');
184
128
  return Zest;
185
129
  }
186
130
 
187
- // Merge config
188
- config = setConfig(userConfig);
189
-
190
- // Push default denied state to vendor consent mode APIs (must happen before scripts load)
191
- applyConsentSignals(
192
- { essential: true, functional: false, analytics: false, marketing: false },
193
- config,
194
- true
195
- );
196
-
197
- // Set patterns if provided
198
- if (config.patterns) {
199
- setPatterns(config.patterns);
200
- }
201
-
202
- // Set up consent checkers
203
- setCookieChecker(checkConsent);
204
- setStorageChecker(checkConsent);
205
- setScriptChecker(checkConsent);
206
-
207
- // Start interception
208
- interceptCookies();
209
- interceptStorage();
210
- startScriptBlocking(config.mode, config.blockedDomains);
211
-
212
- // Load saved consent
213
- const consent = loadConsent();
214
-
215
- initialized = true;
216
-
217
- // Push update for returning visitors with saved consent
218
- if (hasConsentDecision()) {
219
- applyConsentSignals(consent, config, false);
220
- }
221
-
222
- // Check Do Not Track / Global Privacy Control
223
- const dntEnabled = isDoNotTrackEnabled();
224
- let dntApplied = false;
225
-
226
- if (dntEnabled && config.respectDNT && config.dntBehavior !== 'ignore') {
227
- if (config.dntBehavior === 'reject' && !hasConsentDecision()) {
228
- // Auto-reject non-essential cookies silently
229
- const result = storeRejectAll(config.expiration);
230
- dntApplied = true;
131
+ const config = getActiveConfig();
231
132
 
232
- applyConsentSignals(result.current, config, false);
233
-
234
- // Emit events
235
- emitReject(result.current);
236
- emitChange(result.current, result.previous);
237
- config.callbacks?.onReject?.();
238
- config.callbacks?.onChange?.(result.current);
239
- }
240
- // 'preselect' behavior is handled by default (banner shows with defaults off)
241
- }
242
-
243
- // Emit ready event
244
- emitReady(consent);
245
- config.callbacks?.onReady?.(consent);
246
-
247
- // Show UI based on consent state
248
- if (!hasConsentDecision() && !dntApplied) {
249
- // No consent decision yet - show banner
133
+ if (!hasDecision && !dntApplied) {
250
134
  showBanner({
251
135
  onAcceptAll: handleAcceptAll,
252
136
  onRejectAll: handleRejectAll,
253
137
  onSettings: handleShowSettings
254
138
  });
255
139
  emitShow('banner');
256
- } else {
257
- // Consent already given (or DNT auto-rejected) - show widget for reopening settings
258
- if (config.showWidget) {
259
- showWidget({ onClick: handleShowSettings });
260
- }
140
+ } else if (config?.showWidget) {
141
+ showWidget({ onClick: handleShowSettings });
261
142
  }
262
143
 
263
144
  return Zest;
264
145
  }
265
146
 
266
- /**
267
- * Public API
268
- */
269
147
  const Zest = {
270
- // Initialization
271
148
  init,
272
149
 
273
150
  // Banner control
274
151
  show() {
275
- if (!initialized) {
152
+ if (!isInitialized()) {
276
153
  console.warn('[Zest] Not initialized. Call Zest.init() first.');
277
154
  return;
278
155
  }
@@ -293,7 +170,7 @@ const Zest = {
293
170
 
294
171
  // Settings modal
295
172
  showSettings() {
296
- if (!initialized) {
173
+ if (!isInitialized()) {
297
174
  console.warn('[Zest] Not initialized. Call Zest.init() first.');
298
175
  return;
299
176
  }
@@ -305,19 +182,19 @@ const Zest = {
305
182
  emitHide('modal');
306
183
  },
307
184
 
308
- // Consent management
185
+ // Consent state
309
186
  getConsent,
310
187
  hasConsent,
311
188
  hasConsentDecision,
312
189
  getConsentProof,
313
190
 
314
- // DNT detection
191
+ // DNT
315
192
  isDoNotTrackEnabled,
316
193
  getDNTDetails,
317
194
 
318
- // Accept/Reject programmatically
195
+ // Programmatic accept / reject
319
196
  acceptAll() {
320
- if (!initialized) {
197
+ if (!isInitialized()) {
321
198
  console.warn('[Zest] Not initialized. Call Zest.init() first.');
322
199
  return;
323
200
  }
@@ -325,20 +202,19 @@ const Zest = {
325
202
  },
326
203
 
327
204
  rejectAll() {
328
- if (!initialized) {
205
+ if (!isInitialized()) {
329
206
  console.warn('[Zest] Not initialized. Call Zest.init() first.');
330
207
  return;
331
208
  }
332
209
  handleRejectAll();
333
210
  },
334
211
 
335
- // Reset and show banner again
212
+ // Reset everything and reshow the banner
336
213
  reset() {
337
- resetConsent();
214
+ coreReset();
338
215
  hideModal();
339
216
  removeWidget();
340
-
341
- if (initialized) {
217
+ if (isInitialized()) {
342
218
  showBanner({
343
219
  onAcceptAll: handleAcceptAll,
344
220
  onRejectAll: handleRejectAll,
@@ -348,17 +224,30 @@ const Zest = {
348
224
  }
349
225
  },
350
226
 
351
- // Config
227
+ // Config introspection
352
228
  getConfig: getCurrentConfig,
353
229
 
354
230
  // Events
231
+ on,
232
+ once,
355
233
  EVENTS
356
234
  };
357
235
 
358
236
  // Auto-init if config present
359
237
  if (typeof window !== 'undefined') {
360
- // Make Zest available globally
361
- window.Zest = Zest;
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
+ }
362
251
 
363
252
  const autoInit = () => {
364
253
  const cfg = getConfig();
@@ -2,12 +2,27 @@
2
2
  * Consent Store - Manages consent state persistence
3
3
  */
4
4
 
5
- import { getDefaultConsent } from '../core/categories.js';
5
+ import { getDefaultConsent, getCategoryIds } from '../core/categories.js';
6
6
  import { getOriginalCookieDescriptor } from '../core/cookie-interceptor.js';
7
+ import { sanitizeConsentPayload } from '../core/security.js';
7
8
 
8
9
  const COOKIE_NAME = 'zest_consent';
9
10
  const CONSENT_VERSION = '1.0';
10
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
+
11
26
  // Current consent state
12
27
  let consent = null;
13
28
 
@@ -36,7 +51,12 @@ function getRawCookie() {
36
51
  }
37
52
 
38
53
  /**
39
- * Load consent from cookie
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.
40
60
  */
41
61
  export function loadConsent() {
42
62
  try {
@@ -44,9 +64,12 @@ export function loadConsent() {
44
64
  const match = cookies.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
45
65
 
46
66
  if (match) {
47
- const data = JSON.parse(decodeURIComponent(match[1]));
48
- consent = data.categories || getDefaultConsent();
49
- return { ...consent };
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
+ }
50
73
  }
51
74
  } catch (e) {
52
75
  // Invalid or missing cookie
@@ -71,7 +94,7 @@ export function saveConsent(expirationDays = 365) {
71
94
  };
72
95
 
73
96
  const expires = new Date(Date.now() + expirationDays * 24 * 60 * 60 * 1000).toUTCString();
74
- const cookieValue = `${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(data))}; expires=${expires}; path=/; SameSite=Lax`;
97
+ const cookieValue = `${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(data))}; expires=${expires}; path=/; SameSite=Lax${secureAttribute()}`;
75
98
 
76
99
  setRawCookie(cookieValue);
77
100
  }
@@ -142,7 +165,7 @@ export function rejectAll(expirationDays = 365) {
142
165
  * Reset consent (clear cookie)
143
166
  */
144
167
  export function resetConsent() {
145
- setRawCookie(`${COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`);
168
+ setRawCookie(`${COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax${secureAttribute()}`);
146
169
  consent = null;
147
170
  }
148
171
 
@@ -167,7 +190,8 @@ export function getConsentProof() {
167
190
  const match = cookies.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
168
191
 
169
192
  if (match) {
170
- return JSON.parse(decodeURIComponent(match[1]));
193
+ const raw = JSON.parse(decodeURIComponent(match[1]));
194
+ return sanitizeConsentPayload(raw, getCategoryIds());
171
195
  }
172
196
  } catch (e) {
173
197
  // Invalid cookie
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Type definitions for `@freshjuice/zest` (full build with UI).
3
+ *
4
+ * The full build ships the consent engine plus a Shadow-DOM banner,
5
+ * settings modal, and floating widget. It auto-initialises on script
6
+ * load when included via `<script>`, or you can drive it manually via
7
+ * `Zest.init()`.
8
+ *
9
+ * For a logic-only build without UI, import from
10
+ * `@freshjuice/zest/headless` instead.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import Zest from '@freshjuice/zest';
15
+ *
16
+ * Zest.init({
17
+ * position: 'bottom-right',
18
+ * theme: 'auto',
19
+ * accentColor: '#0071e3',
20
+ * policyUrl: '/privacy'
21
+ * });
22
+ * ```
23
+ */
24
+
25
+ /** Built-in consent categories. */
26
+ export type ConsentCategory =
27
+ | 'essential'
28
+ | 'functional'
29
+ | 'analytics'
30
+ | 'marketing';
31
+
32
+ /**
33
+ * Per-category boolean consent state. `essential` is always `true` —
34
+ * consent for it cannot be revoked because it covers strictly-necessary
35
+ * processing.
36
+ */
37
+ export type ConsentState =
38
+ & Partial<Record<ConsentCategory, boolean>>
39
+ & { essential: true };
40
+
41
+ /** Snapshot returned by `init()`. */
42
+ export interface InitSnapshot {
43
+ consent: ConsentState;
44
+ hasDecision: boolean;
45
+ dntApplied: boolean;
46
+ }
47
+
48
+ /** Tamper-evident proof of the user's last consent decision. */
49
+ export interface ConsentProof {
50
+ version: string;
51
+ timestamp: number;
52
+ categories: ConsentState;
53
+ }
54
+
55
+ /** Output of `getDNTDetails()`. */
56
+ export interface DNTDetails {
57
+ dnt: boolean;
58
+ gpc: boolean;
59
+ doNotTrack: string | null;
60
+ globalPrivacyControl: boolean;
61
+ }
62
+
63
+ /** Behaviour when DNT / GPC is detected at init time. */
64
+ export type DNTBehavior = 'reject' | 'preselect' | 'ignore';
65
+
66
+ /** Banner position on the page. */
67
+ export type BannerPosition = 'bottom' | 'bottom-left' | 'bottom-right' | 'top';
68
+
69
+ /** UI theme. `auto` follows `prefers-color-scheme`. */
70
+ export type ZestTheme = 'light' | 'dark' | 'auto';
71
+
72
+ /** Script-blocking strictness. */
73
+ export type ZestMode = 'manual' | 'safe' | 'strict' | 'doomsday';
74
+
75
+ /**
76
+ * Optional consumer callbacks. Each is wrapped in a try/catch internally
77
+ * so a thrown error never breaks the consent pipeline.
78
+ */
79
+ export interface ZestCallbacks {
80
+ onAccept?: (consent: ConsentState) => void;
81
+ onReject?: (consent: ConsentState) => void;
82
+ onChange?: (consent: ConsentState) => void;
83
+ onReady?: (consent: ConsentState) => void;
84
+ }
85
+
86
+ /** Configuration accepted by `init()` and `window.ZestConfig`. */
87
+ export interface InitOptions {
88
+ /** Display language. `'auto'` detects from `<html lang>` / browser. */
89
+ lang?:
90
+ | 'auto'
91
+ | 'en' | 'de' | 'es' | 'fr' | 'it' | 'pt'
92
+ | 'nl' | 'pl' | 'uk' | 'ru' | 'ja' | 'zh';
93
+ /** Banner position. Default `'bottom'`. */
94
+ position?: BannerPosition;
95
+ /** UI theme. Default `'auto'`. */
96
+ theme?: ZestTheme;
97
+ /** Hex accent color for buttons (e.g. `'#0071e3'`). */
98
+ accentColor?: string;
99
+ /** Link to the host site's privacy policy. */
100
+ policyUrl?: string;
101
+ /** Show floating "manage cookies" widget after a decision. Default `true`. */
102
+ showWidget?: boolean;
103
+ /** Cookie expiration in days. Default `365`. */
104
+ expiration?: number;
105
+ /** Script-blocking mode. Default `'safe'`. */
106
+ mode?: ZestMode;
107
+ /** Auto-initialise on script load. Default `true` for the UI build. */
108
+ autoInit?: boolean;
109
+ /** Respect Do Not Track / Global Privacy Control. Default `true`. */
110
+ respectDNT?: boolean;
111
+ /** What to do when DNT/GPC is on. Default `'reject'`. */
112
+ dntBehavior?: DNTBehavior;
113
+ /** Consumer callbacks. */
114
+ callbacks?: ZestCallbacks;
115
+ /** Anything else — Zest tolerates unknown keys at runtime. */
116
+ [key: string]: unknown;
117
+ }
118
+
119
+ /** Event names emitted on `document.documentElement`. */
120
+ export interface ZestEvents {
121
+ READY: 'zest:ready';
122
+ CONSENT: 'zest:consent';
123
+ REJECT: 'zest:reject';
124
+ CHANGE: 'zest:change';
125
+ SHOW: 'zest:show';
126
+ HIDE: 'zest:hide';
127
+ }
128
+
129
+ export type ZestEventName = ZestEvents[keyof ZestEvents];
130
+
131
+ /** Detail payload of consent events. */
132
+ export interface ZestEventDetail {
133
+ consent: ConsentState;
134
+ previous?: ConsentState;
135
+ }
136
+
137
+ declare const Zest: {
138
+ /** Initialise. Auto-called when the script loads unless `autoInit: false`. */
139
+ init(options?: InitOptions): InitSnapshot;
140
+
141
+ /** Show the consent banner. */
142
+ show(): void;
143
+
144
+ /** Hide the consent banner. */
145
+ hide(): void;
146
+
147
+ /** Open the per-category settings modal. */
148
+ showSettings(): void;
149
+
150
+ /** Close the settings modal. */
151
+ hideSettings(): void;
152
+
153
+ /** Show the persistent "manage cookies" widget. */
154
+ showWidget(): void;
155
+
156
+ /** Hide the widget without removing it. */
157
+ hideWidget(): void;
158
+
159
+ /** Current consent state (clone, safe to mutate). */
160
+ getConsent(): ConsentState;
161
+
162
+ /** Has the user granted consent for `category`? */
163
+ hasConsent(category: ConsentCategory): boolean;
164
+
165
+ /** Has the user made any consent decision yet? */
166
+ hasConsentDecision(): boolean;
167
+
168
+ /** Tamper-evident snapshot of the last consent decision. */
169
+ getConsentProof(): ConsentProof | null;
170
+
171
+ /** Grant consent for every category and run accept callbacks. */
172
+ acceptAll(): void;
173
+
174
+ /** Revoke consent for every non-essential category and run reject callbacks. */
175
+ rejectAll(): void;
176
+
177
+ /** Wipe all consent state and reshow the banner. */
178
+ reset(): void;
179
+
180
+ /** True if the browser is sending DNT or GPC. */
181
+ isDoNotTrackEnabled(): boolean;
182
+
183
+ /** Why `isDoNotTrackEnabled()` returned what it did. */
184
+ getDNTDetails(): DNTDetails;
185
+
186
+ /** Subscribe to a consent event. Returns an unsubscribe function. */
187
+ on(
188
+ eventName: ZestEventName,
189
+ handler: (event: CustomEvent<ZestEventDetail>) => void
190
+ ): () => void;
191
+
192
+ /** Subscribe once; auto-unsubscribes after the first call. */
193
+ once(
194
+ eventName: ZestEventName,
195
+ handler: (event: CustomEvent<ZestEventDetail>) => void
196
+ ): () => void;
197
+
198
+ /** Constants for `on()` / `once()`. */
199
+ EVENTS: ZestEvents;
200
+
201
+ /** Active configuration after `init()`. */
202
+ getConfig(): InitOptions | null;
203
+ };
204
+
205
+ export default Zest;
206
+
207
+ declare global {
208
+ interface Window {
209
+ /** Set before loading `zest.min.js` to configure auto-initialisation. */
210
+ ZestConfig?: InitOptions;
211
+ /** The Zest singleton, attached after auto-init. */
212
+ Zest?: typeof Zest;
213
+ }
214
+ }