@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.
- package/README.md +216 -70
- package/dist/zest.de.js +776 -286
- package/dist/zest.de.js.map +1 -1
- package/dist/zest.de.min.js +1 -1
- package/dist/zest.en.js +776 -286
- package/dist/zest.en.js.map +1 -1
- package/dist/zest.en.min.js +1 -1
- package/dist/zest.es.js +776 -286
- package/dist/zest.es.js.map +1 -1
- package/dist/zest.es.min.js +1 -1
- package/dist/zest.esm.js +776 -286
- package/dist/zest.esm.js.map +1 -1
- package/dist/zest.esm.min.js +1 -1
- package/dist/zest.fr.js +776 -286
- package/dist/zest.fr.js.map +1 -1
- package/dist/zest.fr.min.js +1 -1
- 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 +776 -286
- package/dist/zest.it.js.map +1 -1
- package/dist/zest.it.min.js +1 -1
- package/dist/zest.ja.js +776 -286
- package/dist/zest.ja.js.map +1 -1
- package/dist/zest.ja.min.js +1 -1
- package/dist/zest.js +776 -286
- package/dist/zest.js.map +1 -1
- package/dist/zest.min.js +1 -1
- package/dist/zest.nl.js +776 -286
- package/dist/zest.nl.js.map +1 -1
- package/dist/zest.nl.min.js +1 -1
- package/dist/zest.pl.js +776 -286
- package/dist/zest.pl.js.map +1 -1
- package/dist/zest.pl.min.js +1 -1
- package/dist/zest.pt.js +776 -286
- package/dist/zest.pt.js.map +1 -1
- package/dist/zest.pt.min.js +1 -1
- package/dist/zest.ru.js +776 -286
- package/dist/zest.ru.js.map +1 -1
- package/dist/zest.ru.min.js +1 -1
- package/dist/zest.uk.js +776 -286
- package/dist/zest.uk.js.map +1 -1
- package/dist/zest.uk.min.js +1 -1
- package/dist/zest.zh.js +776 -286
- package/dist/zest.zh.js.map +1 -1
- package/dist/zest.zh.min.js +1 -1
- package/package.json +17 -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 +131 -0
- package/src/core/dnt.js +56 -0
- package/src/core/known-trackers.js +195 -0
- package/src/core/pattern-matcher.js +111 -0
- package/src/core/script-blocker.js +314 -0
- package/src/core/security.js +204 -0
- package/src/core/storage-interceptor.js +173 -0
- package/src/core-lifecycle.js +192 -0
- package/src/headless.js +133 -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 +266 -0
- package/src/integrations/consent-signals.js +71 -0
- package/src/storage/consent-store.js +201 -0
- package/src/storage/events.js +84 -0
- package/src/ui/banner.js +134 -0
- package/src/ui/modal.js +215 -0
- package/src/ui/styles.js +519 -0
- 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
|
+
}
|
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,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
|
+
}
|