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,177 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { EventEmitter } from './EventEmitter';
interface TestEvents {
test: string;
count: number;
data: { value: string };
}
describe('EventEmitter', () => {
let emitter: EventEmitter<TestEvents>;
beforeEach(() => {
emitter = new EventEmitter();
});
describe('on', () => {
it('should register an event listener', () => {
const callback = vi.fn();
emitter.on('test', callback);
emitter.emit('test', 'hello');
expect(callback).toHaveBeenCalledWith('hello');
expect(callback).toHaveBeenCalledTimes(1);
});
it('should return an unsubscribe function', () => {
const callback = vi.fn();
const unsubscribe = emitter.on('test', callback);
emitter.emit('test', 'first');
unsubscribe();
emitter.emit('test', 'second');
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('first');
});
it('should allow multiple listeners for the same event', () => {
const callback1 = vi.fn();
const callback2 = vi.fn();
emitter.on('test', callback1);
emitter.on('test', callback2);
emitter.emit('test', 'value');
expect(callback1).toHaveBeenCalledWith('value');
expect(callback2).toHaveBeenCalledWith('value');
});
});
describe('off', () => {
it('should remove an event listener', () => {
const callback = vi.fn();
emitter.on('test', callback);
emitter.emit('test', 'first');
emitter.off('test', callback);
emitter.emit('test', 'second');
expect(callback).toHaveBeenCalledTimes(1);
});
it('should not throw when removing non-existent listener', () => {
const callback = vi.fn();
expect(() => emitter.off('test', callback)).not.toThrow();
});
});
describe('emit', () => {
it('should call all listeners with the data', () => {
const callback = vi.fn();
emitter.on('data', callback);
emitter.emit('data', { value: 'test' });
expect(callback).toHaveBeenCalledWith({ value: 'test' });
});
it('should not throw when emitting to no listeners', () => {
expect(() => emitter.emit('test', 'value')).not.toThrow();
});
it('should catch errors in listeners and continue', () => {
const errorCallback = vi.fn(() => {
throw new Error('Test error');
});
const successCallback = vi.fn();
emitter.on('test', errorCallback);
emitter.on('test', successCallback);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
emitter.emit('test', 'value');
expect(errorCallback).toHaveBeenCalled();
expect(successCallback).toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
});
describe('once', () => {
it('should call listener only once', () => {
const callback = vi.fn();
emitter.once('test', callback);
emitter.emit('test', 'first');
emitter.emit('test', 'second');
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('first');
});
it('should return an unsubscribe function', () => {
const callback = vi.fn();
const unsubscribe = emitter.once('test', callback);
unsubscribe();
emitter.emit('test', 'value');
expect(callback).not.toHaveBeenCalled();
});
});
describe('clear', () => {
it('should remove all listeners', () => {
const callback1 = vi.fn();
const callback2 = vi.fn();
emitter.on('test', callback1);
emitter.on('count', callback2);
emitter.clear();
emitter.emit('test', 'value');
emitter.emit('count', 42);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
});
});
describe('clearEvent', () => {
it('should remove all listeners for a specific event', () => {
const testCallback = vi.fn();
const countCallback = vi.fn();
emitter.on('test', testCallback);
emitter.on('count', countCallback);
emitter.clearEvent('test');
emitter.emit('test', 'value');
emitter.emit('count', 42);
expect(testCallback).not.toHaveBeenCalled();
expect(countCallback).toHaveBeenCalledWith(42);
});
});
describe('listenerCount', () => {
it('should return the number of listeners for an event', () => {
expect(emitter.listenerCount('test')).toBe(0);
emitter.on('test', () => {});
expect(emitter.listenerCount('test')).toBe(1);
emitter.on('test', () => {});
expect(emitter.listenerCount('test')).toBe(2);
});
it('should return 0 for events with no listeners', () => {
expect(emitter.listenerCount('count')).toBe(0);
});
});
});

View File

@@ -0,0 +1,89 @@
/**
* EventEmitter - Typsicherer Event-Handler
*/
type EventCallback<T = unknown> = (data: T) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class EventEmitter<Events extends Record<string, any> = Record<string, unknown>> {
private listeners: Map<keyof Events, Set<EventCallback<unknown>>> = new Map();
/**
* Event-Listener registrieren
* @returns Unsubscribe-Funktion
*/
on<K extends keyof Events>(
event: K,
callback: EventCallback<Events[K]>
): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback as EventCallback<unknown>);
// Unsubscribe-Funktion zurueckgeben
return () => this.off(event, callback);
}
/**
* Event-Listener entfernen
*/
off<K extends keyof Events>(
event: K,
callback: EventCallback<Events[K]>
): void {
this.listeners.get(event)?.delete(callback as EventCallback<unknown>);
}
/**
* Event emittieren
*/
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners.get(event)?.forEach((callback) => {
try {
callback(data);
} catch (error) {
console.error(`Error in event handler for ${String(event)}:`, error);
}
});
}
/**
* Einmaligen Listener registrieren
*/
once<K extends keyof Events>(
event: K,
callback: EventCallback<Events[K]>
): () => void {
const wrapper = (data: Events[K]) => {
this.off(event, wrapper);
callback(data);
};
return this.on(event, wrapper);
}
/**
* Alle Listener entfernen
*/
clear(): void {
this.listeners.clear();
}
/**
* Alle Listener fuer ein Event entfernen
*/
clearEvent<K extends keyof Events>(event: K): void {
this.listeners.delete(event);
}
/**
* Anzahl Listener fuer ein Event
*/
listenerCount<K extends keyof Events>(event: K): number {
return this.listeners.get(event)?.size ?? 0;
}
}
export default EventEmitter;

View File

@@ -0,0 +1,230 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { generateFingerprint, generateFingerprintSync } from './fingerprint';
describe('fingerprint', () => {
describe('generateFingerprint', () => {
it('should generate a fingerprint with fp_ prefix', async () => {
const fingerprint = await generateFingerprint();
expect(fingerprint).toMatch(/^fp_[a-f0-9]{32}$/);
});
it('should generate consistent fingerprints for same environment', async () => {
const fp1 = await generateFingerprint();
const fp2 = await generateFingerprint();
expect(fp1).toBe(fp2);
});
it('should include browser detection in fingerprint components', async () => {
// Chrome is in the mocked userAgent
const fingerprint = await generateFingerprint();
expect(fingerprint).toBeDefined();
});
});
describe('generateFingerprintSync', () => {
it('should generate a fingerprint with fp_ prefix', () => {
const fingerprint = generateFingerprintSync();
expect(fingerprint).toMatch(/^fp_[a-f0-9]+$/);
});
it('should be consistent for same environment', () => {
const fp1 = generateFingerprintSync();
const fp2 = generateFingerprintSync();
expect(fp1).toBe(fp2);
});
});
describe('environment variations', () => {
it('should detect screen categories correctly', async () => {
// Default is 1920px (FHD)
const fingerprint = await generateFingerprint();
expect(fingerprint).toBeDefined();
});
it('should handle touch detection', async () => {
Object.defineProperty(navigator, 'maxTouchPoints', {
value: 5,
configurable: true,
});
const fingerprint = await generateFingerprint();
expect(fingerprint).toBeDefined();
// Reset
Object.defineProperty(navigator, 'maxTouchPoints', {
value: 0,
configurable: true,
});
});
it('should handle Do Not Track', async () => {
Object.defineProperty(navigator, 'doNotTrack', {
value: '1',
configurable: true,
});
const fingerprint = await generateFingerprint();
expect(fingerprint).toBeDefined();
// Reset
Object.defineProperty(navigator, 'doNotTrack', {
value: null,
configurable: true,
});
});
});
describe('browser detection', () => {
it('should detect Firefox', async () => {
Object.defineProperty(navigator, 'userAgent', {
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
configurable: true,
});
const fingerprint = await generateFingerprint();
expect(fingerprint).toBeDefined();
// Reset
Object.defineProperty(navigator, 'userAgent', {
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
configurable: true,
});
});
it('should detect Safari', async () => {
Object.defineProperty(navigator, 'userAgent', {
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
configurable: true,
});
const fingerprint = await generateFingerprint();
expect(fingerprint).toBeDefined();
// Reset
Object.defineProperty(navigator, 'userAgent', {
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
configurable: true,
});
});
it('should detect Edge', async () => {
Object.defineProperty(navigator, 'userAgent', {
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
configurable: true,
});
const fingerprint = await generateFingerprint();
expect(fingerprint).toBeDefined();
// Reset
Object.defineProperty(navigator, 'userAgent', {
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
configurable: true,
});
});
});
describe('platform detection', () => {
it('should detect Windows', async () => {
Object.defineProperty(navigator, 'platform', {
value: 'Win32',
configurable: true,
});
const fingerprint = await generateFingerprint();
expect(fingerprint).toBeDefined();
// Reset
Object.defineProperty(navigator, 'platform', {
value: 'MacIntel',
configurable: true,
});
});
it('should detect Linux', async () => {
Object.defineProperty(navigator, 'platform', {
value: 'Linux x86_64',
configurable: true,
});
const fingerprint = await generateFingerprint();
expect(fingerprint).toBeDefined();
// Reset
Object.defineProperty(navigator, 'platform', {
value: 'MacIntel',
configurable: true,
});
});
it('should detect iOS', async () => {
Object.defineProperty(navigator, 'platform', {
value: 'iPhone',
configurable: true,
});
const fingerprint = await generateFingerprint();
expect(fingerprint).toBeDefined();
// Reset
Object.defineProperty(navigator, 'platform', {
value: 'MacIntel',
configurable: true,
});
});
});
describe('screen categories', () => {
it('should detect 4K screens', async () => {
Object.defineProperty(window, 'screen', {
value: { width: 3840, height: 2160, colorDepth: 24 },
configurable: true,
});
const fingerprint = await generateFingerprint();
expect(fingerprint).toBeDefined();
// Reset
Object.defineProperty(window, 'screen', {
value: { width: 1920, height: 1080, colorDepth: 24 },
configurable: true,
});
});
it('should detect tablet screens', async () => {
Object.defineProperty(window, 'screen', {
value: { width: 1024, height: 768, colorDepth: 24 },
configurable: true,
});
const fingerprint = await generateFingerprint();
expect(fingerprint).toBeDefined();
// Reset
Object.defineProperty(window, 'screen', {
value: { width: 1920, height: 1080, colorDepth: 24 },
configurable: true,
});
});
it('should detect mobile screens', async () => {
Object.defineProperty(window, 'screen', {
value: { width: 375, height: 812, colorDepth: 24 },
configurable: true,
});
const fingerprint = await generateFingerprint();
expect(fingerprint).toBeDefined();
// Reset
Object.defineProperty(window, 'screen', {
value: { width: 1920, height: 1080, colorDepth: 24 },
configurable: true,
});
});
});
});

View File

@@ -0,0 +1,176 @@
/**
* Device Fingerprinting - Datenschutzkonform
*
* Generiert einen anonymen Fingerprint OHNE:
* - Canvas Fingerprinting
* - WebGL Fingerprinting
* - Audio Fingerprinting
* - Hardware-spezifische IDs
*
* Verwendet nur:
* - User Agent
* - Sprache
* - Bildschirmaufloesung
* - Zeitzone
* - Platform
*/
/**
* Fingerprint-Komponenten sammeln
*/
function getComponents(): string[] {
if (typeof window === 'undefined') {
return ['server'];
}
const components: string[] = [];
// User Agent (anonymisiert)
try {
// Nur Browser-Familie, nicht vollstaendiger UA
const ua = navigator.userAgent;
if (ua.includes('Chrome')) components.push('chrome');
else if (ua.includes('Firefox')) components.push('firefox');
else if (ua.includes('Safari')) components.push('safari');
else if (ua.includes('Edge')) components.push('edge');
else components.push('other');
} catch {
components.push('unknown-browser');
}
// Sprache
try {
components.push(navigator.language || 'unknown-lang');
} catch {
components.push('unknown-lang');
}
// Bildschirm-Kategorie (nicht exakte Aufloesung)
try {
const width = window.screen.width;
if (width >= 2560) components.push('4k');
else if (width >= 1920) components.push('fhd');
else if (width >= 1366) components.push('hd');
else if (width >= 768) components.push('tablet');
else components.push('mobile');
} catch {
components.push('unknown-screen');
}
// Farbtiefe (grob)
try {
const depth = window.screen.colorDepth;
if (depth >= 24) components.push('deep-color');
else components.push('standard-color');
} catch {
components.push('unknown-color');
}
// Zeitzone (nur Offset, nicht Name)
try {
const offset = new Date().getTimezoneOffset();
const hours = Math.floor(Math.abs(offset) / 60);
const sign = offset <= 0 ? '+' : '-';
components.push(`tz${sign}${hours}`);
} catch {
components.push('unknown-tz');
}
// Platform-Kategorie
try {
const platform = navigator.platform?.toLowerCase() || '';
if (platform.includes('mac')) components.push('mac');
else if (platform.includes('win')) components.push('win');
else if (platform.includes('linux')) components.push('linux');
else if (platform.includes('iphone') || platform.includes('ipad'))
components.push('ios');
else if (platform.includes('android')) components.push('android');
else components.push('other-platform');
} catch {
components.push('unknown-platform');
}
// Touch-Faehigkeit
try {
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
components.push('touch');
} else {
components.push('no-touch');
}
} catch {
components.push('unknown-touch');
}
// Do Not Track (als Datenschutz-Signal)
try {
if (navigator.doNotTrack === '1') {
components.push('dnt');
}
} catch {
// Ignorieren
}
return components;
}
/**
* SHA-256 Hash (async, nutzt SubtleCrypto)
*/
async function sha256(message: string): Promise<string> {
if (typeof window === 'undefined' || !window.crypto?.subtle) {
// Fallback fuer Server/alte Browser
return simpleHash(message);
}
try {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
} catch {
return simpleHash(message);
}
}
/**
* Fallback Hash-Funktion (djb2)
*/
function 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).padStart(8, '0');
}
/**
* Datenschutzkonformen Fingerprint generieren
*
* Der Fingerprint ist:
* - Nicht eindeutig (viele Nutzer teilen sich denselben)
* - Nicht persistent (aendert sich bei Browser-Updates)
* - Nicht invasiv (keine Canvas/WebGL/Audio)
* - Anonymisiert (SHA-256 Hash)
*/
export async function generateFingerprint(): Promise<string> {
const components = getComponents();
const combined = components.join('|');
const hash = await sha256(combined);
// Prefix fuer Identifikation
return `fp_${hash.substring(0, 32)}`;
}
/**
* Synchrone Version (mit einfachem Hash)
*/
export function generateFingerprintSync(): string {
const components = getComponents();
const combined = components.join('|');
const hash = simpleHash(combined);
return `fp_${hash}`;
}
export default generateFingerprint;