Version 2.0.0
Headless entry for bring-your-own-UI, new Zest.on/once subscribers, and a defense-in-depth security hardening pass
Zest reaches v2.0.0 with a new headless entry for consumers who want to bring their own UI, new event-subscriber helpers, and a comprehensive security hardening pass. See the full release on GitHub.
Breaking changes ahead. If you don't reassign
window.Zestor readdata-blocked-srcdirectly, you're fine. See the Breaking section below.
Highlights
Headless Entry
A brand new entry point that ships the consent engine without any UI, translations, or auto-init — ~11 KB gzipped. Ideal if you want to render your own banner/modal with your own CSS.
import Zest from '@freshjuice/zest/headless';
Zest.init({
mode: 'safe',
callbacks: {
onAccept: (consent) => console.log('Accepted:', consent)
}
});
Zest.on('consent', (consent) => {
if (consent.analytics) initAnalytics();
});The headless build:
- Does not auto-initialize — you call
Zest.init(config)explicitly - Does not define
window.Zest - Contains no Shadow DOM UI and no translations
- Adds
Zest.updateConsent(selections)for saving arbitrary category selections from your custom UI
New Subscriber API
Both entries now ship Zest.on() and Zest.once() for cleaner event handling:
// Subscribe to events
const unsubscribe = Zest.on('consent', (consent) => {
console.log('User consented to:', consent);
});
// Fire once and auto-unsubscribe
Zest.once('ready', (consent) => {
console.log('Zest is ready with:', consent);
});
// Reference event names as constants
Zest.on(Zest.EVENTS.CHANGE, handleChange);Security Hardening
Every config value that ends up in HTML, CSS, URLs, JSON, or a regex now passes through a dedicated validator/sanitizer.
High severity
- XSS via
innerHTMLin banner, modal, and widget — all interpolated config values (labels, category names, aria-labels) now pass throughescapeHTML()before rendering. The bannerpositionclass is additionally validated against an allowlist. javascript:URL viapolicyUrl— now validated withsafeUrl()(allowlist:http:/https:/mailto:/tel:/ relative). Linkrelupgraded tonoopener noreferrer.- Arbitrary CSS injection via
customStyles— sanitized by stripping@import,@charset,expression(),-moz-binding, externalurl()values, and any selectors targeting the accept/reject buttons (prevents clickjacking via invisible-button attacks). 20 KB hard cap.
Medium severity
- ReDoS in user-supplied regex patterns —
setPatterns()now routes throughsafeRegExp()which rejects catastrophic-backtracking shapes and caps pattern length at 500 characters. - DOM-based script-source tampering —
replayScripts()no longer re-readsdata-blocked-srcfrom the DOM. The internal script queue is now the single source of truth, snapshotted before any DOM mutation. - Consent cookie on HTTPS now carries the
Secureflag. - Cookie JSON schema validation — parsed cookies are sanitized via
sanitizeConsentPayload()(allowlisted category keys, forced boolean values, prototype-pollution safe). - Overly broad tracker URL matching — tracker matching is now restricted to hostname (and path prefix for entries containing a slash). The old substring fallback caused false positives on URLs with the domain in query params.
- CSS injection via
accentColor— validated withsafeColor()(hex / named /rgb()/rgba()/hsl()/hsla()).
Low severity
window.Zestis now defined withwritable: false, configurable: falseand the API object is frozen.- All user-supplied callbacks are invoked through a safe wrapper — exceptions are logged and swallowed so the consent flow stays consistent.
- Cookie / localStorage / sessionStorage / script replay queues are size-capped (100 / 200 / 200 / 500 entries) to mitigate memory DoS.
- Cookie-interceptor descriptor installed with
configurable: falseso a later-loaded script cannot re-overridedocument.cookie. - Script self-labeling as
data-consent-category="essential"is now ignored — onlyfunctional,analytics,marketingself-labels are honored, and mode-assigned categories take precedence.
Breaking
data-blocked-srcDOM attribute is no longer written to blocked<script>tags. If you were reading this attribute for debugging, switch toZest.getConsentProof()or subscribe viaZest.on('consent', …).window.Zestis now locked (writable: false, configurable: false). Code that replaced or monkey-patched the global will now fail silently. Note:window.ZestConfigis not locked — the config surface is unchanged.
Changed
src/index.jsrefactored to delegate non-UI work to a sharedcore-lifecycle.jsmodule. Public API and behavior unchanged.
Fixed
- Accent-color CSS now falls back cleanly to the default when a non-hex form (named color,
rgb(), etc.) is supplied that can't be mathematically brightness-shifted.
Install
npm install @freshjuice/zest// Full build (banner + modal + widget, auto-init)
import '@freshjuice/zest';
// Headless (BYO UI, manual init)
import Zest from '@freshjuice/zest/headless';
Zest.init({ /* your config */ });Or via CDN:
<script src="https://unpkg.com/@freshjuice/zest@2"></script> 