@freshjuice/zest 1.0.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 +178 -78
- 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.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 +16 -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/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
package/dist/zest.zh.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var Zest=function(){"use strict";const t={essential:[/^zest_/,/^csrf/i,/^xsrf/i,/^session/i,/^__host-/i,/^__secure-/i],functional:[/^lang/i,/^locale/i,/^theme/i,/^preferences/i,/^ui_/i],analytics:[/^_ga/,/^_gid/,/^_gat/,/^_utm/,/^__utm/,/^plausible/i,/^_pk_/,/^matomo/i,/^_hj/,/^ajs_/],marketing:[/^_fbp/,/^_fbc/,/^_gcl/,/^_ttp/,/^ads/i,/^doubleclick/i,/^__gads/,/^__gpi/,/^_pin_/,/^li_/]};let n={...t};function e(t){for(const[e,o]of Object.entries(n))if(o.some(n=>n.test(t)))return e;return"marketing"}let o=null;const a=[];let s=()=>!1;function r(){return o}let i=null,c=null;const l=[],d=[];let u=()=>!1;function p(t,n,o){return new Proxy(t,{get(t,o){if("setItem"===o)return(o,a)=>{const s=e(o);u(s)?t.setItem(o,a):n.push({key:o,value:a,category:s,timestamp:Date.now()})};const a=t[o];return"function"==typeof a?a.bind(t):a}})}const g={analytics:["google-analytics.com","www.google-analytics.com","analytics.google.com","googletagmanager.com","www.googletagmanager.com","plausible.io","cloudflareinsights.com","static.cloudflareinsights.com"],marketing:["connect.facebook.net","www.facebook.com/tr","ads.google.com","www.googleadservices.com","googleads.g.doubleclick.net","pagead2.googlesyndication.com"]},f={analytics:[...g.analytics,"analytics.tiktok.com","matomo.","hotjar.com","static.hotjar.com","script.hotjar.com","clarity.ms","www.clarity.ms","heapanalytics.com","cdn.heapanalytics.com","mixpanel.com","cdn.mxpnl.com","segment.com","cdn.segment.com","api.segment.io","fullstory.com","rs.fullstory.com","amplitude.com","cdn.amplitude.com","mouseflow.com","cdn.mouseflow.com","luckyorange.com","cdn.luckyorange.net","crazyegg.com","script.crazyegg.com"],marketing:[...g.marketing,"snap.licdn.com","px.ads.linkedin.com","ads.linkedin.com","analytics.twitter.com","static.ads-twitter.com","t.co","analytics.tiktok.com","ads.tiktok.com","sc-static.net","tr.snapchat.com","ct.pinterest.com","pintrk.com","s.pinimg.com","widgets.pinterest.com","bat.bing.com","ads.yahoo.com","sp.analytics.yahoo.com","amazon-adsystem.com","z-na.amazon-adsystem.com","criteo.com","static.criteo.net","dis.criteo.com","taboola.com","cdn.taboola.com","trc.taboola.com","outbrain.com","widgets.outbrain.com","adroll.com","s.adroll.com"],functional:["cdn.onesignal.com","onesignal.com","pusher.com","js.pusher.com","intercom.io","widget.intercom.io","js.intercomcdn.com","crisp.chat","client.crisp.chat","cdn.livechatinc.com","livechatinc.com","tawk.to","embed.tawk.to","zendesk.com","static.zdassets.com"]};function m(t,n){try{const e=new URL(t).hostname.toLowerCase(),o=t.toLowerCase();for(const t of n)if(t.endsWith(".")){if(e.includes(t.slice(0,-1)))return!0}else{if(e===t||e.endsWith("."+t))return!0;if(o.includes(t))return!0}}catch(t){}return!1}function b(t,n="safe"){const e="strict"===n?f:g;for(const[n,o]of Object.entries(e))if(m(t,o))return n;return null}const y=[];let z=null,h="safe",x=[],w=()=>!1;function v(t){const n=t.getAttribute("data-consent-category");if(n)return n;if(t.hasAttribute("data-zest-allow"))return null;const e=t.src;if(!e)return null;const o=function(t){if(!t||0===x.length)return null;try{const n=new URL(t).hostname.toLowerCase();for(const t of x){const e="string"==typeof t?t:t.domain,o="string"==typeof t?"marketing":t.category||"marketing";if(n===e||n.endsWith("."+e))return o}}catch(t){}return null}(e);if(o)return o;switch(h){case"manual":default:return null;case"safe":case"strict":return b(e,h);case"doomsday":return function(t){try{const n=new URL(t).hostname,e=window.location.hostname,o=t=>t.replace(/^www\./,"");return o(n)!==o(e)}catch(t){return!1}}(e)?b(e,"strict")||"marketing":null}}function k(t){if(t.hasAttribute("data-zest-processed"))return!1;const n=v(t);if(!n)return t.setAttribute("data-zest-processed","allowed"),!1;if(w(n))return t.setAttribute("data-zest-processed","allowed"),!1;const e={category:n,src:t.src,inline:t.textContent,type:t.type,async:t.async,defer:t.defer,timestamp:Date.now()};return t.setAttribute("data-zest-processed","blocked"),t.setAttribute("data-consent-category",n),t.type="text/plain",t.src&&(t.setAttribute("data-blocked-src",t.src),t.removeAttribute("src")),y.push(e),!0}function _(t){const n=document.createElement("script");t.src?n.src=t.src:t.inline&&(n.textContent=t.inline),t.async&&(n.async=!0),t.defer&&(n.defer=!0),n.setAttribute("data-zest-processed","executed"),n.setAttribute("data-consent-executed","true"),document.head.appendChild(n)}function A(t){for(const n of t)for(const t of n.addedNodes)if("SCRIPT"!==t.nodeName||t.hasAttribute("data-zest-processed")||k(t),t.querySelectorAll){t.querySelectorAll("script:not([data-zest-processed])").forEach(k)}}function C(t="safe",n=[]){return h=t,x=n,document.querySelectorAll("script:not([data-zest-processed])").forEach(k),z=new MutationObserver(A),z.observe(document.documentElement,{childList:!0,subtree:!0}),!0}const S={essential:{id:"essential",label:"Essential",description:"Required for the website to function properly. Cannot be disabled.",required:!0,default:!0},functional:{id:"functional",label:"Functional",description:"Enable personalized features like language preferences and themes.",required:!1,default:!1},analytics:{id:"analytics",label:"Analytics",description:"Help us understand how visitors interact with our website.",required:!1,default:!1},marketing:{id:"marketing",label:"Marketing",description:"Used to deliver relevant advertisements and track campaign performance.",required:!1,default:!1}};function E(){if("undefined"==typeof navigator)return!1;const t=navigator.doNotTrack||window.doNotTrack||navigator.msDoNotTrack;return"1"===t||"yes"===t||!0===t||!0===navigator.globalPrivacyControl}function j(t,n,e){const o=e?"default":"update";n.consentModeGoogle&&function(t,n){if(window.dataLayer=window.dataLayer||[],"function"==typeof window.gtag)window.gtag("consent",t,n);else{function e(){window.dataLayer.push(arguments)}e("consent",t,n)}}(o,function(t){const n=t=>t?"granted":"denied";return{ad_storage:n(t.marketing),ad_user_data:n(t.marketing),ad_personalization:n(t.marketing),analytics_storage:n(t.analytics),functionality_storage:"granted",personalization_storage:n(t.functional)}}(t)),n.consentModeMicrosoft&&function(t,n){window.uetq=window.uetq||[],window.uetq.push("consent",t,n)}(o,function(t){return{ad_storage:t.marketing?"granted":"denied"}}(t))}const M={labels:{banner:{title:"我们重视您的隐私",description:'我们使用Cookie来改善您的浏览体验、提供个性化内容并分析我们的流量。点击"全部接受"即表示您同意我们使用Cookie。',acceptAll:"全部接受",rejectAll:"全部拒绝",settings:"设置"},modal:{title:"隐私设置",description:"管理您的Cookie偏好设置。您可以在下方启用或禁用不同类型的Cookie。",save:"保存设置",acceptAll:"全部接受",rejectAll:"全部拒绝"},widget:{label:"Cookie设置"}},categories:{essential:{label:"必要",description:"网站正常运行所必需的。无法禁用。"},functional:{label:"功能性",description:"启用个性化功能,如语言偏好和主题设置。"},analytics:{label:"分析",description:"帮助我们了解访问者如何与网站互动。"},marketing:{label:"营销",description:"用于展示相关广告并衡量营销活动效果。"}}};const R={lang:"auto",position:"bottom",theme:"auto",accentColor:"#0071e3",categories:S,labels:{banner:{title:"We value your privacy",description:'We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. By clicking "Accept All", you consent to our use of cookies.',acceptAll:"Accept All",rejectAll:"Reject All",settings:"Settings"},modal:{title:"Privacy Settings",description:"Manage your cookie preferences. You can enable or disable different types of cookies below.",save:"Save Preferences",acceptAll:"Accept All",rejectAll:"Reject All"},widget:{label:"Cookie Settings"}},autoInit:!0,showWidget:!0,expiration:365,respectDNT:!0,dntBehavior:"reject",customStyles:"",consentModeGoogle:!1,consentModeMicrosoft:!1,mode:"safe",blockedDomains:[],policyUrl:null,imprintUrl:null,callbacks:{onAccept:null,onReject:null,onChange:null,onReady:null}};function $(t){const n={...R};t||(t={});const e=["lang","position","theme","accentColor","autoInit","showWidget","expiration","policyUrl","imprintUrl","customStyles","mode","blockedDomains","respectDNT","dntBehavior","consentModeGoogle","consentModeMicrosoft"];for(const o of e)void 0!==t[o]&&(n[o]=t[o]);n.lang="zh";const o=M,a=o.labels||{},s=t.labels||{};n.labels={banner:{...R.labels.banner,...a.banner,...s.banner},modal:{...R.labels.modal,...a.modal,...s.modal},widget:{...R.labels.widget,...a.widget,...s.widget}};const r=o.categories||{},i=t.categories||{};n.categories={...R.categories};for(const t of Object.keys(R.categories))n.categories[t]={...R.categories[t],...r[t],...i[t]};return t.callbacks&&(n.callbacks={...R.callbacks,...t.callbacks}),t.patterns&&(n.patterns=t.patterns),n}function N(){const t="undefined"!=typeof window&&window.ZestConfig?window.ZestConfig:{},n=function(){const t=document.currentScript||document.querySelector("script[data-zest]")||document.querySelector('script[src*="zest"]');if(!t)return{};const n={},e=t.getAttribute("data-position");e&&(n.position=e);const o=t.getAttribute("data-theme");o&&(n.theme=o);const a=t.getAttribute("data-accent")||t.getAttribute("data-accent-color");a&&(n.accentColor=a);const s=t.getAttribute("data-policy-url")||t.getAttribute("data-privacy-url");s&&(n.policyUrl=s);const r=t.getAttribute("data-imprint-url");r&&(n.imprintUrl=r);const i=t.getAttribute("data-show-widget");null!==i&&(n.showWidget="false"!==i);const c=t.getAttribute("data-auto-init");null!==c&&(n.autoInit="false"!==c);const l=t.getAttribute("data-expiration");l&&(n.expiration=parseInt(l,10));const d=t.getAttribute("data-consent-mode-google");null!==d&&(n.consentModeGoogle="false"!==d);const u=t.getAttribute("data-consent-mode-microsoft");return null!==u&&(n.consentModeMicrosoft="false"!==u),n}();return $({...t,...n})}let D=null;function q(){return D||(D=N()),D}const L="zest_consent";let T=null;function O(t){const n=r();n?.set?n.set.call(document,t):document.cookie=t}function I(){const t=r();return t?.get?t.get.call(document):document.cookie}function U(){try{const t=I().match(RegExp(L+"=([^;]+)"));if(t){const n=JSON.parse(decodeURIComponent(t[1]));return T=n.categories||{essential:!0,functional:!1,analytics:!1,marketing:!1},{...T}}}catch(t){}return T={essential:!0,functional:!1,analytics:!1,marketing:!1},{...T}}function W(){return T||(T=U()),{...T}}function Z(t,n=365){const e=T?{...T}:{essential:!0,functional:!1,analytics:!1,marketing:!1};return T={essential:!0,functional:!!t.functional,analytics:!!t.analytics,marketing:!!t.marketing},function(t=365){T||(T={essential:!0,functional:!1,analytics:!1,marketing:!1});const n={version:"1.0",timestamp:Date.now(),categories:T},e=new Date(Date.now()+24*t*60*60*1e3).toUTCString();O(`${L}=${encodeURIComponent(JSON.stringify(n))}; expires=${e}; path=/; SameSite=Lax`)}(n),{current:{...T},previous:e}}function P(t){return T||(T=U()),!0===T[t]}function Y(t=365){return Z({functional:!1,analytics:!1,marketing:!1},t)}function H(){try{return I().includes(L)}catch(t){return!1}}const B={READY:"zest:ready",CONSENT:"zest:consent",REJECT:"zest:reject",CHANGE:"zest:change",SHOW:"zest:show",HIDE:"zest:hide"};function G(t,n={}){const e=new CustomEvent(t,{detail:n,bubbles:!0,cancelable:!0});return document.dispatchEvent(e),e}function J(t,n){return G(B.CONSENT,{consent:t,previous:n})}function F(t){return G(B.REJECT,{consent:t})}function X(t,n){return G(B.CHANGE,{consent:t,previous:n})}function V(t="banner"){return G(B.SHOW,{type:t})}function K(t="banner"){return G(B.HIDE,{type:t})}function Q(t){const n=t.accentColor||"#4F46E5";return`\n:host {\n --zest-accent: ${n};\n --zest-accent-hover: ${function(t,n){const e=parseInt(t.replace("#",""),16),o=Math.round(2.55*n);return"#"+(16777216+65536*Math.min(255,Math.max(0,(e>>16)+o))+256*Math.min(255,Math.max(0,(e>>8&255)+o))+Math.min(255,Math.max(0,(255&e)+o))).toString(16).slice(1)}(n,-15)};\n --zest-bg: #ffffff;\n --zest-bg-secondary: #f3f4f6;\n --zest-text: #1f2937;\n --zest-text-secondary: #6b7280;\n --zest-border: #e5e7eb;\n --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);\n --zest-radius: 12px;\n --zest-radius-sm: 8px;\n --zest-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;\n\n font-family: var(--zest-font);\n font-size: 14px;\n line-height: 1.5;\n color: var(--zest-text);\n box-sizing: border-box;\n}\n\n:host([data-theme="dark"]) {\n --zest-bg: #1f2937;\n --zest-bg-secondary: #374151;\n --zest-text: #f9fafb;\n --zest-text-secondary: #9ca3af;\n --zest-border: #4b5563;\n --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);\n}\n\n@media (prefers-color-scheme: dark) {\n :host([data-theme="auto"]) {\n --zest-bg: #1f2937;\n --zest-bg-secondary: #374151;\n --zest-text: #f9fafb;\n --zest-text-secondary: #9ca3af;\n --zest-border: #4b5563;\n --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);\n }\n}\n\n*, *::before, *::after {\n box-sizing: border-box;\n}\n\n/* Banner */\n.zest-banner {\n position: fixed;\n z-index: 999999;\n max-width: 480px;\n padding: 20px;\n background: var(--zest-bg);\n border-radius: var(--zest-radius);\n box-shadow: var(--zest-shadow);\n animation: zest-slide-in 0.3s ease-out;\n}\n\n.zest-banner--bottom {\n bottom: 20px;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.zest-banner--bottom-left {\n bottom: 20px;\n left: 20px;\n}\n\n.zest-banner--bottom-right {\n bottom: 20px;\n right: 20px;\n}\n\n.zest-banner--top {\n top: 20px;\n left: 50%;\n transform: translateX(-50%);\n}\n\n@keyframes zest-slide-in {\n from {\n opacity: 0;\n transform: translateX(-50%) translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateX(-50%) translateY(0);\n }\n}\n\n.zest-banner--bottom-left {\n animation-name: zest-slide-in-left;\n}\n\n@keyframes zest-slide-in-left {\n from {\n opacity: 0;\n transform: translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.zest-banner--bottom-right {\n animation-name: zest-slide-in-right;\n}\n\n@keyframes zest-slide-in-right {\n from {\n opacity: 0;\n transform: translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .zest-banner,\n .zest-modal {\n animation: none;\n }\n}\n\n.zest-banner__title {\n margin: 0 0 8px 0;\n font-size: 16px;\n font-weight: 600;\n color: var(--zest-text);\n}\n\n.zest-banner__description {\n margin: 0 0 16px 0;\n font-size: 14px;\n color: var(--zest-text-secondary);\n}\n\n.zest-banner__buttons {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* Buttons */\n.zest-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 10px 16px;\n font-size: 14px;\n font-weight: 500;\n font-family: inherit;\n border: none;\n border-radius: var(--zest-radius-sm);\n cursor: pointer;\n transition: background-color 0.15s ease, transform 0.1s ease;\n}\n\n.zest-btn:hover {\n transform: translateY(-1px);\n}\n\n.zest-btn:active {\n transform: translateY(0);\n}\n\n.zest-btn:focus-visible {\n outline: 2px solid var(--zest-accent);\n outline-offset: 2px;\n}\n\n.zest-btn--primary {\n background: var(--zest-accent);\n color: #ffffff;\n}\n\n.zest-btn--primary:hover {\n background: var(--zest-accent-hover);\n}\n\n.zest-btn--secondary {\n background: var(--zest-bg-secondary);\n color: var(--zest-text);\n}\n\n.zest-btn--secondary:hover {\n background: var(--zest-border);\n}\n\n.zest-btn--ghost {\n background: transparent;\n color: var(--zest-text-secondary);\n}\n\n.zest-btn--ghost:hover {\n background: var(--zest-bg-secondary);\n color: var(--zest-text);\n}\n\n/* Modal */\n.zest-modal-overlay {\n position: fixed;\n inset: 0;\n z-index: 999998;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n background: rgba(0, 0, 0, 0.5);\n animation: zest-fade-in 0.2s ease-out;\n}\n\n@keyframes zest-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n.zest-modal {\n width: 100%;\n max-width: 500px;\n max-height: 90vh;\n overflow-y: auto;\n background: var(--zest-bg);\n border-radius: var(--zest-radius);\n box-shadow: var(--zest-shadow);\n animation: zest-modal-in 0.3s ease-out;\n}\n\n@keyframes zest-modal-in {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n.zest-modal__header {\n padding: 20px 20px 0;\n}\n\n.zest-modal__title {\n margin: 0 0 8px 0;\n font-size: 18px;\n font-weight: 600;\n color: var(--zest-text);\n}\n\n.zest-modal__description {\n margin: 0;\n font-size: 14px;\n color: var(--zest-text-secondary);\n}\n\n.zest-modal__body {\n padding: 20px;\n}\n\n.zest-modal__footer {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n padding: 0 20px 20px;\n}\n\n/* Categories */\n.zest-category {\n padding: 16px;\n margin-bottom: 12px;\n background: var(--zest-bg-secondary);\n border-radius: var(--zest-radius-sm);\n}\n\n.zest-category:last-child {\n margin-bottom: 0;\n}\n\n.zest-category__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.zest-category__info {\n flex: 1;\n}\n\n.zest-category__label {\n display: block;\n font-size: 14px;\n font-weight: 600;\n color: var(--zest-text);\n}\n\n.zest-category__description {\n margin: 4px 0 0;\n font-size: 13px;\n color: var(--zest-text-secondary);\n}\n\n/* Toggle Switch */\n.zest-toggle {\n position: relative;\n width: 44px;\n height: 24px;\n flex-shrink: 0;\n}\n\n.zest-toggle__input {\n position: absolute;\n opacity: 0;\n width: 100%;\n height: 100%;\n cursor: pointer;\n margin: 0;\n}\n\n.zest-toggle__input:disabled {\n cursor: not-allowed;\n}\n\n.zest-toggle__slider {\n position: absolute;\n inset: 0;\n background: var(--zest-border);\n border-radius: 12px;\n transition: background-color 0.2s ease;\n pointer-events: none;\n}\n\n.zest-toggle__slider::before {\n content: '';\n position: absolute;\n top: 2px;\n left: 2px;\n width: 20px;\n height: 20px;\n background: #ffffff;\n border-radius: 50%;\n transition: transform 0.2s ease;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);\n}\n\n.zest-toggle__input:checked + .zest-toggle__slider {\n background: var(--zest-accent);\n}\n\n.zest-toggle__input:checked + .zest-toggle__slider::before {\n transform: translateX(20px);\n}\n\n.zest-toggle__input:focus-visible + .zest-toggle__slider {\n outline: 2px solid var(--zest-accent);\n outline-offset: 2px;\n}\n\n.zest-toggle__input:disabled + .zest-toggle__slider {\n opacity: 0.6;\n}\n\n/* Widget */\n.zest-widget {\n position: fixed;\n z-index: 999997;\n bottom: 20px;\n left: 20px;\n}\n\n.zest-widget__btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 48px;\n height: 48px;\n padding: 0;\n background: var(--zest-bg);\n border: 1px solid var(--zest-border);\n border-radius: 50%;\n box-shadow: var(--zest-shadow);\n cursor: pointer;\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n}\n\n.zest-widget__btn:hover {\n transform: scale(1.05);\n box-shadow: 0 12px 28px -5px rgba(0, 0, 0, 0.15);\n}\n\n.zest-widget__btn:focus-visible {\n outline: 2px solid var(--zest-accent);\n outline-offset: 2px;\n}\n\n.zest-widget__icon {\n width: 24px;\n height: 24px;\n fill: var(--zest-text);\n}\n\n/* Link */\n.zest-link {\n color: var(--zest-accent);\n text-decoration: none;\n}\n\n.zest-link:hover {\n text-decoration: underline;\n}\n\n/* Mobile */\n@media (max-width: 480px) {\n .zest-banner {\n left: 10px;\n right: 10px;\n max-width: none;\n transform: none;\n }\n\n .zest-banner--bottom,\n .zest-banner--bottom-left,\n .zest-banner--bottom-right {\n bottom: 10px;\n }\n\n .zest-banner--top {\n top: 10px;\n transform: none;\n }\n\n @keyframes zest-slide-in {\n from {\n opacity: 0;\n transform: translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n .zest-banner__buttons {\n flex-direction: column;\n }\n\n .zest-btn {\n width: 100%;\n }\n\n .zest-modal-overlay {\n padding: 10px;\n }\n\n .zest-widget {\n bottom: 10px;\n left: 10px;\n }\n}\n\n/* Hidden utility */\n.zest-hidden {\n display: none !important;\n}\n${t.customStyles||""}\n`}let tt=null,nt=null;function et(t={}){tt?tt.classList.remove("zest-hidden"):function(t={}){if(tt)return tt;const n=q();tt=document.createElement("zest-banner"),tt.setAttribute("data-theme",n.theme||"light"),nt=tt.attachShadow({mode:"open"});const e=document.createElement("style");e.textContent=Q(n),nt.appendChild(e);const o=document.createElement("div");o.innerHTML=function(t){const n=t.labels.banner;return`\n <div class="zest-banner zest-banner--${t.position||"bottom"}" role="dialog" aria-modal="false" aria-label="${n.title}">\n <h2 class="zest-banner__title">${n.title}</h2>\n <p class="zest-banner__description">${n.description}</p>\n <div class="zest-banner__buttons">\n <button type="button" class="zest-btn zest-btn--primary" data-action="accept-all">\n ${n.acceptAll}\n </button>\n <button type="button" class="zest-btn zest-btn--secondary" data-action="reject-all">\n ${n.rejectAll}\n </button>\n <button type="button" class="zest-btn zest-btn--ghost" data-action="settings">\n ${n.settings}\n </button>\n </div>\n </div>\n `}(n),nt.appendChild(o.firstElementChild);const a=nt.querySelector(".zest-banner");a.addEventListener("click",n=>{const e=n.target.dataset.action;if(e)switch(e){case"accept-all":t.onAcceptAll?.();break;case"reject-all":t.onRejectAll?.();break;case"settings":t.onSettings?.()}}),a.addEventListener("keydown",n=>{"Escape"===n.key&&t.onSettings?.()}),document.body.appendChild(tt),requestAnimationFrame(()=>{const t=nt.querySelector("button");t?.focus()})}(t)}function ot(){tt&&(tt.remove(),tt=null,nt=null)}let at=null,st=null,rt={};function it(){if(!st)return rt;const t=st.querySelectorAll(".zest-toggle__input"),n={essential:!0};return t.forEach(t=>{const e=t.dataset.category;e&&"essential"!==e&&(n[e]=t.checked)}),n}function ct(t={},n={}){if(at)return at;const e=q();rt={...t},at=document.createElement("zest-modal"),at.setAttribute("data-theme",e.theme||"light"),st=at.attachShadow({mode:"open"});const o=document.createElement("style");o.textContent=Q(e),st.appendChild(o);const a=document.createElement("div");a.innerHTML=function(t,n){const e=t.labels.modal,o=Object.values(t.categories||S).map(t=>{return`\n <div class="zest-category">\n <div class="zest-category__header">\n <div class="zest-category__info">\n <span class="zest-category__label">${(e=t).label}</span>\n <p class="zest-category__description">${e.description}</p>\n </div>\n <label class="zest-toggle">\n <input\n type="checkbox"\n class="zest-toggle__input"\n data-category="${e.id}"\n ${n[t.id]??t.default?"checked":""}\n ${t.required?"disabled":""}\n aria-label="${e.label}"\n >\n <span class="zest-toggle__slider"></span>\n </label>\n </div>\n </div>\n `;var e}).join("");return`\n <div class="zest-modal-overlay" role="dialog" aria-modal="true" aria-label="${e.title}">\n <div class="zest-modal">\n <div class="zest-modal__header">\n <h2 class="zest-modal__title">${e.title}</h2>\n <p class="zest-modal__description">${e.description} ${t.policyUrl?`<a href="${t.policyUrl}" class="zest-link" target="_blank" rel="noopener">Privacy Policy</a>`:""}</p>\n </div>\n <div class="zest-modal__body">\n ${o}\n </div>\n <div class="zest-modal__footer">\n <button type="button" class="zest-btn zest-btn--primary" data-action="save">\n ${e.save}\n </button>\n <button type="button" class="zest-btn zest-btn--secondary" data-action="accept-all">\n ${e.acceptAll}\n </button>\n <button type="button" class="zest-btn zest-btn--ghost" data-action="reject-all">\n ${e.rejectAll}\n </button>\n </div>\n </div>\n </div>\n `}(e,t),st.appendChild(a.firstElementChild);const s=st.querySelector(".zest-modal-overlay");return s.addEventListener("click",t=>{const e=t.target.dataset.action;if(e)switch(e){case"save":n.onSave?.(it());break;case"accept-all":n.onAcceptAll?.();break;case"reject-all":n.onRejectAll?.()}else t.target===s&&n.onClose?.()}),s.addEventListener("keydown",t=>{"Escape"===t.key&&n.onClose?.()}),st.querySelectorAll(".zest-toggle__input").forEach(t=>{t.addEventListener("change",()=>{rt=it()})}),document.body.appendChild(at),requestAnimationFrame(()=>{const t=st.querySelector("button");t?.focus()}),at}function lt(){at&&(at.remove(),at=null,st=null)}let dt=null,ut=null;function pt(t={}){dt?dt.style.display="":function(t={}){if(dt)return dt;const n=q();dt=document.createElement("zest-widget"),dt.setAttribute("data-theme",n.theme||"light"),ut=dt.attachShadow({mode:"open"});const e=document.createElement("style");e.textContent=Q(n),ut.appendChild(e);const o=document.createElement("div");o.innerHTML=function(t){const n=t.labels.widget;return`\n <div class="zest-widget">\n <button type="button" class="zest-widget__btn" aria-label="${n.label}" title="${n.label}">\n <span class="zest-widget__icon"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10c0-.728-.078-1.437-.225-2.12a1 1 0 0 0-1.482-.63 3 3 0 0 1-4.086-3.72 1 1 0 0 0-.793-1.263A10.05 10.05 0 0 0 12 2zm0 2c.178 0 .354.006.528.017a5 5 0 0 0 5.955 5.955c.011.174.017.35.017.528 0 4.418-3.582 8-8 8s-8-3.582-8-8 3.582-8 8-8zm-4 6a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm5 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm-2 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"/></svg></span>\n </button>\n </div>\n `}(n),ut.appendChild(o.firstElementChild),ut.querySelector(".zest-widget__btn").addEventListener("click",()=>{t.onClick?.()}),document.body.appendChild(dt)}(t)}function gt(){dt&&(dt.style.display="none")}let ft=!1,mt=null;function bt(t){return P(t)}function yt(t){!function(t){const n=[];for(const e of a)t.includes(e.category)?o?.set&&o.set.call(document,e.value):n.push(e);a.length=0,a.push(...n)}(t),function(t){const n=[];for(const e of l)t.includes(e.category)?i?.setItem(e.key,e.value):n.push(e);l.length=0,l.push(...n);const e=[];for(const n of d)t.includes(n.category)?c?.setItem(n.key,n.value):e.push(n);d.length=0,d.push(...e)}(t),function(t){const n=[];for(const e of y)t.includes(e.category)?_(e):n.push(e);y.length=0,y.push(...n),document.querySelectorAll('script[data-zest-processed="blocked"]').forEach(n=>{const e=n.getAttribute("data-consent-category");if(t.includes(e)){const t=document.createElement("script"),e=n.getAttribute("data-blocked-src");e?t.src=e:t.textContent=n.textContent,n.async&&(t.async=!0),n.defer&&(t.defer=!0),t.setAttribute("data-zest-processed","executed"),t.setAttribute("data-consent-executed","true"),n.parentNode?.replaceChild(t,n)}})}(t)}function zt(){const t=function(t=365){return Z({functional:!0,analytics:!0,marketing:!0},t)}(mt.expiration),n=Object.keys(S);j(t.current,mt,!1),ot(),lt(),yt(n),mt.showWidget&&pt({onClick:wt}),J(t.current,t.previous),X(t.current,t.previous),mt.callbacks?.onAccept?.(t.current),mt.callbacks?.onChange?.(t.current)}function ht(){const t=Y(mt.expiration);j(t.current,mt,!1),ot(),lt(),mt.showWidget&&pt({onClick:wt}),F(t.current),X(t.current,t.previous),mt.callbacks?.onReject?.(),mt.callbacks?.onChange?.(t.current)}function xt(t){const n=Z(t,mt.expiration);j(n.current,mt,!1);const e=Object.keys(n.current).filter(t=>n.current[t]&&!n.previous[t]);e.length>0&&yt(e),lt(),mt.showWidget&&pt({onClick:wt});Object.entries(t).some(([t,n])=>"essential"!==t&&n)?J(n.current,n.previous):F(n.current),X(n.current,n.previous),mt.callbacks?.onChange?.(n.current)}function wt(){ot(),gt(),ct(W(),{onSave:xt,onAcceptAll:zt,onRejectAll:ht,onClose:vt}),V("modal")}function vt(){lt(),K("modal"),H()&&mt.showWidget?pt({onClick:wt}):et({onAcceptAll:zt,onRejectAll:ht,onSettings:wt})}function kt(r={}){if(ft)return console.warn("[Zest] Already initialized"),_t;mt=function(t){return D=$(t),D}(r),j({functional:!1,analytics:!1,marketing:!1},mt,!0),mt.patterns&&function(e){n={...t};for(const[t,o]of Object.entries(e))Array.isArray(o)&&(n[t]=o.map(t=>t instanceof RegExp?t:RegExp(t)))}(mt.patterns),s=bt,function(t){u=t}(bt),function(t){w=t}(bt),o=Object.getOwnPropertyDescriptor(Document.prototype,"cookie"),o?Object.defineProperty(document,"cookie",{get:()=>o.get.call(document),set(t){const n=function(t){const n=t.match(/^([^=]+)/);return n?n[1].trim():null}(t);if(!n)return;const r=e(n);s(r)?o.set.call(document,t):a.push({value:t,name:n,category:r,timestamp:Date.now()})},configurable:!0}):console.warn("[Zest] Could not get cookie descriptor"),function(){try{return i=window.localStorage,c=window.sessionStorage,Object.defineProperty(window,"localStorage",{value:p(i,l),configurable:!0,writable:!1}),Object.defineProperty(window,"sessionStorage",{value:p(c,d),configurable:!0,writable:!1}),!0}catch(t){return console.warn("[Zest] Could not intercept storage APIs:",t),!1}}(),C(mt.mode,mt.blockedDomains);const g=U();ft=!0,H()&&j(g,mt,!1);let f=!1;if(E()&&mt.respectDNT&&"ignore"!==mt.dntBehavior&&"reject"===mt.dntBehavior&&!H()){const t=Y(mt.expiration);f=!0,j(t.current,mt,!1),F(t.current),X(t.current,t.previous),mt.callbacks?.onReject?.(),mt.callbacks?.onChange?.(t.current)}return function(t){G(B.READY,{consent:t})}(g),mt.callbacks?.onReady?.(g),H()||f?mt.showWidget&&pt({onClick:wt}):(et({onAcceptAll:zt,onRejectAll:ht,onSettings:wt}),V("banner")),_t}const _t={init:kt,show(){ft?(lt(),gt(),et({onAcceptAll:zt,onRejectAll:ht,onSettings:wt}),V("banner")):console.warn("[Zest] Not initialized. Call Zest.init() first.")},hide(){ot(),K("banner")},showSettings(){ft?wt():console.warn("[Zest] Not initialized. Call Zest.init() first.")},hideSettings(){lt(),K("modal")},getConsent:W,hasConsent:P,hasConsentDecision:H,getConsentProof:function(){try{const t=I().match(RegExp(L+"=([^;]+)"));if(t)return JSON.parse(decodeURIComponent(t[1]))}catch(t){}return null},isDoNotTrackEnabled:E,getDNTDetails:function(){if("undefined"==typeof navigator)return{enabled:!1,source:null};const t=navigator.doNotTrack||window.doNotTrack||navigator.msDoNotTrack;return"1"===t||"yes"===t||!0===t?{enabled:!0,source:"dnt"}:!0===navigator.globalPrivacyControl?{enabled:!0,source:"gpc"}:{enabled:!1,source:null}},acceptAll(){ft?zt():console.warn("[Zest] Not initialized. Call Zest.init() first.")},rejectAll(){ft?ht():console.warn("[Zest] Not initialized. Call Zest.init() first.")},reset(){O(L+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/"),T=null,lt(),dt&&(dt.remove(),dt=null,ut=null),ft&&(et({onAcceptAll:zt,onRejectAll:ht,onSettings:wt}),V("banner"))},getConfig:q,EVENTS:B};if("undefined"!=typeof window){window.Zest=_t;const t=()=>{!1!==N().autoInit&&kt(window.ZestConfig)};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",t):t()}return _t}();
|
|
1
|
+
var Zest=function(){"use strict";const t={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"};function n(n){if(null==n)return"";return("string"==typeof n?n:n+"").replace(/[&<>"'`]/g,n=>t[n])}const e=new Set(["transparent","black","white","red","green","blue","yellow","orange","purple","pink","gray","grey","brown","cyan","magenta","silver","gold","navy","teal","maroon","olive","lime","aqua","fuchsia","indigo","violet","crimson","coral","salmon","tomato"]);const o=[/(\([^)]*[+*][^)]*\)|\[[^\]]*\]|\\w|\\d|\\s)\s*[+*]/,/\(\?[=!][^)]*[+*][^)]*\)[+*]/];function a(t,n){if(t instanceof RegExp)return t;if("string"!=typeof t)return null;if(t.length>500)return null;for(const n of o)if(n.test(t))return null;try{return RegExp(t,n)}catch(t){return null}}const s=new Set(["__proto__","constructor","prototype"]);function r(t,n){if(!t||"object"!=typeof t||Array.isArray(t))return null;const e={version:"string"==typeof t.version?t.version:null,timestamp:"number"==typeof t.timestamp&&Number.isFinite(t.timestamp)?t.timestamp:null,categories:{}},o=t.categories;if(!o||"object"!=typeof o||Array.isArray(o))return null;for(const t of n)s.has(t)||Object.prototype.hasOwnProperty.call(o,t)&&(e.categories[t]=!0===o[t]);return n.includes("essential")&&(e.categories.essential=!0),e}function i(t,...n){if("function"==typeof t)try{return t(...n)}catch(t){try{console.error("[Zest] User callback threw:",t)}catch(t){}return}}const c={essential:[/^zest_/,/^csrf/i,/^xsrf/i,/^session/i,/^__host-/i,/^__secure-/i],functional:[/^lang/i,/^locale/i,/^theme/i,/^preferences/i,/^ui_/i],analytics:[/^_ga/,/^_gid/,/^_gat/,/^_utm/,/^__utm/,/^plausible/i,/^_pk_/,/^matomo/i,/^_hj/,/^ajs_/],marketing:[/^_fbp/,/^_fbc/,/^_gcl/,/^_ttp/,/^ads/i,/^doubleclick/i,/^__gads/,/^__gpi/,/^_pin_/,/^li_/]};let l={...c};function d(t){for(const[n,e]of Object.entries(l))if(e.some(n=>n.test(t)))return n;return"marketing"}let u=null;const p=[];let f=()=>!1;function g(){return u}let m=null,b=null;const h=[],y=[];let z=()=>!1;function w(t,n,e){return new Proxy(t,{get(t,e){if("setItem"===e)return(e,o)=>{const a=d(e);z(a)?t.setItem(e,o):200>n.length&&n.push({key:e,value:o,category:a,timestamp:Date.now()})};const o=t[e];return"function"==typeof o?o.bind(t):o}})}const x={analytics:["google-analytics.com","www.google-analytics.com","analytics.google.com","googletagmanager.com","www.googletagmanager.com","plausible.io","cloudflareinsights.com","static.cloudflareinsights.com"],marketing:["connect.facebook.net","www.facebook.com/tr","ads.google.com","www.googleadservices.com","googleads.g.doubleclick.net","pagead2.googlesyndication.com"]},v={analytics:[...x.analytics,"analytics.tiktok.com","matomo.","hotjar.com","static.hotjar.com","script.hotjar.com","clarity.ms","www.clarity.ms","heapanalytics.com","cdn.heapanalytics.com","mixpanel.com","cdn.mxpnl.com","segment.com","cdn.segment.com","api.segment.io","fullstory.com","rs.fullstory.com","amplitude.com","cdn.amplitude.com","mouseflow.com","cdn.mouseflow.com","luckyorange.com","cdn.luckyorange.net","crazyegg.com","script.crazyegg.com"],marketing:[...x.marketing,"snap.licdn.com","px.ads.linkedin.com","ads.linkedin.com","analytics.twitter.com","static.ads-twitter.com","t.co","analytics.tiktok.com","ads.tiktok.com","sc-static.net","tr.snapchat.com","ct.pinterest.com","pintrk.com","s.pinimg.com","widgets.pinterest.com","bat.bing.com","ads.yahoo.com","sp.analytics.yahoo.com","amazon-adsystem.com","z-na.amazon-adsystem.com","criteo.com","static.criteo.net","dis.criteo.com","taboola.com","cdn.taboola.com","trc.taboola.com","outbrain.com","widgets.outbrain.com","adroll.com","s.adroll.com"],functional:["cdn.onesignal.com","onesignal.com","pusher.com","js.pusher.com","intercom.io","widget.intercom.io","js.intercomcdn.com","crisp.chat","client.crisp.chat","cdn.livechatinc.com","livechatinc.com","tawk.to","embed.tawk.to","zendesk.com","static.zdassets.com"]};function k(t,n){let e;try{e=new URL(t)}catch(t){return!1}const o=e.hostname.toLowerCase(),a=e.pathname.toLowerCase();for(const t of n){if("string"!=typeof t)continue;const n=t.toLowerCase();if(n.endsWith(".")){const t=n.slice(0,-1);if(o.split(".").some(n=>n===t)||o.startsWith(n))return!0;continue}const e=n.indexOf("/");if(-1!==e){const t=n.slice(0,e),s=n.slice(e);if((o===t||o.endsWith("."+t))&&a.startsWith(s))return!0;continue}if(o===n||o.endsWith("."+n))return!0}return!1}function _(t,n="safe"){const e="strict"===n?v:x;for(const[n,o]of Object.entries(e))if(k(t,o))return n;return null}const A=new Set(["functional","analytics","marketing"]),C=[];let S=null,j="safe",E=[],$=()=>!1;function L(t){if(t.hasAttribute("data-zest-allow"))return null;const n=t.getAttribute("data-consent-category"),e=n&&A.has(n)?n:null,o=t.src;if(!o)return e;const a=function(t){if(!t||0===E.length)return null;try{const n=new URL(t).hostname.toLowerCase();for(const t of E){const e="string"==typeof t?t:t.domain,o="string"==typeof t?"marketing":t.category||"marketing";if(n===e||n.endsWith("."+e))return o}}catch(t){}return null}(o);let s=null;switch(j){case"manual":break;case"safe":case"strict":s=_(o,j);break;case"doomsday":(function(t){try{const n=new URL(t).hostname,e=window.location.hostname,o=t=>t.replace(/^www\./,"");return o(n)!==o(e)}catch(t){return!1}})(o)&&(s=_(o,"strict")||"marketing")}return s||a||e}function R(t){if(t.hasAttribute("data-zest-processed"))return!1;const n=L(t);if(!n)return t.setAttribute("data-zest-processed","allowed"),!1;if($(n))return t.setAttribute("data-zest-processed","allowed"),!1;const e={category:n,src:t.src||"",inline:t.textContent,type:t.type,async:t.async,defer:t.defer,element:t,timestamp:Date.now()};return t.setAttribute("data-zest-processed","blocked"),t.setAttribute("data-consent-category",n),t.type="text/plain",t.src&&t.removeAttribute("src"),500>C.length&&C.push(e),!0}function M(t){for(const n of t)for(const t of n.addedNodes)if("SCRIPT"!==t.nodeName||t.hasAttribute("data-zest-processed")||R(t),t.querySelectorAll){t.querySelectorAll("script:not([data-zest-processed])").forEach(R)}}function N(t="safe",n=[]){return j=t,E=n,document.querySelectorAll("script:not([data-zest-processed])").forEach(R),S=new MutationObserver(M),S.observe(document.documentElement,{childList:!0,subtree:!0}),!0}const D={essential:{id:"essential",label:"Essential",description:"Required for the website to function properly. Cannot be disabled.",required:!0,default:!0},functional:{id:"functional",label:"Functional",description:"Enable personalized features like language preferences and themes.",required:!1,default:!1},analytics:{id:"analytics",label:"Analytics",description:"Help us understand how visitors interact with our website.",required:!1,default:!1},marketing:{id:"marketing",label:"Marketing",description:"Used to deliver relevant advertisements and track campaign performance.",required:!1,default:!1}};function O(){return Object.keys(D)}function q(){if("undefined"==typeof navigator)return!1;const t=navigator.doNotTrack||window.doNotTrack||navigator.msDoNotTrack;return"1"===t||"yes"===t||!0===t||!0===navigator.globalPrivacyControl}function T(t,n,e){const o=e?"default":"update";n.consentModeGoogle&&function(t,n){if(window.dataLayer=window.dataLayer||[],"function"==typeof window.gtag)window.gtag("consent",t,n);else{function e(){window.dataLayer.push(arguments)}e("consent",t,n)}}(o,function(t){const n=t=>t?"granted":"denied";return{ad_storage:n(t.marketing),ad_user_data:n(t.marketing),ad_personalization:n(t.marketing),analytics_storage:n(t.analytics),functionality_storage:"granted",personalization_storage:n(t.functional)}}(t)),n.consentModeMicrosoft&&function(t,n){window.uetq=window.uetq||[],window.uetq.push("consent",t,n)}(o,function(t){return{ad_storage:t.marketing?"granted":"denied"}}(t))}const W={labels:{banner:{title:"我们重视您的隐私",description:'我们使用Cookie来改善您的浏览体验、提供个性化内容并分析我们的流量。点击"全部接受"即表示您同意我们使用Cookie。',acceptAll:"全部接受",rejectAll:"全部拒绝",settings:"设置"},modal:{title:"隐私设置",description:"管理您的Cookie偏好设置。您可以在下方启用或禁用不同类型的Cookie。",save:"保存设置",acceptAll:"全部接受",rejectAll:"全部拒绝"},widget:{label:"Cookie设置"}},categories:{essential:{label:"必要",description:"网站正常运行所必需的。无法禁用。"},functional:{label:"功能性",description:"启用个性化功能,如语言偏好和主题设置。"},analytics:{label:"分析",description:"帮助我们了解访问者如何与网站互动。"},marketing:{label:"营销",description:"用于展示相关广告并衡量营销活动效果。"}}};const I={lang:"auto",position:"bottom",theme:"auto",accentColor:"#0071e3",categories:D,labels:{banner:{title:"We value your privacy",description:'We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. By clicking "Accept All", you consent to our use of cookies.',acceptAll:"Accept All",rejectAll:"Reject All",settings:"Settings"},modal:{title:"Privacy Settings",description:"Manage your cookie preferences. You can enable or disable different types of cookies below.",save:"Save Preferences",acceptAll:"Accept All",rejectAll:"Reject All"},widget:{label:"Cookie Settings"}},autoInit:!0,showWidget:!0,expiration:365,respectDNT:!0,dntBehavior:"reject",customStyles:"",consentModeGoogle:!1,consentModeMicrosoft:!1,mode:"safe",blockedDomains:[],policyUrl:null,imprintUrl:null,callbacks:{onAccept:null,onReject:null,onChange:null,onReady:null}};function Z(t){const n={...I};t||(t={});const e=["lang","position","theme","accentColor","autoInit","showWidget","expiration","policyUrl","imprintUrl","customStyles","mode","blockedDomains","respectDNT","dntBehavior","consentModeGoogle","consentModeMicrosoft"];for(const o of e)void 0!==t[o]&&(n[o]=t[o]);n.lang="zh";const o=W,a=o.labels||{},s=t.labels||{};n.labels={banner:{...I.labels.banner,...a.banner,...s.banner},modal:{...I.labels.modal,...a.modal,...s.modal},widget:{...I.labels.widget,...a.widget,...s.widget}};const r=o.categories||{},i=t.categories||{};n.categories={...I.categories};for(const t of Object.keys(I.categories))n.categories[t]={...I.categories[t],...r[t],...i[t]};return t.callbacks&&(n.callbacks={...I.callbacks,...t.callbacks}),t.patterns&&(n.patterns=t.patterns),n}function U(){const t="undefined"!=typeof window&&window.ZestConfig?window.ZestConfig:{},n=function(){const t=document.currentScript||document.querySelector("script[data-zest]")||document.querySelector('script[src*="zest"]');if(!t)return{};const n={},e=t.getAttribute("data-position");e&&(n.position=e);const o=t.getAttribute("data-theme");o&&(n.theme=o);const a=t.getAttribute("data-accent")||t.getAttribute("data-accent-color");a&&(n.accentColor=a);const s=t.getAttribute("data-policy-url")||t.getAttribute("data-privacy-url");s&&(n.policyUrl=s);const r=t.getAttribute("data-imprint-url");r&&(n.imprintUrl=r);const i=t.getAttribute("data-show-widget");null!==i&&(n.showWidget="false"!==i);const c=t.getAttribute("data-auto-init");null!==c&&(n.autoInit="false"!==c);const l=t.getAttribute("data-expiration");l&&(n.expiration=parseInt(l,10));const d=t.getAttribute("data-consent-mode-google");null!==d&&(n.consentModeGoogle="false"!==d);const u=t.getAttribute("data-consent-mode-microsoft");return null!==u&&(n.consentModeMicrosoft="false"!==u),n}();return Z({...t,...n})}let P=null;function Y(){return P||(P=U()),P}const H="zest_consent";function B(){try{return"undefined"!=typeof location&&"https:"===location.protocol?"; Secure":""}catch(t){return""}}let F=null;function G(t){const n=g();n?.set?n.set.call(document,t):document.cookie=t}function J(){const t=g();return t?.get?t.get.call(document):document.cookie}function X(){try{const t=J().match(RegExp(H+"=([^;]+)"));if(t){const n=r(JSON.parse(decodeURIComponent(t[1])),O());if(n&&n.categories)return F={essential:!0,functional:!1,analytics:!1,marketing:!1,...n.categories},{...F}}}catch(t){}return F={essential:!0,functional:!1,analytics:!1,marketing:!1},{...F}}function V(){return F||(F=X()),{...F}}function K(t,n=365){const e=F?{...F}:{essential:!0,functional:!1,analytics:!1,marketing:!1};return F={essential:!0,functional:!!t.functional,analytics:!!t.analytics,marketing:!!t.marketing},function(t=365){F||(F={essential:!0,functional:!1,analytics:!1,marketing:!1});const n={version:"1.0",timestamp:Date.now(),categories:F},e=new Date(Date.now()+24*t*60*60*1e3).toUTCString();G(`${H}=${encodeURIComponent(JSON.stringify(n))}; expires=${e}; path=/; SameSite=Lax${B()}`)}(n),{current:{...F},previous:e}}function Q(t){return F||(F=X()),!0===F[t]}function tt(t=365){return K({functional:!1,analytics:!1,marketing:!1},t)}function nt(){try{return J().includes(H)}catch(t){return!1}}const et={READY:"zest:ready",CONSENT:"zest:consent",REJECT:"zest:reject",CHANGE:"zest:change",SHOW:"zest:show",HIDE:"zest:hide"};function ot(t,n={}){const e=new CustomEvent(t,{detail:n,bubbles:!0,cancelable:!0});return document.dispatchEvent(e),e}function at(t,n){return ot(et.CONSENT,{consent:t,previous:n})}function st(t){return ot(et.REJECT,{consent:t})}function rt(t,n){return ot(et.CHANGE,{consent:t,previous:n})}function it(t="banner"){return ot(et.SHOW,{type:t})}function ct(t="banner"){return ot(et.HIDE,{type:t})}let lt=!1,dt=null;function ut(t){return Q(t)}function pt(t){!function(t){const n=[];for(const e of p)t.includes(e.category)?u?.set&&u.set.call(document,e.value):n.push(e);p.length=0,p.push(...n)}(t),function(t){const n=[];for(const e of h)t.includes(e.category)?m?.setItem(e.key,e.value):n.push(e);h.length=0,h.push(...n);const e=[];for(const n of y)t.includes(n.category)?b?.setItem(n.key,n.value):e.push(n);y.length=0,y.push(...e)}(t),function(t){const n=[];for(const e of C){if(!t.includes(e.category)){n.push(e);continue}const o=document.createElement("script");e.src?o.src=e.src:e.inline&&(o.textContent=e.inline),e.async&&(o.async=!0),e.defer&&(o.defer=!0),e.type&&"text/plain"!==e.type&&(o.type=e.type),o.setAttribute("data-zest-processed","executed"),o.setAttribute("data-consent-executed","true");const a=e.element;a&&a.isConnected&&a.parentNode?a.parentNode.replaceChild(o,a):document.head.appendChild(o)}C.length=0,C.push(...n)}(t)}function ft(t={}){if(lt)return{alreadyInitialized:!0,config:dt,consent:X(),hasDecision:nt(),dntApplied:!1};P=Z(t),dt=P,T({functional:!1,analytics:!1,marketing:!1},dt,!0),dt.patterns&&function(t){if(l={...c},t&&"object"==typeof t)for(const[n,e]of Object.entries(t)){if(!Array.isArray(e))continue;const t=[];for(const n of e){const e=a(n);if(e)t.push(e);else try{console.warn("[Zest] Rejected unsafe pattern:",n)}catch(t){}}l[n]=t}}(dt.patterns),f=ut,function(t){z=t}(ut),function(t){$=t}(ut),u=Object.getOwnPropertyDescriptor(Document.prototype,"cookie"),u?Object.defineProperty(document,"cookie",{get:()=>u.get.call(document),set(t){const n=function(t){const n=t.match(/^([^=]+)/);return n?n[1].trim():null}(t);if(!n)return;const e=d(n);f(e)?u.set.call(document,t):100>p.length&&p.push({value:t,name:n,category:e,timestamp:Date.now()})},configurable:!1}):console.warn("[Zest] Could not get cookie descriptor"),function(){try{return m=window.localStorage,b=window.sessionStorage,Object.defineProperty(window,"localStorage",{value:w(m,h),configurable:!0,writable:!1}),Object.defineProperty(window,"sessionStorage",{value:w(b,y),configurable:!0,writable:!1}),!0}catch(t){return console.warn("[Zest] Could not intercept storage APIs:",t),!1}}(),N(dt.mode,dt.blockedDomains);const n=X();lt=!0,nt()&&T(n,dt,!1);let e=!1;if(q()&&dt.respectDNT&&"ignore"!==dt.dntBehavior&&"reject"===dt.dntBehavior&&!nt()){const t=tt(dt.expiration);e=!0,T(t.current,dt,!1),st(t.current),rt(t.current,t.previous),i(dt.callbacks?.onReject),i(dt.callbacks?.onChange,t.current)}return function(t){ot(et.READY,{consent:t})}(n),i(dt.callbacks?.onReady,n),{alreadyInitialized:!1,config:dt,consent:n,hasDecision:nt(),dntApplied:e}}function gt(){if(!lt)return null;const t=function(t=365){return K({functional:!0,analytics:!0,marketing:!0},t)}(dt.expiration);return T(t.current,dt,!1),pt(O()),at(t.current,t.previous),rt(t.current,t.previous),i(dt.callbacks?.onAccept,t.current),i(dt.callbacks?.onChange,t.current),t}function mt(){G(`${H}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax${B()}`),F=null}function bt(){return lt}function ht(){return dt}const yt="#4F46E5";function zt(t){const n=function(t){if("string"!=typeof t)return null;const n=t.trim();return/^#[0-9a-fA-F]{3,8}$/.test(n)||e.has(n.toLowerCase())||/^(rgb|rgba|hsl|hsla)\(\s*[\d.,%\s/]+\s*\)$/i.test(n)?n:null}(t.accentColor)||yt,o=function(t){if("string"!=typeof t||0===t.length)return"";if(t.length>2e4)return"";let n=t.replace(/\/\*[\s\S]*?\*\//g,"");return n=n.replace(/@import\s+[^;]+;?/gi,""),n=n.replace(/@charset\s+[^;]+;?/gi,""),n=n.replace(/url\(\s*(['"]?)([^)'"]+)\1\s*\)/gi,(t,n,e)=>{const o=e.trim().toLowerCase();return o.startsWith("https:")||o.startsWith("data:image/")||o.startsWith("/")||o.startsWith("#")?t:"url(#)"}),n=n.replace(/\.zest-btn--secondary\s*\{[^}]*\}/gi,""),n=n.replace(/\[data-action\s*=\s*["']reject-all["']\]\s*\{[^}]*\}/gi,""),n=n.replace(/\[data-action\s*=\s*["']accept-all["']\]\s*\{[^}]*\}/gi,""),n=n.replace(/expression\s*\([^)]*\)/gi,""),n=n.replace(/-moz-binding\s*:[^;}]*/gi,""),n}(t.customStyles);return`\n:host {\n --zest-accent: ${n};\n --zest-accent-hover: ${function(t,n){"string"==typeof t&&/^#[0-9a-fA-F]{3,8}$/.test(t.trim())||(t=yt);let e=t.trim().replace("#","");3===e.length&&(e=e.split("").map(t=>t+t).join(""));8===e.length&&(e=e.slice(0,6));6!==e.length&&(e="4F46E5");const o=parseInt(e,16),a=Math.round(2.55*n);return"#"+(16777216+65536*Math.min(255,Math.max(0,(o>>16)+a))+256*Math.min(255,Math.max(0,(o>>8&255)+a))+Math.min(255,Math.max(0,(255&o)+a))).toString(16).slice(1)}(n,-15)};\n --zest-bg: #ffffff;\n --zest-bg-secondary: #f3f4f6;\n --zest-text: #1f2937;\n --zest-text-secondary: #6b7280;\n --zest-border: #e5e7eb;\n --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);\n --zest-radius: 12px;\n --zest-radius-sm: 8px;\n --zest-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;\n\n font-family: var(--zest-font);\n font-size: 14px;\n line-height: 1.5;\n color: var(--zest-text);\n box-sizing: border-box;\n}\n\n:host([data-theme="dark"]) {\n --zest-bg: #1f2937;\n --zest-bg-secondary: #374151;\n --zest-text: #f9fafb;\n --zest-text-secondary: #9ca3af;\n --zest-border: #4b5563;\n --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);\n}\n\n@media (prefers-color-scheme: dark) {\n :host([data-theme="auto"]) {\n --zest-bg: #1f2937;\n --zest-bg-secondary: #374151;\n --zest-text: #f9fafb;\n --zest-text-secondary: #9ca3af;\n --zest-border: #4b5563;\n --zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);\n }\n}\n\n*, *::before, *::after {\n box-sizing: border-box;\n}\n\n/* Banner */\n.zest-banner {\n position: fixed;\n z-index: 999999;\n max-width: 480px;\n padding: 20px;\n background: var(--zest-bg);\n border-radius: var(--zest-radius);\n box-shadow: var(--zest-shadow);\n animation: zest-slide-in 0.3s ease-out;\n}\n\n.zest-banner--bottom {\n bottom: 20px;\n left: 50%;\n transform: translateX(-50%);\n}\n\n.zest-banner--bottom-left {\n bottom: 20px;\n left: 20px;\n}\n\n.zest-banner--bottom-right {\n bottom: 20px;\n right: 20px;\n}\n\n.zest-banner--top {\n top: 20px;\n left: 50%;\n transform: translateX(-50%);\n}\n\n@keyframes zest-slide-in {\n from {\n opacity: 0;\n transform: translateX(-50%) translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateX(-50%) translateY(0);\n }\n}\n\n.zest-banner--bottom-left {\n animation-name: zest-slide-in-left;\n}\n\n@keyframes zest-slide-in-left {\n from {\n opacity: 0;\n transform: translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.zest-banner--bottom-right {\n animation-name: zest-slide-in-right;\n}\n\n@keyframes zest-slide-in-right {\n from {\n opacity: 0;\n transform: translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .zest-banner,\n .zest-modal {\n animation: none;\n }\n}\n\n.zest-banner__title {\n margin: 0 0 8px 0;\n font-size: 16px;\n font-weight: 600;\n color: var(--zest-text);\n}\n\n.zest-banner__description {\n margin: 0 0 16px 0;\n font-size: 14px;\n color: var(--zest-text-secondary);\n}\n\n.zest-banner__buttons {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* Buttons */\n.zest-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 10px 16px;\n font-size: 14px;\n font-weight: 500;\n font-family: inherit;\n border: none;\n border-radius: var(--zest-radius-sm);\n cursor: pointer;\n transition: background-color 0.15s ease, transform 0.1s ease;\n}\n\n.zest-btn:hover {\n transform: translateY(-1px);\n}\n\n.zest-btn:active {\n transform: translateY(0);\n}\n\n.zest-btn:focus-visible {\n outline: 2px solid var(--zest-accent);\n outline-offset: 2px;\n}\n\n.zest-btn--primary {\n background: var(--zest-accent);\n color: #ffffff;\n}\n\n.zest-btn--primary:hover {\n background: var(--zest-accent-hover);\n}\n\n.zest-btn--secondary {\n background: var(--zest-bg-secondary);\n color: var(--zest-text);\n}\n\n.zest-btn--secondary:hover {\n background: var(--zest-border);\n}\n\n.zest-btn--ghost {\n background: transparent;\n color: var(--zest-text-secondary);\n}\n\n.zest-btn--ghost:hover {\n background: var(--zest-bg-secondary);\n color: var(--zest-text);\n}\n\n/* Modal */\n.zest-modal-overlay {\n position: fixed;\n inset: 0;\n z-index: 999998;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n background: rgba(0, 0, 0, 0.5);\n animation: zest-fade-in 0.2s ease-out;\n}\n\n@keyframes zest-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n.zest-modal {\n width: 100%;\n max-width: 500px;\n max-height: 90vh;\n overflow-y: auto;\n background: var(--zest-bg);\n border-radius: var(--zest-radius);\n box-shadow: var(--zest-shadow);\n animation: zest-modal-in 0.3s ease-out;\n}\n\n@keyframes zest-modal-in {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n.zest-modal__header {\n padding: 20px 20px 0;\n}\n\n.zest-modal__title {\n margin: 0 0 8px 0;\n font-size: 18px;\n font-weight: 600;\n color: var(--zest-text);\n}\n\n.zest-modal__description {\n margin: 0;\n font-size: 14px;\n color: var(--zest-text-secondary);\n}\n\n.zest-modal__body {\n padding: 20px;\n}\n\n.zest-modal__footer {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n padding: 0 20px 20px;\n}\n\n/* Categories */\n.zest-category {\n padding: 16px;\n margin-bottom: 12px;\n background: var(--zest-bg-secondary);\n border-radius: var(--zest-radius-sm);\n}\n\n.zest-category:last-child {\n margin-bottom: 0;\n}\n\n.zest-category__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.zest-category__info {\n flex: 1;\n}\n\n.zest-category__label {\n display: block;\n font-size: 14px;\n font-weight: 600;\n color: var(--zest-text);\n}\n\n.zest-category__description {\n margin: 4px 0 0;\n font-size: 13px;\n color: var(--zest-text-secondary);\n}\n\n/* Toggle Switch */\n.zest-toggle {\n position: relative;\n width: 44px;\n height: 24px;\n flex-shrink: 0;\n}\n\n.zest-toggle__input {\n position: absolute;\n opacity: 0;\n width: 100%;\n height: 100%;\n cursor: pointer;\n margin: 0;\n}\n\n.zest-toggle__input:disabled {\n cursor: not-allowed;\n}\n\n.zest-toggle__slider {\n position: absolute;\n inset: 0;\n background: var(--zest-border);\n border-radius: 12px;\n transition: background-color 0.2s ease;\n pointer-events: none;\n}\n\n.zest-toggle__slider::before {\n content: '';\n position: absolute;\n top: 2px;\n left: 2px;\n width: 20px;\n height: 20px;\n background: #ffffff;\n border-radius: 50%;\n transition: transform 0.2s ease;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);\n}\n\n.zest-toggle__input:checked + .zest-toggle__slider {\n background: var(--zest-accent);\n}\n\n.zest-toggle__input:checked + .zest-toggle__slider::before {\n transform: translateX(20px);\n}\n\n.zest-toggle__input:focus-visible + .zest-toggle__slider {\n outline: 2px solid var(--zest-accent);\n outline-offset: 2px;\n}\n\n.zest-toggle__input:disabled + .zest-toggle__slider {\n opacity: 0.6;\n}\n\n/* Widget */\n.zest-widget {\n position: fixed;\n z-index: 999997;\n bottom: 20px;\n left: 20px;\n}\n\n.zest-widget__btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 48px;\n height: 48px;\n padding: 0;\n background: var(--zest-bg);\n border: 1px solid var(--zest-border);\n border-radius: 50%;\n box-shadow: var(--zest-shadow);\n cursor: pointer;\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n}\n\n.zest-widget__btn:hover {\n transform: scale(1.05);\n box-shadow: 0 12px 28px -5px rgba(0, 0, 0, 0.15);\n}\n\n.zest-widget__btn:focus-visible {\n outline: 2px solid var(--zest-accent);\n outline-offset: 2px;\n}\n\n.zest-widget__icon {\n width: 24px;\n height: 24px;\n fill: var(--zest-text);\n}\n\n/* Link */\n.zest-link {\n color: var(--zest-accent);\n text-decoration: none;\n}\n\n.zest-link:hover {\n text-decoration: underline;\n}\n\n/* Mobile */\n@media (max-width: 480px) {\n .zest-banner {\n left: 10px;\n right: 10px;\n max-width: none;\n transform: none;\n }\n\n .zest-banner--bottom,\n .zest-banner--bottom-left,\n .zest-banner--bottom-right {\n bottom: 10px;\n }\n\n .zest-banner--top {\n top: 10px;\n transform: none;\n }\n\n @keyframes zest-slide-in {\n from {\n opacity: 0;\n transform: translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n .zest-banner__buttons {\n flex-direction: column;\n }\n\n .zest-btn {\n width: 100%;\n }\n\n .zest-modal-overlay {\n padding: 10px;\n }\n\n .zest-widget {\n bottom: 10px;\n left: 10px;\n }\n}\n\n/* Hidden utility */\n.zest-hidden {\n display: none !important;\n}\n${o}\n`}let wt=null,xt=null;const vt=new Set(["bottom","bottom-left","bottom-right","top"]);function kt(t={}){if(wt)return wt;const e=Y();wt=document.createElement("zest-banner"),wt.setAttribute("data-theme",e.theme||"light"),xt=wt.attachShadow({mode:"open"});const o=document.createElement("style");o.textContent=zt(e),xt.appendChild(o);const a=document.createElement("div");a.innerHTML=function(t){const e=t.labels.banner,o=t.position||"bottom";return`\n <div class="zest-banner zest-banner--${vt.has(o)?o:"bottom"}" role="dialog" aria-modal="false" aria-label="${n(e.title)}">\n <h2 class="zest-banner__title">${n(e.title)}</h2>\n <p class="zest-banner__description">${n(e.description)}</p>\n <div class="zest-banner__buttons">\n <button type="button" class="zest-btn zest-btn--primary" data-action="accept-all">\n ${n(e.acceptAll)}\n </button>\n <button type="button" class="zest-btn zest-btn--secondary" data-action="reject-all">\n ${n(e.rejectAll)}\n </button>\n <button type="button" class="zest-btn zest-btn--ghost" data-action="settings">\n ${n(e.settings)}\n </button>\n </div>\n </div>\n `}(e),xt.appendChild(a.firstElementChild);const s=xt.querySelector(".zest-banner");return s.addEventListener("click",n=>{const e=n.target.dataset.action;if(e)switch(e){case"accept-all":t.onAcceptAll?.();break;case"reject-all":t.onRejectAll?.();break;case"settings":t.onSettings?.()}}),s.addEventListener("keydown",n=>{"Escape"===n.key&&t.onSettings?.()}),document.body.appendChild(wt),requestAnimationFrame(()=>{const t=xt.querySelector("button");t?.focus()}),wt}function _t(t={}){wt?wt.classList.remove("zest-hidden"):kt(t)}function At(){wt&&(wt.remove(),wt=null,xt=null)}let Ct=null,St=null,jt={};function Et(t,e){const o=t.labels.modal,a=Object.values(t.categories||D).map(t=>function(t,e,o){const a=o?"disabled":"",s=e?"checked":"",r=n(t.id),i=n(t.label);return`\n <div class="zest-category">\n <div class="zest-category__header">\n <div class="zest-category__info">\n <span class="zest-category__label">${i}</span>\n <p class="zest-category__description">${n(t.description)}</p>\n </div>\n <label class="zest-toggle">\n <input\n type="checkbox"\n class="zest-toggle__input"\n data-category="${r}"\n ${s}\n ${a}\n aria-label="${i}"\n >\n <span class="zest-toggle__slider"></span>\n </label>\n </div>\n </div>\n `}(t,e[t.id]??t.default,t.required)).join(""),s=t.policyUrl?function(t){if("string"!=typeof t||0===t.length)return null;const n=t.trim();if(0===n.length)return null;if(/^[/?#]/.test(n))return n;const e=n.match(/^([a-z][a-z0-9+.-]*):/i);if(!e)return n;const o=e[1].toLowerCase();return"http"===o||"https"===o||"mailto"===o||"tel"===o?n:null}(t.policyUrl):null,r=s?`<a href="${n(s)}" class="zest-link" target="_blank" rel="noopener noreferrer">Privacy Policy</a>`:"";return`\n <div class="zest-modal-overlay" role="dialog" aria-modal="true" aria-label="${n(o.title)}">\n <div class="zest-modal">\n <div class="zest-modal__header">\n <h2 class="zest-modal__title">${n(o.title)}</h2>\n <p class="zest-modal__description">${n(o.description)} ${r}</p>\n </div>\n <div class="zest-modal__body">\n ${a}\n </div>\n <div class="zest-modal__footer">\n <button type="button" class="zest-btn zest-btn--primary" data-action="save">\n ${n(o.save)}\n </button>\n <button type="button" class="zest-btn zest-btn--secondary" data-action="accept-all">\n ${n(o.acceptAll)}\n </button>\n <button type="button" class="zest-btn zest-btn--ghost" data-action="reject-all">\n ${n(o.rejectAll)}\n </button>\n </div>\n </div>\n </div>\n `}function $t(){if(!St)return jt;const t=St.querySelectorAll(".zest-toggle__input"),n={essential:!0};return t.forEach(t=>{const e=t.dataset.category;e&&"essential"!==e&&(n[e]=t.checked)}),n}function Lt(){Ct&&(Ct.remove(),Ct=null,St=null)}let Rt=null,Mt=null;function Nt(t={}){if(Rt)return Rt;const e=Y();Rt=document.createElement("zest-widget"),Rt.setAttribute("data-theme",e.theme||"light"),Mt=Rt.attachShadow({mode:"open"});const o=document.createElement("style");o.textContent=zt(e),Mt.appendChild(o);const a=document.createElement("div");a.innerHTML=function(t){const e=n(t.labels.widget.label);return`\n <div class="zest-widget">\n <button type="button" class="zest-widget__btn" aria-label="${e}" title="${e}">\n <span class="zest-widget__icon"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10c0-.728-.078-1.437-.225-2.12a1 1 0 0 0-1.482-.63 3 3 0 0 1-4.086-3.72 1 1 0 0 0-.793-1.263A10.05 10.05 0 0 0 12 2zm0 2c.178 0 .354.006.528.017a5 5 0 0 0 5.955 5.955c.011.174.017.35.017.528 0 4.418-3.582 8-8 8s-8-3.582-8-8 3.582-8 8-8zm-4 6a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm5 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm-2 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"/></svg></span>\n </button>\n </div>\n `}(e),Mt.appendChild(a.firstElementChild);return Mt.querySelector(".zest-widget__btn").addEventListener("click",()=>{t.onClick?.()}),document.body.appendChild(Rt),Rt}function Dt(t={}){Rt?Rt.style.display="":Nt(t)}function Ot(){Rt&&(Rt.style.display="none")}function qt(){gt();const t=ht();At(),Lt(),t?.showWidget&&Dt({onClick:It})}function Tt(){!function(){if(!lt)return null;const t=tt(dt.expiration);T(t.current,dt,!1),st(t.current),rt(t.current,t.previous),i(dt.callbacks?.onReject),i(dt.callbacks?.onChange,t.current)}();const t=ht();At(),Lt(),t?.showWidget&&Dt({onClick:It})}function Wt(t){!function(t){if(!lt)return null;const n=K(t,dt.expiration);T(n.current,dt,!1);const e=Object.keys(n.current).filter(t=>n.current[t]&&!n.previous[t]);e.length>0&&pt(e),Object.entries(t||{}).some(([t,n])=>"essential"!==t&&n)?at(n.current,n.previous):st(n.current),rt(n.current,n.previous),i(dt.callbacks?.onChange,n.current)}(t);const n=ht();Lt(),n?.showWidget&&Dt({onClick:It})}function It(){At(),Ot(),function(t={},n={}){if(Ct)return Ct;const e=Y();jt={...t},Ct=document.createElement("zest-modal"),Ct.setAttribute("data-theme",e.theme||"light"),St=Ct.attachShadow({mode:"open"});const o=document.createElement("style");o.textContent=zt(e),St.appendChild(o);const a=document.createElement("div");a.innerHTML=Et(e,t),St.appendChild(a.firstElementChild);const s=St.querySelector(".zest-modal-overlay");s.addEventListener("click",t=>{const e=t.target.dataset.action;if(e)switch(e){case"save":n.onSave?.($t());break;case"accept-all":n.onAcceptAll?.();break;case"reject-all":n.onRejectAll?.()}else t.target===s&&n.onClose?.()}),s.addEventListener("keydown",t=>{"Escape"===t.key&&n.onClose?.()}),St.querySelectorAll(".zest-toggle__input").forEach(t=>{t.addEventListener("change",()=>{jt=$t()})}),document.body.appendChild(Ct),requestAnimationFrame(()=>{const t=St.querySelector("button");t?.focus()})}(V(),{onSave:Wt,onAcceptAll:qt,onRejectAll:Tt,onClose:Zt}),it("modal")}function Zt(){Lt(),ct("modal");const t=ht();nt()&&t?.showWidget?Dt({onClick:It}):_t({onAcceptAll:qt,onRejectAll:Tt,onSettings:It})}function Ut(t={}){const{alreadyInitialized:n,hasDecision:e,dntApplied:o}=ft(t);if(n)return console.warn("[Zest] Already initialized"),Pt;const a=ht();return e||o?a?.showWidget&&Dt({onClick:It}):(_t({onAcceptAll:qt,onRejectAll:Tt,onSettings:It}),it("banner")),Pt}const Pt={init:Ut,show(){bt()?(Lt(),Ot(),_t({onAcceptAll:qt,onRejectAll:Tt,onSettings:It}),it("banner")):console.warn("[Zest] Not initialized. Call Zest.init() first.")},hide(){At(),ct("banner")},showSettings(){bt()?It():console.warn("[Zest] Not initialized. Call Zest.init() first.")},hideSettings(){Lt(),ct("modal")},getConsent:V,hasConsent:Q,hasConsentDecision:nt,getConsentProof:function(){try{const t=J().match(RegExp(H+"=([^;]+)"));if(t){return r(JSON.parse(decodeURIComponent(t[1])),O())}}catch(t){}return null},isDoNotTrackEnabled:q,getDNTDetails:function(){if("undefined"==typeof navigator)return{enabled:!1,source:null};const t=navigator.doNotTrack||window.doNotTrack||navigator.msDoNotTrack;return"1"===t||"yes"===t||!0===t?{enabled:!0,source:"dnt"}:!0===navigator.globalPrivacyControl?{enabled:!0,source:"gpc"}:{enabled:!1,source:null}},acceptAll(){bt()?qt():console.warn("[Zest] Not initialized. Call Zest.init() first.")},rejectAll(){bt()?Tt():console.warn("[Zest] Not initialized. Call Zest.init() first.")},reset(){mt(),Lt(),Rt&&(Rt.remove(),Rt=null,Mt=null),bt()&&(_t({onAcceptAll:qt,onRejectAll:Tt,onSettings:It}),it("banner"))},getConfig:Y,on:function(t,n){return document.addEventListener(t,n),()=>document.removeEventListener(t,n)},once:function(t,n){document.addEventListener(t,n,{once:!0})},EVENTS:et};if("undefined"!=typeof window){try{Object.defineProperty(window,"Zest",{value:Object.freeze(Pt),writable:!1,configurable:!1,enumerable:!0})}catch(t){window.Zest=Pt}const t=()=>{!1!==U().autoInit&&Ut(window.ZestConfig)};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",t):t()}return Pt}();
|
package/package.json
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@freshjuice/zest",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lightweight cookie consent toolkit for GDPR/CCPA compliance",
|
|
6
6
|
"main": "dist/zest.min.js",
|
|
7
7
|
"module": "dist/zest.esm.min.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/zest.esm.min.js",
|
|
11
|
+
"default": "./dist/zest.min.js"
|
|
12
|
+
},
|
|
13
|
+
"./headless": {
|
|
14
|
+
"import": "./dist/zest.headless.esm.min.js",
|
|
15
|
+
"default": "./dist/zest.headless.esm.min.js"
|
|
16
|
+
},
|
|
17
|
+
"./src/headless": "./src/headless.js",
|
|
18
|
+
"./src/*": "./src/*.js"
|
|
19
|
+
},
|
|
8
20
|
"files": [
|
|
9
21
|
"dist",
|
|
10
22
|
"src",
|
|
@@ -57,8 +69,8 @@
|
|
|
57
69
|
"devDependencies": {
|
|
58
70
|
"@rollup/plugin-alias": "^6.0.0",
|
|
59
71
|
"@rollup/plugin-terser": "^0.4.4",
|
|
60
|
-
"eslint": "^10.
|
|
61
|
-
"rollup": "^4.
|
|
62
|
-
"vitest": "^4.
|
|
72
|
+
"eslint": "^10.2.1",
|
|
73
|
+
"rollup": "^4.60.2",
|
|
74
|
+
"vitest": "^4.1.5"
|
|
63
75
|
}
|
|
64
76
|
}
|
|
@@ -7,6 +7,11 @@ import { getCategoryForName, parseCookieName } from './pattern-matcher.js';
|
|
|
7
7
|
// Store original descriptor
|
|
8
8
|
let originalCookieDescriptor = null;
|
|
9
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
|
+
|
|
10
15
|
// Queue for blocked cookies
|
|
11
16
|
const cookieQueue = [];
|
|
12
17
|
|
|
@@ -90,8 +95,8 @@ export function interceptCookies() {
|
|
|
90
95
|
if (checkConsent(category)) {
|
|
91
96
|
// Consent given - set cookie
|
|
92
97
|
originalCookieDescriptor.set.call(document, value);
|
|
93
|
-
} else {
|
|
94
|
-
// No consent - queue for later
|
|
98
|
+
} else if (cookieQueue.length < MAX_QUEUE_SIZE) {
|
|
99
|
+
// No consent - queue for later (capped to prevent DoS)
|
|
95
100
|
cookieQueue.push({
|
|
96
101
|
value,
|
|
97
102
|
name,
|
|
@@ -100,17 +105,27 @@ export function interceptCookies() {
|
|
|
100
105
|
});
|
|
101
106
|
}
|
|
102
107
|
},
|
|
103
|
-
configurable:
|
|
108
|
+
// configurable: false prevents a later-loaded script from
|
|
109
|
+
// overriding our descriptor and bypassing the interceptor.
|
|
110
|
+
configurable: false
|
|
104
111
|
});
|
|
105
112
|
|
|
106
113
|
return true;
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
/**
|
|
110
|
-
* Restore original cookie behavior
|
|
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.
|
|
111
123
|
*/
|
|
112
124
|
export function restoreCookies() {
|
|
113
|
-
if (originalCookieDescriptor)
|
|
125
|
+
if (!originalCookieDescriptor) return;
|
|
126
|
+
try {
|
|
114
127
|
Object.defineProperty(document, 'cookie', originalCookieDescriptor);
|
|
128
|
+
} catch (_) {
|
|
129
|
+
// Descriptor is locked; nothing we can do.
|
|
115
130
|
}
|
|
116
131
|
}
|
|
@@ -109,29 +109,56 @@ export const STRICT_TRACKERS = {
|
|
|
109
109
|
};
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* Check if a URL matches any tracker in the list
|
|
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
|
|
113
118
|
*/
|
|
114
119
|
export function matchesTrackerList(url, trackerList) {
|
|
120
|
+
let urlObj;
|
|
115
121
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
urlObj = new URL(url);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const hostname = urlObj.hostname.toLowerCase();
|
|
127
|
+
const path = urlObj.pathname.toLowerCase();
|
|
119
128
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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)) {
|
|
127
139
|
return true;
|
|
128
|
-
}
|
|
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)) {
|
|
129
151
|
return true;
|
|
130
152
|
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Plain hostname: exact or subdomain match only
|
|
157
|
+
if (hostname === entry || hostname.endsWith('.' + entry)) {
|
|
158
|
+
return true;
|
|
131
159
|
}
|
|
132
|
-
} catch (e) {
|
|
133
|
-
// Invalid URL
|
|
134
160
|
}
|
|
161
|
+
|
|
135
162
|
return false;
|
|
136
163
|
}
|
|
137
164
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Pattern Matcher - Categorizes cookies and storage keys by pattern
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { safeRegExp } from './security.js';
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Default patterns for each category
|
|
7
9
|
*/
|
|
@@ -50,16 +52,29 @@ export const DEFAULT_PATTERNS = {
|
|
|
50
52
|
let patterns = { ...DEFAULT_PATTERNS };
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
|
-
* Set custom patterns
|
|
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.
|
|
54
58
|
*/
|
|
55
59
|
export function setPatterns(customPatterns) {
|
|
56
60
|
patterns = { ...DEFAULT_PATTERNS };
|
|
61
|
+
if (!customPatterns || typeof customPatterns !== 'object') return;
|
|
62
|
+
|
|
57
63
|
for (const [category, regexList] of Object.entries(customPatterns)) {
|
|
58
|
-
if (Array.isArray(regexList))
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
}
|
|
62
76
|
}
|
|
77
|
+
patterns[category] = compiled;
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
80
|
|
|
@@ -10,7 +10,17 @@
|
|
|
10
10
|
|
|
11
11
|
import { getCategoryForScript, isThirdParty } from './known-trackers.js';
|
|
12
12
|
|
|
13
|
-
//
|
|
13
|
+
// Categories the author has declared blockable. A script can self-label
|
|
14
|
+
// into one of these, but not into 'essential' (a common bypass).
|
|
15
|
+
const BLOCKABLE_CATEGORIES = new Set(['functional', 'analytics', 'marketing']);
|
|
16
|
+
|
|
17
|
+
// Upper bound on queued scripts awaiting consent replay — prevents a
|
|
18
|
+
// hostile page from flooding the queue with <script> nodes.
|
|
19
|
+
const MAX_QUEUE_SIZE = 500;
|
|
20
|
+
|
|
21
|
+
// Queue for blocked scripts — the authoritative source for replay,
|
|
22
|
+
// snapshotting src/inline BEFORE any DOM mutation so later tampering
|
|
23
|
+
// cannot hijack what gets executed.
|
|
14
24
|
const scriptQueue = [];
|
|
15
25
|
|
|
16
26
|
// MutationObserver instance
|
|
@@ -85,55 +95,64 @@ function matchesCustomDomains(url) {
|
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
/**
|
|
88
|
-
* Determine if a script should be blocked and get its category
|
|
98
|
+
* Determine if a script should be blocked and get its category.
|
|
99
|
+
*
|
|
100
|
+
* A self-applied 'essential' label is ignored — only explicit blockable
|
|
101
|
+
* categories are accepted. That prevents a third-party script from
|
|
102
|
+
* stamping itself with data-consent-category="essential" to slip past
|
|
103
|
+
* mode-based blocking.
|
|
89
104
|
*/
|
|
90
105
|
function getScriptBlockCategory(script) {
|
|
91
|
-
//
|
|
92
|
-
const explicitCategory = script.getAttribute('data-consent-category');
|
|
93
|
-
if (explicitCategory) {
|
|
94
|
-
return explicitCategory;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// 2. Skip if script has data-zest-allow attribute
|
|
106
|
+
// Skip if script has data-zest-allow attribute (opt-out)
|
|
98
107
|
if (script.hasAttribute('data-zest-allow')) {
|
|
99
108
|
return null;
|
|
100
109
|
}
|
|
101
110
|
|
|
111
|
+
// 1. Check for explicit data-consent-category attribute.
|
|
112
|
+
// Only honor values from the blockable set; 'essential' and unknown
|
|
113
|
+
// values fall through to the other checks.
|
|
114
|
+
const explicitCategory = script.getAttribute('data-consent-category');
|
|
115
|
+
const explicitBlockable = explicitCategory && BLOCKABLE_CATEGORIES.has(explicitCategory)
|
|
116
|
+
? explicitCategory
|
|
117
|
+
: null;
|
|
118
|
+
|
|
102
119
|
const src = script.src;
|
|
103
120
|
|
|
104
|
-
// No src = inline script, only block if explicitly tagged
|
|
121
|
+
// No src = inline script, only block if explicitly tagged (blockable only)
|
|
105
122
|
if (!src) {
|
|
106
|
-
return
|
|
123
|
+
return explicitBlockable;
|
|
107
124
|
}
|
|
108
125
|
|
|
109
|
-
//
|
|
126
|
+
// 2. Check custom blocked domains
|
|
110
127
|
const customCategory = matchesCustomDomains(src);
|
|
111
|
-
if (customCategory) {
|
|
112
|
-
return customCategory;
|
|
113
|
-
}
|
|
114
128
|
|
|
115
|
-
//
|
|
129
|
+
// 3. Mode-based blocking
|
|
130
|
+
let modeCategory = null;
|
|
116
131
|
switch (blockingMode) {
|
|
117
132
|
case 'manual':
|
|
118
|
-
|
|
119
|
-
return null;
|
|
133
|
+
break;
|
|
120
134
|
|
|
121
135
|
case 'safe':
|
|
122
136
|
case 'strict':
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
modeCategory = getCategoryForScript(src, blockingMode);
|
|
138
|
+
break;
|
|
125
139
|
|
|
126
140
|
case 'doomsday':
|
|
127
|
-
// Block all third-party scripts
|
|
128
141
|
if (isThirdParty(src)) {
|
|
129
|
-
|
|
130
|
-
return getCategoryForScript(src, 'strict') || 'marketing';
|
|
142
|
+
modeCategory = getCategoryForScript(src, 'strict') || 'marketing';
|
|
131
143
|
}
|
|
132
|
-
|
|
144
|
+
break;
|
|
133
145
|
|
|
134
146
|
default:
|
|
135
|
-
|
|
147
|
+
break;
|
|
136
148
|
}
|
|
149
|
+
|
|
150
|
+
// Use the strictest category among explicit/custom/mode decisions.
|
|
151
|
+
// We collect all categories the script matches and pick the first
|
|
152
|
+
// that appears in the blockable set (any match wins — but we prefer
|
|
153
|
+
// the mode-assigned one since it's authoritative for third-party
|
|
154
|
+
// trackers that try to self-label as 'functional').
|
|
155
|
+
return modeCategory || customCategory || explicitBlockable;
|
|
137
156
|
}
|
|
138
157
|
|
|
139
158
|
/**
|
|
@@ -158,14 +177,17 @@ function blockScript(script) {
|
|
|
158
177
|
return false;
|
|
159
178
|
}
|
|
160
179
|
|
|
161
|
-
// Store script info for later execution
|
|
180
|
+
// Store script info for later execution. Snapshot the src/text BEFORE
|
|
181
|
+
// mutating the DOM — this snapshot is the authoritative replay source
|
|
182
|
+
// so later DOM tampering cannot hijack the replayed script URL.
|
|
162
183
|
const scriptInfo = {
|
|
163
184
|
category,
|
|
164
|
-
src: script.src,
|
|
185
|
+
src: script.src || '',
|
|
165
186
|
inline: script.textContent,
|
|
166
187
|
type: script.type,
|
|
167
188
|
async: script.async,
|
|
168
189
|
defer: script.defer,
|
|
190
|
+
element: script,
|
|
169
191
|
timestamp: Date.now()
|
|
170
192
|
};
|
|
171
193
|
|
|
@@ -176,77 +198,61 @@ function blockScript(script) {
|
|
|
176
198
|
// Disable the script
|
|
177
199
|
script.type = 'text/plain';
|
|
178
200
|
|
|
179
|
-
//
|
|
201
|
+
// Remove src to prevent loading. We no longer stash it on the element
|
|
202
|
+
// (data-blocked-src was a tampering vector); scriptQueue is the single
|
|
203
|
+
// source of truth for replay.
|
|
180
204
|
if (script.src) {
|
|
181
|
-
script.setAttribute('data-blocked-src', script.src);
|
|
182
205
|
script.removeAttribute('src');
|
|
183
206
|
}
|
|
184
207
|
|
|
185
|
-
scriptQueue.
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Execute a queued script
|
|
191
|
-
*/
|
|
192
|
-
function executeScript(scriptInfo) {
|
|
193
|
-
const script = document.createElement('script');
|
|
194
|
-
|
|
195
|
-
if (scriptInfo.src) {
|
|
196
|
-
script.src = scriptInfo.src;
|
|
197
|
-
} else if (scriptInfo.inline) {
|
|
198
|
-
script.textContent = scriptInfo.inline;
|
|
208
|
+
if (scriptQueue.length < MAX_QUEUE_SIZE) {
|
|
209
|
+
scriptQueue.push(scriptInfo);
|
|
199
210
|
}
|
|
200
|
-
|
|
201
|
-
if (scriptInfo.async) script.async = true;
|
|
202
|
-
if (scriptInfo.defer) script.defer = true;
|
|
203
|
-
|
|
204
|
-
script.setAttribute('data-zest-processed', 'executed');
|
|
205
|
-
script.setAttribute('data-consent-executed', 'true');
|
|
206
|
-
|
|
207
|
-
document.head.appendChild(script);
|
|
211
|
+
return true;
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
/**
|
|
211
|
-
* Replay queued scripts for allowed categories
|
|
215
|
+
* Replay queued scripts for allowed categories.
|
|
216
|
+
*
|
|
217
|
+
* scriptQueue is the single source of truth for src and inline body —
|
|
218
|
+
* we never re-read data-* attributes from the DOM (which an attacker
|
|
219
|
+
* could have rewritten in the intervening time).
|
|
212
220
|
*/
|
|
213
221
|
export function replayScripts(allowedCategories) {
|
|
214
222
|
const remaining = [];
|
|
215
223
|
|
|
216
224
|
for (const scriptInfo of scriptQueue) {
|
|
217
|
-
if (allowedCategories.includes(scriptInfo.category)) {
|
|
218
|
-
executeScript(scriptInfo);
|
|
219
|
-
} else {
|
|
225
|
+
if (!allowedCategories.includes(scriptInfo.category)) {
|
|
220
226
|
remaining.push(scriptInfo);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const newScript = document.createElement('script');
|
|
231
|
+
if (scriptInfo.src) {
|
|
232
|
+
newScript.src = scriptInfo.src;
|
|
233
|
+
} else if (scriptInfo.inline) {
|
|
234
|
+
newScript.textContent = scriptInfo.inline;
|
|
235
|
+
}
|
|
236
|
+
if (scriptInfo.async) newScript.async = true;
|
|
237
|
+
if (scriptInfo.defer) newScript.defer = true;
|
|
238
|
+
if (scriptInfo.type && scriptInfo.type !== 'text/plain') {
|
|
239
|
+
newScript.type = scriptInfo.type;
|
|
240
|
+
}
|
|
241
|
+
newScript.setAttribute('data-zest-processed', 'executed');
|
|
242
|
+
newScript.setAttribute('data-consent-executed', 'true');
|
|
243
|
+
|
|
244
|
+
// If the original element is still in the DOM, replace it in place
|
|
245
|
+
// so execution order is preserved. Otherwise append to <head>.
|
|
246
|
+
const original = scriptInfo.element;
|
|
247
|
+
if (original && original.isConnected && original.parentNode) {
|
|
248
|
+
original.parentNode.replaceChild(newScript, original);
|
|
249
|
+
} else {
|
|
250
|
+
document.head.appendChild(newScript);
|
|
221
251
|
}
|
|
222
252
|
}
|
|
223
253
|
|
|
224
254
|
scriptQueue.length = 0;
|
|
225
255
|
scriptQueue.push(...remaining);
|
|
226
|
-
|
|
227
|
-
// Also re-enable any blocked scripts in the DOM
|
|
228
|
-
const blockedScripts = document.querySelectorAll('script[data-zest-processed="blocked"]');
|
|
229
|
-
blockedScripts.forEach(script => {
|
|
230
|
-
const category = script.getAttribute('data-consent-category');
|
|
231
|
-
if (allowedCategories.includes(category)) {
|
|
232
|
-
// Clone and replace to execute
|
|
233
|
-
const newScript = document.createElement('script');
|
|
234
|
-
|
|
235
|
-
const blockedSrc = script.getAttribute('data-blocked-src');
|
|
236
|
-
if (blockedSrc) {
|
|
237
|
-
newScript.src = blockedSrc;
|
|
238
|
-
} else {
|
|
239
|
-
newScript.textContent = script.textContent;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (script.async) newScript.async = true;
|
|
243
|
-
if (script.defer) newScript.defer = true;
|
|
244
|
-
|
|
245
|
-
newScript.setAttribute('data-zest-processed', 'executed');
|
|
246
|
-
newScript.setAttribute('data-consent-executed', 'true');
|
|
247
|
-
script.parentNode?.replaceChild(newScript, script);
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
256
|
}
|
|
251
257
|
|
|
252
258
|
/**
|