1/** 2 * Log scrubber (TASK-0134, ASVS L2 §14.4.10 / OWASP logging guidance). 3 * 4 * Redacts secret-bearing values from the strings that the API writes to 5 * structured logs. The only request-coupled log paths in the API are the 6 * global exception filter (`req.originalUrl` + exception message/stack) and 7 * ad-hoc `logger.*` calls; anything that can carry a credential is 8 * scrubbed at those boundaries: 9 * 10 * - `Authorization: Bearer <token>` header values 11 * - JWT-shaped tokens (three base64url segments) 12 * - factory + common third-party tokens (`fk_…`, `whsec_…`, `apk_…`, 13 * `ghp_…`, `xox[baprs]-…`, `sk-…`, `AKIA…`) 14 * - credential query parameters (`token=`, `refresh_token=`, `api_key=`, 15 * `secret=`, `signature=`, `password=`, …) 16 * - `Cookie:` / `Set-Cookie:` values 17 * 18 * The web/worker/realtime apps were audited for the same risk and have NO 19 * request-coupled HTTP log paths that emit headers, cookies, or auth values 20 * (the worker logs job/processor lifecycle; realtime logs channel lifecycle) 21 * — so the scrubber is applied where the exposure exists (the API). See 22 * docs/security/audit-report.md finding F-121. 23 * 24 * Deterministic + side-effect-free; safe to call on `undefined`-prone 25 * values (non-strings pass through untouched). 26 */ 27 28export const REDACTED = "[REDACTED]"; 29 30const BEARER = /\bBearer\s+[A-Za-z0-9\-._~+/]+={0,2}\b/gi; 31const JWT = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g; 32const TOKEN_PREFIX = /\b(fk|whsec|apk|ghp|gho|ghs|ghr|xox[baprs]|sk|AKIA)[A-Za-z0-9_-]{8,}\b/g; 33// Exact credential param names only (not substring matches, so an innocent 34// param can't be redacted): ?token=, ?refresh_token=, ?api_key=, … 35// Group 1 = the `?name=` prefix (preserved), group 2 = the value (redacted). 36const CRED_PARAM = /([?&](?:access_token|refresh_token|id_token|reset_token|token|api_key|apikey|secret|signature|password|session|session_id|state|code)=)([^&\s]*)/gi; 37const COOKIE = /\b(cookie|set-cookie):\s*[^\n,]+/gi; 38 39/** 40 * Redact secret-looking values in a log string. Returns the input 41 * unchanged (same type) when it is not a string. 42 */ 43export function scrubSecrets<T>(input: T): T { 44 if (typeof input !== "string" || input.length === 0) return input; 45 const s: string = input; 46 let out = s; 47 out = out.replace(BEARER, `Bearer ${REDACTED}`); 48 out = out.replace(JWT, REDACTED); 49 out = out.replace(TOKEN_PREFIX, REDACTED); 50 out = out.replace(CRED_PARAM, `$1${REDACTED}`); 51 out = out.replace(COOKIE, `$1: ${REDACTED}`); 52 return out as T; 53} 54