Files
consentos/apps/banner/src/gpc.ts
James Cottrill fbf26453f2 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.
2026-04-14 09:18:18 +00:00

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 };
}