@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.
- package/README.md +178 -78
- package/dist/zest.d.ts +214 -0
- package/dist/zest.de.js +692 -305
- package/dist/zest.de.js.map +1 -1
- package/dist/zest.de.min.js +1 -1
- package/dist/zest.en.js +692 -305
- package/dist/zest.en.js.map +1 -1
- package/dist/zest.en.min.js +1 -1
- package/dist/zest.es.js +692 -305
- package/dist/zest.es.js.map +1 -1
- package/dist/zest.es.min.js +1 -1
- package/dist/zest.esm.js +692 -305
- package/dist/zest.esm.js.map +1 -1
- package/dist/zest.esm.min.js +1 -1
- package/dist/zest.fr.js +692 -305
- package/dist/zest.fr.js.map +1 -1
- package/dist/zest.fr.min.js +1 -1
- package/dist/zest.headless.d.ts +178 -0
- 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 +692 -305
- package/dist/zest.it.js.map +1 -1
- package/dist/zest.it.min.js +1 -1
- package/dist/zest.ja.js +692 -305
- package/dist/zest.ja.js.map +1 -1
- package/dist/zest.ja.min.js +1 -1
- package/dist/zest.js +692 -305
- package/dist/zest.js.map +1 -1
- package/dist/zest.min.js +1 -1
- package/dist/zest.nl.js +692 -305
- package/dist/zest.nl.js.map +1 -1
- package/dist/zest.nl.min.js +1 -1
- package/dist/zest.pl.js +692 -305
- package/dist/zest.pl.js.map +1 -1
- package/dist/zest.pl.min.js +1 -1
- package/dist/zest.pt.js +692 -305
- package/dist/zest.pt.js.map +1 -1
- package/dist/zest.pt.min.js +1 -1
- package/dist/zest.ru.js +692 -305
- package/dist/zest.ru.js.map +1 -1
- package/dist/zest.ru.min.js +1 -1
- package/dist/zest.uk.js +692 -305
- package/dist/zest.uk.js.map +1 -1
- package/dist/zest.uk.min.js +1 -1
- package/dist/zest.zh.js +692 -305
- package/dist/zest.zh.js.map +1 -1
- package/dist/zest.zh.min.js +1 -1
- package/package.json +23 -4
- package/src/core/cookie-interceptor.js +20 -5
- package/src/core/known-trackers.js +41 -14
- package/src/core/pattern-matcher.js +20 -5
- package/src/core/script-blocker.js +85 -79
- package/src/core/security.js +204 -0
- package/src/core/storage-interceptor.js +5 -1
- package/src/core-lifecycle.js +192 -0
- package/src/headless.js +133 -0
- package/src/index.js +73 -184
- package/src/storage/consent-store.js +32 -8
- package/src/types/zest.d.ts +214 -0
- package/src/types/zest.headless.d.ts +178 -0
- package/src/ui/banner.js +11 -7
- package/src/ui/modal.js +16 -12
- package/src/ui/styles.js +25 -4
- package/src/ui/widget.js +3 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for `@freshjuice/zest/headless`.
|
|
3
|
+
*
|
|
4
|
+
* The headless build ships the consent engine without any UI: no Shadow
|
|
5
|
+
* DOM, no styles, no DOM mounting. You bring your own banner / modal /
|
|
6
|
+
* settings markup and call into Zest for the consent state.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import Zest from '@freshjuice/zest/headless';
|
|
11
|
+
*
|
|
12
|
+
* Zest.init({ respectDNT: true, expiration: 365 });
|
|
13
|
+
*
|
|
14
|
+
* if (!Zest.hasConsentDecision()) myBanner.show();
|
|
15
|
+
*
|
|
16
|
+
* acceptBtn.addEventListener('click', () => Zest.acceptAll());
|
|
17
|
+
* rejectBtn.addEventListener('click', () => Zest.rejectAll());
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Built-in consent categories. */
|
|
22
|
+
export type ConsentCategory =
|
|
23
|
+
| 'essential'
|
|
24
|
+
| 'functional'
|
|
25
|
+
| 'analytics'
|
|
26
|
+
| 'marketing';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Per-category boolean consent state. `essential` is always `true` —
|
|
30
|
+
* consent for it cannot be revoked because it covers strictly-necessary
|
|
31
|
+
* processing.
|
|
32
|
+
*/
|
|
33
|
+
export type ConsentState =
|
|
34
|
+
& Partial<Record<ConsentCategory, boolean>>
|
|
35
|
+
& { essential: true };
|
|
36
|
+
|
|
37
|
+
/** Snapshot returned by `init()`. */
|
|
38
|
+
export interface InitSnapshot {
|
|
39
|
+
consent: ConsentState;
|
|
40
|
+
hasDecision: boolean;
|
|
41
|
+
dntApplied: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Tamper-evident proof of the user's last consent decision. */
|
|
45
|
+
export interface ConsentProof {
|
|
46
|
+
version: string;
|
|
47
|
+
timestamp: number;
|
|
48
|
+
categories: ConsentState;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Output of `getDNTDetails()`. */
|
|
52
|
+
export interface DNTDetails {
|
|
53
|
+
dnt: boolean;
|
|
54
|
+
gpc: boolean;
|
|
55
|
+
doNotTrack: string | null;
|
|
56
|
+
globalPrivacyControl: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Behaviour when DNT / GPC is detected at init time. */
|
|
60
|
+
export type DNTBehavior = 'reject' | 'preselect' | 'ignore';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Optional consumer callbacks. Each is wrapped in a try/catch internally
|
|
64
|
+
* so a thrown error never breaks the consent pipeline.
|
|
65
|
+
*/
|
|
66
|
+
export interface ZestCallbacks {
|
|
67
|
+
onAccept?: (consent: ConsentState) => void;
|
|
68
|
+
onReject?: (consent: ConsentState) => void;
|
|
69
|
+
onChange?: (consent: ConsentState) => void;
|
|
70
|
+
onReady?: (consent: ConsentState) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Configuration accepted by `init()`. */
|
|
74
|
+
export interface InitOptions {
|
|
75
|
+
/** Respect Do Not Track / Global Privacy Control. Default `true`. */
|
|
76
|
+
respectDNT?: boolean;
|
|
77
|
+
/** What to do when DNT/GPC is on. Default `'reject'`. */
|
|
78
|
+
dntBehavior?: DNTBehavior;
|
|
79
|
+
/** Cookie expiration in days. Default `365`. */
|
|
80
|
+
expiration?: number;
|
|
81
|
+
/** Consumer callbacks. */
|
|
82
|
+
callbacks?: ZestCallbacks;
|
|
83
|
+
/** Anything else — Zest tolerates unknown keys at runtime. */
|
|
84
|
+
[key: string]: unknown;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Event names emitted on the `window` `document.documentElement`. */
|
|
88
|
+
export interface ZestEvents {
|
|
89
|
+
READY: 'zest:ready';
|
|
90
|
+
CONSENT: 'zest:consent';
|
|
91
|
+
REJECT: 'zest:reject';
|
|
92
|
+
CHANGE: 'zest:change';
|
|
93
|
+
SHOW: 'zest:show';
|
|
94
|
+
HIDE: 'zest:hide';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type ZestEventName = ZestEvents[keyof ZestEvents];
|
|
98
|
+
|
|
99
|
+
/** Detail payload of the consent-change event. */
|
|
100
|
+
export interface ZestEventDetail {
|
|
101
|
+
consent: ConsentState;
|
|
102
|
+
previous?: ConsentState;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
declare const Zest: {
|
|
106
|
+
/** Initialise the consent engine. Must be called before any other API. */
|
|
107
|
+
init(options?: InitOptions): InitSnapshot;
|
|
108
|
+
|
|
109
|
+
/** Current consent state (clone, safe to mutate). */
|
|
110
|
+
getConsent(): ConsentState;
|
|
111
|
+
|
|
112
|
+
/** Has the user granted consent for `category`? */
|
|
113
|
+
hasConsent(category: ConsentCategory): boolean;
|
|
114
|
+
|
|
115
|
+
/** Has the user made any consent decision yet (accept, reject, or
|
|
116
|
+
* partial)? */
|
|
117
|
+
hasConsentDecision(): boolean;
|
|
118
|
+
|
|
119
|
+
/** Tamper-evident snapshot of the last consent decision. */
|
|
120
|
+
getConsentProof(): ConsentProof | null;
|
|
121
|
+
|
|
122
|
+
/** Grant consent for every category. */
|
|
123
|
+
acceptAll(): ConsentState | null;
|
|
124
|
+
|
|
125
|
+
/** Revoke consent for every non-essential category. */
|
|
126
|
+
rejectAll(): ConsentState | null;
|
|
127
|
+
|
|
128
|
+
/** Set per-category consent. Missing keys are left untouched. */
|
|
129
|
+
updateConsent(
|
|
130
|
+
selections: Partial<Record<ConsentCategory, boolean>>
|
|
131
|
+
): ConsentState | null;
|
|
132
|
+
|
|
133
|
+
/** Wipe all consent state. Useful for "I changed my mind" flows. */
|
|
134
|
+
reset(): void;
|
|
135
|
+
|
|
136
|
+
/** True if the browser is sending DNT or GPC. */
|
|
137
|
+
isDoNotTrackEnabled(): boolean;
|
|
138
|
+
|
|
139
|
+
/** Why `isDoNotTrackEnabled()` returned what it did. */
|
|
140
|
+
getDNTDetails(): DNTDetails;
|
|
141
|
+
|
|
142
|
+
/** Subscribe to a consent event. Returns an unsubscribe function. */
|
|
143
|
+
on(
|
|
144
|
+
eventName: ZestEventName,
|
|
145
|
+
handler: (event: CustomEvent<ZestEventDetail>) => void
|
|
146
|
+
): () => void;
|
|
147
|
+
|
|
148
|
+
/** Subscribe once; auto-unsubscribes after the first call. */
|
|
149
|
+
once(
|
|
150
|
+
eventName: ZestEventName,
|
|
151
|
+
handler: (event: CustomEvent<ZestEventDetail>) => void
|
|
152
|
+
): () => void;
|
|
153
|
+
|
|
154
|
+
/** Constants for `on()` / `once()`. */
|
|
155
|
+
EVENTS: ZestEvents;
|
|
156
|
+
|
|
157
|
+
/** Active configuration after `init()`. */
|
|
158
|
+
getConfig(): InitOptions | null;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export default Zest;
|
|
162
|
+
|
|
163
|
+
// Named tree-shake-friendly exports.
|
|
164
|
+
export const init: typeof Zest.init;
|
|
165
|
+
export const acceptAll: typeof Zest.acceptAll;
|
|
166
|
+
export const rejectAll: typeof Zest.rejectAll;
|
|
167
|
+
export const updateConsent: typeof Zest.updateConsent;
|
|
168
|
+
export const reset: typeof Zest.reset;
|
|
169
|
+
export const getConsent: typeof Zest.getConsent;
|
|
170
|
+
export const hasConsent: typeof Zest.hasConsent;
|
|
171
|
+
export const hasConsentDecision: typeof Zest.hasConsentDecision;
|
|
172
|
+
export const getConsentProof: typeof Zest.getConsentProof;
|
|
173
|
+
export const isDoNotTrackEnabled: typeof Zest.isDoNotTrackEnabled;
|
|
174
|
+
export const getDNTDetails: typeof Zest.getDNTDetails;
|
|
175
|
+
export const on: typeof Zest.on;
|
|
176
|
+
export const once: typeof Zest.once;
|
|
177
|
+
export const EVENTS: ZestEvents;
|
|
178
|
+
export const getConfig: typeof Zest.getConfig;
|
package/src/ui/banner.js
CHANGED
|
@@ -4,30 +4,34 @@
|
|
|
4
4
|
|
|
5
5
|
import { generateStyles } from './styles.js';
|
|
6
6
|
import { getCurrentConfig } from '../config/parser.js';
|
|
7
|
+
import { escapeHTML } from '../core/security.js';
|
|
7
8
|
|
|
8
9
|
let bannerElement = null;
|
|
9
10
|
let shadowRoot = null;
|
|
10
11
|
|
|
12
|
+
const SAFE_POSITIONS = new Set(['bottom', 'bottom-left', 'bottom-right', 'top']);
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* Create the banner HTML
|
|
13
16
|
*/
|
|
14
17
|
function createBannerHTML(config) {
|
|
15
18
|
const labels = config.labels.banner;
|
|
16
|
-
const
|
|
19
|
+
const rawPosition = config.position || 'bottom';
|
|
20
|
+
const position = SAFE_POSITIONS.has(rawPosition) ? rawPosition : 'bottom';
|
|
17
21
|
|
|
18
22
|
return `
|
|
19
|
-
<div class="zest-banner zest-banner--${position}" role="dialog" aria-modal="false" aria-label="${labels.title}">
|
|
20
|
-
<h2 class="zest-banner__title">${labels.title}</h2>
|
|
21
|
-
<p class="zest-banner__description">${labels.description}</p>
|
|
23
|
+
<div class="zest-banner zest-banner--${position}" role="dialog" aria-modal="false" aria-label="${escapeHTML(labels.title)}">
|
|
24
|
+
<h2 class="zest-banner__title">${escapeHTML(labels.title)}</h2>
|
|
25
|
+
<p class="zest-banner__description">${escapeHTML(labels.description)}</p>
|
|
22
26
|
<div class="zest-banner__buttons">
|
|
23
27
|
<button type="button" class="zest-btn zest-btn--primary" data-action="accept-all">
|
|
24
|
-
${labels.acceptAll}
|
|
28
|
+
${escapeHTML(labels.acceptAll)}
|
|
25
29
|
</button>
|
|
26
30
|
<button type="button" class="zest-btn zest-btn--secondary" data-action="reject-all">
|
|
27
|
-
${labels.rejectAll}
|
|
31
|
+
${escapeHTML(labels.rejectAll)}
|
|
28
32
|
</button>
|
|
29
33
|
<button type="button" class="zest-btn zest-btn--ghost" data-action="settings">
|
|
30
|
-
${labels.settings}
|
|
34
|
+
${escapeHTML(labels.settings)}
|
|
31
35
|
</button>
|
|
32
36
|
</div>
|
|
33
37
|
</div>
|
package/src/ui/modal.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { generateStyles } from './styles.js';
|
|
6
6
|
import { getCurrentConfig } from '../config/parser.js';
|
|
7
7
|
import { DEFAULT_CATEGORIES } from '../core/categories.js';
|
|
8
|
+
import { escapeHTML, safeUrl } from '../core/security.js';
|
|
8
9
|
|
|
9
10
|
let modalElement = null;
|
|
10
11
|
let shadowRoot = null;
|
|
@@ -16,22 +17,24 @@ let currentSelections = {};
|
|
|
16
17
|
function createCategoryHTML(category, isChecked, isRequired) {
|
|
17
18
|
const disabled = isRequired ? 'disabled' : '';
|
|
18
19
|
const checked = isChecked ? 'checked' : '';
|
|
20
|
+
const safeId = escapeHTML(category.id);
|
|
21
|
+
const safeLabel = escapeHTML(category.label);
|
|
19
22
|
|
|
20
23
|
return `
|
|
21
24
|
<div class="zest-category">
|
|
22
25
|
<div class="zest-category__header">
|
|
23
26
|
<div class="zest-category__info">
|
|
24
|
-
<span class="zest-category__label">${
|
|
25
|
-
<p class="zest-category__description">${category.description}</p>
|
|
27
|
+
<span class="zest-category__label">${safeLabel}</span>
|
|
28
|
+
<p class="zest-category__description">${escapeHTML(category.description)}</p>
|
|
26
29
|
</div>
|
|
27
30
|
<label class="zest-toggle">
|
|
28
31
|
<input
|
|
29
32
|
type="checkbox"
|
|
30
33
|
class="zest-toggle__input"
|
|
31
|
-
data-category="${
|
|
34
|
+
data-category="${safeId}"
|
|
32
35
|
${checked}
|
|
33
36
|
${disabled}
|
|
34
|
-
aria-label="${
|
|
37
|
+
aria-label="${safeLabel}"
|
|
35
38
|
>
|
|
36
39
|
<span class="zest-toggle__slider"></span>
|
|
37
40
|
</label>
|
|
@@ -55,29 +58,30 @@ function createModalHTML(config, consent) {
|
|
|
55
58
|
))
|
|
56
59
|
.join('');
|
|
57
60
|
|
|
58
|
-
const
|
|
59
|
-
|
|
61
|
+
const validatedPolicyUrl = config.policyUrl ? safeUrl(config.policyUrl) : null;
|
|
62
|
+
const policyLink = validatedPolicyUrl
|
|
63
|
+
? `<a href="${escapeHTML(validatedPolicyUrl)}" class="zest-link" target="_blank" rel="noopener noreferrer">Privacy Policy</a>`
|
|
60
64
|
: '';
|
|
61
65
|
|
|
62
66
|
return `
|
|
63
|
-
<div class="zest-modal-overlay" role="dialog" aria-modal="true" aria-label="${labels.title}">
|
|
67
|
+
<div class="zest-modal-overlay" role="dialog" aria-modal="true" aria-label="${escapeHTML(labels.title)}">
|
|
64
68
|
<div class="zest-modal">
|
|
65
69
|
<div class="zest-modal__header">
|
|
66
|
-
<h2 class="zest-modal__title">${labels.title}</h2>
|
|
67
|
-
<p class="zest-modal__description">${labels.description} ${policyLink}</p>
|
|
70
|
+
<h2 class="zest-modal__title">${escapeHTML(labels.title)}</h2>
|
|
71
|
+
<p class="zest-modal__description">${escapeHTML(labels.description)} ${policyLink}</p>
|
|
68
72
|
</div>
|
|
69
73
|
<div class="zest-modal__body">
|
|
70
74
|
${categoriesHTML}
|
|
71
75
|
</div>
|
|
72
76
|
<div class="zest-modal__footer">
|
|
73
77
|
<button type="button" class="zest-btn zest-btn--primary" data-action="save">
|
|
74
|
-
${labels.save}
|
|
78
|
+
${escapeHTML(labels.save)}
|
|
75
79
|
</button>
|
|
76
80
|
<button type="button" class="zest-btn zest-btn--secondary" data-action="accept-all">
|
|
77
|
-
${labels.acceptAll}
|
|
81
|
+
${escapeHTML(labels.acceptAll)}
|
|
78
82
|
</button>
|
|
79
83
|
<button type="button" class="zest-btn zest-btn--ghost" data-action="reject-all">
|
|
80
|
-
${labels.rejectAll}
|
|
84
|
+
${escapeHTML(labels.rejectAll)}
|
|
81
85
|
</button>
|
|
82
86
|
</div>
|
|
83
87
|
</div>
|
package/src/ui/styles.js
CHANGED
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
* Styles - Shadow DOM encapsulated CSS with theming
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { safeColor, sanitizeCustomStyles } from '../core/security.js';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_ACCENT = '#4F46E5';
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
10
|
* Generate CSS with custom properties
|
|
7
11
|
*/
|
|
8
12
|
export function generateStyles(config) {
|
|
9
|
-
|
|
13
|
+
// Only accept colors that pass strict validation — an unvalidated
|
|
14
|
+
// value is a CSS-injection vector (e.g. `red; } * { display:none; /*`).
|
|
15
|
+
const accentColor = safeColor(config.accentColor) || DEFAULT_ACCENT;
|
|
16
|
+
const customCss = sanitizeCustomStyles(config.customStyles);
|
|
10
17
|
|
|
11
18
|
return `
|
|
12
19
|
:host {
|
|
@@ -476,15 +483,29 @@ export function generateStyles(config) {
|
|
|
476
483
|
.zest-hidden {
|
|
477
484
|
display: none !important;
|
|
478
485
|
}
|
|
479
|
-
${
|
|
486
|
+
${customCss}
|
|
480
487
|
`;
|
|
481
488
|
}
|
|
482
489
|
|
|
483
490
|
/**
|
|
484
|
-
* Adjust color brightness
|
|
491
|
+
* Adjust color brightness. Falls back to the default accent if the input
|
|
492
|
+
* cannot be parsed as a hex color (non-hex inputs pass safeColor but
|
|
493
|
+
* can't be brightness-shifted mathematically).
|
|
485
494
|
*/
|
|
486
495
|
function adjustColor(hex, percent) {
|
|
487
|
-
|
|
496
|
+
if (typeof hex !== 'string' || !/^#[0-9a-fA-F]{3,8}$/.test(hex.trim())) {
|
|
497
|
+
hex = DEFAULT_ACCENT;
|
|
498
|
+
}
|
|
499
|
+
let clean = hex.trim().replace('#', '');
|
|
500
|
+
// Expand 3-digit form to 6
|
|
501
|
+
if (clean.length === 3) {
|
|
502
|
+
clean = clean.split('').map(c => c + c).join('');
|
|
503
|
+
}
|
|
504
|
+
// Strip alpha if present
|
|
505
|
+
if (clean.length === 8) clean = clean.slice(0, 6);
|
|
506
|
+
if (clean.length !== 6) clean = DEFAULT_ACCENT.slice(1);
|
|
507
|
+
|
|
508
|
+
const num = parseInt(clean, 16);
|
|
488
509
|
const amt = Math.round(2.55 * percent);
|
|
489
510
|
const R = Math.min(255, Math.max(0, (num >> 16) + amt));
|
|
490
511
|
const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) + amt));
|
package/src/ui/widget.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { generateStyles, COOKIE_ICON } from './styles.js';
|
|
6
6
|
import { getCurrentConfig } from '../config/parser.js';
|
|
7
|
+
import { escapeHTML } from '../core/security.js';
|
|
7
8
|
|
|
8
9
|
let widgetElement = null;
|
|
9
10
|
let shadowRoot = null;
|
|
@@ -13,10 +14,11 @@ let shadowRoot = null;
|
|
|
13
14
|
*/
|
|
14
15
|
function createWidgetHTML(config) {
|
|
15
16
|
const labels = config.labels.widget;
|
|
17
|
+
const safeLabel = escapeHTML(labels.label);
|
|
16
18
|
|
|
17
19
|
return `
|
|
18
20
|
<div class="zest-widget">
|
|
19
|
-
<button type="button" class="zest-widget__btn" aria-label="${
|
|
21
|
+
<button type="button" class="zest-widget__btn" aria-label="${safeLabel}" title="${safeLabel}">
|
|
20
22
|
<span class="zest-widget__icon">${COOKIE_ICON}</span>
|
|
21
23
|
</button>
|
|
22
24
|
</div>
|