Skip to content

cryptoxinu/compliance-stack

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Compliance Stack

Drop-in legal compliance toolkit for React apps. Cookie consent, CCPA/GDPR, device fingerprinting, forensic consent logging, analytics, and data rights — everything you need to ship a legally compliant web app.

Built from production code powering a real marketplace with real legal requirements.

What's Included

Module What it does
Cookie Consent Banner + preferences dialog with 4 categories (essential, functional, analytics, marketing)
CCPA / Do Not Sell "Do Not Sell My Personal Information" opt-out with California residency detection (IP geo + email domain)
Device Fingerprinting Canvas, WebGL, audio, font, and hardware signal collection with SHA-256 hashing
Forensic Consent Logging Append-only audit trail with content hashes, exact UI text snapshots, and device metadata
Legal Document Versioning Versioned policies with SHA-256 hashes, material vs minor change types, automatic re-consent
Data Rights GDPR/CCPA data export (JSON download), data retention display, Do Not Sell toggle in settings
Analytics Consent-aware event queue with batched server sync and configurable flush intervals
Database Schema 6 production-ready Drizzle ORM tables (PostgreSQL)

Quick Start

1. Install

# Core (required)
npm install compliance-stack react zod

# Optional: database schema
npm install drizzle-orm

2. Configure

// app.tsx or layout.tsx
import { configureCompliance } from "compliance-stack";

configureCompliance({
  companyName: "Acme Corp",
  supportEmail: "support@acme.com",
  privacyEmail: "privacy@acme.com",
  websiteUrl: "https://acme.com",

  // Optional: wire up server callbacks
  onSaveCookiePreferences: async (prefs) => {
    await fetch("/api/privacy/cookies", {
      method: "POST",
      body: JSON.stringify(prefs),
    });
  },
  onSubmitDoNotSell: async ({ email, reason }) => {
    await fetch("/api/privacy/do-not-sell", {
      method: "POST",
      body: JSON.stringify({ email, reason }),
    });
  },
});

3. Add Cookie Consent

import { CookieConsent } from "compliance-stack";

function RootLayout({ children }) {
  return (
    <>
      {children}
      <CookieConsent
        userEmail={currentUser?.email}  // For CCPA state detection
        cookiePolicyUrl="/legal/cookies"
        privacyPolicyUrl="/legal/privacy"
      />
    </>
  );
}

4. Add Data Rights Panel (Settings Page)

import { DataRightsPanel } from "compliance-stack";

function SettingsPage() {
  return (
    <DataRightsPanel
      onGetPrivacySettings={async () => {
        const res = await fetch("/api/privacy/settings");
        return res.json();
      }}
      onUpdateDoNotSell={async (doNotSell) => {
        await fetch("/api/privacy/do-not-sell", {
          method: "PUT",
          body: JSON.stringify({ doNotSell }),
        });
      }}
      onExportUserData={async () => {
        const res = await fetch("/api/privacy/export");
        return res.json();
      }}
    />
  );
}

5. Add Footer Link

import { CookieSettingsLink } from "compliance-stack";

function Footer() {
  return (
    <footer>
      <CookieSettingsLink onClick={() => {
        // Re-opens the preferences dialog
        // You'll need to lift state or use a global event
      }} />
    </footer>
  );
}

Database Schema

6 Drizzle ORM tables ready for drizzle-kit push:

import {
  legalDocuments,
  legalConsents,
  userConsents,
  deviceFingerprints,
  dataRightsRequests,
  analyticsEvents,
} from "compliance-stack/schema";

// Add to your Drizzle schema config

Table Overview

legal_documents         — Versioned legal docs (ToS, Privacy, etc.)
  ├── policy, version, contentHtml, contentHash (SHA-256)
  ├── pdfKey (object storage snapshot)
  ├── changeType: "material" | "minor" (triggers re-consent)
  └── status: draft → active → retired

legal_consents          — Append-only forensic audit log (NEVER update/delete)
  ├── userId, role, policy, version, contentHash
  ├── assentType: clickwrap | continued_use | email_acknowledgement
  ├── assentAction: "checkbox+button", "accept_all_button", etc.
  ├── assentTextSnapshot: exact label text shown to user
  └── forensics: IP, user agent, locale, geo, device fingerprint

user_consents           — Mutable consent state (current status per user)
  ├── consentType: tos, privacy, cookies, marketing_emails, do_not_sell, ...
  ├── consented: boolean
  └── consentedAt / withdrawnAt timestamps

device_fingerprints     — Browser fingerprinting for fraud detection
  ├── deviceHash: salted SHA-256 of all signals
  ├── signalsJson: screen, timezone, canvas, WebGL, audio, fonts
  ├── ipAddresses, userAgents, geoCountries (accumulated arrays)
  └── riskScore: 0.0 (safe) to 1.0 (high risk)

data_rights_requests    — CCPA/GDPR subject access requests
  ├── requestType: access | deletion | portability | correction | opt_out_sale
  ├── status: pending → in_progress → completed/denied
  └── verificationCode, processedBy, responseFileUrl

analytics_events        — First-party event tracking
  ├── eventType, data (JSONB), userId
  └── clientTimestamp, serverTimestamp, sessionId

Server Utilities

IP Geolocation (CCPA Detection)

import { detectCaliforniaIp } from "compliance-stack";

// In your API route:
const { isCaliforniaResident, region, country } = await detectCaliforniaIp(
  request.headers.get("x-forwarded-for")?.split(",")[0]
);

Device Fingerprinting

// Client-side: collect signals
import { collectDeviceSignals, computeDeviceHash } from "compliance-stack";

const signals = await collectDeviceSignals();
const hash = await computeDeviceHash(signals);
// Send { hash, signals } to your server for storage

Forensic Consent Records

import { buildConsentRecord, verifyConsentHash } from "compliance-stack";

// When user accepts ToS:
const record = await buildConsentRecord({
  userId: "user_123",
  role: "user",
  policy: "tos",
  version: "1.0.0",
  effectiveAt: new Date("2024-01-01"),
  contentHtml: tosHtmlContent,
  assentType: "clickwrap",
  assentAction: "checkbox+button",
  assentTextSnapshot: "I agree to the Terms of Service",
  ipAddress: req.ip,
  userAgent: req.headers["user-agent"],
});
// record.contentHash is computed automatically
// Insert into legal_consents table

// Later: verify document hasn't been tampered with
const isValid = await verifyConsentHash(tosHtmlContent, record.contentHash);

Analytics (Consent-Aware)

import { analytics } from "compliance-stack";
import { useCookiePreferences } from "compliance-stack";

// Configure with consent check
const { hasAnalytics } = useCookiePreferences();
analytics.configure({
  endpoint: "/api/analytics",
  hasConsent: () => hasAnalytics,
});

// Track events (automatically skipped if no consent)
analytics.track("signup", { userId: "123", plan: "pro" });
analytics.pageView("/dashboard");
analytics.error("Something broke", error.stack);

Styling

Components use CSS custom properties with sensible defaults. Override them in your CSS:

:root {
  --cs-bg: #ffffff;
  --cs-text: #111827;
  --cs-muted: #6b7280;
  --cs-muted-bg: #f3f4f6;
  --cs-border: #e5e7eb;
  --cs-primary: #2563eb;
  --cs-accent-bg: #eff6ff;
}

/* Dark mode */
.dark {
  --cs-bg: #1f2937;
  --cs-text: #f9fafb;
  --cs-muted: #9ca3af;
  --cs-muted-bg: #374151;
  --cs-border: #4b5563;
  --cs-primary: #3b82f6;
  --cs-accent-bg: #1e3a5f;
}

The toggle switch uses a .cs-switch class. Add this CSS for the switch styling:

.cs-switch {
  position: relative;
  display: inline-block;
  width: 44px;
  height: 24px;
}
.cs-switch input {
  opacity: 0;
  width: 0;
  height: 0;
}
.cs-switch .cs-switch-slider {
  position: absolute;
  cursor: pointer;
  inset: 0;
  background: #d1d5db;
  border-radius: 24px;
  transition: background 0.2s;
}
.cs-switch .cs-switch-slider::before {
  content: "";
  position: absolute;
  height: 18px;
  width: 18px;
  left: 3px;
  bottom: 3px;
  background: white;
  border-radius: 50%;
  transition: transform 0.2s;
}
.cs-switch input:checked + .cs-switch-slider {
  background: var(--cs-primary, #2563eb);
}
.cs-switch input:checked + .cs-switch-slider::before {
  transform: translateX(20px);
}
.cs-switch input:disabled + .cs-switch-slider {
  opacity: 0.5;
  cursor: not-allowed;
}

Configuration Reference

configureCompliance({
  // Required
  companyName: "Acme Corp",
  supportEmail: "support@acme.com",
  privacyEmail: "privacy@acme.com",
  websiteUrl: "https://acme.com",

  // Cookie consent
  storagePrefix: "acme",           // localStorage key prefix (default: companyName lowercase)
  consentVersion: "1.0",           // Bump to force re-consent
  cookieCategories: [...],         // Override default categories

  // CCPA
  ccpaStates: ["CA", "CO", ...],   // US states with privacy laws (default: CA,CO,CT,VA,UT)
  emailDomainStateMap: {           // Map .edu domains to states
    "berkeley.edu": "CA",
    "ucdavis.edu": "CA",
  },
  geoIpApiUrl: "...",              // IP geolocation endpoint

  // Data retention (displayed in UI)
  dataRetention: {
    activeAccounts: "Retained while active",
    inactiveAccounts: "Deleted after 3 years",
    paymentRecords: "7 years (legal)",
  },

  // Analytics
  analyticsEnabled: true,
  analyticsFlushIntervalMs: 5000,
  analyticsEndpoint: "/api/analytics",

  // Server callbacks
  onSaveCookiePreferences: async (prefs) => { ... },
  onSubmitDoNotSell: async ({ email, reason }) => { ... },
  onExportUserData: async () => { ... },
  onGetPrivacySettings: async () => { ... },
  onUpdateDoNotSell: async (doNotSell) => { ... },
});

Legal Compliance Checklist

Use this checklist to ensure your implementation covers all requirements:

  • Cookie consent banner appears on first visit
  • Users can accept all, reject non-essential, or customize
  • Cookie preferences persist across sessions (localStorage + server sync)
  • Bumping consentVersion forces re-consent
  • "Do Not Sell" link appears for CCPA-state users
  • Do Not Sell requests are processed within 45 days (CCPA)
  • Legal documents are versioned with SHA-256 content hashes
  • Consent records are append-only (never updated or deleted)
  • Users can download their data (GDPR Article 20)
  • Data retention periods are clearly displayed
  • Analytics only fire when analytics consent is granted
  • Device fingerprints are used for fraud detection, not tracking
  • Cookie settings link in footer allows preference changes

Architecture

User visits site
  │
  ├─ hasConsented() checks localStorage
  │   ├─ No → show ConsentBanner (1s delay)
  │   │        ├─ Accept All → save all categories
  │   │        └─ Customize → PreferencesDialog
  │   │             ├─ Toggle categories → Save
  │   │             └─ Do Not Sell (CCPA states) → DoNotSellDialog
  │   └─ Yes → load stored preferences
  │
  ├─ Preferences saved to:
  │   ├─ localStorage (immediate, client-side)
  │   └─ Server via onSaveCookiePreferences callback
  │
  ├─ Settings Page:
  │   ├─ DataRightsPanel
  │   │   ├─ Do Not Sell toggle (IP geolocation detection)
  │   │   ├─ Data Export (JSON download)
  │   │   └─ Data Retention display
  │   └─ Syncs with server via callbacks
  │
  └─ Database:
      ├─ legal_documents (versioned, SHA-256 hashed)
      ├─ legal_consents (append-only forensic audit log)
      ├─ user_consents (mutable current state)
      ├─ device_fingerprints (fraud detection)
      ├─ data_rights_requests (CCPA/GDPR requests)
      └─ analytics_events (consent-aware tracking)

License

MIT

About

Drop-in legal compliance toolkit for React apps — cookie consent, CCPA/GDPR, device fingerprinting, forensic consent logging, analytics, and data rights

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors