fix(banner): bridge blocker state loader↔bundle and sweep stale cookies on consent change (#4)
* fix(banner): bridge blocker state between loader and bundle
``consent-loader.js`` and ``consent-bundle.js`` are built as two
separate rollup IIFEs, so each inlines a private copy of
``blocker.ts``. The loader's copy is the one that actually matters
— it installs the ``document.cookie`` / ``Storage.prototype.setItem``
/ ``MutationObserver`` proxies, and those proxies close over the
loader's private ``acceptedCategories`` set and its
``blockedScripts`` queue.
The bundle used to ``import { updateAcceptedCategories } from './blocker'``
and call it from ``handleConsent``. That updated the **bundle's**
dead-end copy of the blocker module — a different ``Set`` object in
a different scope from the one the proxies read — so after the user
granted consent the loader's proxies stayed stuck on
``Set(['necessary'])``, any non-necessary cookie write kept being
silently dropped, and the loader's queue of blocked scripts was
never released.
Fix by exposing the loader's live ``updateAcceptedCategories`` on
``window.__consentos._updateBlocker`` right after
``installBlocker()``, and replacing the bundle's dead-end import
with a helper that calls through the bridge. The bundle no longer
imports from ``./blocker`` at all; rollup tree-shakes the bundle's
copy out, so ``consent-bundle.js`` gets slightly smaller.
Tests (``__tests__/blocker-bridge.test.ts``) cover:
* Bridge is called with the exact accepted list.
* Bridge is forwarded verbatim (no slug filtering).
* Missing bridge logs a warning and doesn't throw.
* Missing ``window.__consentos`` logs a warning and doesn't throw.
``vi.hoisted`` seeds ``window.__consentos`` before banner.ts's
module-level ``init()`` runs so the import-time IIFE doesn't throw
on the empty global.
* fix(banner): sweep disallowed cookies + storage on consent update
When the loader runs it now proactively deletes any pre-existing
cookies (and localStorage / sessionStorage keys) that classify into
a category the visitor hasn't consented to. Fixes the common case
of an ``_ga`` that slipped through before the loader was installed
— e.g. on a host page that loads the loader with ``async`` or
places another tracker above it in ``<head>`` — sitting there
forever because the blocker's only defence was a setter proxy on
future writes.
Sweep semantics:
- Runs on every ``updateAcceptedCategories`` call (consent narrowed
→ the just-revoked categories' cookies are wiped) plus an
explicit call from the loader's "no consent yet" branch (initial
visit with pre-existing trackers from elsewhere).
- Only deletes cookies / keys whose classifier hits a known
pattern (``_ga``, ``_fbp``, ``intercom-*`` etc. — same lists as
the proxied setters). Unknown / unclassified cookies are left
alone: we can't attribute them and won't risk clobbering
first-party session state.
- ``_consentos_*`` is always preserved.
- For cookies, we don't know the original ``domain`` / ``path``
(``document.cookie`` doesn't expose them), so we fire deletes
against every plausible domain variant — bare hostname, leading
``.``, and every parent domain walked up from the left. Covers
the GA "set on ``.example.com`` from a subdomain page" case
without over-deleting.
- Deletions bypass the proxied setter and go directly through the
cached ``originalCookieDescriptor`` captured before the proxy was
installed, so the blocker doesn't eat its own expiry writes.
- Storage access is wrapped in ``safeStorage`` — sandboxed /
cross-origin iframes that throw on ``window.localStorage`` are
skipped rather than crashing the loader.
Tests in ``__tests__/blocker.test.ts`` cover: non-consented analytics
cookies are deleted, consented ones are preserved, unknown cookies
survive, ``_consentos_*`` is untouched, revoking a category after
seeding new cookies triggers a follow-up sweep, and localStorage
cleanup follows the same rules. 6 new cases, 373 passing total in
the banner suite.
This commit is contained in:
@@ -10,12 +10,39 @@
|
||||
*/
|
||||
|
||||
import { announce, createLiveRegion, focusFirst, onEscape, prefersReducedMotion, trapFocus } from './a11y';
|
||||
import { updateAcceptedCategories } from './blocker';
|
||||
// NB: intentionally NOT importing from './blocker'. The loader already
|
||||
// installed the blocker proxies in its own IIFE module scope, and
|
||||
// the bundle can't share that state via a direct import — rollup
|
||||
// builds ``consent-loader.js`` and ``consent-bundle.js`` as separate
|
||||
// IIFEs so each one inlines its own private copy of every module.
|
||||
// The loader exposes ``_updateBlocker`` on ``window.__consentos``
|
||||
// for us to drive its proxies — see ``updateAcceptedCategories``
|
||||
// below and ``apps/banner/src/loader.ts``.
|
||||
import { buildConsentState, readConsent, writeConsent } from './consent';
|
||||
import { buildGcmStateFromCategories, updateGcm } from './gcm';
|
||||
import { type TranslationStrings, DEFAULT_TRANSLATIONS, detectLocale, interpolate, loadTranslations, renderLinks } from './i18n';
|
||||
import type { BannerConfig, ButtonConfig, CategorySlug, SiteConfig } from './types';
|
||||
|
||||
/**
|
||||
* Drive the loader's blocker proxies with a new accepted-categories
|
||||
* set. Falls back to a ``console.warn`` if the bridge is missing,
|
||||
* which would mean the loader hasn't finished ``installBlocker`` yet
|
||||
* (shouldn't happen — the bundle only loads after the loader's
|
||||
* synchronous init phase). Exported for unit testing only.
|
||||
*/
|
||||
export function updateAcceptedCategories(accepted: CategorySlug[]): void {
|
||||
const bridge = window.__consentos?._updateBlocker;
|
||||
if (typeof bridge === 'function') {
|
||||
bridge(accepted);
|
||||
} else if (typeof console !== 'undefined') {
|
||||
console.warn(
|
||||
'[ConsentOS] blocker bridge missing — consent granted but ' +
|
||||
'cookie/script blocker state was not updated. The loader ' +
|
||||
'may not have initialised correctly.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Preference-centre closure captured during init() ---------------------
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user