/** * Pure Consent Mode v2 helpers (no $app / $env) — safe for node:test. * * Categories: * - Necessary: always on (session, CSRF, theme, locale, consent preference) * - Analytics → analytics_storage * - Marketing → ad_storage, ad_user_data, ad_personalization */ export const CONSENT_STORAGE_KEY = "descrybe-cookie-consent"; export const CONSENT_VERSION = 1; export const CONSENT_WAIT_FOR_UPDATE_MS = 500; export type ConsentPreferences = { analytics: boolean; marketing: boolean; }; export type StoredConsent = ConsentPreferences & { v: number; updatedAt: string; }; /** Google Consent Mode v2 signal map (string literals for gtag). */ export type ConsentModeSignals = { ad_storage: "granted" | "denied"; ad_user_data: "granted" | "denied"; ad_personalization: "granted" | "denied"; analytics_storage: "granted" | "denied"; functionality_storage: "granted" | "denied"; personalization_storage: "granted" | "denied"; security_storage: "granted" | "denied"; }; export const DENIED_CONSENT_DEFAULTS: ConsentModeSignals = { ad_storage: "denied", ad_user_data: "denied", ad_personalization: "denied", analytics_storage: "denied", functionality_storage: "granted", personalization_storage: "denied", security_storage: "granted" }; export function preferencesToConsentSignals( prefs: ConsentPreferences ): ConsentModeSignals { const analytics = prefs.analytics ? "granted" : "denied"; const marketing = prefs.marketing ? "granted" : "denied"; return { ad_storage: marketing, ad_user_data: marketing, ad_personalization: marketing, analytics_storage: analytics, functionality_storage: "granted", personalization_storage: analytics, security_storage: "granted" }; } export function parseStoredConsent(raw: string | null | undefined): StoredConsent | null { if (!raw?.trim()) return null; try { const parsed: unknown = JSON.parse(raw); if (!parsed || typeof parsed !== "object") return null; const obj = parsed as Record; if (obj.v !== CONSENT_VERSION) return null; if (typeof obj.analytics !== "boolean" || typeof obj.marketing !== "boolean") { return null; } const updatedAt = typeof obj.updatedAt === "string" && obj.updatedAt.trim() ? obj.updatedAt : new Date(0).toISOString(); return { v: CONSENT_VERSION, analytics: obj.analytics, marketing: obj.marketing, updatedAt }; } catch { return null; } } export function serializeConsent(prefs: ConsentPreferences, updatedAt = new Date().toISOString()): string { const stored: StoredConsent = { v: CONSENT_VERSION, analytics: prefs.analytics, marketing: prefs.marketing, updatedAt }; return JSON.stringify(stored); } /** Accept all non-necessary categories. */ export function acceptAllPreferences(): ConsentPreferences { return { analytics: true, marketing: true }; } /** Reject analytics and marketing (necessary remains on). */ export function rejectNonEssentialPreferences(): ConsentPreferences { return { analytics: false, marketing: false }; } /** Coarse SKU-volume bucket for lead events — never send the raw count as PII-adjacent precision. */ export function estimatedSkusBucket(n: number | null | undefined): string { if (n == null || !Number.isFinite(n) || n < 0) return "unknown"; if (n < 1_000) return "0_999"; if (n < 5_000) return "1000_4999"; if (n < 20_000) return "5000_19999"; if (n < 100_000) return "20000_99999"; return "100000_plus"; }