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
```typescript
// 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 {
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.
```typescript
// 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 {
// 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 {
// 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 {
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 {
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 => {
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.
```typescript
// 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, 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, allowedFields: string[]): Record {
const filtered: Record = {};
allowedFields.forEach(field => {
if (data[field] !== undefined) {
filtered[field] = data[field];
}
});
return filtered;
}
// Sanitize data before collection (remove unnecessary fields)
sanitizeForCollection(data: Record, purposeId: string): Record {
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 {
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 = {};
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[] {
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;
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.
```typescript
// 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 = new Map();
private audits: Map = 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.
```typescript
// 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;
private consents: Map = 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): 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:
```typescript
// 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: ''
},