ConsentOS — a privacy-first cookie consent management platform. Self-hosted, source-available alternative to OneTrust, Cookiebot, and CookieYes. Full standards coverage (IAB TCF v2.2, GPP v1, Google Consent Mode v2, GPC, Shopify Customer Privacy API), multi-tenant architecture with role-based access, configuration cascade (system → org → group → site → region), dark-pattern detection in the scanner, and a tamper-evident consent record audit trail. This is the initial public release. Prior development history is retained internally. See README.md for the feature list, architecture overview, and quick-start instructions. Licensed under the Elastic Licence 2.0 — self-host freely; do not resell as a managed service.
149 lines
5.1 KiB
TypeScript
149 lines
5.1 KiB
TypeScript
/**
|
|
* Global Privacy Control (GPC) signal detection and jurisdiction-aware
|
|
* auto-opt-out.
|
|
*
|
|
* GPC is a browser-level signal (`navigator.globalPrivacyControl === true`)
|
|
* that communicates a user's intent to opt out of the sale/sharing of their
|
|
* personal data. Several US state laws legally require businesses to honour
|
|
* this signal: California (CCPA/CPRA), Colorado (CPA), Connecticut (CTDPA),
|
|
* Texas (TDPSA), and Montana (MTCDPA).
|
|
*
|
|
* @see https://globalprivacycontrol.github.io/gpc-spec/
|
|
*/
|
|
|
|
import type { CategorySlug } from './types';
|
|
|
|
// ── Types ────────────────────────────────────────────────────────────
|
|
|
|
declare global {
|
|
interface Navigator {
|
|
globalPrivacyControl?: boolean;
|
|
}
|
|
}
|
|
|
|
/** Result of GPC detection and evaluation. */
|
|
export interface GpcResult {
|
|
/** Whether the browser is sending the GPC signal. */
|
|
detected: boolean;
|
|
/** Whether GPC was honoured (auto-opt-out applied). */
|
|
honoured: boolean;
|
|
/** The visitor's detected region code (e.g. 'US-CA'), if available. */
|
|
region: string | null;
|
|
}
|
|
|
|
/** GPC-related site configuration fields. */
|
|
export interface GpcConfig {
|
|
/** Whether to detect GPC at all. */
|
|
gpc_enabled: boolean;
|
|
/** Region codes where GPC is legally required. */
|
|
gpc_jurisdictions: string[];
|
|
/** If true, honour GPC regardless of jurisdiction. */
|
|
gpc_global_honour: boolean;
|
|
}
|
|
|
|
/** Default jurisdictions where GPC must be legally honoured. */
|
|
export const DEFAULT_GPC_JURISDICTIONS: string[] = [
|
|
'US-CA', // California — CCPA/CPRA
|
|
'US-CO', // Colorado — CPA
|
|
'US-CT', // Connecticut — CTDPA
|
|
'US-TX', // Texas — TDPSA
|
|
'US-MT', // Montana — MTCDPA
|
|
];
|
|
|
|
// ── Detection ────────────────────────────────────────────────────────
|
|
|
|
/** Check whether the browser is sending the GPC signal. */
|
|
export function isGpcEnabled(): boolean {
|
|
if (typeof navigator === 'undefined') return false;
|
|
return navigator.globalPrivacyControl === true;
|
|
}
|
|
|
|
// ── Region detection ─────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Detect the visitor's region from the CMP context.
|
|
*
|
|
* Uses the `__cmp.visitorRegion` field, which is set by the loader
|
|
* from GeoIP headers (e.g. Cloudflare's `CF-IPCountry` + `CF-Region`)
|
|
* or from a GeoIP API call.
|
|
*/
|
|
export function getVisitorRegion(): string | null {
|
|
if (typeof window === 'undefined') return null;
|
|
return (window as { __cmp?: { visitorRegion?: string } }).__cmp?.visitorRegion ?? null;
|
|
}
|
|
|
|
// ── Jurisdiction check ───────────────────────────────────────────────
|
|
|
|
/**
|
|
* Determine whether GPC should be honoured for the given region.
|
|
*
|
|
* @param region The visitor's region code (e.g. 'US-CA').
|
|
* @param config GPC configuration from the site config.
|
|
* @returns true if GPC should be honoured.
|
|
*/
|
|
export function shouldHonourGpc(
|
|
region: string | null,
|
|
config: GpcConfig,
|
|
): boolean {
|
|
if (!config.gpc_enabled) return false;
|
|
|
|
// Global honour overrides jurisdiction check
|
|
if (config.gpc_global_honour) return true;
|
|
|
|
if (!region) return false;
|
|
|
|
const jurisdictions = config.gpc_jurisdictions.length > 0
|
|
? config.gpc_jurisdictions
|
|
: DEFAULT_GPC_JURISDICTIONS;
|
|
|
|
return jurisdictions.includes(region);
|
|
}
|
|
|
|
// ── Auto-opt-out categories ──────────────────────────────────────────
|
|
|
|
/**
|
|
* Categories that should be rejected when GPC is honoured.
|
|
* GPC specifically relates to sale/sharing/targeted advertising,
|
|
* which maps to the 'marketing' and 'personalisation' categories.
|
|
* Analytics may also be affected depending on interpretation.
|
|
*/
|
|
export const GPC_OPTOUT_CATEGORIES: CategorySlug[] = [
|
|
'marketing',
|
|
'personalisation',
|
|
];
|
|
|
|
/**
|
|
* Categories that remain accepted when GPC auto-opt-out is applied.
|
|
* 'necessary' is always accepted; 'functional' and 'analytics' are
|
|
* not directly related to sale/sharing.
|
|
*/
|
|
export const GPC_ACCEPTED_CATEGORIES: CategorySlug[] = [
|
|
'necessary',
|
|
'functional',
|
|
'analytics',
|
|
];
|
|
|
|
// ── Full evaluation ──────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Evaluate GPC signal and determine whether to apply auto-opt-out.
|
|
*
|
|
* @param config GPC configuration from the site config.
|
|
* @param region The visitor's region code, or null if unknown.
|
|
* @returns GpcResult with detection and honouring status.
|
|
*/
|
|
export function evaluateGpc(
|
|
config: GpcConfig,
|
|
region: string | null = null,
|
|
): GpcResult {
|
|
const detected = isGpcEnabled();
|
|
|
|
if (!detected || !config.gpc_enabled) {
|
|
return { detected, honoured: false, region };
|
|
}
|
|
|
|
const honoured = shouldHonourGpc(region, config);
|
|
|
|
return { detected, honoured, region };
|
|
}
|