import type { EventHint, ErrorEvent as SentryErrorEvent } from '@sentry/vue' const SENSITIVE_BODY_KEYS = [ 'password', 'password_confirmation', 'current_password', 'token', 'api_key', 'secret', 'webhook_secret', 'dsn', 'signature', 'authorization', 'cookie', 'bearer', 'iban', 'bic', 'passport_number', 'bsn', ] as const const SENSITIVE_HEADERS = [ 'authorization', 'cookie', 'set-cookie', 'x-api-key', 'x-impersonation-token', ] as const const SENSITIVE_QUERY_KEYS = ['token', 'api_key'] as const const SCRUBBED = '[scrubbed]' const FORM_VALUES_REPLACEMENT = '[scrubbed_form_values]' const MAX_DEPTH = 10 /** * RFC-WS-7 §3.7 frontend block: scrubs PII from outgoing Sentry events. * * Mirrors the backend SentryEventScrubber semantics so a captured request * looks the same regardless of which side originated the event. Adds * frontend-specific scrubs (cookies via document.cookie exposure, * localStorage/sessionStorage never in event context). */ export function scrubEvent(event: SentryErrorEvent, _hint?: EventHint): SentryErrorEvent | null { if (event.request) { if (event.request.data !== undefined && event.request.data !== null && typeof event.request.data === 'object') event.request.data = scrubBody(event.request.data, 0) if (event.request.headers && typeof event.request.headers === 'object') event.request.headers = scrubHeaders(event.request.headers as Record) if (typeof event.request.query_string === 'string' && event.request.query_string !== '') event.request.query_string = scrubQueryString(event.request.query_string) // RFC §3.7 frontend point 1: cookies wholesale. Sentry types the field // as Record; replacing with a single sentinel key keeps // the structure shape while removing any real values. if (event.request.cookies !== undefined) event.request.cookies = { scrubbed: SCRUBBED } } // RFC §3.7 frontend point 2: localStorage / sessionStorage never in event // context. Sentry doesn't add these by default but defensively strip. if (event.contexts && 'storage' in event.contexts) delete (event.contexts as Record).storage // RFC §3.7 frontend point 1: scrub document.cookie if Sentry's // BrowserSession integration injected it under user. if (event.user && 'cookies' in event.user) delete (event.user as Record).cookies return event } function scrubBody(data: unknown, depth: number): unknown { if (depth > MAX_DEPTH) return ['[max_depth]'] if (Array.isArray(data)) return data.map(item => scrubBody(item, depth + 1)) if (data === null || typeof data !== 'object') return data const result: Record = {} for (const [key, value] of Object.entries(data as Record)) { const lowerKey = key.toLowerCase() if (lowerKey === 'form_values') { result[key] = FORM_VALUES_REPLACEMENT continue } if ((SENSITIVE_BODY_KEYS as readonly string[]).includes(lowerKey)) { result[key] = SCRUBBED continue } if (typeof value === 'object' && value !== null) { result[key] = scrubBody(value, depth + 1) continue } result[key] = value } return result } function scrubHeaders(headers: Record): Record { const result: Record = {} for (const [name, value] of Object.entries(headers)) { if ((SENSITIVE_HEADERS as readonly string[]).includes(name.toLowerCase())) result[name] = SCRUBBED else result[name] = value } return result } function scrubQueryString(queryString: string): string { const params = new URLSearchParams(queryString) for (const key of Array.from(params.keys())) { if ((SENSITIVE_QUERY_KEYS as readonly string[]).includes(key.toLowerCase())) params.set(key, SCRUBBED) } return params.toString() }