// Regex patterns for common secret shapes. Tuned for low false-positive on prose, // high recall on anything matching a documented token format. export type SecretMatch = { type: string preview: string start: number end: number source: 'plain' | 'base64' | 'url' } type Pattern = { type: string; re: RegExp } const PATTERNS: Pattern[] = [ { type: 'anthropic_api_key', re: /sk-ant-(?:api|admin)\d{2}-[A-Za-z0-9_\-]{32,}/g }, { type: 'claude_oauth_token', re: /sk-ant-oat\d{2}-[A-Za-z0-9_\-]{32,}/g }, { type: 'openai_api_key', re: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}T3BlbkFJ[A-Za-z0-9_-]{20,}/g }, { type: 'aws_access_key', re: /(? 8 ? `${mid.slice(0, 4)}…${mid.slice(-3)}` : '***' return `${head}${masked}${tail}` } function scanPlain(text: string): SecretMatch[] { const out: SecretMatch[] = [] for (const { type, re } of PATTERNS) { re.lastIndex = 0 let m: RegExpExecArray | null while ((m = re.exec(text)) !== null) { out.push({ type, preview: preview(text, m.index, m.index + m[0].length), start: m.index, end: m.index + m[0].length, source: 'plain', }) } } return out } function scanBase64(text: string): SecretMatch[] { const matches: SecretMatch[] = [] BASE64_CANDIDATE_RE.lastIndex = 0 let m: RegExpExecArray | null while ((m = BASE64_CANDIDATE_RE.exec(text)) !== null) { let decoded: string try { decoded = Buffer.from(m[0], 'base64').toString('utf8') } catch { continue } // Only flag if decoded form contains a known secret pattern. const inner = scanPlain(decoded) if (inner.length === 0) continue for (const hit of inner) { matches.push({ type: hit.type, preview: preview(text, m.index, m.index + m[0].length), start: m.index, end: m.index + m[0].length, source: 'base64', }) } } return matches } function scanUrl(text: string): SecretMatch[] { const matches: SecretMatch[] = [] URL_ENCODED_RE.lastIndex = 0 let m: RegExpExecArray | null while ((m = URL_ENCODED_RE.exec(text)) !== null) { let decoded: string try { decoded = decodeURIComponent(m[0]) } catch { continue } const inner = scanPlain(decoded) if (inner.length === 0) continue for (const hit of inner) { matches.push({ type: hit.type, preview: preview(text, m.index, m.index + m[0].length), start: m.index, end: m.index + m[0].length, source: 'url', }) } } return matches } export function scanForSecrets(text: string): SecretMatch[] { return [...scanPlain(text), ...scanBase64(text), ...scanUrl(text)].sort( (a, b) => a.start - b.start ) } export function redactSecrets(text: string): { text: string; matches: SecretMatch[] } { const matches = scanForSecrets(text) if (matches.length === 0) return { text, matches } // Redact from the end so earlier indices stay valid. let out = text for (const hit of [...matches].sort((a, b) => b.start - a.start)) { out = `${out.slice(0, hit.start)}[REDACTED:${hit.type}]${out.slice(hit.end)}` } return { text: out, matches } }