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:
James Cottrill
2026-04-13 14:20:15 +00:00
commit fbf26453f2
341 changed files with 62807 additions and 0 deletions

522
apps/banner/src/blocker.ts Normal file
View File

@@ -0,0 +1,522 @@
/**
* blocker.ts — Script interceptor, cookie blocker, and release manager.
*
* Installs before any third-party scripts run. Intercepts script creation,
* proxies document.cookie and Storage writes, and maintains a queue of
* blocked resources that are released per-category when consent is granted.
*/
import type { CategorySlug, InitiatorMapping } from './types';
/** A script element that was blocked, along with its assigned category. */
interface BlockedScript {
/** The original script element or a clone of it. */
element: HTMLScriptElement;
/** The consent category this script belongs to. */
category: CategorySlug;
}
/** Pattern-to-category mapping for URL-based script classification. */
interface ScriptPattern {
pattern: RegExp;
category: CategorySlug;
}
/** Categories that have been consented to. */
let acceptedCategories: Set<CategorySlug> = new Set(['necessary']);
/** Queue of blocked scripts awaiting consent. */
const blockedScripts: BlockedScript[] = [];
/** URL patterns for classifying scripts by category. */
const scriptPatterns: ScriptPattern[] = [];
/** Root initiator URL → category mappings for root-level blocking. */
const initiatorMappings: Array<{ pattern: RegExp; category: CategorySlug }> = [];
/** Whether the blocker has been installed. */
let installed = false;
/** Original document.createElement reference. */
let originalCreateElement: typeof document.createElement;
/** Original document.cookie descriptor. */
let originalCookieDescriptor: PropertyDescriptor | undefined;
/** Original Storage.prototype.setItem reference. */
let originalLocalStorageSetItem: typeof Storage.prototype.setItem;
// ─── Well-known script patterns (built-in defaults) ───
const BUILTIN_PATTERNS: ScriptPattern[] = [
// Analytics
{ pattern: /google-analytics\.com/i, category: 'analytics' },
{ pattern: /googletagmanager\.com/i, category: 'analytics' },
{ pattern: /gtag\/js/i, category: 'analytics' },
{ pattern: /analytics\./i, category: 'analytics' },
{ pattern: /hotjar\.com/i, category: 'analytics' },
{ pattern: /clarity\.ms/i, category: 'analytics' },
{ pattern: /plausible\.io/i, category: 'analytics' },
{ pattern: /matomo\./i, category: 'analytics' },
// Marketing
{ pattern: /doubleclick\.net/i, category: 'marketing' },
{ pattern: /facebook\.net/i, category: 'marketing' },
{ pattern: /fbevents\.js/i, category: 'marketing' },
{ pattern: /connect\.facebook/i, category: 'marketing' },
{ pattern: /ads-twitter\.com/i, category: 'marketing' },
{ pattern: /linkedin\.com\/insight/i, category: 'marketing' },
{ pattern: /snap\.licdn\.com/i, category: 'marketing' },
{ pattern: /tiktok\.com\/i18n/i, category: 'marketing' },
{ pattern: /googlesyndication\.com/i, category: 'marketing' },
{ pattern: /adservice\.google/i, category: 'marketing' },
// Functional
{ pattern: /intercom\.com/i, category: 'functional' },
{ pattern: /crisp\.chat/i, category: 'functional' },
{ pattern: /livechatinc\.com/i, category: 'functional' },
{ pattern: /zendesk\.com/i, category: 'functional' },
];
// ─── Public API ───
/** Install all interception hooks. Call once, as early as possible. */
export function installBlocker(): void {
if (installed) return;
installed = true;
// Merge built-in patterns
scriptPatterns.push(...BUILTIN_PATTERNS);
// Install hooks
installCreateElementOverride();
installMutationObserver();
installCookieProxy();
installStorageProxy();
}
/** Add custom URL-to-category patterns (e.g. from site config allow-list). */
export function addScriptPatterns(patterns: Array<{ pattern: string; category: CategorySlug }>): void {
for (const p of patterns) {
try {
scriptPatterns.push({ pattern: new RegExp(p.pattern, 'i'), category: p.category });
} catch {
console.warn(`[ConsentOS] Invalid script pattern: ${p.pattern}`);
}
}
}
/**
* Load initiator mappings from the site config. Each mapping identifies a root
* script URL that is known to set cookies in a given category via a chain of
* child scripts. Blocking the root prevents the entire chain from executing.
*/
export function loadInitiatorMappings(mappings: InitiatorMapping[]): void {
for (const m of mappings) {
try {
initiatorMappings.push({ pattern: new RegExp(m.root_script, 'i'), category: m.category });
} catch {
console.warn(`[ConsentOS] Invalid initiator pattern: ${m.root_script}`);
}
}
}
/**
* Update the set of accepted categories and release any blocked scripts
* that now have consent.
*/
export function updateAcceptedCategories(categories: CategorySlug[]): void {
acceptedCategories = new Set(categories);
releaseBlockedScripts();
}
/** Get the current blocked script count (useful for debugging/reporting). */
export function getBlockedCount(): number {
return blockedScripts.length;
}
/** Check whether a given category is currently accepted. */
export function isCategoryAllowed(category: CategorySlug): boolean {
return acceptedCategories.has(category);
}
// ─── Script interception ───
/**
* Override document.createElement to intercept <script> creation.
* When a script is created and its src matches a known pattern,
* we set its type to 'text/blocked' to prevent execution.
*/
function installCreateElementOverride(): void {
originalCreateElement = document.createElement.bind(document);
document.createElement = function (
tagName: string,
options?: ElementCreationOptions
): HTMLElement {
const element = originalCreateElement(tagName, options);
if (tagName.toLowerCase() === 'script') {
const script = element as HTMLScriptElement;
wrapScriptElement(script);
}
return element;
} as typeof document.createElement;
}
/**
* Wrap a script element's `src` setter so that when a src is assigned,
* we can classify and potentially block it.
*/
function wrapScriptElement(script: HTMLScriptElement): void {
const originalSrcDescriptor = Object.getOwnPropertyDescriptor(
HTMLScriptElement.prototype,
'src'
);
if (!originalSrcDescriptor) return;
let pendingSrc = '';
Object.defineProperty(script, 'src', {
get() {
return pendingSrc || originalSrcDescriptor.get?.call(this) || '';
},
set(value: string) {
pendingSrc = value;
const category = classifyScript(value, script);
if (category && category !== 'necessary' && !acceptedCategories.has(category)) {
// Block: change type to prevent execution
script.type = 'text/blocked';
script.setAttribute('data-consentos-blocked', 'true');
script.setAttribute('data-consentos-category', category);
script.setAttribute('data-consentos-original-src', value);
}
originalSrcDescriptor.set?.call(this, value);
},
configurable: true,
enumerable: true,
});
}
/**
* MutationObserver watches for script elements being added to the DOM.
* If a script should be blocked, we remove it and queue it.
*/
function installMutationObserver(): void {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLScriptElement) {
handleInsertedScript(node);
}
}
}
});
// Observe as early as possible
if (document.documentElement) {
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
}
}
/** Handle a script element that was just inserted into the DOM. */
function handleInsertedScript(script: HTMLScriptElement): void {
// Skip if it's our own script or already processed
if (script.hasAttribute('data-consentos-allowed') || script.hasAttribute('data-consentos-queued')) {
return;
}
// Check explicit data-category attribute first
const explicitCategory = script.getAttribute('data-category') as CategorySlug | null;
const src = script.getAttribute('data-consentos-original-src') || script.src || '';
const category = explicitCategory || classifyScript(src, script);
// Necessary scripts always pass through
if (!category || category === 'necessary') {
return;
}
// If already consented, allow through
if (acceptedCategories.has(category)) {
return;
}
// Block: remove from DOM and queue
script.setAttribute('data-consentos-queued', 'true');
// Clone the script for later re-insertion
const clone = originalCreateElement('script') as HTMLScriptElement;
// Copy attributes
for (const attr of Array.from(script.attributes)) {
if (attr.name !== 'type' && attr.name !== 'data-consentos-blocked' && attr.name !== 'data-consentos-queued') {
clone.setAttribute(attr.name, attr.value);
}
}
// Copy inline content
if (script.textContent) {
clone.textContent = script.textContent;
}
// Restore original src if it was rewritten
const originalSrc = script.getAttribute('data-consentos-original-src');
if (originalSrc) {
clone.setAttribute('data-consentos-original-src', originalSrc);
}
blockedScripts.push({ element: clone, category });
// Remove from DOM to prevent execution
if (script.parentNode) {
script.parentNode.removeChild(script);
}
}
// ─── Cookie proxy ───
/**
* Proxy document.cookie setter to block cookie writes from
* non-essential categories. We check the cookie name against
* known patterns and the ConsentOS's own cookie is always allowed.
*/
function installCookieProxy(): void {
originalCookieDescriptor = Object.getOwnPropertyDescriptor(
Document.prototype,
'cookie'
);
if (!originalCookieDescriptor) return;
Object.defineProperty(document, 'cookie', {
get() {
return originalCookieDescriptor!.get?.call(document) ?? '';
},
set(value: string) {
// Always allow ConsentOS's own cookies
if (value.startsWith('_consentos_')) {
originalCookieDescriptor!.set?.call(document, value);
return;
}
// If consent hasn't been collected yet and we're in opt-in mode,
// block all non-essential cookie writes
if (!allNonEssentialConsented()) {
const cookieName = parseCookieName(value);
const category = classifyCookie(cookieName);
if (category && category !== 'necessary' && !acceptedCategories.has(category)) {
// Silently block
return;
}
}
originalCookieDescriptor!.set?.call(document, value);
},
configurable: true,
});
}
// ─── Storage proxy ───
/** Proxy localStorage and sessionStorage setItem to block non-essential writes. */
function installStorageProxy(): void {
if (typeof Storage !== 'undefined') {
originalLocalStorageSetItem = Storage.prototype.setItem;
Storage.prototype.setItem = function (key: string, value: string): void {
if (shouldBlockStorageWrite(key)) return;
originalLocalStorageSetItem.call(this, key, value);
};
}
}
/** Check if a storage write should be blocked. */
function shouldBlockStorageWrite(key: string): boolean {
// Always allow ConsentOS's own storage
if (key.startsWith('_consentos_')) return false;
// If all non-essential categories are consented, allow everything
if (allNonEssentialConsented()) return false;
// Block known tracking storage keys
const category = classifyStorageKey(key);
if (category && category !== 'necessary' && !acceptedCategories.has(category)) {
return true;
}
return false;
}
// ─── Release manager ───
/** Release blocked scripts whose categories are now accepted. */
function releaseBlockedScripts(): void {
const toRelease: BlockedScript[] = [];
const remaining: BlockedScript[] = [];
for (const blocked of blockedScripts) {
if (acceptedCategories.has(blocked.category)) {
toRelease.push(blocked);
} else {
remaining.push(blocked);
}
}
// Clear and repopulate the queue
blockedScripts.length = 0;
blockedScripts.push(...remaining);
// Re-insert released scripts in order
for (const { element } of toRelease) {
const script = originalCreateElement('script') as HTMLScriptElement;
// Copy all attributes
for (const attr of Array.from(element.attributes)) {
if (attr.name !== 'data-consentos-blocked' && attr.name !== 'data-consentos-queued' && attr.name !== 'data-consentos-category') {
script.setAttribute(attr.name, attr.value);
}
}
// Use original src if stored
const originalSrc = element.getAttribute('data-consentos-original-src');
if (originalSrc) {
script.src = originalSrc;
script.removeAttribute('data-consentos-original-src');
}
// Copy inline script content
if (element.textContent && !script.src) {
script.textContent = element.textContent;
}
// Mark as allowed so the observer doesn't re-block it
script.setAttribute('data-consentos-allowed', 'true');
// Insert into head
(document.head || document.documentElement).appendChild(script);
}
}
// ─── Classification helpers ───
/** Classify a script by its URL against known patterns and initiator mappings. */
function classifyScript(src: string, script: HTMLScriptElement): CategorySlug | null {
if (!src) return null;
// Explicit data-category always wins
const explicit = script.getAttribute('data-category') as CategorySlug | null;
if (explicit) return explicit;
// Match against URL patterns
for (const { pattern, category } of scriptPatterns) {
if (pattern.test(src)) return category;
}
// Check initiator mappings — block root scripts that are known to set
// cookies in non-consented categories via downstream child scripts
for (const { pattern, category } of initiatorMappings) {
if (pattern.test(src)) return category;
}
return null;
}
/** Well-known cookie name patterns mapped to categories. */
const COOKIE_PATTERNS: Array<{ pattern: RegExp; category: CategorySlug }> = [
// Analytics
{ pattern: /^_ga/i, category: 'analytics' },
{ pattern: /^_gid$/i, category: 'analytics' },
{ pattern: /^_gat/i, category: 'analytics' },
{ pattern: /^_hjSession/i, category: 'analytics' },
{ pattern: /^_hj/i, category: 'analytics' },
{ pattern: /^_pk_/i, category: 'analytics' },
{ pattern: /^_clck$/i, category: 'analytics' },
{ pattern: /^_clsk$/i, category: 'analytics' },
// Marketing
{ pattern: /^_fbp$/i, category: 'marketing' },
{ pattern: /^_fbc$/i, category: 'marketing' },
{ pattern: /^_gcl_/i, category: 'marketing' },
{ pattern: /^IDE$/i, category: 'marketing' },
{ pattern: /^NID$/i, category: 'marketing' },
{ pattern: /^test_cookie$/i, category: 'marketing' },
{ pattern: /^_uetsid/i, category: 'marketing' },
{ pattern: /^_uetvid/i, category: 'marketing' },
// Functional
{ pattern: /^intercom-/i, category: 'functional' },
{ pattern: /^crisp-/i, category: 'functional' },
];
/** Classify a cookie by its name. */
function classifyCookie(name: string): CategorySlug | null {
for (const { pattern, category } of COOKIE_PATTERNS) {
if (pattern.test(name)) return category;
}
return null;
}
/** Well-known storage key patterns. */
const STORAGE_PATTERNS: Array<{ pattern: RegExp; category: CategorySlug }> = [
{ pattern: /^_ga/i, category: 'analytics' },
{ pattern: /^_hj/i, category: 'analytics' },
{ pattern: /^intercom\./i, category: 'functional' },
{ pattern: /^crisp-/i, category: 'functional' },
{ pattern: /^fb_/i, category: 'marketing' },
];
/** Classify a storage key by known patterns. */
function classifyStorageKey(key: string): CategorySlug | null {
for (const { pattern, category } of STORAGE_PATTERNS) {
if (pattern.test(key)) return category;
}
return null;
}
/** Parse the cookie name from a Set-Cookie string. */
function parseCookieName(cookieString: string): string {
const eqIndex = cookieString.indexOf('=');
if (eqIndex === -1) return cookieString.trim();
return cookieString.substring(0, eqIndex).trim();
}
/** Check if all non-essential categories have been consented to. */
function allNonEssentialConsented(): boolean {
return (
acceptedCategories.has('functional') &&
acceptedCategories.has('analytics') &&
acceptedCategories.has('marketing') &&
acceptedCategories.has('personalisation')
);
}
// ─── Teardown (for testing) ───
/** Remove all interception hooks. Used in tests. */
export function uninstallBlocker(): void {
if (!installed) return;
// Restore document.createElement
if (originalCreateElement) {
document.createElement = originalCreateElement;
}
// Restore document.cookie
if (originalCookieDescriptor) {
Object.defineProperty(document, 'cookie', originalCookieDescriptor);
}
// Restore storage
if (originalLocalStorageSetItem) {
Storage.prototype.setItem = originalLocalStorageSetItem;
}
// Clear state
blockedScripts.length = 0;
scriptPatterns.length = 0;
initiatorMappings.length = 0;
acceptedCategories = new Set(['necessary']);
installed = false;
}