/** * lib/redact.mjs — 비밀값 가리기(§37). * * 감사 기록·알림·작업 결과·오류 메시지·로그에 들어가는 모든 객체는 이 함수를 거친다. * - 키 이름이 비밀로 보이면(password, secret, token, key, credential, cookie, authorization, recovery, otp, privkey …) 값을 '[redacted]' 로 바꾼다. * - 문자열 값이 비밀 형태(sk-…, re_…, au_…, JWT, 40자 이상 base64url/hex, PEM 블록)면 가린다. * - 깊이 6 까지 재귀. 배열·객체·문자열 모두. */ const KEY_RE = /pass(word|wd)?|secret|token|api[-_]?key|private[-_]?key|privkey|credential|cookie|authorization|recovery|otp|totp|session|vault|dkim|\bkey\b/i; const VALUE_RES = [ /^(sk|rk|re|au|aw|ag|ghp|gho|xox[a-z])[-_][A-Za-z0-9_-]{16,}$/, // 서비스 키 접두어 형식 /^eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}$/, // JWT /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM 개인키 /^[A-Za-z0-9_-]{40,}$/, // 긴 무작위 토큰 /^[a-f0-9]{48,}$/i, // 긴 hex 비밀 ]; const KEEP_KEYS = /^(id|_id|site_id|group_id|user_id|job_id|idempotency_?key|incident_?key|public_?key_?fingerprint|key_?name|keyName|kind|resource)$/i; /** 값 하나가 비밀 형태인지 */ export function looksSecret(v) { if (typeof v !== 'string' || v.length < 12) return false; return VALUE_RES.some((re) => re.test(v)); } /** * 객체/배열/문자열에서 비밀값을 가린다. 원본은 바꾸지 않는다. * @param {*} v * @param {number} depth 내부 재귀 깊이 */ export function redact(v, depth = 0) { if (depth > 6) return '[depth]'; if (v == null) return v; if (typeof v === 'string') return looksSecret(v) ? '[redacted]' : v; if (Array.isArray(v)) return v.map((x) => redact(x, depth + 1)); if (typeof v === 'object') { const out = {}; for (const [k, val] of Object.entries(v)) { if (!KEEP_KEYS.test(k) && KEY_RE.test(k)) { out[k] = val == null || val === '' ? val : '[redacted]'; continue; } out[k] = redact(val, depth + 1); } return out; } return v; } /** 오류 객체를 기록용 안전 문자열로 */ export function safeError(e) { const m = e && e.message ? String(e.message) : String(e || ''); return redact(m.replace(/(Bearer\s+)[A-Za-z0-9._-]+/g, '$1[redacted]')); } /** 비밀값의 앞뒤만 보이는 힌트(예: au_12…9f) — 화면에 "등록됨" 을 보여 주되 값은 못 알아보게 */ export function hint(secret) { const s = String(secret || ''); if (s.length < 10) return s ? '••••' : ''; return `${s.slice(0, 4)}…${s.slice(-2)}`; }