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:
177
consent-sdk/src/utils/EventEmitter.test.ts
Normal file
177
consent-sdk/src/utils/EventEmitter.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
89
consent-sdk/src/utils/EventEmitter.ts
Normal file
89
consent-sdk/src/utils/EventEmitter.ts
Normal 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;
|
||||
230
consent-sdk/src/utils/fingerprint.test.ts
Normal file
230
consent-sdk/src/utils/fingerprint.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
176
consent-sdk/src/utils/fingerprint.ts
Normal file
176
consent-sdk/src/utils/fingerprint.ts
Normal 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;
|
||||
Reference in New Issue
Block a user