/* RFC-1192: the wg_consent localStorage flag — the platform's single cookie-consent record — and cookieWriteAllowed(), the mandatory write-gate every catalog module calls before setting a cookie. Also owns writeConsentFlag() so the flag shape has exactly one writer. Never throw on absent, malformed, or unavailable storage — the gate fails closed. Never log or persist PII — the flag is {v, at, granted} only. Do not render UI — the consent surface lives in cookie-consent.ts. RFC-1192: initial consent flag + write-gate (RFC-1192 amends RFC-0188 storage policy). RFC-1198: cookie-free referral attribution — /r/ partner landings + voluntary referrer field Replace the RFC-1191 wg_ref cookie capture with URL+form attribution: static /r/ and //r/ partner landings (noindex, zero JS, consent-gated via publicNameConsentRef), ?ref prefill on the existing referrerField rail, server-side slug resolution against the bundled partners.yaml, referral_partner_slug/name on the Lagebild ingress contract, legacy /?ref -> /r/ 302 in the generated worker, and full removal of the cookie capture module (wg_ref, referral.attribution entitlement, payload.attribution). */ /** * Parses a `name=value` pair out of a cookie source string — the shared * split/trim/decode used by the consent-gated readers below (RFC-1192). */ function parseCookiePair(source: string | null, name: string): string | null { if (!source) return null; for (const part of source.split(";")) { const eq = part.indexOf("="); if (eq <= 0) continue; if (part.slice(0, eq).trim() === name) return decodeURIComponent(part.slice(eq + 1).trim()); } return null; } /** localStorage key holding the cookie-consent record. */ export const CONSENT_FLAG_KEY = "wg_consent"; /** Consent record persisted in localStorage under `wg_consent`. */ export interface ConsentFlag { /** Flag schema version — currently 1. */ v: number; /** ISO-8601 timestamp of the recorded choice. */ at: string; /** The visitor's choice — true: cookies permitted; false: recorded decline. */ granted: boolean; } function isConsentFlag(value: unknown): value is ConsentFlag { return ( typeof value === "object" && value !== null && (value as { v?: unknown }).v === 1 && typeof (value as { at?: unknown }).at === "string" && typeof (value as { granted?: unknown }).granted === "boolean" ); } /** * Reads the flag; returns null when absent, malformed, or storage is * unavailable (private mode) — never throws. */ export function readConsentFlag(): ConsentFlag | null { try { const raw = window.localStorage.getItem(CONSENT_FLAG_KEY); if (!raw) return null; const parsed: unknown = JSON.parse(raw); return isConsentFlag(parsed) ? parsed : null; } catch { return null; } } /** * Writes the consent record — the single writer for the flag shape. * Returns false when storage is unavailable; never throws. */ export function writeConsentFlag(granted: boolean): boolean { try { const flag: ConsentFlag = { v: 1, at: new Date().toISOString(), granted }; window.localStorage.setItem(CONSENT_FLAG_KEY, JSON.stringify(flag)); return true; } catch { return false; } } /** * The single write-gate. Returns true iff the flag exists AND granted is * true; callers MUST NOT set a cookie on false (absent or declined). * Never throws, never logs PII. */ export function cookieWriteAllowed(): boolean { return readConsentFlag()?.granted === true; } /** * Reads a first-party cookie by name. Returns null when absent or * `document.cookie` is unavailable — never throws. */ export function readCookie(name: string): string | null { try { return parseCookiePair(document.cookie, name); } catch { return null; } } /** * Consent-gated first-party cookie write — the only path catalog modules use * to persist state. Returns false (and writes nothing) when the gate is * closed; the caller keeps its captured state in-memory for same-request * pass-through. Never throws. */ export function writeCookie(name: string, value: string, maxAgeDays: number): boolean { if (!cookieWriteAllowed()) return false; try { const maxAge = Math.floor(maxAgeDays * 86400); document.cookie = `${name}=${encodeURIComponent(value)}; Max-Age=${maxAge}; Path=/; SameSite=Lax; Secure`; return true; } catch { return false; } }