feat: initial public release
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.
This commit is contained in:
148
apps/banner/src/gpc.ts
Normal file
148
apps/banner/src/gpc.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user