import { PIIDetection, PIIType } from './detectors.js'; /** * Pseudonymizer - replaces PII with numbered placeholders * and supports optional restoration */ export class Pseudonymizer { private counters: Map = new Map(); /** * Pseudonymize text by replacing PII with placeholders * Returns the modified text and a mapping for restoration */ pseudonymize( text: string, detections: PIIDetection[] ): { text: string; mappings: Map } { const mappings = new Map(); let result = text; let offset = 0; // Reset counters for consistent numbering this.counters.clear(); for (const detection of detections) { const placeholder = this.generatePlaceholder(detection.type); mappings.set(placeholder, detection.value); // Replace in text (accounting for previous replacements) const start = detection.startIndex + offset; const end = detection.endIndex + offset; result = result.substring(0, start) + placeholder + result.substring(end); // Adjust offset for next replacement offset += placeholder.length - detection.value.length; } return { text: result, mappings }; } /** * Restore placeholders in text with original values */ restore(text: string, mappings: Map): string { let result = text; for (const [placeholder, original] of mappings) { // Use global replace to handle multiple occurrences result = result.replace(new RegExp(this.escapeRegex(placeholder), 'g'), original); } return result; } /** * Generate a placeholder for a PII type */ private generatePlaceholder(type: PIIType): string { const count = (this.counters.get(type) || 0) + 1; this.counters.set(type, count); const typeLabels: Record = { email: 'EMAIL', phone: 'PHONE', ssn: 'SSN', api_key: 'API_KEY', credit_card: 'CARD', ip_address: 'IP', custom: 'REDACTED', }; return `[${typeLabels[type]}_${count}]`; } /** * Escape special regex characters in a string */ private escapeRegex(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Reset counters (for testing) */ reset(): void { this.counters.clear(); } }