@freshjuice/zest 0.1.0 → 1.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 +46 -0
- package/dist/zest.de.js +104 -1
- package/dist/zest.de.js.map +1 -1
- package/dist/zest.de.min.js +1 -1
- package/dist/zest.en.js +104 -1
- package/dist/zest.en.js.map +1 -1
- package/dist/zest.en.min.js +1 -1
- package/dist/zest.es.js +104 -1
- package/dist/zest.es.js.map +1 -1
- package/dist/zest.es.min.js +1 -1
- package/dist/zest.esm.js +104 -1
- package/dist/zest.esm.js.map +1 -1
- package/dist/zest.esm.min.js +1 -1
- package/dist/zest.fr.js +104 -1
- package/dist/zest.fr.js.map +1 -1
- package/dist/zest.fr.min.js +1 -1
- package/dist/zest.it.js +104 -1
- package/dist/zest.it.js.map +1 -1
- package/dist/zest.it.min.js +1 -1
- package/dist/zest.ja.js +104 -1
- package/dist/zest.ja.js.map +1 -1
- package/dist/zest.ja.min.js +1 -1
- package/dist/zest.js +104 -1
- package/dist/zest.js.map +1 -1
- package/dist/zest.min.js +1 -1
- package/dist/zest.nl.js +104 -1
- package/dist/zest.nl.js.map +1 -1
- package/dist/zest.nl.min.js +1 -1
- package/dist/zest.pl.js +104 -1
- package/dist/zest.pl.js.map +1 -1
- package/dist/zest.pl.min.js +1 -1
- package/dist/zest.pt.js +104 -1
- package/dist/zest.pt.js.map +1 -1
- package/dist/zest.pt.min.js +1 -1
- package/dist/zest.ru.js +104 -1
- package/dist/zest.ru.js.map +1 -1
- package/dist/zest.ru.min.js +1 -1
- package/dist/zest.uk.js +104 -1
- package/dist/zest.uk.js.map +1 -1
- package/dist/zest.uk.min.js +1 -1
- package/dist/zest.zh.js +104 -1
- package/dist/zest.zh.js.map +1 -1
- package/dist/zest.zh.min.js +1 -1
- package/package.json +5 -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 +116 -0
- package/src/core/dnt.js +56 -0
- package/src/core/known-trackers.js +168 -0
- package/src/core/pattern-matcher.js +96 -0
- package/src/core/script-blocker.js +308 -0
- package/src/core/storage-interceptor.js +169 -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 +377 -0
- package/src/integrations/consent-signals.js +71 -0
- package/src/storage/consent-store.js +177 -0
- package/src/storage/events.js +84 -0
- package/src/ui/banner.js +130 -0
- package/src/ui/modal.js +211 -0
- package/src/ui/styles.js +498 -0
- package/src/ui/widget.js +103 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie Interceptor - Intercepts document.cookie operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getCategoryForName, parseCookieName } from './pattern-matcher.js';
|
|
6
|
+
|
|
7
|
+
// Store original descriptor
|
|
8
|
+
let originalCookieDescriptor = null;
|
|
9
|
+
|
|
10
|
+
// Queue for blocked cookies
|
|
11
|
+
const cookieQueue = [];
|
|
12
|
+
|
|
13
|
+
// Reference to consent checker function
|
|
14
|
+
let checkConsent = () => false;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Set the consent checker function
|
|
18
|
+
*/
|
|
19
|
+
export function setConsentChecker(fn) {
|
|
20
|
+
checkConsent = fn;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the original cookie descriptor
|
|
25
|
+
*/
|
|
26
|
+
export function getOriginalCookieDescriptor() {
|
|
27
|
+
return originalCookieDescriptor;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get queued cookies
|
|
32
|
+
*/
|
|
33
|
+
export function getCookieQueue() {
|
|
34
|
+
return [...cookieQueue];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Clear the cookie queue
|
|
39
|
+
*/
|
|
40
|
+
export function clearCookieQueue() {
|
|
41
|
+
cookieQueue.length = 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Replay queued cookies for allowed categories
|
|
46
|
+
*/
|
|
47
|
+
export function replayCookies(allowedCategories) {
|
|
48
|
+
const remaining = [];
|
|
49
|
+
|
|
50
|
+
for (const item of cookieQueue) {
|
|
51
|
+
if (allowedCategories.includes(item.category)) {
|
|
52
|
+
// Set the cookie using original setter
|
|
53
|
+
if (originalCookieDescriptor?.set) {
|
|
54
|
+
originalCookieDescriptor.set.call(document, item.value);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
remaining.push(item);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
cookieQueue.length = 0;
|
|
62
|
+
cookieQueue.push(...remaining);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Start intercepting cookies
|
|
67
|
+
*/
|
|
68
|
+
export function interceptCookies() {
|
|
69
|
+
// Store original
|
|
70
|
+
originalCookieDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
|
|
71
|
+
|
|
72
|
+
if (!originalCookieDescriptor) {
|
|
73
|
+
console.warn('[Zest] Could not get cookie descriptor');
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Object.defineProperty(document, 'cookie', {
|
|
78
|
+
get() {
|
|
79
|
+
// Always allow reading
|
|
80
|
+
return originalCookieDescriptor.get.call(document);
|
|
81
|
+
},
|
|
82
|
+
set(value) {
|
|
83
|
+
const name = parseCookieName(value);
|
|
84
|
+
if (!name) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const category = getCategoryForName(name);
|
|
89
|
+
|
|
90
|
+
if (checkConsent(category)) {
|
|
91
|
+
// Consent given - set cookie
|
|
92
|
+
originalCookieDescriptor.set.call(document, value);
|
|
93
|
+
} else {
|
|
94
|
+
// No consent - queue for later
|
|
95
|
+
cookieQueue.push({
|
|
96
|
+
value,
|
|
97
|
+
name,
|
|
98
|
+
category,
|
|
99
|
+
timestamp: Date.now()
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
configurable: true
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Restore original cookie behavior
|
|
111
|
+
*/
|
|
112
|
+
export function restoreCookies() {
|
|
113
|
+
if (originalCookieDescriptor) {
|
|
114
|
+
Object.defineProperty(document, 'cookie', originalCookieDescriptor);
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/core/dnt.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Do Not Track (DNT) Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects browser DNT/GPC signals for privacy compliance
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check if Do Not Track is enabled
|
|
9
|
+
* Checks both DNT header and Global Privacy Control (GPC)
|
|
10
|
+
*/
|
|
11
|
+
export function isDoNotTrackEnabled() {
|
|
12
|
+
if (typeof navigator === 'undefined') {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check DNT (Do Not Track)
|
|
17
|
+
// Values: "1" = enabled, "0" = disabled, null/undefined = not set
|
|
18
|
+
const dnt = navigator.doNotTrack ||
|
|
19
|
+
window.doNotTrack ||
|
|
20
|
+
navigator.msDoNotTrack;
|
|
21
|
+
|
|
22
|
+
if (dnt === '1' || dnt === 'yes' || dnt === true) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check GPC (Global Privacy Control) - newer standard
|
|
27
|
+
// https://globalprivacycontrol.org/
|
|
28
|
+
if (navigator.globalPrivacyControl === true) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get DNT signal details for logging/debugging
|
|
37
|
+
*/
|
|
38
|
+
export function getDNTDetails() {
|
|
39
|
+
if (typeof navigator === 'undefined') {
|
|
40
|
+
return { enabled: false, source: null };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const dnt = navigator.doNotTrack ||
|
|
44
|
+
window.doNotTrack ||
|
|
45
|
+
navigator.msDoNotTrack;
|
|
46
|
+
|
|
47
|
+
if (dnt === '1' || dnt === 'yes' || dnt === true) {
|
|
48
|
+
return { enabled: true, source: 'dnt' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (navigator.globalPrivacyControl === true) {
|
|
52
|
+
return { enabled: true, source: 'gpc' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { enabled: false, source: null };
|
|
56
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Known Trackers - Lists of known tracking script domains by category
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Safe mode - Major, well-known trackers only
|
|
7
|
+
*/
|
|
8
|
+
export const SAFE_TRACKERS = {
|
|
9
|
+
analytics: [
|
|
10
|
+
'google-analytics.com',
|
|
11
|
+
'www.google-analytics.com',
|
|
12
|
+
'analytics.google.com',
|
|
13
|
+
'googletagmanager.com',
|
|
14
|
+
'www.googletagmanager.com',
|
|
15
|
+
'plausible.io',
|
|
16
|
+
'cloudflareinsights.com',
|
|
17
|
+
'static.cloudflareinsights.com'
|
|
18
|
+
],
|
|
19
|
+
marketing: [
|
|
20
|
+
'connect.facebook.net',
|
|
21
|
+
'www.facebook.com/tr',
|
|
22
|
+
'ads.google.com',
|
|
23
|
+
'www.googleadservices.com',
|
|
24
|
+
'googleads.g.doubleclick.net',
|
|
25
|
+
'pagead2.googlesyndication.com'
|
|
26
|
+
]
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Strict mode - Extended list including less common trackers
|
|
31
|
+
*/
|
|
32
|
+
export const STRICT_TRACKERS = {
|
|
33
|
+
analytics: [
|
|
34
|
+
...SAFE_TRACKERS.analytics,
|
|
35
|
+
'analytics.tiktok.com',
|
|
36
|
+
'matomo.', // partial match
|
|
37
|
+
'hotjar.com',
|
|
38
|
+
'static.hotjar.com',
|
|
39
|
+
'script.hotjar.com',
|
|
40
|
+
'clarity.ms',
|
|
41
|
+
'www.clarity.ms',
|
|
42
|
+
'heapanalytics.com',
|
|
43
|
+
'cdn.heapanalytics.com',
|
|
44
|
+
'mixpanel.com',
|
|
45
|
+
'cdn.mxpnl.com',
|
|
46
|
+
'segment.com',
|
|
47
|
+
'cdn.segment.com',
|
|
48
|
+
'api.segment.io',
|
|
49
|
+
'fullstory.com',
|
|
50
|
+
'rs.fullstory.com',
|
|
51
|
+
'amplitude.com',
|
|
52
|
+
'cdn.amplitude.com',
|
|
53
|
+
'mouseflow.com',
|
|
54
|
+
'cdn.mouseflow.com',
|
|
55
|
+
'luckyorange.com',
|
|
56
|
+
'cdn.luckyorange.net',
|
|
57
|
+
'crazyegg.com',
|
|
58
|
+
'script.crazyegg.com'
|
|
59
|
+
],
|
|
60
|
+
marketing: [
|
|
61
|
+
...SAFE_TRACKERS.marketing,
|
|
62
|
+
'snap.licdn.com',
|
|
63
|
+
'px.ads.linkedin.com',
|
|
64
|
+
'ads.linkedin.com',
|
|
65
|
+
'analytics.twitter.com',
|
|
66
|
+
'static.ads-twitter.com',
|
|
67
|
+
't.co',
|
|
68
|
+
'analytics.tiktok.com',
|
|
69
|
+
'ads.tiktok.com',
|
|
70
|
+
'sc-static.net', // Snapchat
|
|
71
|
+
'tr.snapchat.com',
|
|
72
|
+
'ct.pinterest.com',
|
|
73
|
+
'pintrk.com',
|
|
74
|
+
's.pinimg.com',
|
|
75
|
+
'widgets.pinterest.com',
|
|
76
|
+
'bat.bing.com',
|
|
77
|
+
'ads.yahoo.com',
|
|
78
|
+
'sp.analytics.yahoo.com',
|
|
79
|
+
'amazon-adsystem.com',
|
|
80
|
+
'z-na.amazon-adsystem.com',
|
|
81
|
+
'criteo.com',
|
|
82
|
+
'static.criteo.net',
|
|
83
|
+
'dis.criteo.com',
|
|
84
|
+
'taboola.com',
|
|
85
|
+
'cdn.taboola.com',
|
|
86
|
+
'trc.taboola.com',
|
|
87
|
+
'outbrain.com',
|
|
88
|
+
'widgets.outbrain.com',
|
|
89
|
+
'adroll.com',
|
|
90
|
+
's.adroll.com'
|
|
91
|
+
],
|
|
92
|
+
functional: [
|
|
93
|
+
'cdn.onesignal.com',
|
|
94
|
+
'onesignal.com',
|
|
95
|
+
'pusher.com',
|
|
96
|
+
'js.pusher.com',
|
|
97
|
+
'intercom.io',
|
|
98
|
+
'widget.intercom.io',
|
|
99
|
+
'js.intercomcdn.com',
|
|
100
|
+
'crisp.chat',
|
|
101
|
+
'client.crisp.chat',
|
|
102
|
+
'cdn.livechatinc.com',
|
|
103
|
+
'livechatinc.com',
|
|
104
|
+
'tawk.to',
|
|
105
|
+
'embed.tawk.to',
|
|
106
|
+
'zendesk.com',
|
|
107
|
+
'static.zdassets.com'
|
|
108
|
+
]
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if a URL matches any tracker in the list
|
|
113
|
+
*/
|
|
114
|
+
export function matchesTrackerList(url, trackerList) {
|
|
115
|
+
try {
|
|
116
|
+
const urlObj = new URL(url);
|
|
117
|
+
const hostname = urlObj.hostname.toLowerCase();
|
|
118
|
+
const fullUrl = url.toLowerCase();
|
|
119
|
+
|
|
120
|
+
for (const domain of trackerList) {
|
|
121
|
+
// Support partial matches (e.g., "matomo." matches "analytics.matomo.cloud")
|
|
122
|
+
if (domain.endsWith('.')) {
|
|
123
|
+
if (hostname.includes(domain.slice(0, -1))) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
} else if (hostname === domain || hostname.endsWith('.' + domain)) {
|
|
127
|
+
return true;
|
|
128
|
+
} else if (fullUrl.includes(domain)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch (e) {
|
|
133
|
+
// Invalid URL
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get category for a script URL based on tracker lists
|
|
140
|
+
*/
|
|
141
|
+
export function getCategoryForScript(url, mode = 'safe') {
|
|
142
|
+
const trackers = mode === 'strict' ? STRICT_TRACKERS : SAFE_TRACKERS;
|
|
143
|
+
|
|
144
|
+
for (const [category, domains] of Object.entries(trackers)) {
|
|
145
|
+
if (matchesTrackerList(url, domains)) {
|
|
146
|
+
return category;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if URL is third-party (different domain)
|
|
155
|
+
*/
|
|
156
|
+
export function isThirdParty(url) {
|
|
157
|
+
try {
|
|
158
|
+
const scriptHost = new URL(url).hostname;
|
|
159
|
+
const pageHost = window.location.hostname;
|
|
160
|
+
|
|
161
|
+
// Remove www. for comparison
|
|
162
|
+
const normalizeHost = (h) => h.replace(/^www\./, '');
|
|
163
|
+
|
|
164
|
+
return normalizeHost(scriptHost) !== normalizeHost(pageHost);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Matcher - Categorizes cookies and storage keys by pattern
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default patterns for each category
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_PATTERNS = {
|
|
9
|
+
essential: [
|
|
10
|
+
/^zest_/,
|
|
11
|
+
/^csrf/i,
|
|
12
|
+
/^xsrf/i,
|
|
13
|
+
/^session/i,
|
|
14
|
+
/^__host-/i,
|
|
15
|
+
/^__secure-/i
|
|
16
|
+
],
|
|
17
|
+
functional: [
|
|
18
|
+
/^lang/i,
|
|
19
|
+
/^locale/i,
|
|
20
|
+
/^theme/i,
|
|
21
|
+
/^preferences/i,
|
|
22
|
+
/^ui_/i
|
|
23
|
+
],
|
|
24
|
+
analytics: [
|
|
25
|
+
/^_ga/,
|
|
26
|
+
/^_gid/,
|
|
27
|
+
/^_gat/,
|
|
28
|
+
/^_utm/,
|
|
29
|
+
/^__utm/,
|
|
30
|
+
/^plausible/i,
|
|
31
|
+
/^_pk_/,
|
|
32
|
+
/^matomo/i,
|
|
33
|
+
/^_hj/,
|
|
34
|
+
/^ajs_/
|
|
35
|
+
],
|
|
36
|
+
marketing: [
|
|
37
|
+
/^_fbp/,
|
|
38
|
+
/^_fbc/,
|
|
39
|
+
/^_gcl/,
|
|
40
|
+
/^_ttp/,
|
|
41
|
+
/^ads/i,
|
|
42
|
+
/^doubleclick/i,
|
|
43
|
+
/^__gads/,
|
|
44
|
+
/^__gpi/,
|
|
45
|
+
/^_pin_/,
|
|
46
|
+
/^li_/
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let patterns = { ...DEFAULT_PATTERNS };
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Set custom patterns
|
|
54
|
+
*/
|
|
55
|
+
export function setPatterns(customPatterns) {
|
|
56
|
+
patterns = { ...DEFAULT_PATTERNS };
|
|
57
|
+
for (const [category, regexList] of Object.entries(customPatterns)) {
|
|
58
|
+
if (Array.isArray(regexList)) {
|
|
59
|
+
patterns[category] = regexList.map(p =>
|
|
60
|
+
p instanceof RegExp ? p : new RegExp(p)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get current patterns
|
|
68
|
+
*/
|
|
69
|
+
export function getPatterns() {
|
|
70
|
+
return { ...patterns };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Determine category for a cookie/storage key name
|
|
75
|
+
* @param {string} name - Cookie or storage key name
|
|
76
|
+
* @returns {string} Category ID (defaults to 'marketing' for unknown)
|
|
77
|
+
*/
|
|
78
|
+
export function getCategoryForName(name) {
|
|
79
|
+
for (const [category, regexList] of Object.entries(patterns)) {
|
|
80
|
+
if (regexList.some(regex => regex.test(name))) {
|
|
81
|
+
return category;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Unknown items default to marketing (strictest)
|
|
85
|
+
return 'marketing';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parse cookie string to extract name
|
|
90
|
+
* @param {string} cookieString - Full cookie string (e.g., "name=value; path=/")
|
|
91
|
+
* @returns {string|null} Cookie name or null
|
|
92
|
+
*/
|
|
93
|
+
export function parseCookieName(cookieString) {
|
|
94
|
+
const match = cookieString.match(/^([^=]+)/);
|
|
95
|
+
return match ? match[1].trim() : null;
|
|
96
|
+
}
|