Terug naar blog
Compliance

CCPA to CPRA: The Complete California Privacy Compliance Update for 2025

Het GetCookies-teamNovember 24, 202517 min leestijd
CCPACPRACaliforniaGPCCompliance

TLDR: CPRA enforcement is fully active. In 2025, honoring Global Privacy Control (GPC) signals is mandatory, not optional. Sephora's $1.2M fine was just the beginning.

Read full summary

The California Privacy Rights Act significantly expanded CCPA obligations. Key 2025 requirements include mandatory GPC signal recognition, new sensitive personal information assessments, and strict "dark pattern" prohibitions. This guide provides complete implementation code for GPC detection, sensitive PI handling, and compliance automation.

Summary by Claude AI

The $1.2 Million Mistake

In August 2022, Sephora paid California $1.2 million for one oversight: their website didn't respect Global Privacy Control signals.

Their banner looked compliant. Users could "opt out" on a page buried three clicks deep. But when a California user's browser sent a GPC signal—an automatic "do not sell my data" request—Sephora ignored it. They kept sharing customer data with advertising networks as if nothing happened.

The California Attorney General made an example of them. And that was under the old rules.

In 2025, enforcement is worse. The California Privacy Protection Agency (CPPA) has full authority, larger budgets, and a mandate to crack down on exactly the behavior that got Sephora fined. The difference? Now there's no cure period for many violations. No warning letter. Just an investigation and a penalty.

If your website ignores GPC signals today, you're not taking a calculated risk. You're hoping not to get caught.

What Changed in 2025

The California privacy landscape looks nothing like it did when CCPA launched in 2020. The California Privacy Rights Act (CPRA) transformed a basic consumer protection law into something approaching GDPR-level regulation:

Mandatory GPC Recognition: The CPPA has confirmed that Global Privacy Control signals must be treated as valid opt-out requests. No exceptions. No "we'll consider it." When a browser sends GPC, you stop selling that user's data. Period.

Dark Pattern Crackdown: Those clever consent banners with giant "Accept All" buttons and tiny "Reject" links? They're now explicitly illegal. Equal prominence is required. The CPPA has fined companies for asymmetric design.

Sensitive Personal Information: New category, new obligations. Health data, financial information, precise geolocation—all now require separate consent flows and enhanced protections.

No Cure Period: For many violations, the 30-day "fix it" window is gone. If the CPPA investigates and finds problems, penalties start immediately.

This guide covers everything you need to stay compliant: GPC detection code, dark pattern prevention, sensitive PI handling, and service provider contract requirements.

CPPA Enforcement Priorities in 2025

The California Privacy Protection Agency has identified four priority enforcement areas, each requiring specific technical implementations.

Priority 1: Dark Patterns in Opt-Out Mechanisms

The CPPA has made clear that dark patterns in privacy interfaces will result in enforcement actions. This includes:

  • Asymmetric choice architecture: Making "Accept All" prominent while hiding "Reject All"
  • Confirm-shaming: Language designed to make users feel guilty for protecting privacy
  • Obstruction: Multi-step processes for opting out vs. single-click for opting in
  • Forced action: Requiring account creation or excessive information to exercise rights
// cpra-compliant-banner.ts
// Banner implementation that avoids dark patterns

interface CPRABannerConfig {
  domain: string;
  privacyPolicyUrl: string;
  doNotSellUrl: string;
  requestDeletionUrl: string;
  contactEmail: string;
}

interface BannerButton {
  text: string;
  action: () => void;
  style: 'primary' | 'secondary' | 'tertiary';
  prominence: number; // 1-10 scale, must be equal for opt-in/opt-out
}

class CPRACompliantBanner {
  private config: CPRABannerConfig;
  private gpcDetected: boolean = false;
  private userOptedOut: boolean = false;

  constructor(config: CPRABannerConfig) {
    this.config = config;
    this.detectGPC();
    this.checkExistingPreference();
  }

  private detectGPC(): void {
    // GPC detection must happen before any data collection
    if (typeof navigator !== 'undefined') {
      this.gpcDetected = !!(navigator as any).globalPrivacyControl;

      if (this.gpcDetected) {
        console.log('[CPRA] GPC signal detected - treating as opt-out');
        this.handleGPCOptOut();
      }
    }
  }

  private handleGPCOptOut(): void {
    // Immediate opt-out - no confirmation required
    this.userOptedOut = true;
    this.disableDataSaleAndSharing();
    this.recordOptOutEvent('gpc_signal');

    // Do NOT show banner asking to confirm GPC choice
    // This would violate CPRA requirements
  }

  private checkExistingPreference(): void {
    const stored = localStorage.getItem('cpra_consent_preference');
    if (stored) {
      const preference = JSON.parse(stored);
      this.userOptedOut = preference.optedOut;
    }
  }

  // Dark pattern prevention: Equal prominence for all choices
  generateButtons(): BannerButton[] {
    const buttons: BannerButton[] = [
      {
        text: 'Accept All',
        action: () => this.handleAcceptAll(),
        style: 'primary',
        prominence: 8  // Same prominence
      },
      {
        text: 'Reject All',
        action: () => this.handleRejectAll(),
        style: 'primary',  // Same style - not secondary or tertiary
        prominence: 8      // Same prominence
      },
      {
        text: 'Customize',
        action: () => this.showPreferenceCenter(),
        style: 'secondary',
        prominence: 6
      }
    ];

    return buttons;
  }

  // Validate banner doesn't use dark patterns
  validateCompliance(): ComplianceReport {
    const issues: string[] = [];
    const buttons = this.generateButtons();

    const acceptBtn = buttons.find(b => b.text.toLowerCase().includes('accept'));
    const rejectBtn = buttons.find(b => b.text.toLowerCase().includes('reject'));

    if (acceptBtn && rejectBtn) {
      // Check equal prominence
      if (acceptBtn.prominence !== rejectBtn.prominence) {
        issues.push('Unequal button prominence detected - potential dark pattern');
      }

      // Check equal styling
      if (acceptBtn.style !== rejectBtn.style) {
        issues.push('Unequal button styling detected - potential dark pattern');
      }
    }

    // Check for confirm-shaming language
    const shamingPhrases = [
      'no thanks', 'i don\'t care', 'i accept risk',
      'not interested in', 'miss out', 'lose access'
    ];

    buttons.forEach(btn => {
      const lowerText = btn.text.toLowerCase();
      shamingPhrases.forEach(phrase => {
        if (lowerText.includes(phrase)) {
          issues.push(`Potential confirm-shaming detected: "${btn.text}"`);
        }
      });
    });

    return {
      compliant: issues.length === 0,
      issues,
      checkedAt: new Date().toISOString(),
      regulation: 'CPRA'
    };
  }

  private handleAcceptAll(): void {
    this.recordConsent({
      analytics: true,
      advertising: true,
      personalization: true,
      dataSale: true,
      dataSharing: true
    });
    this.hideBanner();
  }

  private handleRejectAll(): void {
    this.userOptedOut = true;
    this.recordConsent({
      analytics: false,
      advertising: false,
      personalization: false,
      dataSale: false,
      dataSharing: false
    });
    this.disableDataSaleAndSharing();
    this.hideBanner();
  }

  private disableDataSaleAndSharing(): void {
    // Immediately stop all sale/sharing activities
    window.dispatchEvent(new CustomEvent('cpra:optout', {
      detail: { type: 'sale_and_sharing', timestamp: Date.now() }
    }));

    // Disable advertising pixels
    this.disableAdvertisingPixels();

    // Stop cross-context behavioral tracking
    this.disableCrossContextTracking();
  }

  private disableAdvertisingPixels(): void {
    // Remove Facebook Pixel
    if (typeof fbq !== 'undefined') {
      fbq('consent', 'revoke');
    }

    // Remove Google Ads remarketing
    if (typeof gtag !== 'undefined') {
      gtag('consent', 'update', {
        'ad_storage': 'denied',
        'ad_user_data': 'denied',
        'ad_personalization': 'denied'
      });
    }

    // Remove TikTok Pixel
    if (typeof ttq !== 'undefined') {
      // TikTok doesn't have native consent mode
      // Must remove script entirely
      const tiktokScript = document.querySelector('script[src*="tiktok"]');
      tiktokScript?.remove();
    }
  }

  private disableCrossContextTracking(): void {
    // Clear third-party cookies where possible
    document.cookie.split(';').forEach(cookie => {
      const name = cookie.split('=')[0].trim();
      // Clear known tracking cookies
      const trackingCookies = ['_fbp', '_fbc', '_gcl_au', '_ga', '_gid'];
      if (trackingCookies.includes(name)) {
        document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
      }
    });
  }

  private recordConsent(preferences: ConsentPreferences): void {
    const record = {
      preferences,
      timestamp: new Date().toISOString(),
      gpcSignal: this.gpcDetected,
      userAgent: navigator.userAgent,
      optedOut: !preferences.dataSale
    };

    localStorage.setItem('cpra_consent_preference', JSON.stringify(record));

    // Send to server for audit trail
    this.sendConsentToServer(record);
  }

  private async sendConsentToServer(record: any): Promise<void> {
    try {
      await fetch('/api/v1/consent/cpra', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(record)
      });
    } catch (error) {
      console.error('[CPRA] Failed to record consent:', error);
      // Queue for retry
      this.queueForRetry(record);
    }
  }

  private recordOptOutEvent(source: string): void {
    const event = {
      type: 'opt_out',
      source,
      timestamp: new Date().toISOString(),
      gpcDetected: this.gpcDetected
    };

    // Store locally for audit
    const events = JSON.parse(localStorage.getItem('cpra_events') || '[]');
    events.push(event);
    localStorage.setItem('cpra_events', JSON.stringify(events));
  }

  private showPreferenceCenter(): void {
    window.dispatchEvent(new CustomEvent('cpra:showPreferences'));
  }

  private hideBanner(): void {
    window.dispatchEvent(new CustomEvent('cpra:hideBanner'));
  }

  private queueForRetry(record: any): void {
    const queue = JSON.parse(localStorage.getItem('cpra_retry_queue') || '[]');
    queue.push({ record, attempts: 0, nextRetry: Date.now() + 60000 });
    localStorage.setItem('cpra_retry_queue', JSON.stringify(queue));
  }
}

interface ConsentPreferences {
  analytics: boolean;
  advertising: boolean;
  personalization: boolean;
  dataSale: boolean;
  dataSharing: boolean;
}

interface ComplianceReport {
  compliant: boolean;
  issues: string[];
  checkedAt: string;
  regulation: string;
}

// Declare global types for third-party pixels
declare const fbq: any;
declare const gtag: any;
declare const ttq: any;

Priority 2: Global Privacy Control Recognition (Mandatory)

GPC recognition is mandatory in 2025. The California Attorney General and CPPA have confirmed that treating the GPC signal as a valid consumer request to opt-out of sale/sharing is required by law. Failure to honor this signal is a primary trigger for enforcement actions.

// gpc-detection-service.ts
// Comprehensive GPC detection and handling

interface GPCState {
  detected: boolean;
  source: 'navigator' | 'header' | 'extension' | 'unknown';
  timestamp: string;
  honored: boolean;
}

interface GPCComplianceConfig {
  honorImmediately: boolean;
  logAllDetections: boolean;
  notifyBackend: boolean;
  applyToAllSessions: boolean;
}

class GPCDetectionService {
  private state: GPCState;
  private config: GPCComplianceConfig;
  private detectionListeners: Set<(state: GPCState) => void> = new Set();

  constructor(config: GPCComplianceConfig) {
    this.config = config;
    this.state = {
      detected: false,
      source: 'unknown',
      timestamp: new Date().toISOString(),
      honored: false
    };
  }

  // Must be called before ANY data collection
  async detectGPC(): Promise<GPCState> {
    // Method 1: Navigator API (most common)
    const navigatorGPC = this.detectNavigatorGPC();

    // Method 2: Check for GPC header (server-side indication)
    const headerGPC = await this.checkGPCHeader();

    // Method 3: Check for known privacy extensions
    const extensionGPC = this.detectPrivacyExtensions();

    // Any positive signal means opt-out
    const detected = navigatorGPC || headerGPC || extensionGPC;

    this.state = {
      detected,
      source: navigatorGPC ? 'navigator' :
              headerGPC ? 'header' :
              extensionGPC ? 'extension' : 'unknown',
      timestamp: new Date().toISOString(),
      honored: false
    };

    if (detected && this.config.honorImmediately) {
      await this.honorGPCSignal();
    }

    if (this.config.logAllDetections) {
      this.logDetection();
    }

    return this.state;
  }

  private detectNavigatorGPC(): boolean {
    if (typeof navigator === 'undefined') return false;

    // Standard GPC API
    const nav = navigator as any;

    // Check globalPrivacyControl property
    if (nav.globalPrivacyControl === true) {
      return true;
    }

    // Some browsers use different property names
    if (nav.doNotTrack === '1' || nav.doNotTrack === 'yes') {
      // Note: DNT is deprecated but some treat it as GPC equivalent
      // CPPA guidance suggests honoring DNT as well
      return true;
    }

    return false;
  }

  private async checkGPCHeader(): Promise<boolean> {
    // Server should pass GPC header status to client
    // This catches cases where extension sets header but not navigator
    try {
      const response = await fetch('/api/v1/privacy/gpc-status', {
        method: 'HEAD',
        credentials: 'same-origin'
      });

      return response.headers.get('X-GPC-Detected') === 'true';
    } catch {
      return false;
    }
  }

  private detectPrivacyExtensions(): boolean {
    // Detect common privacy extensions that set GPC
    // These may not always set navigator.globalPrivacyControl

    if (typeof document === 'undefined') return false;

    // Check for Privacy Badger
    const privacyBadger = document.documentElement.getAttribute('data-pb-enabled');
    if (privacyBadger === 'true') return true;

    // Check for DuckDuckGo Privacy Essentials
    const ddg = (window as any).__DDG_EXTENSION__;
    if (ddg && ddg.gpc) return true;

    // Check for Brave browser
    const brave = (navigator as any).brave;
    if (brave && typeof brave.isBrave === 'function') {
      // Brave has GPC enabled by default
      return true;
    }

    return false;
  }

  async honorGPCSignal(): Promise<void> {
    if (!this.state.detected) return;

    console.log('[GPC] Honoring Global Privacy Control signal');

    // 1. Disable sale of personal information
    this.disableDataSale();

    // 2. Disable sharing for cross-context behavioral advertising
    this.disableDataSharing();

    // 3. Update consent state
    this.updateConsentForGPC();

    // 4. Notify backend
    if (this.config.notifyBackend) {
      await this.notifyBackendOfGPC();
    }

    // 5. Mark as honored
    this.state.honored = true;

    // 6. Dispatch event for other components
    this.dispatchGPCEvent();

    // 7. Notify listeners
    this.notifyListeners();
  }

  private disableDataSale(): void {
    // Set consent mode to deny sale
    if (typeof gtag !== 'undefined') {
      gtag('consent', 'update', {
        'ad_storage': 'denied',
        'ad_user_data': 'denied',
        'ad_personalization': 'denied'
      });
    }

    // Disable any sale-related processing
    window.dispatchEvent(new CustomEvent('privacy:disableSale'));
  }

  private disableDataSharing(): void {
    // Stop cross-context behavioral advertising
    const sharingCategories = [
      'advertising',
      'cross_site_tracking',
      'remarketing',
      'audience_building'
    ];

    sharingCategories.forEach(category => {
      window.dispatchEvent(new CustomEvent('privacy:disableCategory', {
        detail: { category }
      }));
    });
  }

  private updateConsentForGPC(): void {
    const gpcConsent = {
      necessary: true,        // Always allowed
      functional: true,       // Usually allowed
      analytics: true,        // First-party analytics OK
      advertising: false,     // Disabled by GPC
      dataSale: false,        // Disabled by GPC
      dataSharing: false,     // Disabled by GPC
      gpcApplied: true,
      timestamp: new Date().toISOString()
    };

    localStorage.setItem('consent_preferences', JSON.stringify(gpcConsent));

    // Also set a specific GPC flag
    localStorage.setItem('gpc_honored', JSON.stringify({
      honored: true,
      timestamp: this.state.timestamp,
      source: this.state.source
    }));
  }

  private async notifyBackendOfGPC(): Promise<void> {
    try {
      await fetch('/api/v1/consent/gpc', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          gpcDetected: true,
          source: this.state.source,
          timestamp: this.state.timestamp,
          userAgent: navigator.userAgent,
          sessionId: this.getSessionId()
        })
      });
    } catch (error) {
      console.error('[GPC] Failed to notify backend:', error);
    }
  }

  private dispatchGPCEvent(): void {
    window.dispatchEvent(new CustomEvent('gpc:honored', {
      detail: {
        source: this.state.source,
        timestamp: this.state.timestamp
      }
    }));
  }

  private logDetection(): void {
    const log = {
      event: 'gpc_detection',
      detected: this.state.detected,
      source: this.state.source,
      timestamp: this.state.timestamp,
      honored: this.state.honored,
      url: window.location.href
    };

    // Store locally for audit
    const logs = JSON.parse(localStorage.getItem('gpc_detection_logs') || '[]');
    logs.push(log);

    // Keep last 100 logs
    if (logs.length > 100) {
      logs.shift();
    }

    localStorage.setItem('gpc_detection_logs', JSON.stringify(logs));
  }

  private getSessionId(): string {
    let sessionId = sessionStorage.getItem('privacy_session_id');
    if (!sessionId) {
      sessionId = crypto.randomUUID();
      sessionStorage.setItem('privacy_session_id', sessionId);
    }
    return sessionId;
  }

  // Subscribe to GPC state changes
  onGPCChange(listener: (state: GPCState) => void): () => void {
    this.detectionListeners.add(listener);
    return () => this.detectionListeners.delete(listener);
  }

  private notifyListeners(): void {
    this.detectionListeners.forEach(listener => {
      try {
        listener(this.state);
      } catch (error) {
        console.error('[GPC] Listener error:', error);
      }
    });
  }

  getState(): GPCState {
    return { ...this.state };
  }

  // Check if GPC should apply to this user/session
  shouldApplyGPC(): boolean {
    if (this.state.detected) return true;

    // Check if previously detected in this session
    if (this.config.applyToAllSessions) {
      const stored = localStorage.getItem('gpc_honored');
      if (stored) {
        const { honored } = JSON.parse(stored);
        return honored;
      }
    }

    return false;
  }
}

// Initialize GPC detection on page load
const initializeGPCDetection = async (): Promise<void> => {
  const gpcService = new GPCDetectionService({
    honorImmediately: true,
    logAllDetections: true,
    notifyBackend: true,
    applyToAllSessions: true
  });

  // Detect before any scripts load
  await gpcService.detectGPC();

  // Make available globally for other scripts
  (window as any).__GPCService__ = gpcService;
};

// Run immediately
if (typeof document !== 'undefined') {
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initializeGPCDetection);
  } else {
    initializeGPCDetection();
  }
}

export { GPCDetectionService, GPCState, GPCComplianceConfig };

Priority 3: Data Minimization Enforcement

CPRA requires businesses to collect only the personal information that is "reasonably necessary and proportionate" to achieve the purposes disclosed to consumers.

// data-minimization-service.ts
// Enforce CPRA data minimization requirements

interface DataField {
  name: string;
  category: 'necessary' | 'functional' | 'analytics' | 'advertising';
  purpose: string;
  retentionDays: number;
  sensitivePI: boolean;
}

interface CollectionPurpose {
  id: string;
  name: string;
  description: string;
  legalBasis: 'consent' | 'contract' | 'legitimate_interest';
  requiredFields: string[];
  optionalFields: string[];
}

interface MinimizationConfig {
  purposes: CollectionPurpose[];
  dataFields: DataField[];
  strictMode: boolean;
  auditLogging: boolean;
}

class DataMinimizationService {
  private config: MinimizationConfig;
  private collectionLog: CollectionEvent[] = [];

  constructor(config: MinimizationConfig) {
    this.config = config;
    this.validateConfig();
  }

  private validateConfig(): void {
    // Ensure all required fields are mapped to purposes
    this.config.purposes.forEach(purpose => {
      purpose.requiredFields.forEach(field => {
        const fieldDef = this.config.dataFields.find(f => f.name === field);
        if (!fieldDef) {
          console.warn(`[Minimization] Field "${field}" required by purpose "${purpose.id}" is not defined`);
        }
      });
    });
  }

  // Check if data collection is allowed for given purpose
  canCollect(fieldName: string, purposeId: string): CollectionDecision {
    const purpose = this.config.purposes.find(p => p.id === purposeId);
    const field = this.config.dataFields.find(f => f.name === fieldName);

    if (!purpose) {
      return {
        allowed: false,
        reason: `Unknown purpose: ${purposeId}`,
        requiresConsent: false
      };
    }

    if (!field) {
      return {
        allowed: false,
        reason: `Unknown field: ${fieldName}`,
        requiresConsent: false
      };
    }

    // Check if field is necessary for purpose
    const isRequired = purpose.requiredFields.includes(fieldName);
    const isOptional = purpose.optionalFields.includes(fieldName);

    if (!isRequired && !isOptional) {
      return {
        allowed: false,
        reason: `Field "${fieldName}" is not necessary for purpose "${purposeId}" `,
        requiresConsent: false,
        minimizationViolation: true
      };
    }

    // Check consent requirements
    const requiresConsent = field.sensitivePI ||
                           field.category === 'advertising' ||
                           purpose.legalBasis === 'consent';

    return {
      allowed: true,
      reason: isRequired ? 'Required for purpose' : 'Optional for purpose',
      requiresConsent,
      sensitivePI: field.sensitivePI,
      retentionDays: field.retentionDays
    };
  }

  // Validate a data collection request
  validateCollection(data: Record<string, any>, purposeId: string): ValidationResult {
    const issues: ValidationIssue[] = [];
    const allowedFields: string[] = [];
    const blockedFields: string[] = [];

    Object.keys(data).forEach(fieldName => {
      const decision = this.canCollect(fieldName, purposeId);

      if (decision.allowed) {
        allowedFields.push(fieldName);
      } else {
        blockedFields.push(fieldName);
        issues.push({
          field: fieldName,
          reason: decision.reason,
          severity: decision.minimizationViolation ? 'error' : 'warning'
        });
      }
    });

    // Log the collection attempt
    if (this.config.auditLogging) {
      this.logCollectionAttempt({
        purposeId,
        requestedFields: Object.keys(data),
        allowedFields,
        blockedFields,
        timestamp: new Date().toISOString()
      });
    }

    return {
      valid: issues.filter(i => i.severity === 'error').length === 0,
      issues,
      allowedData: this.filterToAllowed(data, allowedFields),
      blockedFields
    };
  }

  // Filter data to only allowed fields
  private filterToAllowed(data: Record<string, any>, allowedFields: string[]): Record<string, any> {
    const filtered: Record<string, any> = {};
    allowedFields.forEach(field => {
      if (data[field] !== undefined) {
        filtered[field] = data[field];
      }
    });
    return filtered;
  }

  // Sanitize data before collection (remove unnecessary fields)
  sanitizeForCollection(data: Record<string, any>, purposeId: string): Record<string, any> {
    const validation = this.validateCollection(data, purposeId);

    if (!validation.valid && this.config.strictMode) {
      throw new Error(`Data minimization violation: ${validation.issues.map(i => i.reason).join(', ')}`);
    }

    return validation.allowedData;
  }

  // Check retention requirements
  checkRetention(fieldName: string): RetentionInfo {
    const field = this.config.dataFields.find(f => f.name === fieldName);

    if (!field) {
      return {
        found: false,
        retentionDays: 0,
        deleteAfter: new Date()
      };
    }

    const deleteAfter = new Date();
    deleteAfter.setDate(deleteAfter.getDate() + field.retentionDays);

    return {
      found: true,
      retentionDays: field.retentionDays,
      deleteAfter,
      category: field.category,
      sensitivePI: field.sensitivePI
    };
  }

  // Generate retention schedule for all data
  generateRetentionSchedule(): RetentionSchedule[] {
    return this.config.dataFields.map(field => {
      const deleteAfter = new Date();
      deleteAfter.setDate(deleteAfter.getDate() + field.retentionDays);

      return {
        fieldName: field.name,
        category: field.category,
        retentionDays: field.retentionDays,
        deleteAfter: deleteAfter.toISOString(),
        sensitivePI: field.sensitivePI,
        purpose: field.purpose
      };
    }).sort((a, b) => a.retentionDays - b.retentionDays);
  }

  // Log collection attempts for audit
  private logCollectionAttempt(event: CollectionEvent): void {
    this.collectionLog.push(event);

    // Also send to server for permanent audit trail
    this.sendAuditLog(event);
  }

  private async sendAuditLog(event: CollectionEvent): Promise<void> {
    try {
      await fetch('/api/v1/audit/data-collection', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(event)
      });
    } catch (error) {
      console.error('[Minimization] Failed to send audit log:', error);
    }
  }

  // Get audit log
  getCollectionLog(): CollectionEvent[] {
    return [...this.collectionLog];
  }

  // Generate compliance report
  generateComplianceReport(): MinimizationReport {
    const totalAttempts = this.collectionLog.length;
    const violations = this.collectionLog.filter(e => e.blockedFields.length > 0);

    const fieldViolationCounts: Record<string, number> = {};
    violations.forEach(v => {
      v.blockedFields.forEach(field => {
        fieldViolationCounts[field] = (fieldViolationCounts[field] || 0) + 1;
      });
    });

    return {
      period: {
        start: this.collectionLog[0]?.timestamp || new Date().toISOString(),
        end: new Date().toISOString()
      },
      totalCollectionAttempts: totalAttempts,
      violationCount: violations.length,
      violationRate: totalAttempts > 0 ? violations.length / totalAttempts : 0,
      topViolatedFields: Object.entries(fieldViolationCounts)
        .sort((a, b) => b[1] - a[1])
        .slice(0, 10)
        .map(([field, count]) => ({ field, count })),
      recommendations: this.generateRecommendations(fieldViolationCounts)
    };
  }

  private generateRecommendations(violations: Record<string, number>): string[] {
    const recommendations: string[] = [];

    if (Object.keys(violations).length > 0) {
      recommendations.push('Review and remove collection of frequently blocked fields');
    }

    // Check for sensitive PI violations
    const sensitiveViolations = Object.keys(violations).filter(field => {
      const fieldDef = this.config.dataFields.find(f => f.name === field);
      return fieldDef?.sensitivePI;
    });

    if (sensitiveViolations.length > 0) {
      recommendations.push(`Urgent: Review sensitive PI collection for fields: ${sensitiveViolations.join(', ')}`);
    }

    return recommendations;
  }
}

interface CollectionDecision {
  allowed: boolean;
  reason: string;
  requiresConsent: boolean;
  minimizationViolation?: boolean;
  sensitivePI?: boolean;
  retentionDays?: number;
}

interface ValidationIssue {
  field: string;
  reason: string;
  severity: 'error' | 'warning';
}

interface ValidationResult {
  valid: boolean;
  issues: ValidationIssue[];
  allowedData: Record<string, any>;
  blockedFields: string[];
}

interface RetentionInfo {
  found: boolean;
  retentionDays: number;
  deleteAfter: Date;
  category?: string;
  sensitivePI?: boolean;
}

interface RetentionSchedule {
  fieldName: string;
  category: string;
  retentionDays: number;
  deleteAfter: string;
  sensitivePI: boolean;
  purpose: string;
}

interface CollectionEvent {
  purposeId: string;
  requestedFields: string[];
  allowedFields: string[];
  blockedFields: string[];
  timestamp: string;
}

interface MinimizationReport {
  period: { start: string; end: string };
  totalCollectionAttempts: number;
  violationCount: number;
  violationRate: number;
  topViolatedFields: { field: string; count: number }[];
  recommendations: string[];
}

export { DataMinimizationService, MinimizationConfig, DataField, CollectionPurpose };

Priority 4: Service Provider Contracts

CPRA requires updated contracts with all service providers that process California consumer data.

// service-provider-contract-tracker.ts
// Track and validate CPRA service provider contract requirements

interface ServiceProvider {
  id: string;
  name: string;
  type: 'service_provider' | 'contractor' | 'third_party';
  dataCategories: string[];
  purposes: string[];
  contractDate: string;
  contractExpiry: string;
  cpraCompliant: boolean;
  lastAuditDate?: string;
}

interface ContractRequirement {
  id: string;
  description: string;
  required: boolean;
  cpraSection: string;
}

interface ContractAudit {
  providerId: string;
  auditDate: string;
  requirements: ContractRequirementStatus[];
  overallCompliant: boolean;
  findings: string[];
  nextAuditDate: string;
}

interface ContractRequirementStatus {
  requirementId: string;
  status: 'met' | 'not_met' | 'partial' | 'not_applicable';
  notes?: string;
}

class ServiceProviderContractTracker {
  private providers: Map<string, ServiceProvider> = new Map();
  private audits: Map<string, ContractAudit[]> = new Map();

  // CPRA required contract provisions
  private readonly cpraRequirements: ContractRequirement[] = [
    {
      id: 'purpose_limitation',
      description: 'Contract specifies and limits processing purposes',
      required: true,
      cpraSection: '1798.100(d)(1)'
    },
    {
      id: 'no_selling',
      description: 'Prohibition on selling personal information',
      required: true,
      cpraSection: '1798.100(d)(2)'
    },
    {
      id: 'no_sharing',
      description: 'Prohibition on sharing for cross-context behavioral advertising',
      required: true,
      cpraSection: '1798.100(d)(2)'
    },
    {
      id: 'no_retention_beyond_purpose',
      description: 'Cannot retain data longer than necessary for specified purpose',
      required: true,
      cpraSection: '1798.100(d)(3)'
    },
    {
      id: 'subcontractor_flow_down',
      description: 'Same obligations flow down to subcontractors',
      required: true,
      cpraSection: '1798.100(d)(4)'
    },
    {
      id: 'business_audit_rights',
      description: 'Business has right to audit service provider compliance',
      required: true,
      cpraSection: '1798.100(d)(5)'
    },
    {
      id: 'notify_inability_comply',
      description: 'Notify business if unable to meet CPRA obligations',
      required: true,
      cpraSection: '1798.100(d)(6)'
    },
    {
      id: 'consumer_rights_assistance',
      description: 'Enable business to respond to consumer rights requests',
      required: true,
      cpraSection: '1798.100(d)(7)'
    },
    {
      id: 'security_measures',
      description: 'Implement appropriate security measures',
      required: true,
      cpraSection: '1798.100(d)(8)'
    },
    {
      id: 'breach_notification',
      description: 'Notify business of data breaches affecting consumer data',
      required: true,
      cpraSection: '1798.100(d)(9)'
    }
  ];

  addProvider(provider: ServiceProvider): void {
    this.providers.set(provider.id, provider);
    this.audits.set(provider.id, []);
  }

  conductAudit(providerId: string, requirementStatuses: ContractRequirementStatus[]): ContractAudit {
    const provider = this.providers.get(providerId);
    if (!provider) {
      throw new Error(`Provider not found: ${providerId}`);
    }

    // Check all required requirements are addressed
    const requiredReqs = this.cpraRequirements.filter(r => r.required);
    const missingRequirements = requiredReqs.filter(req =>
      !requirementStatuses.find(s => s.requirementId === req.id)
    );

    if (missingRequirements.length > 0) {
      throw new Error(`Missing requirement statuses: ${missingRequirements.map(r => r.id).join(', ')}`);
    }

    // Determine overall compliance
    const nonCompliantRequired = requirementStatuses.filter(s => {
      const req = this.cpraRequirements.find(r => r.id === s.requirementId);
      return req?.required && s.status === 'not_met';
    });

    const overallCompliant = nonCompliantRequired.length === 0;

    // Generate findings
    const findings: string[] = [];
    requirementStatuses.forEach(status => {
      if (status.status === 'not_met') {
        const req = this.cpraRequirements.find(r => r.id === status.requirementId);
        findings.push(`${req?.description || status.requirementId}: Not met${status.notes ? ` - ${status.notes}` : ''}`);
      } else if (status.status === 'partial') {
        const req = this.cpraRequirements.find(r => r.id === status.requirementId);
        findings.push(`${req?.description || status.requirementId}: Partially met${status.notes ? ` - ${status.notes}` : ''}`);
      }
    });

    // Calculate next audit date (annual minimum, 6 months if issues found)
    const nextAuditDate = new Date();
    nextAuditDate.setMonth(nextAuditDate.getMonth() + (overallCompliant ? 12 : 6));

    const audit: ContractAudit = {
      providerId,
      auditDate: new Date().toISOString(),
      requirements: requirementStatuses,
      overallCompliant,
      findings,
      nextAuditDate: nextAuditDate.toISOString()
    };

    // Store audit
    const providerAudits = this.audits.get(providerId) || [];
    providerAudits.push(audit);
    this.audits.set(providerId, providerAudits);

    // Update provider compliance status
    provider.cpraCompliant = overallCompliant;
    provider.lastAuditDate = audit.auditDate;
    this.providers.set(providerId, provider);

    return audit;
  }

  getProviderStatus(providerId: string): ProviderComplianceStatus {
    const provider = this.providers.get(providerId);
    if (!provider) {
      throw new Error(`Provider not found: ${providerId}`);
    }

    const audits = this.audits.get(providerId) || [];
    const latestAudit = audits[audits.length - 1];

    const isContractExpired = new Date(provider.contractExpiry) < new Date();
    const needsAudit = !latestAudit || new Date(latestAudit.nextAuditDate) < new Date();

    return {
      provider,
      latestAudit,
      isContractExpired,
      needsAudit,
      riskLevel: this.calculateRiskLevel(provider, latestAudit, isContractExpired, needsAudit)
    };
  }

  private calculateRiskLevel(
    provider: ServiceProvider,
    latestAudit: ContractAudit | undefined,
    isContractExpired: boolean,
    needsAudit: boolean
  ): 'low' | 'medium' | 'high' | 'critical' {
    if (isContractExpired) return 'critical';
    if (!latestAudit) return 'high';
    if (!latestAudit.overallCompliant) return 'high';
    if (needsAudit) return 'medium';
    if (latestAudit.findings.length > 0) return 'medium';
    return 'low';
  }

  getAllProvidersStatus(): ProviderComplianceStatus[] {
    const statuses: ProviderComplianceStatus[] = [];
    this.providers.forEach((_, providerId) => {
      statuses.push(this.getProviderStatus(providerId));
    });
    return statuses.sort((a, b) => {
      const riskOrder = { critical: 0, high: 1, medium: 2, low: 3 };
      return riskOrder[a.riskLevel] - riskOrder[b.riskLevel];
    });
  }

  generateComplianceReport(): ServiceProviderReport {
    const allStatuses = this.getAllProvidersStatus();

    const byRiskLevel = {
      critical: allStatuses.filter(s => s.riskLevel === 'critical'),
      high: allStatuses.filter(s => s.riskLevel === 'high'),
      medium: allStatuses.filter(s => s.riskLevel === 'medium'),
      low: allStatuses.filter(s => s.riskLevel === 'low')
    };

    const expiredContracts = allStatuses.filter(s => s.isContractExpired);
    const needingAudit = allStatuses.filter(s => s.needsAudit);
    const nonCompliant = allStatuses.filter(s => !s.provider.cpraCompliant);

    return {
      generatedAt: new Date().toISOString(),
      totalProviders: allStatuses.length,
      complianceRate: allStatuses.filter(s => s.provider.cpraCompliant).length / allStatuses.length,
      byRiskLevel: {
        critical: byRiskLevel.critical.length,
        high: byRiskLevel.high.length,
        medium: byRiskLevel.medium.length,
        low: byRiskLevel.low.length
      },
      actionItems: [
        ...expiredContracts.map(s => ({
          priority: 'critical' as const,
          action: `Renew contract with ${s.provider.name}`,
          providerId: s.provider.id
        })),
        ...nonCompliant.map(s => ({
          priority: 'high' as const,
          action: `Address compliance findings for ${s.provider.name}`,
          providerId: s.provider.id
        })),
        ...needingAudit.map(s => ({
          priority: 'medium' as const,
          action: `Conduct audit for ${s.provider.name}`,
          providerId: s.provider.id
        }))
      ],
      requirements: this.cpraRequirements
    };
  }

  exportForLegal(): string {
    const report = this.generateComplianceReport();
    const statuses = this.getAllProvidersStatus();

    let output = `# CPRA Service Provider Compliance Report\n`;
    output += `Generated: ${report.generatedAt}\n\n`;
    output += `## Summary\n`;
    output += `- Total Providers: ${report.totalProviders}\n`;
    output += `- Compliance Rate: ${(report.complianceRate * 100).toFixed(1)}%\n`;
    output += `- Critical Issues: ${report.byRiskLevel.critical}\n`;
    output += `- High Risk: ${report.byRiskLevel.high}\n\n`;

    output += `## Action Items\n`;
    report.actionItems.forEach(item => {
      output += `- [${item.priority.toUpperCase()}] ${item.action}\n`;
    });

    output += `\n## Provider Details\n`;
    statuses.forEach(status => {
      output += `\n### ${status.provider.name}\n`;
      output += `- Type: ${status.provider.type}\n`;
      output += `- Risk Level: ${status.riskLevel}\n`;
      output += `- Contract Expiry: ${status.provider.contractExpiry}\n`;
      output += `- Last Audit: ${status.provider.lastAuditDate || 'Never'}\n`;
      if (status.latestAudit?.findings.length) {
        output += `- Findings:\n`;
        status.latestAudit.findings.forEach(f => {
          output += `  - ${f}\n`;
        });
      }
    });

    return output;
  }
}

interface ProviderComplianceStatus {
  provider: ServiceProvider;
  latestAudit?: ContractAudit;
  isContractExpired: boolean;
  needsAudit: boolean;
  riskLevel: 'low' | 'medium' | 'high' | 'critical';
}

interface ServiceProviderReport {
  generatedAt: string;
  totalProviders: number;
  complianceRate: number;
  byRiskLevel: {
    critical: number;
    high: number;
    medium: number;
    low: number;
  };
  actionItems: {
    priority: 'critical' | 'high' | 'medium' | 'low';
    action: string;
    providerId: string;
  }[];
  requirements: ContractRequirement[];
}

export { ServiceProviderContractTracker, ServiceProvider, ContractAudit };

Sensitive Personal Information Categories

CPRA created a new category of "sensitive personal information" (SPI) with enhanced protections. Businesses must provide consumers with the right to limit the use of their SPI.

// sensitive-pi-handler.ts
// Handle CPRA Sensitive Personal Information requirements

type SensitivePICategory =
  | 'government_id'
  | 'financial_account'
  | 'precise_geolocation'
  | 'race_ethnicity'
  | 'religion'
  | 'union_membership'
  | 'genetic_data'
  | 'biometric_data'
  | 'health_data'
  | 'sex_life_orientation'
  | 'mail_email_text_content';

interface SensitivePIConfig {
  category: SensitivePICategory;
  displayName: string;
  description: string;
  examples: string[];
  collectionPurposes: string[];
  retentionDays: number;
  requiresExplicitConsent: boolean;
  canBeUsedForAdvertising: boolean;
}

interface SensitivePIConsent {
  category: SensitivePICategory;
  consented: boolean;
  purpose: string;
  timestamp: string;
  expiresAt?: string;
}

class SensitivePIHandler {
  private configs: Map<SensitivePICategory, SensitivePIConfig>;
  private consents: Map<string, SensitivePIConsent[]> = new Map();

  constructor() {
    this.configs = new Map([
      ['government_id', {
        category: 'government_id',
        displayName: 'Government Identifiers',
        description: 'Social Security numbers, driver\'s license numbers, state ID numbers, passport numbers',
        examples: ['SSN', 'Driver\'s License', 'State ID', 'Passport Number'],
        collectionPurposes: ['identity_verification', 'legal_compliance'],
        retentionDays: 365,
        requiresExplicitConsent: true,
        canBeUsedForAdvertising: false
      }],
      ['financial_account', {
        category: 'financial_account',
        displayName: 'Financial Information',
        description: 'Account numbers, debit/credit card numbers with security codes, account credentials',
        examples: ['Bank Account Numbers', 'Credit Card Numbers', 'Financial Credentials'],
        collectionPurposes: ['payment_processing', 'fraud_prevention'],
        retentionDays: 180,
        requiresExplicitConsent: true,
        canBeUsedForAdvertising: false
      }],
      ['precise_geolocation', {
        category: 'precise_geolocation',
        displayName: 'Precise Geolocation',
        description: 'Geographic location data that can identify a consumer within a geographic area with radius of 1,850 feet or less',
        examples: ['GPS Coordinates', 'Precise Location Data'],
        collectionPurposes: ['location_services', 'delivery', 'emergency_services'],
        retentionDays: 30,
        requiresExplicitConsent: true,
        canBeUsedForAdvertising: false
      }],
      ['race_ethnicity', {
        category: 'race_ethnicity',
        displayName: 'Racial or Ethnic Origin',
        description: 'Information revealing racial or ethnic origin',
        examples: ['Race', 'Ethnicity', 'National Origin'],
        collectionPurposes: ['diversity_reporting', 'equal_opportunity'],
        retentionDays: 365,
        requiresExplicitConsent: true,
        canBeUsedForAdvertising: false
      }],
      ['religion', {
        category: 'religion',
        displayName: 'Religious Beliefs',
        description: 'Information revealing religious or philosophical beliefs',
        examples: ['Religion', 'Religious Affiliation', 'Philosophical Beliefs'],
        collectionPurposes: ['accommodation_requests', 'diversity_reporting'],
        retentionDays: 365,
        requiresExplicitConsent: true,
        canBeUsedForAdvertising: false
      }],
      ['union_membership', {
        category: 'union_membership',
        displayName: 'Union Membership',
        description: 'Information about union membership',
        examples: ['Union Affiliation', 'Collective Bargaining Status'],
        collectionPurposes: ['employment_records', 'benefits_administration'],
        retentionDays: 365,
        requiresExplicitConsent: true,
        canBeUsedForAdvertising: false
      }],
      ['genetic_data', {
        category: 'genetic_data',
        displayName: 'Genetic Data',
        description: 'Genetic information',
        examples: ['DNA Test Results', 'Genetic Markers', 'Hereditary Information'],
        collectionPurposes: ['medical_treatment', 'research_with_consent'],
        retentionDays: 365,
        requiresExplicitConsent: true,
        canBeUsedForAdvertising: false
      }],
      ['biometric_data', {
        category: 'biometric_data',
        displayName: 'Biometric Information',
        description: 'Unique biometric data used for identification purposes',
        examples: ['Fingerprints', 'Face Geometry', 'Voiceprints', 'Retina Scans'],
        collectionPurposes: ['authentication', 'security_access'],
        retentionDays: 365,
        requiresExplicitConsent: true,
        canBeUsedForAdvertising: false
      }],
      ['health_data', {
        category: 'health_data',
        displayName: 'Health Information',
        description: 'Information concerning a consumer\'s health',
        examples: ['Medical Conditions', 'Treatments', 'Health Records'],
        collectionPurposes: ['medical_treatment', 'insurance', 'wellness_programs'],
        retentionDays: 365,
        requiresExplicitConsent: true,
        canBeUsedForAdvertising: false
      }],
      ['sex_life_orientation', {
        category: 'sex_life_orientation',
        displayName: 'Sex Life or Sexual Orientation',
        description: 'Information about sex life or sexual orientation',
        examples: ['Sexual Orientation', 'Gender Identity'],
        collectionPurposes: ['diversity_reporting', 'accommodation_requests'],
        retentionDays: 365,
        requiresExplicitConsent: true,
        canBeUsedForAdvertising: false
      }],
      ['mail_email_text_content', {
        category: 'mail_email_text_content',
        displayName: 'Private Communications',
        description: 'Contents of mail, email, and text messages (unless business is intended recipient)',
        examples: ['Email Content', 'Text Message Content', 'Private Messages'],
        collectionPurposes: ['communication_services', 'customer_support'],
        retentionDays: 90,
        requiresExplicitConsent: true,
        canBeUsedForAdvertising: false
      }]
    ]);
  }

  // Check if data is sensitive PI
  classifyData(fieldName: string, value: any): SensitivePICategory | null {
    const fieldLower = fieldName.toLowerCase();

    // Government ID detection
    if (fieldLower.includes('ssn') || fieldLower.includes('social_security') ||
        fieldLower.includes('drivers_license') || fieldLower.includes('passport')) {
      return 'government_id';
    }

    // Financial data detection
    if (fieldLower.includes('card_number') || fieldLower.includes('account_number') ||
        fieldLower.includes('cvv') || fieldLower.includes('routing_number')) {
      return 'financial_account';
    }

    // Geolocation detection
    if (fieldLower.includes('latitude') || fieldLower.includes('longitude') ||
        fieldLower.includes('gps') || fieldLower.includes('precise_location')) {
      // Check if precision indicates "precise" (within 1850 feet)
      if (this.isPreciseLocation(value)) {
        return 'precise_geolocation';
      }
    }

    // Health data detection
    if (fieldLower.includes('diagnosis') || fieldLower.includes('medical') ||
        fieldLower.includes('health_condition') || fieldLower.includes('treatment')) {
      return 'health_data';
    }

    // Biometric detection
    if (fieldLower.includes('fingerprint') || fieldLower.includes('face_scan') ||
        fieldLower.includes('voiceprint') || fieldLower.includes('retina')) {
      return 'biometric_data';
    }

    // Race/ethnicity detection
    if (fieldLower.includes('race') || fieldLower.includes('ethnicity') ||
        fieldLower.includes('national_origin')) {
      return 'race_ethnicity';
    }

    return null;
  }

  private isPreciseLocation(value: any): boolean {
    // If lat/lng with more than 3 decimal places, it's "precise"
    // 3 decimal places = ~111m precision (within 1850 feet threshold)
    if (typeof value === 'object' && value.latitude && value.longitude) {
      const latDecimals = (value.latitude.toString().split('.')[1] || '').length;
      const lngDecimals = (value.longitude.toString().split('.')[1] || '').length;
      return latDecimals > 3 || lngDecimals > 3;
    }
    return false;
  }

  // Record consent for sensitive PI
  recordConsent(userId: string, consent: SensitivePIConsent): void {
    const userConsents = this.consents.get(userId) || [];

    // Remove any existing consent for same category/purpose
    const filtered = userConsents.filter(c =>
      !(c.category === consent.category && c.purpose === consent.purpose)
    );

    filtered.push(consent);
    this.consents.set(userId, filtered);
  }

  // Check if user has consented to sensitive PI collection
  hasConsent(userId: string, category: SensitivePICategory, purpose: string): boolean {
    const userConsents = this.consents.get(userId) || [];
    const consent = userConsents.find(c =>
      c.category === category &&
      c.purpose === purpose &&
      c.consented === true
    );

    if (!consent) return false;

    // Check if expired
    if (consent.expiresAt && new Date(consent.expiresAt) < new Date()) {
      return false;
    }

    return true;
  }

  // Get all sensitive PI categories
  getAllCategories(): SensitivePIConfig[] {
    return Array.from(this.configs.values());
  }

  // Generate disclosure text for privacy notice
  generateDisclosure(): string {
    let disclosure = '## Sensitive Personal Information We Collect\n\n';
    disclosure += 'We may collect the following categories of sensitive personal information:\n\n';

    this.configs.forEach(config => {
      disclosure += `### ${config.displayName}\n`;
      disclosure += `${config.description}\n\n`;
      disclosure += `**Examples**: ${config.examples.join(', ')}\n\n`;
      disclosure += `**Purposes**: ${config.collectionPurposes.join(', ')}\n\n`;
      disclosure += `**Retention Period**: ${config.retentionDays} days\n\n`;
    });

    disclosure += '\n## Your Rights Regarding Sensitive Personal Information\n\n';
    disclosure += 'You have the right to limit the use and disclosure of your sensitive personal information. ';
    disclosure += 'To exercise this right, click the "Limit Use of My Sensitive Personal Information" link ';
    disclosure += 'in the footer of our website or contact us using the information provided in this policy.\n';

    return disclosure;
  }

  // Validate data collection request
  validateCollection(userId: string, data: Record<string, any>): SPIValidationResult {
    const sensitiveFields: { field: string; category: SensitivePICategory }[] = [];
    const blockedFields: { field: string; category: SensitivePICategory; reason: string }[] = [];
    const allowedFields: string[] = [];

    Object.entries(data).forEach(([field, value]) => {
      const category = this.classifyData(field, value);

      if (category) {
        sensitiveFields.push({ field, category });
        const config = this.configs.get(category)!;

        // Check if any valid purpose has consent
        const hasValidConsent = config.collectionPurposes.some(purpose =>
          this.hasConsent(userId, category, purpose)
        );

        if (!hasValidConsent && config.requiresExplicitConsent) {
          blockedFields.push({
            field,
            category,
            reason: `Explicit consent required for ${config.displayName}`
          });
        } else {
          allowedFields.push(field);
        }
      } else {
        allowedFields.push(field);
      }
    });

    return {
      valid: blockedFields.length === 0,
      sensitiveFields,
      blockedFields,
      allowedFields,
      requiresConsentPrompt: blockedFields.length > 0
    };
  }

  // Generate consent request for blocked fields
  generateConsentRequest(blockedFields: { field: string; category: SensitivePICategory }[]): ConsentRequest {
    const categories = [...new Set(blockedFields.map(f => f.category))];

    return {
      id: crypto.randomUUID(),
      type: 'sensitive_pi',
      categories: categories.map(cat => {
        const config = this.configs.get(cat)!;
        return {
          category: cat,
          displayName: config.displayName,
          description: config.description,
          purposes: config.collectionPurposes
        };
      }),
      createdAt: new Date().toISOString()
    };
  }
}

interface SPIValidationResult {
  valid: boolean;
  sensitiveFields: { field: string; category: SensitivePICategory }[];
  blockedFields: { field: string; category: SensitivePICategory; reason: string }[];
  allowedFields: string[];
  requiresConsentPrompt: boolean;
}

interface ConsentRequest {
  id: string;
  type: string;
  categories: {
    category: SensitivePICategory;
    displayName: string;
    description: string;
    purposes: string[];
  }[];
  createdAt: string;
}

export { SensitivePIHandler, SensitivePICategory, SensitivePIConfig };

CPRA Compliance Checklist Implementation

Here's a comprehensive checklist system for tracking CPRA compliance:

// cpra-compliance-checklist.ts
// Complete CPRA compliance tracking system

interface ChecklistItem {
  id: string;
  category: string;
  requirement: string;
  cpraSection: string;
  status: 'not_started' | 'in_progress' | 'completed' | 'not_applicable';
  notes: string;
  dueDate?: string;
  completedDate?: string;
  assignee?: string;
  evidence?: string[];
}

interface ComplianceCategory {
  name: string;
  description: string;
  items: ChecklistItem[];
}

class CPRAComplianceChecklist {
  private categories: ComplianceCategory[] = [
    {
      name: 'Privacy Notice Updates',
      description: 'Required disclosures in privacy policy',
      items: [
        {
          id: 'pn_1',
          category: 'Privacy Notice Updates',
          requirement: 'Include all required disclosures per 1798.100',
          cpraSection: '1798.100(a)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'pn_2',
          category: 'Privacy Notice Updates',
          requirement: 'List categories of PI collected in preceding 12 months',
          cpraSection: '1798.110(c)(1)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'pn_3',
          category: 'Privacy Notice Updates',
          requirement: 'Describe business purposes for each category',
          cpraSection: '1798.110(c)(2)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'pn_4',
          category: 'Privacy Notice Updates',
          requirement: 'List categories of sensitive PI collected',
          cpraSection: '1798.121',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'pn_5',
          category: 'Privacy Notice Updates',
          requirement: 'Explain consumer rights and how to exercise them',
          cpraSection: '1798.135',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'pn_6',
          category: 'Privacy Notice Updates',
          requirement: 'Provide opt-out links (Do Not Sell/Share)',
          cpraSection: '1798.135(a)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'pn_7',
          category: 'Privacy Notice Updates',
          requirement: 'Include retention periods for each PI category',
          cpraSection: '1798.100(a)(3)',
          status: 'not_started',
          notes: ''
        }
      ]
    },
    {
      name: 'Consumer Rights',
      description: 'Mechanisms for handling consumer rights requests',
      items: [
        {
          id: 'cr_1',
          category: 'Consumer Rights',
          requirement: 'Right to Know - provide access mechanism',
          cpraSection: '1798.110',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'cr_2',
          category: 'Consumer Rights',
          requirement: 'Right to Delete - implement deletion process',
          cpraSection: '1798.105',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'cr_3',
          category: 'Consumer Rights',
          requirement: 'Right to Correct - enable data correction',
          cpraSection: '1798.106',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'cr_4',
          category: 'Consumer Rights',
          requirement: 'Right to Opt-Out of Sale/Sharing',
          cpraSection: '1798.120',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'cr_5',
          category: 'Consumer Rights',
          requirement: 'Right to Limit Use of Sensitive PI',
          cpraSection: '1798.121',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'cr_6',
          category: 'Consumer Rights',
          requirement: 'Right to Data Portability',
          cpraSection: '1798.130',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'cr_7',
          category: 'Consumer Rights',
          requirement: 'Respond within 45 days (extendable to 90)',
          cpraSection: '1798.130(a)(2)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'cr_8',
          category: 'Consumer Rights',
          requirement: 'Verify consumer identity before fulfilling requests',
          cpraSection: '1798.140',
          status: 'not_started',
          notes: ''
        }
      ]
    },
    {
      name: 'Opt-Out Mechanisms',
      description: 'Technical implementations for opt-out rights',
      items: [
        {
          id: 'oo_1',
          category: 'Opt-Out Mechanisms',
          requirement: 'Implement GPC signal detection',
          cpraSection: '1798.135(e)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'oo_2',
          category: 'Opt-Out Mechanisms',
          requirement: 'Honor GPC as valid opt-out request',
          cpraSection: '1798.135(e)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'oo_3',
          category: 'Opt-Out Mechanisms',
          requirement: '"Do Not Sell or Share" link on homepage',
          cpraSection: '1798.135(a)(1)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'oo_4',
          category: 'Opt-Out Mechanisms',
          requirement: '"Limit Use of Sensitive PI" link if applicable',
          cpraSection: '1798.135(a)(2)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'oo_5',
          category: 'Opt-Out Mechanisms',
          requirement: 'No dark patterns in opt-out process',
          cpraSection: '1798.140(l)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'oo_6',
          category: 'Opt-Out Mechanisms',
          requirement: 'Opt-out must be as easy as opt-in',
          cpraSection: '1798.185(a)(4)(C)',
          status: 'not_started',
          notes: ''
        }
      ]
    },
    {
      name: 'Service Provider Contracts',
      description: 'Contractual requirements with vendors',
      items: [
        {
          id: 'sp_1',
          category: 'Service Provider Contracts',
          requirement: 'Written contracts with all service providers',
          cpraSection: '1798.100(d)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'sp_2',
          category: 'Service Provider Contracts',
          requirement: 'Contracts prohibit selling/sharing consumer PI',
          cpraSection: '1798.100(d)(2)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'sp_3',
          category: 'Service Provider Contracts',
          requirement: 'Contracts limit processing to specified purposes',
          cpraSection: '1798.100(d)(1)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'sp_4',
          category: 'Service Provider Contracts',
          requirement: 'Flow-down provisions to subcontractors',
          cpraSection: '1798.100(d)(4)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'sp_5',
          category: 'Service Provider Contracts',
          requirement: 'Audit rights included in contracts',
          cpraSection: '1798.100(d)(5)',
          status: 'not_started',
          notes: ''
        }
      ]
    },
    {
      name: 'Data Minimization',
      description: 'Collection and retention limitations',
      items: [
        {
          id: 'dm_1',
          category: 'Data Minimization',
          requirement: 'Collect only PI reasonably necessary for disclosed purposes',
          cpraSection: '1798.100(c)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'dm_2',
          category: 'Data Minimization',
          requirement: 'Retain PI only as long as necessary',
          cpraSection: '1798.100(a)(3)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'dm_3',
          category: 'Data Minimization',
          requirement: 'Document retention periods for each PI category',
          cpraSection: '1798.100(a)(3)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'dm_4',
          category: 'Data Minimization',
          requirement: 'Implement automated data deletion',
          cpraSection: '1798.100(c)',
          status: 'not_started',
          notes: ''
        }
      ]
    },
    {
      name: 'Security Measures',
      description: 'Data protection requirements',
      items: [
        {
          id: 'sec_1',
          category: 'Security Measures',
          requirement: 'Implement reasonable security procedures',
          cpraSection: '1798.100(e)',
          status: 'not_started',
          notes: ''
        },
        {
          id: 'sec_2',
          category: 'Security Measures',
          requirement: 'Conduct annual security assessments',
          cpraSection: '1798.185(a)(15)',
          status: 'not_started',
          notes: ''
        },

Veelgestelde vragen

Is GPC mandatory under CPRA?
Yes. The California Attorney General and the CPPA have both confirmed that treating the Global Privacy Control (GPC) signal as a valid consumer request to opt-out of sale/sharing is mandatory.
What qualifies as sensitive personal information in California?
Sensitive PI includes social security numbers, financial account logins, precise geolocation, racial/ethnic origin, religious beliefs, genetic data, biometric data, health data, and sexual orientation.
G

Het GetCookies-team

Geschreven en bijgehouden door het GetCookies-team van Getia AS in Oslo, Noorwegen. Dit artikel is algemene informatie over cookietoestemming, geen juridisch advies.

Scan je eigen site gratis

Het gratis plan draait de banner op één site met drie cookiescans per maand. Geen creditcard, geen verkoopgesprek.