const ACCESS_KEY_PATTERN = /\b(A[KS]IA[A-Z0-9]{16})\b/g
const SECRET_KEY_PATTERN =
/(?:aws_secret_access_key|secret[_-]?access[_-]?key|secretAccessKey)\s*[:=]\s*["']?[A-Za-z0-9/+=]{20,}["']?/gi
const PRIVATE_KEY_PATTERN =
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
const SESSION_TOKEN_PATTERN =
/(?:aws_session_token|sessionToken|x-amz-security-token)\s*[:=]\s*["']?[A-Za-z0-9/+=]{20,}["']?/gi
const PLACEHOLDER_PATTERN = /\{\{secret:[A-Za-z0-9._-]+(?:\|[^}]*)?\}\}/g
export function looksLikeCredentialText(value: string): boolean {
return (
ACCESS_KEY_PATTERN.test(value) ||
SECRET_KEY_PATTERN.test(value) ||
PRIVATE_KEY_PATTERN.test(value) ||
SESSION_TOKEN_PATTERN.test(value) ||
PLACEHOLDER_PATTERN.test(value)
)
}
export function redactSecrets(value: string): string {
return value
.replace(PRIVATE_KEY_PATTERN, '[redacted-private-key]')
.replace(SECRET_KEY_PATTERN, '[redacted-secret-key]')
.replace(SESSION_TOKEN_PATTERN, '[redacted-session-token]')
.replace(ACCESS_KEY_PATTERN, '[redacted-access-key]')
.replace(PLACEHOLDER_PATTERN, '{{secret:<name>}}')
}
export function redactUnknown(value: unknown, depth = 0): unknown {
if (depth > 6) return '[truncated]'
if (typeof value === 'string') {
const trimmed = value.length > 4000 ? value.slice(0, 4000) + '…' : value
return redactSecrets(trimmed)
}
if (Array.isArray(value)) {
return value.slice(0, 50).map((item) => redactUnknown(item, depth + 1))
}
if (value && typeof value === 'object') {
const out: Record<string, unknown> = {}
for (const [key, item] of Object.entries(value)) {
if (/secret|token|password|authorization|credential/i.test(key)) {
out[key] = '[redacted]'
continue
}
out[key] = redactUnknown(item, depth + 1)
}
return out
}
return value
}