Smart object redaction for JavaScript applications - safe AND fast!
Redact JS objects with the same API as fast-redact, but uses innovative selective cloning instead of mutating the original. This provides immutability guarantees with performance competitive to fast-redact for real-world usage patterns.
npm install @pinojs/redact
const pinoRedact = require('@pinojs/redact')
const redact = pinoRedact({
paths: ['headers.cookie', 'headers.authorization', 'user.password']
})
const obj = {
headers: {
cookie: 'secret-session-token',
authorization: 'Bearer abc123',
'x-forwarded-for': '192.168.1.1'
},
user: {
name: 'john',
password: 'secret123'
}
}
console.log(redact(obj))
// Output: {"headers":{"cookie":"[REDACTED]","authorization":"[REDACTED]","x-forwarded-for":"192.168.1.1"},"user":{"name":"john","password":"[REDACTED]"}}
// Original object is completely unchanged:
console.log(obj.headers.cookie) // 'secret-session-token'
Creates a redaction function with the specified options.
- paths
string[]
(required): An array of strings describing the nested location of a key in an object - censor
any
(optional, default:'[REDACTED]'
): The value to replace sensitive data with. Can be a static value or function. - serialize
Function|boolean
(optional, default:JSON.stringify
): Serialization function. Set tofalse
to return the redacted object. - remove
boolean
(optional, default:false
): Remove redacted keys from serialized output - strict
boolean
(optional, default:true
): Throw on non-object values or pass through primitives
Supports the same path syntax as fast-redact:
- Dot notation:
'user.name'
,'headers.cookie'
- Bracket notation:
'user["password"]'
,'headers["X-Forwarded-For"]'
- Array indices:
'users[0].password'
,'items[1].secret'
- Wildcards:
- Terminal:
'users.*.password'
(redacts password for all users) - Intermediate:
'*.password'
(redacts password at any level) - Array wildcard:
'items.*'
(redacts all array elements)
- Terminal:
Custom censor value:
const redact = pinoRedact({
paths: ['password'],
censor: '***HIDDEN***'
})
Dynamic censor function:
const redact = pinoRedact({
paths: ['password'],
censor: (value, path) => `REDACTED:${path}`
})
Return object instead of JSON string:
const redact = pinoRedact({
paths: ['secret'],
serialize: false
})
const result = redact({ secret: 'hidden', public: 'data' })
console.log(result.secret) // '[REDACTED]'
console.log(result.public) // 'data'
// Restore original values
const restored = result.restore()
console.log(restored.secret) // 'hidden'
Custom serialization:
const redact = pinoRedact({
paths: ['password'],
serialize: obj => JSON.stringify(obj, null, 2)
})
Remove keys instead of redacting:
const redact = pinoRedact({
paths: ['password', 'user.secret'],
remove: true
})
const obj = { username: 'john', password: 'secret123', user: { name: 'Jane', secret: 'hidden' } }
console.log(redact(obj))
// Output: {"username":"john","user":{"name":"Jane"}}
// Note: 'password' and 'user.secret' are completely absent, not redacted
Wildcard patterns:
// Redact all properties in secrets object
const redact1 = pinoRedact({ paths: ['secrets.*'] })
// Redact password for any user
const redact2 = pinoRedact({ paths: ['users.*.password'] })
// Redact all items in an array
const redact3 = pinoRedact({ paths: ['items.*'] })
// Remove all secrets instead of redacting them
const redact4 = pinoRedact({ paths: ['secrets.*'], remove: true })
- No mutation: Original objects are never modified
- Selective cloning: Only clones paths that need redaction, shares references for everything else
- Restore capability: Can restore original values when
serialize: false
- Remove option: Full compatibility with fast-redact's
remove: true
option to completely omit keys from output - All path patterns: Supports same syntax including wildcards, bracket notation, and array indices
- Censor functions: Dynamic censoring with path information passed as arrays
- Serialization: Custom serializers and
serialize: false
mode
- Selective cloning: Analyzes redaction paths and only clones necessary object branches
- Reference sharing: Non-redacted properties maintain original object references
- Memory efficiency: Dramatically reduced memory usage for large objects with minimal redaction
- Setup-time optimization: Path analysis happens once during setup, not per redaction
- When immutability is critical
- When you need to preserve original objects
- When objects are shared across multiple contexts
- In functional programming environments
- When debugging and you need to compare before/after
- Large objects with selective redaction (now performance-competitive!)
- When memory efficiency with reference sharing is important
- When absolute maximum performance is critical
- In extremely high-throughput scenarios (>100,000 ops/sec)
- When you control the object lifecycle and mutation is acceptable
- Very small objects where setup overhead matters
@pinojs/redact uses selective cloning that provides good performance while maintaining immutability guarantees:
Operation Type | @pinojs/redact | fast-redact | Performance Ratio |
---|---|---|---|
Small objects | ~690ns | ~200ns | ~3.5x slower |
Large objects (minimal redaction) | ~18μs | ~17μs | ~same performance |
Large objects (wildcards) | ~48μs | ~37μs | ~1.3x slower |
No redaction (large objects) | ~18μs | ~17μs | ~same performance |
@pinojs/redact is performance-competitive with fast-redact for large objects.
- Selective cloning approach: Only clones object paths that need redaction
- Reference sharing: Non-redacted properties share original object references
- Setup-time optimization: Path analysis happens once, not per redaction
- Memory efficiency: Dramatically reduced memory usage for typical use cases
Small Objects (~180 bytes):
- @pinojs/redact: 690ns per operation
- fast-redact: 200ns per operation
- Slight setup overhead for small objects
Large Objects (~18KB, minimal redaction):
- @pinojs/redact: 18μs per operation
- fast-redact: 17μs per operation
- Near-identical performance
Large Objects (~18KB, wildcard patterns):
- @pinojs/redact: 48μs per operation
- fast-redact: 37μs per operation
- Competitive performance for complex patterns
Memory Considerations:
- @pinojs/redact: Selective reference sharing (much lower memory usage than before)
- fast-redact: Mutates in-place (lowest memory usage)
- Large objects with few redacted paths now share most references
Choose fast-redact when:
- Absolute maximum performance is critical (>100,000 ops/sec)
- Working with very small objects frequently
- Mutation is acceptable and controlled
- Every microsecond counts
Choose @pinojs/redact when:
- Immutability is required (with competitive performance)
- Objects are shared across contexts
- Large objects with selective redaction
- Memory efficiency through reference sharing is important
- Safety and functionality are priorities
- Most production applications (performance gap is minimal)
Run benchmarks yourself:
npm run bench
@pinojs/redact uses an innovative selective cloning approach that provides immutability guarantees while dramatically improving performance:
// Old approach: Deep clone entire object, then redact
const fullClone = deepClone(originalObject) // Clone everything
redact(fullClone, paths) // Then redact specific paths
// New approach: Analyze paths, clone only what's needed
const pathStructure = buildPathStructure(paths) // One-time setup
const selectiveClone = cloneOnlyNeededPaths(obj, pathStructure) // Smart cloning
redact(selectiveClone, paths) // Redact pre-identified paths
- Path Analysis: Pre-processes redaction paths into an efficient tree structure
- Selective Cloning: Only creates new objects for branches that contain redaction targets
- Reference Sharing: Non-redacted properties maintain exact same object references
- Setup Optimization: Path parsing happens once during redactor creation, not per redaction
const largeConfig = {
database: { /* large config object */ },
api: { /* another large config */ },
secrets: { password: 'hidden', apiKey: 'secret' }
}
const redact = pinoRedact({ paths: ['secrets.password'] })
const result = redact(largeConfig)
// Only secrets object is cloned, database and api share original references
console.log(result.database === largeConfig.database) // true - shared reference!
console.log(result.api === largeConfig.api) // true - shared reference!
console.log(result.secrets === largeConfig.secrets) // false - cloned for redaction
This approach provides immutability where it matters while sharing references where it's safe.
The remove: true
option provides full compatibility with fast-redact's key removal functionality:
const redact = pinoRedact({
paths: ['password', 'secrets.*', 'users.*.credentials'],
remove: true
})
const data = {
username: 'john',
password: 'secret123',
secrets: { apiKey: 'abc', token: 'xyz' },
users: [
{ name: 'Alice', credentials: { password: 'pass1' } },
{ name: 'Bob', credentials: { password: 'pass2' } }
]
}
console.log(redact(data))
// Output: {"username":"john","secrets":{},"users":[{"name":"Alice"},{"name":"Bob"}]}
Option | Behavior | Output Example |
---|---|---|
Default (redact) | Replaces values with censor | {"password":"[REDACTED]"} |
remove: true |
Completely omits keys | {} |
- Same output as fast-redact: Identical JSON output when using
remove: true
- Wildcard support: Works with all wildcard patterns (
*
,users.*
,items.*.secret
) - Array handling: Array items are set to
undefined
(omitted in JSON output) - Nested paths: Supports deep removal (
users.*.credentials.password
) - Serialize compatibility: Only works with
JSON.stringify
serializer (like fast-redact)
# Run unit tests
npm test
# Run integration tests comparing with fast-redact
npm run test:integration
# Run all tests (unit + integration)
npm run test:all
# Run benchmarks
npm run bench
- 16 unit tests: Core functionality and edge cases
- 16 integration tests: Output compatibility with fast-redact
- All major features: Paths, wildcards, serialization, custom censors
- Performance benchmarks: Direct comparison with fast-redact
MIT
Pull requests welcome! Please ensure all tests pass and add tests for new features.