redact secrets from logs by key, by path, and by what the value actually looks like.
npm i secret-redact
path based redaction only catches what you predicted:
pino({ redact: ['password', 'user.token'] })it does not catch a seed phrase pasted into a support note, a JWT inside an error message, or an api key in a url. those are the ones that hurt.
import { redact } from 'secret-redact'
redact({
user: 'alice',
password: 'hunter2',
note: 'cook voyage document eight skate token alien guide drink uncle term abuse'
})
// { user: 'alice', password: '[redacted]', note: '[redacted]' }it never mutates the input. you get a clean copy.
| detector | catches |
|---|---|
mnemonic |
12/15/18/21/24 word bip39 phrases |
private-key |
-----BEGIN … PRIVATE KEY----- |
hex-key |
64 hex chars, with or without 0x |
jwt |
eyJ….….…, scrubbed in place |
bearer |
Bearer <token>, keeps the word |
api-key |
npm_ ghp_ sk- sk_live_ AKIA xox[bp]- |
card-number |
13–19 digits, luhn checked so order ids do not trip it |
detectors that can locate the match only replace that part:
redactString('auth failed for eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.sig then retried')
// 'auth failed for [redacted] then retried'redact(data, {
keys: ['internalId'], // added to the built-in list
paths: ['items.*.card', 'user.ssn']
})built-in keys cover password secret token authorization cookie mnemonic seed pin cvv ssn signature and friends. matching ignores case and _/-, so API_KEY, apiKey and api-key are the same key.
for values you hold in memory rather than log:
import { secret } from 'secret-redact'
const apiKey = secret(process.env.API_KEY, 'api key')
console.log(apiKey) // [api key]
`${apiKey}` // '[api key]'
JSON.stringify({ apiKey }) // {"apiKey":"[api key]"}
apiKey.expose() // the real value, and it grepsthe value is a #private field, so it survives spread, Object.keys and JSON.stringify.
import { guard } from 'secret-redact'
const log = guard(console)
log.info('login', { user: 'alice', password: 'hunter2' })
// login { user: 'alice', password: '[redacted]' }circular references become [circular], depth is capped, Error keeps its name and stack but scrubs the message, and Map/Set/Date are handled.
hex-key flags any 64 hex chars — which includes transaction hashes. that is on purpose. a 32 byte hex blob in a log is not worth the risk of it being a key. pass detectors yourself if you disagree.
redact(value, opts?) |
deep copy, redacted |
redactString(str, opts?) |
detectors only |
secret(value, label?) |
Secret<T> |
guard(logger, opts?) |
wraps every method |
DEFAULT_DETECTORS |
the array, to extend or filter |
zero dependencies. types included.
MIT