/** * PII masking utility for structured logs. * Redacts sensitive fields in log data before they reach the transport. */ const SENSITIVE_KEYS = new Set([ 'password', 'token', 'accessToken', 'refreshToken', 'secret', 'authorization', 'cookie', 'creditCard', 'cardNumber', 'cvv', 'ssn', 'cmnd', 'cccd', ]); const EMAIL_REGEX = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g; const PHONE_REGEX = /(?:\+84|0)\d{9,10}/g; export function maskPii(obj: unknown): unknown { if (obj === null || obj === undefined) return obj; if (typeof obj === 'string') return maskString(obj); if (Array.isArray(obj)) return obj.map(maskPii); if (typeof obj === 'object') { const masked: Record = {}; for (const [key, value] of Object.entries(obj as Record)) { if (SENSITIVE_KEYS.has(key)) { masked[key] = '[REDACTED]'; } else { masked[key] = maskPii(value); } } return masked; } return obj; } function maskString(value: string): string { let result = value; result = result.replace(EMAIL_REGEX, (match) => { const [local, domain] = match.split('@'); return `${local![0]}***@${domain}`; }); result = result.replace(PHONE_REGEX, (match) => { return match.slice(0, 3) + '****' + match.slice(-3); }); return result; }