DSGVO-Cookie-Einwilligung: Banner mit gleichwertigem Ablehnen, Kategorien, Nachweis, ConsentGate und Consent Mode v2
Installieren
genpm add @core/consentWas du bekommst
- Quellcode in src/lib/consent/, 9 Dateien. (22,3 kB)
- KI-Regeln in src/lib/consent/AGENTS.md, dazu Regeldateien für die IDE.
- Umgebungsvariablen in .env.example ergänzt: CONSENT_POLICY_VERSION, SITE_URL, CONSENT_COOKIE_DOMAIN.
- Löst @core/db für dich auf.
README
Dieses Paket hat keine README.
Genau das liest deine KI, wenn sie in src/lib/consent arbeitet. Sonst wird ihrem Kontext nichts hinzugefügt.
@core/consent — rules for AI agents
Purpose
Cookie consent that meets GDPR/ePrivacy: categories (necessary always on; analytics, marketing, preferences off by
default), a banner where "Reject all" is as prominent as "Accept all", a preferences center, proof of each decision
(consent_records: choices, policy version, date; no IP), server checks (hasConsent), a client ConsentGate and
Google Consent Mode v2 values. Changing CONSENT_POLICY_VERSION asks everyone again.
Map
index.ts— server API:getConsent,hasConsent,recordConsent,consentModeDefaultsScript,consentHistory,cookieDomain.react.ts— client:ConsentBanner,ConsentGate,CookieSettingsButton,openConsentPreferences,readClientConsent,currentPolicyVersion.adapters/hono.ts—consentRoutes().adapters/next.ts—consentRoute()→{ GET, POST }.
Integration
- Env:
CONSENT_POLICY_VERSION(bump it when the cookie policy changes),SITE_URL(its host gives the registrable domain where third-party tags set their cookies: the last two labels) and, when that guess is wrong (e.g..co.uk),CONSENT_COOKIE_DOMAIN=example.co.uk. Migrations as insrc/lib/db/AGENTS.md. - Mount
/api/consent(passcookiesByCategory, e.g.{ analytics: ['_ga'], marketing: ['_fbp', '_ttp'] }, so withdrawing deletes them, host-only and on the registrable domain). The endpoint only accepts same-origin JSON POSTs (CSRF): a cross-siteOrigin/Sec-Fetch-Sitegets 403 and other content types 415; the banner already posts that way. - Root layout:
<ConsentBanner policyVersion={policyVersion()} policyHref="/cookies" labels={translated} />and in the footer<CookieSettingsButton />(a button that callsopenConsentPreferences()). The banner also renders<meta name="consent-policy-version">: client checks treat a decision made under another version as undecided (everything off) until the visitor decides again. - Wrap every analytics/marketing script:
<ConsentGate category="marketing" policyVersion={policyVersion()}>…</ConsentGate>(policyVersionis optional when the banner is on the page); on the server usehasConsent(cookieHeader, 'analytics'). - With Google tags, render
consentModeDefaultsScript(getConsent(cookieHeader))inline before them. - Verify (browser devtools): before deciding, no request goes to third-party analytics or ad domains.
Conventions
- Labels must be translated (
labelsprop); keep both main buttons equal in style and size. - The consent cookie is first-party, not HttpOnly (scripts must read it), and holds no personal data.
- Keep the cookie policy page listing every cookie per category.
Don't
- Don't pre-check categories, use "scroll means consent" or hide "Reject all" behind extra clicks.
- Don't load analytics/marketing scripts outside
ConsentGate, or send hit data server-side without consent. - Don't block the site behind the banner (cookie walls).
# @core/consent — rules for AI agents
## Purpose
Cookie consent that meets GDPR/ePrivacy: categories (necessary always on; analytics, marketing, preferences off by
default), a banner where "Reject all" is as prominent as "Accept all", a preferences center, proof of each decision
(`consent_records`: choices, policy version, date; no IP), server checks (`hasConsent`), a client `ConsentGate` and
Google Consent Mode v2 values. Changing `CONSENT_POLICY_VERSION` asks everyone again.
## Map
- `index.ts` — server API: `getConsent`, `hasConsent`, `recordConsent`, `consentModeDefaultsScript`, `consentHistory`, `cookieDomain`.
- `react.ts` — client: `ConsentBanner`, `ConsentGate`, `CookieSettingsButton`, `openConsentPreferences`, `readClientConsent`, `currentPolicyVersion`.
- `adapters/hono.ts` — `consentRoutes()`. `adapters/next.ts` — `consentRoute()` → `{ GET, POST }`.
## Integration
1. Env: `CONSENT_POLICY_VERSION` (bump it when the cookie policy changes), `SITE_URL` (its host gives the registrable
domain where third-party tags set their cookies: the last two labels) and, when that guess is wrong (e.g. `.co.uk`),
`CONSENT_COOKIE_DOMAIN=example.co.uk`. Migrations as in `src/lib/db/AGENTS.md`.
2. Mount `/api/consent` (pass `cookiesByCategory`, e.g. `{ analytics: ['_ga'], marketing: ['_fbp', '_ttp'] }`, so withdrawing
deletes them, host-only and on the registrable domain). The endpoint only accepts same-origin JSON POSTs (CSRF):
a cross-site `Origin`/`Sec-Fetch-Site` gets 403 and other content types 415; the banner already posts that way.
3. Root layout: `<ConsentBanner policyVersion={policyVersion()} policyHref="/cookies" labels={translated} />` and in the footer
`<CookieSettingsButton />` (a button that calls `openConsentPreferences()`). The banner also renders `<meta name="consent-policy-version">`: client checks
treat a decision made under another version as undecided (everything off) until the visitor decides again.
4. Wrap every analytics/marketing script: `<ConsentGate category="marketing" policyVersion={policyVersion()}>…</ConsentGate>`
(`policyVersion` is optional when the banner is on the page); on the server use `hasConsent(cookieHeader, 'analytics')`.
5. With Google tags, render `consentModeDefaultsScript(getConsent(cookieHeader))` inline before them.
6. Verify (browser devtools): before deciding, no request goes to third-party analytics or ad domains.
## Conventions
- Labels must be translated (`labels` prop); keep both main buttons equal in style and size.
- The consent cookie is first-party, not HttpOnly (scripts must read it), and holds no personal data.
- Keep the cookie policy page listing every cookie per category.
## Don't
- Don't pre-check categories, use "scroll means consent" or hide "Reject all" behind extra clicks.
- Don't load analytics/marketing scripts outside `ConsentGate`, or send hit data server-side without consent.
- Don't block the site behind the banner (cookie walls).
Der genaue Baum, der nach .genpmignore eingebunden wird. Gepinnt an
// POST /api/consent {analytics, marketing, preferences, action?} → guarda y fija la cookie. GET → estado actual.
// Solo JSON y del mismo origen (anti-CSRF): un formulario `text/plain` de otro sitio no puede "aceptar todo".
import { z } from 'zod';
import { ChoicesInput, getConsent, recordConsent } from '../index.ts';
const Body = ChoicesInput.extend({ action: z.enum(['banner', 'preferences', 'withdraw']).default('banner') });
/** Rechaza peticiones de otro sitio: `Sec-Fetch-Site: cross-site` u `Origin` con otro host (o `null`). */
function crossSite(req: Request): boolean {
if (req.headers.get('sec-fetch-site') === 'cross-site') return true;
const origin = req.headers.get('origin');
if (!origin) return false;
try {
return new URL(origin).host !== new URL(req.url).host;
} catch {
return true;
}
}
export async function handleConsent(req: Request, opts: { cookiesByCategory?: Partial<Record<'analytics' | 'marketing' | 'preferences', string[]>> } = {}): Promise<Response> {
if (req.method === 'GET') return Response.json(getConsent(req.headers.get('cookie')), { headers: { 'cache-control': 'private, no-store' } });
if (req.method !== 'POST') return Response.json({ error: 'method_not_allowed' }, { status: 405 });
if (crossSite(req)) return Response.json({ error: 'forbidden' }, { status: 403 });
if (!(req.headers.get('content-type') ?? '').toLowerCase().startsWith('application/json')) return Response.json({ error: 'unsupported_media_type' }, { status: 415 });
const body = Body.safeParse(await req.json().catch(() => null));
if (!body.success) return Response.json({ error: 'invalid' }, { status: 400 });
const { action, ...choices } = body.data;
const { state, setCookies } = await recordConsent({ choices, action, cookieHeader: req.headers.get('cookie') }, { cookiesByCategory: opts.cookiesByCategory, secure: new URL(req.url).protocol === 'https:' });
const headers = new Headers({ 'cache-control': 'no-store', 'content-type': 'application/json' });
for (const c of setCookies) headers.append('set-cookie', c);
return new Response(JSON.stringify(state), { headers });
}
Dieses Paket deklariert keine MCP-Server.
| Version | Commit | Veröffentlicht | Prüfung |
|---|---|---|---|
| 1.1.0 | 37d6a8f | vor 4 Stunden | Prüfung bestanden |
- genpm
- @core/db ^1.0.0
- vorgeschlagen
- GenPM schlägt den npm-Befehl vor und führt ihn nur aus, wenn du zustimmst.
- Verwendet von (3)
- @core/experiments ^1.0.0@core/kit-landing ^1.1.0@core/pixels ^1.0.0
- Prüfung
- Prüfung bestanden · 0 Befunde
- Commit
- v1.1.0 → 37d6a8faf85f764fdc34ddf9623da7b2b19bcd35 · nach dem Abruf verifiziert
- Skripte
- Keine. GenPM führt niemals Paketcode aus.
- Lizenz
- MIT
- Qualität
- 100/100
- Anerkannte Lizenzerfüllt
- AGENTS.md erklärt den Zweckerfüllt
- AGENTS.md enthält Integrationsschritteerfüllt
- AGENTS.md nennt Konventionen oder Verboteerfüllt
- Enthält Testserfüllt
- Sicherheitsscan bestandenerfüllt
- In den letzten 6 Monaten veröffentlichterfüllt
- Verifizierter Herausgebererfüllt
- Zusammenfassung und Schlagwörtererfüllt
- Meldung
- Stimmt etwas nicht?