// sentry.scrub.ts — strip secrets/PII before any event leaves the process. // Shared by sentry.client.config.ts (browser) and sentry.server.config.ts (node). // // This app stores the iDempiere JWT in the `logship_it` cookie and a base64-encoded // password in `logship_py` — those must NEVER reach Sentry. We also drop request // bodies, auth headers and query strings, which can carry credentials or customer PII. const SENSITIVE_HEADERS = ['cookie', 'set-cookie', 'authorization', 'x-api-key'] // Keys whose VALUES must never leave the process (secrets/credentials only — business // data like names/addresses/IDs is intentionally kept so server errors stay debuggable). const SENSITIVE_KEY_RX = /(pass|secret|token|credential|authorization|api[_-]?key|marketplace_key|marketplace_secret|logship_py|logship_it|vapid)/i const MAX_STRING = 2000 // truncate long strings (base64 blobs, HTML, label data) const MAX_DEPTH = 8 // guard against deeply nested / pathological payloads const MAX_BYTES = 16000 // overall cap on the serialised payload Sentry stores export function scrubEvent(event: any): any | null { if (event?.request) { // Cookies carry the JWT (logship_it) and the base64 password (logship_py) — never send. delete event.request.cookies delete event.request.data // raw request body auto-attached by Sentry (may carry secrets) if (event.request.query_string) event.request.query_string = '[stripped]' if (event.request.headers) { for (const h of Object.keys(event.request.headers)) { if (SENSITIVE_HEADERS.includes(h.toLowerCase())) delete event.request.headers[h] } } } // Drop any captured fetch/xhr breadcrumb bodies (keep method + url for context). if (Array.isArray(event?.breadcrumbs)) { for (const b of event.breadcrumbs) { if (b?.data && typeof b.data === 'object' && 'body' in b.data) delete b.data.body } } // 2nd line of defence: re-scrub the request_payload context attached by the // sentry-payload middleware, in case it was ever set elsewhere unscrubbed. const payload = event?.contexts?.request_payload if (payload) { if (payload.body) payload.body = scrubPayload(payload.body) if (payload.query) payload.query = scrubPayload(payload.query) } return event } // Deep-clone a request payload, redacting secret-bearing keys and capping size. // Idempotent so beforeSend can safely re-run it as a 2nd line of defence. export function scrubPayload(input: any): any { const seen = new WeakSet() const walk = (val: any, depth: number): any => { if (val === null || val === undefined) return val if (typeof val === 'string') { return val.length > MAX_STRING ? val.slice(0, MAX_STRING) + `…[+${val.length - MAX_STRING} chars]` : val } if (typeof val !== 'object') return val if (depth >= MAX_DEPTH) return '[depth-limited]' if (seen.has(val)) return '[circular]' seen.add(val) if (Array.isArray(val)) return val.map((v) => walk(v, depth + 1)) const out: any = {} for (const k of Object.keys(val)) { out[k] = SENSITIVE_KEY_RX.test(k) ? '[redacted]' : walk(val[k], depth + 1) } return out } let result = walk(input, 0) try { const json = JSON.stringify(result) if (json && json.length > MAX_BYTES) { result = { _truncated: true, _bytes: json.length, preview: json.slice(0, MAX_BYTES) } } } catch { /* non-serialisable → leave as-is */ } return result }