// ============================================================ // PHI redactor for audit log details // ============================================================ // Audit details should describe *what happened*, not contain clinical // text. This defensively strips obvious PHI patterns and caps length. // ============================================================ var MAX_LEN = 500; function redact(text) { if (text == null) return text; var s = typeof text === 'string' ? text : JSON.stringify(text); // Credentials and tokens. These patterns intentionally run before PHI // redaction so accidental pasted headers/URLs are removed from logs. s = s.replace(/Authorization\s*:\s*Bearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Authorization: Bearer [REDACTED]'); s = s.replace(/Bearer\s+[A-Za-z0-9._~+/=-]{20,}/gi, 'Bearer [REDACTED]'); s = s.replace(/\b[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, '[JWT]'); s = s.replace(/\b(api[_-]?key|password|secret|token|app[_-]?password)(\s*[=:]\s*)([^\s&"']+)/gi, '$1$2[REDACTED]'); s = s.replace(/(https?:\/\/)([^\s/@:]+):([^\s/@]+)@/gi, '$1[REDACTED]@'); // SSN s = s.replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN]'); // US phone numbers s = s.replace(/\b\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b/g, '[PHONE]'); // Email addresses (except domain-only) s = s.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '[EMAIL]'); // Dates of birth / MRN-ish long digit runs s = s.replace(/\b\d{2}\/\d{2}\/\d{4}\b/g, '[DATE]'); s = s.replace(/\b\d{8,}\b/g, '[ID]'); // Note-body signal: many newlines or very long prose → indicates the // caller passed a clinical note as `details`. Truncate aggressively. var newlines = (s.match(/\n/g) || []).length; if (newlines > 4 || s.length > MAX_LEN * 2) { s = s.slice(0, 120) + ' [TRUNCATED:possible-note-body]'; } if (s.length > MAX_LEN) s = s.slice(0, MAX_LEN) + '…'; return s; } module.exports = { redact: redact };