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

View File

@@ -0,0 +1,156 @@
"""Configuration hierarchy resolver.
Resolves site configuration by merging:
System Defaults → Org Defaults → Site Group Defaults → Site Config → Regional Overrides
Produces a fully resolved public config suitable for the banner script.
"""
from __future__ import annotations
from typing import Any
# System-level defaults (hard-coded, lowest priority)
SYSTEM_DEFAULTS: dict[str, Any] = {
"blocking_mode": "opt_in",
"tcf_enabled": False,
"gpp_enabled": True,
"gpp_supported_apis": ["usnat"],
"gpc_enabled": True,
"gpc_jurisdictions": ["US-CA", "US-CO", "US-CT", "US-TX", "US-MT"],
"gpc_global_honour": False,
"gcm_enabled": True,
"shopify_privacy_enabled": False,
"gcm_default": {
"ad_storage": "denied",
"ad_user_data": "denied",
"ad_personalization": "denied",
"analytics_storage": "denied",
"functionality_storage": "denied",
"personalization_storage": "denied",
"security_storage": "granted",
},
"banner_config": None,
"privacy_policy_url": None,
"terms_url": None,
"consent_expiry_days": 365,
}
def resolve_config(
site_config: dict[str, Any],
org_defaults: dict[str, Any] | None = None,
group_defaults: dict[str, Any] | None = None,
region: str | None = None,
) -> dict[str, Any]:
"""Resolve the full configuration by merging layers.
Args:
site_config: Site-specific configuration from the database.
org_defaults: Organisation-level default overrides (optional).
group_defaults: Site-group-level default overrides (optional).
region: ISO region code for regional mode override (optional).
Returns:
Fully resolved configuration dictionary.
"""
# Start with system defaults
resolved = {**SYSTEM_DEFAULTS}
# Apply organisation defaults (if any)
if org_defaults:
_merge_non_none(resolved, org_defaults)
# Apply site group defaults (if any)
if group_defaults:
_merge_non_none(resolved, group_defaults)
# Apply site-specific config
_merge_non_none(resolved, site_config)
# Apply regional blocking mode override
if region and site_config.get("regional_modes"):
regional_modes = site_config["regional_modes"]
if isinstance(regional_modes, dict):
# Try exact match first, then fall back to DEFAULT
regional_mode = regional_modes.get(region) or regional_modes.get("DEFAULT")
if regional_mode:
resolved["blocking_mode"] = regional_mode
return resolved
def build_public_config(
site_id: str,
resolved: dict[str, Any],
) -> dict[str, Any]:
"""Build a public configuration JSON for the banner script.
Strips internal fields and adds the site_id for identification.
"""
return {
"id": resolved.get("id", ""),
"site_id": site_id,
"blocking_mode": resolved["blocking_mode"],
"regional_modes": resolved.get("regional_modes"),
"tcf_enabled": resolved["tcf_enabled"],
"gpp_enabled": resolved["gpp_enabled"],
"gpp_supported_apis": resolved.get("gpp_supported_apis"),
"gpc_enabled": resolved["gpc_enabled"],
"gpc_jurisdictions": resolved.get("gpc_jurisdictions"),
"gpc_global_honour": resolved["gpc_global_honour"],
"gcm_enabled": resolved["gcm_enabled"],
"gcm_default": resolved.get("gcm_default"),
"shopify_privacy_enabled": resolved["shopify_privacy_enabled"],
"banner_config": resolved.get("banner_config"),
"privacy_policy_url": resolved.get("privacy_policy_url"),
"terms_url": resolved.get("terms_url"),
"consent_expiry_days": resolved["consent_expiry_days"],
"consent_group_id": resolved.get("consent_group_id"),
"ab_test": resolved.get("ab_test"),
}
CONFIG_FIELDS = (
"blocking_mode",
"regional_modes",
"tcf_enabled",
"tcf_publisher_cc",
"gpp_enabled",
"gpp_supported_apis",
"gpc_enabled",
"gpc_jurisdictions",
"gpc_global_honour",
"gcm_enabled",
"gcm_default",
"shopify_privacy_enabled",
"banner_config",
"privacy_policy_url",
"terms_url",
"consent_expiry_days",
)
def orm_to_config_dict(obj: Any, *, include_id: bool = False) -> dict[str, Any]:
"""Convert a SiteConfig or OrgConfig ORM object to a dict of config fields.
Only includes fields that are explicitly set (not NULL). This allows the
hierarchy to work correctly: unset fields at higher-priority layers don't
block inheritance from lower-priority layers.
"""
d: dict[str, Any] = {}
if include_id and hasattr(obj, "id"):
d["id"] = str(obj.id)
for field in CONFIG_FIELDS:
if hasattr(obj, field):
value = getattr(obj, field)
if value is not None:
d[field] = value
return d
def _merge_non_none(target: dict[str, Any], source: dict[str, Any]) -> None:
"""Merge source into target, skipping None values in source."""
for key, value in source.items():
if value is not None:
target[key] = value