Initial commit: breakpilot-compliance - Compliance SDK Platform

Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:28 +01:00
commit 4435e7ea0a
734 changed files with 251369 additions and 0 deletions

View File

@@ -0,0 +1,525 @@
/**
* ConsentManager - Hauptklasse fuer das Consent Management
*
* DSGVO/TTDSG-konformes Consent Management fuer Web, PWA und Mobile.
*/
import type {
ConsentConfig,
ConsentState,
ConsentCategory,
ConsentCategories,
ConsentInput,
ConsentEventType,
ConsentEventCallback,
ConsentEventData,
} from '../types';
import { ConsentStorage } from './ConsentStorage';
import { ScriptBlocker } from './ScriptBlocker';
import { ConsentAPI } from './ConsentAPI';
import { EventEmitter } from '../utils/EventEmitter';
import { generateFingerprint } from '../utils/fingerprint';
import { SDK_VERSION } from '../version';
/**
* Default-Konfiguration
*/
const DEFAULT_CONFIG: Partial<ConsentConfig> = {
language: 'de',
fallbackLanguage: 'en',
ui: {
position: 'bottom',
layout: 'modal',
theme: 'auto',
zIndex: 999999,
blockScrollOnModal: true,
},
consent: {
required: true,
rejectAllVisible: true,
acceptAllVisible: true,
granularControl: true,
vendorControl: false,
rememberChoice: true,
rememberDays: 365,
geoTargeting: false,
recheckAfterDays: 180,
},
categories: ['essential', 'functional', 'analytics', 'marketing', 'social'],
debug: false,
};
/**
* Default Consent-State (nur Essential aktiv)
*/
const DEFAULT_CONSENT: ConsentCategories = {
essential: true,
functional: false,
analytics: false,
marketing: false,
social: false,
};
/**
* ConsentManager - Zentrale Klasse fuer Consent-Verwaltung
*/
export class ConsentManager {
private config: ConsentConfig;
private storage: ConsentStorage;
private scriptBlocker: ScriptBlocker;
private api: ConsentAPI;
private events: EventEmitter<ConsentEventData>;
private currentConsent: ConsentState | null = null;
private initialized = false;
private bannerVisible = false;
private deviceFingerprint: string = '';
constructor(config: ConsentConfig) {
this.config = this.mergeConfig(config);
this.storage = new ConsentStorage(this.config);
this.scriptBlocker = new ScriptBlocker(this.config);
this.api = new ConsentAPI(this.config);
this.events = new EventEmitter();
this.log('ConsentManager created with config:', this.config);
}
/**
* SDK initialisieren
*/
async init(): Promise<void> {
if (this.initialized) {
this.log('Already initialized, skipping');
return;
}
try {
this.log('Initializing ConsentManager...');
// Device Fingerprint generieren
this.deviceFingerprint = await generateFingerprint();
// Consent aus Storage laden
this.currentConsent = this.storage.get();
if (this.currentConsent) {
this.log('Loaded consent from storage:', this.currentConsent);
// Pruefen ob Consent abgelaufen
if (this.isConsentExpired()) {
this.log('Consent expired, clearing');
this.storage.clear();
this.currentConsent = null;
} else {
// Consent anwenden
this.applyConsent();
}
}
// Script-Blocker initialisieren
this.scriptBlocker.init();
this.initialized = true;
this.emit('init', this.currentConsent);
// Banner anzeigen falls noetig
if (this.needsConsent()) {
this.showBanner();
}
this.log('ConsentManager initialized successfully');
} catch (error) {
this.handleError(error as Error);
throw error;
}
}
// ===========================================================================
// Public API
// ===========================================================================
/**
* Pruefen ob Consent fuer Kategorie vorhanden
*/
hasConsent(category: ConsentCategory): boolean {
if (!this.currentConsent) {
return category === 'essential';
}
return this.currentConsent.categories[category] ?? false;
}
/**
* Pruefen ob Consent fuer Vendor vorhanden
*/
hasVendorConsent(vendorId: string): boolean {
if (!this.currentConsent) {
return false;
}
return this.currentConsent.vendors[vendorId] ?? false;
}
/**
* Aktuellen Consent-State abrufen
*/
getConsent(): ConsentState | null {
return this.currentConsent ? { ...this.currentConsent } : null;
}
/**
* Consent setzen
*/
async setConsent(input: ConsentInput): Promise<void> {
const categories = this.normalizeConsentInput(input);
// Essential ist immer aktiv
categories.essential = true;
const newConsent: ConsentState = {
categories,
vendors: 'vendors' in input && input.vendors ? input.vendors : {},
timestamp: new Date().toISOString(),
version: SDK_VERSION,
};
try {
// An Backend senden
const response = await this.api.saveConsent({
siteId: this.config.siteId,
deviceFingerprint: this.deviceFingerprint,
consent: newConsent,
});
newConsent.consentId = response.consentId;
newConsent.expiresAt = response.expiresAt;
// Lokal speichern
this.storage.set(newConsent);
this.currentConsent = newConsent;
// Consent anwenden
this.applyConsent();
// Event emittieren
this.emit('change', newConsent);
this.config.onConsentChange?.(newConsent);
this.log('Consent saved:', newConsent);
} catch (error) {
// Bei Netzwerkfehler trotzdem lokal speichern
this.log('API error, saving locally:', error);
this.storage.set(newConsent);
this.currentConsent = newConsent;
this.applyConsent();
this.emit('change', newConsent);
}
}
/**
* Alle Kategorien akzeptieren
*/
async acceptAll(): Promise<void> {
const allCategories: ConsentCategories = {
essential: true,
functional: true,
analytics: true,
marketing: true,
social: true,
};
await this.setConsent(allCategories);
this.emit('accept_all', this.currentConsent!);
this.hideBanner();
}
/**
* Alle nicht-essentiellen Kategorien ablehnen
*/
async rejectAll(): Promise<void> {
const minimalCategories: ConsentCategories = {
essential: true,
functional: false,
analytics: false,
marketing: false,
social: false,
};
await this.setConsent(minimalCategories);
this.emit('reject_all', this.currentConsent!);
this.hideBanner();
}
/**
* Alle Einwilligungen widerrufen
*/
async revokeAll(): Promise<void> {
if (this.currentConsent?.consentId) {
try {
await this.api.revokeConsent(this.currentConsent.consentId);
} catch (error) {
this.log('Failed to revoke on server:', error);
}
}
this.storage.clear();
this.currentConsent = null;
this.scriptBlocker.blockAll();
this.log('All consents revoked');
}
/**
* Consent-Daten exportieren (DSGVO Art. 20)
*/
async exportConsent(): Promise<string> {
const exportData = {
currentConsent: this.currentConsent,
exportedAt: new Date().toISOString(),
siteId: this.config.siteId,
deviceFingerprint: this.deviceFingerprint,
};
return JSON.stringify(exportData, null, 2);
}
// ===========================================================================
// Banner Control
// ===========================================================================
/**
* Pruefen ob Consent-Abfrage noetig
*/
needsConsent(): boolean {
if (!this.currentConsent) {
return true;
}
if (this.isConsentExpired()) {
return true;
}
// Recheck nach X Tagen
if (this.config.consent?.recheckAfterDays) {
const consentDate = new Date(this.currentConsent.timestamp);
const recheckDate = new Date(consentDate);
recheckDate.setDate(
recheckDate.getDate() + this.config.consent.recheckAfterDays
);
if (new Date() > recheckDate) {
return true;
}
}
return false;
}
/**
* Banner anzeigen
*/
showBanner(): void {
if (this.bannerVisible) {
return;
}
this.bannerVisible = true;
this.emit('banner_show', undefined);
this.config.onBannerShow?.();
// Banner wird von UI-Komponente gerendert
// Hier nur Status setzen
this.log('Banner shown');
}
/**
* Banner verstecken
*/
hideBanner(): void {
if (!this.bannerVisible) {
return;
}
this.bannerVisible = false;
this.emit('banner_hide', undefined);
this.config.onBannerHide?.();
this.log('Banner hidden');
}
/**
* Einstellungs-Modal oeffnen
*/
showSettings(): void {
this.emit('settings_open', undefined);
this.log('Settings opened');
}
/**
* Pruefen ob Banner sichtbar
*/
isBannerVisible(): boolean {
return this.bannerVisible;
}
// ===========================================================================
// Event Handling
// ===========================================================================
/**
* Event-Listener registrieren
*/
on<T extends ConsentEventType>(
event: T,
callback: ConsentEventCallback<ConsentEventData[T]>
): () => void {
return this.events.on(event, callback);
}
/**
* Event-Listener entfernen
*/
off<T extends ConsentEventType>(
event: T,
callback: ConsentEventCallback<ConsentEventData[T]>
): void {
this.events.off(event, callback);
}
// ===========================================================================
// Internal Methods
// ===========================================================================
/**
* Konfiguration zusammenfuehren
*/
private mergeConfig(config: ConsentConfig): ConsentConfig {
return {
...DEFAULT_CONFIG,
...config,
ui: { ...DEFAULT_CONFIG.ui, ...config.ui },
consent: { ...DEFAULT_CONFIG.consent, ...config.consent },
} as ConsentConfig;
}
/**
* Consent-Input normalisieren
*/
private normalizeConsentInput(input: ConsentInput): ConsentCategories {
if ('categories' in input && input.categories) {
return { ...DEFAULT_CONSENT, ...input.categories };
}
return { ...DEFAULT_CONSENT, ...(input as Partial<ConsentCategories>) };
}
/**
* Consent anwenden (Skripte aktivieren/blockieren)
*/
private applyConsent(): void {
if (!this.currentConsent) {
return;
}
for (const [category, allowed] of Object.entries(
this.currentConsent.categories
)) {
if (allowed) {
this.scriptBlocker.enableCategory(category as ConsentCategory);
} else {
this.scriptBlocker.disableCategory(category as ConsentCategory);
}
}
// Google Consent Mode aktualisieren
this.updateGoogleConsentMode();
}
/**
* Google Consent Mode v2 aktualisieren
*/
private updateGoogleConsentMode(): void {
if (typeof window === 'undefined' || !this.currentConsent) {
return;
}
const gtag = (window as unknown as { gtag?: (...args: unknown[]) => void }).gtag;
if (typeof gtag !== 'function') {
return;
}
const { categories } = this.currentConsent;
gtag('consent', 'update', {
ad_storage: categories.marketing ? 'granted' : 'denied',
ad_user_data: categories.marketing ? 'granted' : 'denied',
ad_personalization: categories.marketing ? 'granted' : 'denied',
analytics_storage: categories.analytics ? 'granted' : 'denied',
functionality_storage: categories.functional ? 'granted' : 'denied',
personalization_storage: categories.functional ? 'granted' : 'denied',
security_storage: 'granted',
});
this.log('Google Consent Mode updated');
}
/**
* Pruefen ob Consent abgelaufen
*/
private isConsentExpired(): boolean {
if (!this.currentConsent?.expiresAt) {
// Fallback: Nach rememberDays ablaufen
if (this.currentConsent?.timestamp && this.config.consent?.rememberDays) {
const consentDate = new Date(this.currentConsent.timestamp);
const expiryDate = new Date(consentDate);
expiryDate.setDate(
expiryDate.getDate() + this.config.consent.rememberDays
);
return new Date() > expiryDate;
}
return false;
}
return new Date() > new Date(this.currentConsent.expiresAt);
}
/**
* Event emittieren
*/
private emit<T extends ConsentEventType>(
event: T,
data: ConsentEventData[T]
): void {
this.events.emit(event, data);
}
/**
* Fehler behandeln
*/
private handleError(error: Error): void {
this.log('Error:', error);
this.emit('error', error);
this.config.onError?.(error);
}
/**
* Debug-Logging
*/
private log(...args: unknown[]): void {
if (this.config.debug) {
console.log('[ConsentSDK]', ...args);
}
}
// ===========================================================================
// Static Methods
// ===========================================================================
/**
* SDK-Version abrufen
*/
static getVersion(): string {
return SDK_VERSION;
}
}
// Default-Export
export default ConsentManager;