@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
@@ -0,0 +1,131 @@
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
+ // Upper bound on the number of queued cookies awaiting consent replay.
11
+ // An unbounded queue is a memory-exhaustion DoS vector — a hostile
12
+ // script could flood it with document.cookie writes.
13
+ const MAX_QUEUE_SIZE = 100;
14
+
15
+ // Queue for blocked cookies
16
+ const cookieQueue = [];
17
+
18
+ // Reference to consent checker function
19
+ let checkConsent = () => false;
20
+
21
+ /**
22
+ * Set the consent checker function
23
+ */
24
+ export function setConsentChecker(fn) {
25
+ checkConsent = fn;
26
+ }
27
+
28
+ /**
29
+ * Get the original cookie descriptor
30
+ */
31
+ export function getOriginalCookieDescriptor() {
32
+ return originalCookieDescriptor;
33
+ }
34
+
35
+ /**
36
+ * Get queued cookies
37
+ */
38
+ export function getCookieQueue() {
39
+ return [...cookieQueue];
40
+ }
41
+
42
+ /**
43
+ * Clear the cookie queue
44
+ */
45
+ export function clearCookieQueue() {
46
+ cookieQueue.length = 0;
47
+ }
48
+
49
+ /**
50
+ * Replay queued cookies for allowed categories
51
+ */
52
+ export function replayCookies(allowedCategories) {
53
+ const remaining = [];
54
+
55
+ for (const item of cookieQueue) {
56
+ if (allowedCategories.includes(item.category)) {
57
+ // Set the cookie using original setter
58
+ if (originalCookieDescriptor?.set) {
59
+ originalCookieDescriptor.set.call(document, item.value);
60
+ }
61
+ } else {
62
+ remaining.push(item);
63
+ }
64
+ }
65
+
66
+ cookieQueue.length = 0;
67
+ cookieQueue.push(...remaining);
68
+ }
69
+
70
+ /**
71
+ * Start intercepting cookies
72
+ */
73
+ export function interceptCookies() {
74
+ // Store original
75
+ originalCookieDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
76
+
77
+ if (!originalCookieDescriptor) {
78
+ console.warn('[Zest] Could not get cookie descriptor');
79
+ return false;
80
+ }
81
+
82
+ Object.defineProperty(document, 'cookie', {
83
+ get() {
84
+ // Always allow reading
85
+ return originalCookieDescriptor.get.call(document);
86
+ },
87
+ set(value) {
88
+ const name = parseCookieName(value);
89
+ if (!name) {
90
+ return;
91
+ }
92
+
93
+ const category = getCategoryForName(name);
94
+
95
+ if (checkConsent(category)) {
96
+ // Consent given - set cookie
97
+ originalCookieDescriptor.set.call(document, value);
98
+ } else if (cookieQueue.length < MAX_QUEUE_SIZE) {
99
+ // No consent - queue for later (capped to prevent DoS)
100
+ cookieQueue.push({
101
+ value,
102
+ name,
103
+ category,
104
+ timestamp: Date.now()
105
+ });
106
+ }
107
+ },
108
+ // configurable: false prevents a later-loaded script from
109
+ // overriding our descriptor and bypassing the interceptor.
110
+ configurable: false
111
+ });
112
+
113
+ return true;
114
+ }
115
+
116
+ /**
117
+ * Restore original cookie behavior.
118
+ *
119
+ * Note: after interceptCookies() has locked the descriptor with
120
+ * configurable:false, this call will throw. The lock is intentional —
121
+ * it stops a later-loaded script from unwinding the interceptor. If you
122
+ * need a reset, call this before the first interceptCookies() call.
123
+ */
124
+ export function restoreCookies() {
125
+ if (!originalCookieDescriptor) return;
126
+ try {
127
+ Object.defineProperty(document, 'cookie', originalCookieDescriptor);
128
+ } catch (_) {
129
+ // Descriptor is locked; nothing we can do.
130
+ }
131
+ }
@@ -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,195 @@
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
+ * Matching is restricted to hostname (and, when the list entry contains
115
+ * a path, the URL path prefix). A naive `fullUrl.includes(domain)` was
116
+ * previously used, which would false-positive on e.g.
117
+ * https://mysite.com/page?ref=google-analytics.com
118
+ */
119
+ export function matchesTrackerList(url, trackerList) {
120
+ let urlObj;
121
+ try {
122
+ urlObj = new URL(url);
123
+ } catch (e) {
124
+ return false;
125
+ }
126
+ const hostname = urlObj.hostname.toLowerCase();
127
+ const path = urlObj.pathname.toLowerCase();
128
+
129
+ for (const rawEntry of trackerList) {
130
+ if (typeof rawEntry !== 'string') continue;
131
+ const entry = rawEntry.toLowerCase();
132
+
133
+ // Partial-prefix match on hostname (entry ends with a dot),
134
+ // e.g. "matomo." matches "analytics.matomo.cloud"
135
+ if (entry.endsWith('.')) {
136
+ const needle = entry.slice(0, -1);
137
+ const segments = hostname.split('.');
138
+ if (segments.some(seg => seg === needle) || hostname.startsWith(entry)) {
139
+ return true;
140
+ }
141
+ continue;
142
+ }
143
+
144
+ // Entries containing a slash specify hostname + path prefix
145
+ const slashIdx = entry.indexOf('/');
146
+ if (slashIdx !== -1) {
147
+ const entryHost = entry.slice(0, slashIdx);
148
+ const entryPath = entry.slice(slashIdx);
149
+ if ((hostname === entryHost || hostname.endsWith('.' + entryHost)) &&
150
+ path.startsWith(entryPath)) {
151
+ return true;
152
+ }
153
+ continue;
154
+ }
155
+
156
+ // Plain hostname: exact or subdomain match only
157
+ if (hostname === entry || hostname.endsWith('.' + entry)) {
158
+ return true;
159
+ }
160
+ }
161
+
162
+ return false;
163
+ }
164
+
165
+ /**
166
+ * Get category for a script URL based on tracker lists
167
+ */
168
+ export function getCategoryForScript(url, mode = 'safe') {
169
+ const trackers = mode === 'strict' ? STRICT_TRACKERS : SAFE_TRACKERS;
170
+
171
+ for (const [category, domains] of Object.entries(trackers)) {
172
+ if (matchesTrackerList(url, domains)) {
173
+ return category;
174
+ }
175
+ }
176
+
177
+ return null;
178
+ }
179
+
180
+ /**
181
+ * Check if URL is third-party (different domain)
182
+ */
183
+ export function isThirdParty(url) {
184
+ try {
185
+ const scriptHost = new URL(url).hostname;
186
+ const pageHost = window.location.hostname;
187
+
188
+ // Remove www. for comparison
189
+ const normalizeHost = (h) => h.replace(/^www\./, '');
190
+
191
+ return normalizeHost(scriptHost) !== normalizeHost(pageHost);
192
+ } catch (e) {
193
+ return false;
194
+ }
195
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Pattern Matcher - Categorizes cookies and storage keys by pattern
3
+ */
4
+
5
+ import { safeRegExp } from './security.js';
6
+
7
+ /**
8
+ * Default patterns for each category
9
+ */
10
+ export const DEFAULT_PATTERNS = {
11
+ essential: [
12
+ /^zest_/,
13
+ /^csrf/i,
14
+ /^xsrf/i,
15
+ /^session/i,
16
+ /^__host-/i,
17
+ /^__secure-/i
18
+ ],
19
+ functional: [
20
+ /^lang/i,
21
+ /^locale/i,
22
+ /^theme/i,
23
+ /^preferences/i,
24
+ /^ui_/i
25
+ ],
26
+ analytics: [
27
+ /^_ga/,
28
+ /^_gid/,
29
+ /^_gat/,
30
+ /^_utm/,
31
+ /^__utm/,
32
+ /^plausible/i,
33
+ /^_pk_/,
34
+ /^matomo/i,
35
+ /^_hj/,
36
+ /^ajs_/
37
+ ],
38
+ marketing: [
39
+ /^_fbp/,
40
+ /^_fbc/,
41
+ /^_gcl/,
42
+ /^_ttp/,
43
+ /^ads/i,
44
+ /^doubleclick/i,
45
+ /^__gads/,
46
+ /^__gpi/,
47
+ /^_pin_/,
48
+ /^li_/
49
+ ]
50
+ };
51
+
52
+ let patterns = { ...DEFAULT_PATTERNS };
53
+
54
+ /**
55
+ * Set custom patterns. User-supplied strings are validated with safeRegExp,
56
+ * which rejects catastrophic-backtracking shapes and syntax errors.
57
+ * Invalid patterns are silently dropped with a console warning.
58
+ */
59
+ export function setPatterns(customPatterns) {
60
+ patterns = { ...DEFAULT_PATTERNS };
61
+ if (!customPatterns || typeof customPatterns !== 'object') return;
62
+
63
+ for (const [category, regexList] of Object.entries(customPatterns)) {
64
+ if (!Array.isArray(regexList)) continue;
65
+
66
+ const compiled = [];
67
+ for (const p of regexList) {
68
+ const re = safeRegExp(p);
69
+ if (re) {
70
+ compiled.push(re);
71
+ } else {
72
+ try {
73
+ console.warn('[Zest] Rejected unsafe pattern:', p);
74
+ } catch (_) { /* no-op */ }
75
+ }
76
+ }
77
+ patterns[category] = compiled;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Get current patterns
83
+ */
84
+ export function getPatterns() {
85
+ return { ...patterns };
86
+ }
87
+
88
+ /**
89
+ * Determine category for a cookie/storage key name
90
+ * @param {string} name - Cookie or storage key name
91
+ * @returns {string} Category ID (defaults to 'marketing' for unknown)
92
+ */
93
+ export function getCategoryForName(name) {
94
+ for (const [category, regexList] of Object.entries(patterns)) {
95
+ if (regexList.some(regex => regex.test(name))) {
96
+ return category;
97
+ }
98
+ }
99
+ // Unknown items default to marketing (strictest)
100
+ return 'marketing';
101
+ }
102
+
103
+ /**
104
+ * Parse cookie string to extract name
105
+ * @param {string} cookieString - Full cookie string (e.g., "name=value; path=/")
106
+ * @returns {string|null} Cookie name or null
107
+ */
108
+ export function parseCookieName(cookieString) {
109
+ const match = cookieString.match(/^([^=]+)/);
110
+ return match ? match[1].trim() : null;
111
+ }