headless-consent
v0.1.0
Published
Framework-agnostic, type-safe consent engine with a headless API and TCF bridge.
Maintainers
Readme
headless-consent
Framework-agnostic, strongly typed consent engine for cookie banners and privacy preferences.
headless-consent provides:
- Consent state machine (
unknown,accepted_all,rejected_all,custom) - Purpose and vendor preference updates
- Pluggable storage
- Pluggable analytics event adapters
- A lightweight TCF
__tcfapibridge (ping,getTCData, event listeners)
This package intentionally does not include UI or legal policy text.
Install
pnpm add headless-consentQuick start
import { ConsentEngine, createMemoryStorage, createTcfApiBridge } from "headless-consent";
type PurposeId = "necessary" | "analytics" | "marketing";
type VendorId = "ga" | "meta";
const engine = new ConsentEngine<PurposeId, VendorId>({
purposes: [{ id: "necessary", required: true }, { id: "analytics" }, { id: "marketing" }],
vendors: [
{ id: "ga", purposeIds: ["analytics"] },
{ id: "meta", purposeIds: ["marketing"] },
],
storage: createMemoryStorage(),
});
engine.acceptAll();
engine.savePreferences({
purposes: { analytics: true, marketing: false },
vendors: { ga: true, meta: false },
});
const tcfApi = createTcfApiBridge(engine);
const tcfTarget: Record<string, unknown> = {};
tcfApi.mount(tcfTarget);localStorage example (common simple-site setup)
For small sites, browser storage is typically the simplest way to persist consent state.
import { ConsentEngine, createLocalStorageLikeStorage } from "headless-consent";
type PurposeId = "necessary" | "analytics" | "marketing";
type VendorId = "ga" | "meta";
const storage = createLocalStorageLikeStorage(window.localStorage, "headless-consent:v1");
const engine = new ConsentEngine<PurposeId, VendorId>({
purposes: [{ id: "necessary", required: true }, { id: "analytics" }, { id: "marketing" }],
vendors: [
{ id: "ga", purposeIds: ["analytics"] },
{ id: "meta", purposeIds: ["marketing"] },
],
storage,
});Analytics adapter example
import { ConsentEngine, createMemoryStorage, type AnalyticsAdapter } from "headless-consent";
const gtagAdapter: AnalyticsAdapter = {
track(event, snapshot) {
globalThis.gtag?.("event", event, {
consent_status: snapshot.status,
tc_string: snapshot.tcString,
});
},
};
const engine = new ConsentEngine({
purposes: [{ id: "necessary", required: true }, { id: "analytics" }],
storage: createMemoryStorage(),
analytics: [gtagAdapter],
});Google Analytics integration example
Google Analytics typically uses Consent Mode signals. A common pattern is:
- Set consent defaults to denied before GA initialization.
- Update consent mode when user preferences change.
- Optionally expose
__tcfapifor other partners that consume TCF signals.
import { ConsentEngine, createLocalStorageLikeStorage, createTcfApiBridge } from "headless-consent";
type PurposeId = "necessary" | "analytics" | "marketing";
type VendorId = "ga";
type GtagConsent = "granted" | "denied";
type GtagCommand = "default" | "update";
type Gtag = (
command: "consent",
action: GtagCommand,
payload: {
analytics_storage: GtagConsent;
ad_storage: GtagConsent;
ad_user_data: GtagConsent;
ad_personalization: GtagConsent;
},
) => void;
const gtag = globalThis.gtag as Gtag | undefined;
const engine = new ConsentEngine<PurposeId, VendorId>({
purposes: [{ id: "necessary", required: true }, { id: "analytics" }, { id: "marketing" }],
vendors: [{ id: "ga", purposeIds: ["analytics"] }],
storage: createLocalStorageLikeStorage(window.localStorage, "headless-consent:v1"),
});
gtag?.("consent", "default", {
analytics_storage: "denied",
ad_storage: "denied",
ad_user_data: "denied",
ad_personalization: "denied",
});
const applyGoogleConsent = () => {
const analyticsAllowed = engine.canRun({ vendorId: "ga", purposeIds: ["analytics"] });
const value: GtagConsent = analyticsAllowed ? "granted" : "denied";
gtag?.("consent", "update", {
analytics_storage: value,
ad_storage: "denied",
ad_user_data: "denied",
ad_personalization: "denied",
});
};
applyGoogleConsent();
engine.subscribe(() => {
applyGoogleConsent();
});
const tcfApi = createTcfApiBridge(engine);
tcfApi.mount(window as unknown as Record<string, unknown>);Adjust the mapping from purposes and vendors to Google signals based on your legal basis and policy decisions.
Storage choices: required vs recommended
- Required for runtime/spec correctness: Persist consent in browser storage (cookie or
localStorage) so your runtime can restore state and provide consistent__tcfapiresponses. - Recommended for stronger compliance evidence: Also persist consent events server-side (for example: timestamp, consent action, policy version, region, and consent payload).
In other words, browser storage is usually enough to satisfy technical runtime behavior, while backend persistence improves auditability and legal defensibility.
Compliance responsibilities for consumers
This library is a technical runtime, not legal advice and not a turnkey CMP product. To run a compliant implementation (GDPR/ePrivacy/TCF or local equivalents), consumers must implement and validate all items below.
1) Legal and policy controls
- Define legal entities, data controller relationships, and regions where consent applies.
- Define legal basis per purpose/vendor (consent or other legal basis where allowed).
- Publish and maintain accurate privacy and cookie policy text.
- Keep policy version history and map runtime config to policy versions.
2) UX requirements
- Build first-layer and second-layer UI for banner and preferences.
- Offer
Accept,Reject, andManage Preferenceswith equal ease of access. - Provide clear purpose and vendor information in plain language.
- Provide persistent ability to reopen preferences and withdraw consent.
- Localize content and accessibility (keyboard, focus management, screen readers).
3) Pre-consent enforcement
- Block all non-essential cookies, SDK initialization, and tracking requests before consent.
- Ensure tags do not run during hydration, route transitions, or async script races.
- Gate each integration by purpose/vendor checks via
engine.canRun(...). - Revoke or disable integrations when consent is withdrawn.
4) Data and storage design
- Select secure storage strategy (cookie/localStorage/server) per jurisdiction and risk profile.
- Implement retention and deletion policies for consent records.
- Handle consent expiration and renewal prompts.
- Ensure cross-subdomain and cross-device behavior matches your policy commitments.
5) Consent recordkeeping and auditability
- Persist timestamped consent snapshots and policy version metadata.
- Record user action source (accept all, reject all, granular save, withdrawal).
- Store the consent payload needed to defend processing decisions.
- Provide internal tooling to inspect and export consent records when required.
6) TCF-specific responsibilities (if applicable)
- Register and maintain correct CMP metadata and IDs in the relevant ecosystem.
- Keep vendor/purpose data synchronized with current framework requirements.
- Validate
__tcfapibehavior in page and iframe contexts used by partners. - Track TCF policy updates and run migration tests for new framework versions.
7) Integration responsibilities
- Map each analytics/ads SDK to specific purposes/vendors.
- Verify each provider respects granted/denied states.
- Validate Google/Meta/other partner-specific consent APIs where needed.
- Ensure server-side pipelines honor consent state, not just client-side tags.
8) Testing and release controls
- Add automated tests for default-deny behavior and each consent transition.
- Run browser matrix tests (including private mode and adblock edge cases).
- Test SPA navigation and delayed script loading scenarios.
- Add regression tests that fail if any non-essential cookie is set pre-consent.
9) Ongoing operations
- Monitor production for unauthorized cookie writes and tracker calls.
- Re-audit when adding vendors, changing UI copy, or changing tag manager rules.
- Re-run legal and technical review on regulatory or framework changes.
Suggested implementation checklist
- Initialize
ConsentEngineearly, before third-party SDK bootstrap. - Render banner/preferences UI from
engine.getState()+engine.subscribe(...). - Wire UI actions to
acceptAll,rejectAll, andsavePreferences. - Gate every provider call through a dedicated adapter that checks
canRun(...). - Mount the TCF bridge when partners require
__tcfapi. - Emit consent events to your observability stack for auditing.
Development
vp install
vp check
vp test
vp pack