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:
203
consent-sdk/src/core/ConsentStorage.ts
Normal file
203
consent-sdk/src/core/ConsentStorage.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* ConsentStorage - Lokale Speicherung des Consent-Status
|
||||
*
|
||||
* Speichert Consent-Daten im localStorage mit HMAC-Signatur
|
||||
* zur Manipulationserkennung.
|
||||
*/
|
||||
|
||||
import type { ConsentConfig, ConsentState } from '../types';
|
||||
|
||||
const STORAGE_KEY = 'bp_consent';
|
||||
const STORAGE_VERSION = '1';
|
||||
|
||||
/**
|
||||
* Gespeichertes Format
|
||||
*/
|
||||
interface StoredConsent {
|
||||
version: string;
|
||||
consent: ConsentState;
|
||||
signature: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ConsentStorage - Persistente Speicherung
|
||||
*/
|
||||
export class ConsentStorage {
|
||||
private config: ConsentConfig;
|
||||
private storageKey: string;
|
||||
|
||||
constructor(config: ConsentConfig) {
|
||||
this.config = config;
|
||||
// Pro Site ein separater Key
|
||||
this.storageKey = `${STORAGE_KEY}_${config.siteId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consent laden
|
||||
*/
|
||||
get(): ConsentState | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(this.storageKey);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stored: StoredConsent = JSON.parse(raw);
|
||||
|
||||
// Version pruefen
|
||||
if (stored.version !== STORAGE_VERSION) {
|
||||
this.log('Storage version mismatch, clearing');
|
||||
this.clear();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Signatur pruefen
|
||||
if (!this.verifySignature(stored.consent, stored.signature)) {
|
||||
this.log('Invalid signature, clearing');
|
||||
this.clear();
|
||||
return null;
|
||||
}
|
||||
|
||||
return stored.consent;
|
||||
} catch (error) {
|
||||
this.log('Failed to load consent:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consent speichern
|
||||
*/
|
||||
set(consent: ConsentState): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const signature = this.generateSignature(consent);
|
||||
|
||||
const stored: StoredConsent = {
|
||||
version: STORAGE_VERSION,
|
||||
consent,
|
||||
signature,
|
||||
};
|
||||
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(stored));
|
||||
|
||||
// Auch als Cookie setzen (fuer Server-Side Rendering)
|
||||
this.setCookie(consent);
|
||||
|
||||
this.log('Consent saved to storage');
|
||||
} catch (error) {
|
||||
this.log('Failed to save consent:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consent loeschen
|
||||
*/
|
||||
clear(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.removeItem(this.storageKey);
|
||||
this.clearCookie();
|
||||
this.log('Consent cleared from storage');
|
||||
} catch (error) {
|
||||
this.log('Failed to clear consent:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pruefen ob Consent existiert
|
||||
*/
|
||||
exists(): boolean {
|
||||
return this.get() !== null;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Cookie Management
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Consent als Cookie setzen
|
||||
*/
|
||||
private setCookie(consent: ConsentState): void {
|
||||
const days = this.config.consent?.rememberDays ?? 365;
|
||||
const expires = new Date();
|
||||
expires.setDate(expires.getDate() + days);
|
||||
|
||||
// Nur Kategorien als Cookie (fuer SSR)
|
||||
const cookieValue = JSON.stringify(consent.categories);
|
||||
const encoded = encodeURIComponent(cookieValue);
|
||||
|
||||
document.cookie = [
|
||||
`${this.storageKey}=${encoded}`,
|
||||
`expires=${expires.toUTCString()}`,
|
||||
'path=/',
|
||||
'SameSite=Lax',
|
||||
location.protocol === 'https:' ? 'Secure' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cookie loeschen
|
||||
*/
|
||||
private clearCookie(): void {
|
||||
document.cookie = `${this.storageKey}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Signature (Simple HMAC-like)
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Signatur generieren
|
||||
*/
|
||||
private generateSignature(consent: ConsentState): string {
|
||||
const data = JSON.stringify(consent);
|
||||
const key = this.config.siteId;
|
||||
|
||||
// Einfache Hash-Funktion (fuer Client-Side)
|
||||
// In Produktion wuerde man SubtleCrypto verwenden
|
||||
return this.simpleHash(data + key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signatur verifizieren
|
||||
*/
|
||||
private verifySignature(consent: ConsentState, signature: string): boolean {
|
||||
const expected = this.generateSignature(consent);
|
||||
return expected === signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Einfache Hash-Funktion (djb2)
|
||||
*/
|
||||
private simpleHash(str: string): string {
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = (hash * 33) ^ str.charCodeAt(i);
|
||||
}
|
||||
return (hash >>> 0).toString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug-Logging
|
||||
*/
|
||||
private log(...args: unknown[]): void {
|
||||
if (this.config.debug) {
|
||||
console.log('[ConsentStorage]', ...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ConsentStorage;
|
||||
Reference in New Issue
Block a user