npm.io
0.6.0 • Published 3d ago

@growth-labs/consent

Licence
Version
0.6.0
Deps
1
Size
106 kB
Vulns
0
Weekly
0

@growth-labs/consent

Geography-aware consent management. Banner UI, cookie gating, and a utility other packages use to check consent state. Generic — not analytics-specific.

Config

import consent from '@growth-labs/consent'

consent({
  mode: 'geography',                             // 'required' | 'geography' | 'disabled'
  consentCookieName: 'gl_consent',
  autoInject: true, // default: package injects the banner script on every page
  purposes: {
    analytics: { label: 'Analytics', description: 'Helps us understand...', required: false },
    marketing: { label: 'Marketing', description: 'Personalized content...', required: false },
  },
  banner: {
    position: 'bottom',          // 'bottom' | 'top' | 'center'
    showManageLink: true,
    defaults: 'auto',            // 'auto' | 'unchecked' | 'checked'
    privacyPolicyUrl: '/privacy-policy/',
    privacyPolicyLabel: 'Privacy policy',
    showCloseButton: false,      // first-impression banner has no X (see below)
  },
  // requireConsentRegions defaults to EU/EEA + UK + Brazil
})

GDPR-Compliant Defaults

banner.defaults controls the pre-tick behaviour of non-required purpose checkboxes:

  • 'auto' (default): unchecked when the visitor's country is in requireConsentRegions (EU/UK/EEA/BR by default), checked otherwise. Per EDPB Guidelines 05/2020 and the CJEU Planet49 ruling, pre-ticked boxes do not constitute valid consent in the EU/UK.
  • 'unchecked': always unchecked. Strict opt-in everywhere.
  • 'checked': always pre-ticked. Legacy pre-0.5.x behaviour. Only legal in non-GDPR regions.

Required-purpose checkboxes (required: true) always stay checked and disabled — they are operational, not consent-bearing.

Setting banner.privacyPolicyUrl renders a link below the action buttons. Omit the option to suppress the link entirely. EDPB Guidelines 05/2020 §3.3.1 expects informed consent, which the link disclosure supports.

First-Impression Close Button

banner.showCloseButton defaults to false. Closing the first-impression banner via an "X" dismisses without recording a decision, which both re-triggers the banner on every subsequent pageview AND fails the EDPB affirmative-act test. The close affordance is therefore gated to the manage-preferences sub-view (opened from inside an active consent flow, where closing is safe).

What It Injects

Middleware: Reads gl_consent cookie → populates context.locals.consent with { analytics: boolean, marketing: boolean, required: boolean }.

Banner: Auto-injected by the integration by default. It shows when consent is required and no cookie exists, supports Accept all / Decline / Manage toggles, and listens for gl:consent_reopen.

With Astro's native security.csp enabled, the middleware registers the exact SHA-256 hash of its injected banner controller before rendering. Do not add unsafe-inline for consent; Astro combines this hash with its own generated script and style hashes. If WebCrypto cannot produce that hash, rendering fails closed with a contextual error and the cached computation is cleared so a later request can retry; the package never injects an unhashed banner script.

Set autoInject: false only when a site intentionally renders the package component itself. The manual component must receive the same runtime values the injected banner uses:

---
import ConsentBanner from '@growth-labs/consent/components/ConsentBanner.astro'

const purposes = [
  { key: 'analytics', label: 'Analytics', description: 'Helps us understand traffic' },
  { key: 'marketing', label: 'Marketing', description: 'Personalized content' },
]
---

<ConsentBanner
  consentRequired={Astro.locals.consent?.required ?? false}
  cookieName="gl_consent"
  purposes={purposes}
  position="bottom"
  showManageLink={true}
/>

Programmatic Reopen

For footer links or privacy pages that should reopen the in-page banner instead of sending users to a separate route:

document.dispatchEvent(
  new CustomEvent('gl:consent_reopen', {
    detail: { view: 'preferences' }, // or 'main'
  }),
)

The banner reuses any existing consent cookie to prefill the checkboxes before it opens.

How Geography Detection Works

Uses CF-IPCountry header (available on all Cloudflare Workers requests). If visitor's country is in requireConsentRegions list → consent required. US visitors → consent not required (mode: geography).

import { isConsentGranted } from '@growth-labs/consent/utils'

if (isConsentGranted(context, 'analytics')) {
  // Set tracking cookies, fire analytics
}

If @growth-labs/consent is NOT installed, isConsentGranted() always returns true (appropriate for US-only sites).

Integration Order

Consent middleware runs AFTER auth, BEFORE analytics. List order in astro.config.mjs: auth → consent → analytics.

Key Patterns

  • Virtual module: virtual:growth-labs/consent/config
  • Runtime state self-seeds from the virtual config in middleware, routes, and utils
  • Cookie gl_consent stores analytics:true,marketing:false
  • Banner is minimal HTML — consumers style with CSS/Tailwind
  • Client-side control uses gl:* CustomEvent events on document, including gl:consent_reopen
  • Zaraz Consent Mode gates GA4 + third-party scripts