First commit
This commit is contained in:
13
node_modules/@pinojs/redact/.github/dependabot.yml
generated
vendored
Normal file
13
node_modules/@pinojs/redact/.github/dependabot.yml
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 10
|
||||
48
node_modules/@pinojs/redact/.github/workflows/ci.yml
generated
vendored
Normal file
48
node_modules/@pinojs/redact/.github/workflows/ci.yml
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'v*'
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
|
||||
# This allows a subsequently queued workflow run to interrupt previous runs
|
||||
concurrency:
|
||||
group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: ${{ matrix.node-version }} ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macOS-latest, windows-latest, ubuntu-latest]
|
||||
node-version: [18, 20, 22, 24]
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm i --ignore-scripts
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
43
node_modules/@pinojs/redact/.github/workflows/publish-release.yml
generated
vendored
Normal file
43
node_modules/@pinojs/redact/.github/workflows/publish-release.yml
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Publish release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'The version number to tag and release'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Release as pre-release'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
release-npm:
|
||||
runs-on: ubuntu-latest
|
||||
environment: main
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v4
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: npm install npm -g
|
||||
- run: npm install
|
||||
- name: Change version number and sync
|
||||
run: |
|
||||
node scripts/sync-version.mjs ${{ inputs.version }}
|
||||
- name: GIT commit and push all changed files
|
||||
run: |
|
||||
git config --global user.name "mcollina"
|
||||
git config --global user.email "hello@matteocollina.com"
|
||||
git commit -n -a -m "Bumped v${{ inputs.version }}"
|
||||
git push origin HEAD:${{ github.ref }}
|
||||
- run: npm publish --access public --tag ${{ inputs.prerelease == true && 'next' || 'latest' }}
|
||||
- name: 'Create release notes'
|
||||
run: |
|
||||
npx @matteo.collina/release-notes -a ${{ secrets.GITHUB_TOKEN }} -t v${{ inputs.version }} -r redact -o pinojs ${{ github.event.inputs.prerelease == 'true' && '-p' || '' }} -c ${{ github.ref }}
|
||||
21
node_modules/@pinojs/redact/LICENSE
generated
vendored
Normal file
21
node_modules/@pinojs/redact/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 pinojs contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
350
node_modules/@pinojs/redact/README.md
generated
vendored
Normal file
350
node_modules/@pinojs/redact/README.md
generated
vendored
Normal file
@@ -0,0 +1,350 @@
|
||||
# @pinojs/redact
|
||||
|
||||
> Smart object redaction for JavaScript applications - safe AND fast!
|
||||
|
||||
Redact JS objects with the same API as [fast-redact](https://github.com/davidmarkclements/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.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @pinojs/redact
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const slowRedact = require('@pinojs/redact')
|
||||
|
||||
const redact = slowRedact({
|
||||
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'
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### slowRedact(options) → Function
|
||||
|
||||
Creates a redaction function with the specified options.
|
||||
|
||||
#### 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 to `false` 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
|
||||
|
||||
#### Path Syntax
|
||||
|
||||
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)
|
||||
|
||||
#### Examples
|
||||
|
||||
**Custom censor value:**
|
||||
```js
|
||||
const redact = slowRedact({
|
||||
paths: ['password'],
|
||||
censor: '***HIDDEN***'
|
||||
})
|
||||
```
|
||||
|
||||
**Dynamic censor function:**
|
||||
```js
|
||||
const redact = slowRedact({
|
||||
paths: ['password'],
|
||||
censor: (value, path) => `REDACTED:${path}`
|
||||
})
|
||||
```
|
||||
|
||||
**Return object instead of JSON string:**
|
||||
```js
|
||||
const redact = slowRedact({
|
||||
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:**
|
||||
```js
|
||||
const redact = slowRedact({
|
||||
paths: ['password'],
|
||||
serialize: obj => JSON.stringify(obj, null, 2)
|
||||
})
|
||||
```
|
||||
|
||||
**Remove keys instead of redacting:**
|
||||
```js
|
||||
const redact = slowRedact({
|
||||
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:**
|
||||
```js
|
||||
// Redact all properties in secrets object
|
||||
const redact1 = slowRedact({ paths: ['secrets.*'] })
|
||||
|
||||
// Redact password for any user
|
||||
const redact2 = slowRedact({ paths: ['users.*.password'] })
|
||||
|
||||
// Redact all items in an array
|
||||
const redact3 = slowRedact({ paths: ['items.*'] })
|
||||
|
||||
// Remove all secrets instead of redacting them
|
||||
const redact4 = slowRedact({ paths: ['secrets.*'], remove: true })
|
||||
```
|
||||
|
||||
## Key Differences from fast-redact
|
||||
|
||||
### Safety First
|
||||
- **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`
|
||||
|
||||
### Feature Compatibility
|
||||
- **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
|
||||
|
||||
### Smart Performance Approach
|
||||
- **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 to Use @pinojs/redact
|
||||
- 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 to Use fast-redact
|
||||
- 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
|
||||
|
||||
## Performance Benchmarks
|
||||
|
||||
@pinojs/redact uses **selective cloning** that provides good performance while maintaining immutability guarantees:
|
||||
|
||||
### Performance Results
|
||||
|
||||
| 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** |
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
@pinojs/redact is performance-competitive with fast-redact for large objects.
|
||||
|
||||
1. **Selective cloning approach**: Only clones object paths that need redaction
|
||||
2. **Reference sharing**: Non-redacted properties share original object references
|
||||
3. **Setup-time optimization**: Path analysis happens once, not per redaction
|
||||
4. **Memory efficiency**: Dramatically reduced memory usage for typical use cases
|
||||
|
||||
### Benchmark Details
|
||||
|
||||
**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
|
||||
|
||||
### When Performance Matters
|
||||
|
||||
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:
|
||||
```bash
|
||||
npm run bench
|
||||
```
|
||||
|
||||
## How Selective Cloning Works
|
||||
|
||||
@pinojs/redact uses an innovative **selective cloning** approach that provides immutability guarantees while dramatically improving performance:
|
||||
|
||||
### Traditional Approach (before optimization)
|
||||
```js
|
||||
// Old approach: Deep clone entire object, then redact
|
||||
const fullClone = deepClone(originalObject) // Clone everything
|
||||
redact(fullClone, paths) // Then redact specific paths
|
||||
```
|
||||
|
||||
### Selective Cloning Approach (current)
|
||||
```js
|
||||
// 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
|
||||
```
|
||||
|
||||
### Key Innovations
|
||||
|
||||
1. **Path Analysis**: Pre-processes redaction paths into an efficient tree structure
|
||||
2. **Selective Cloning**: Only creates new objects for branches that contain redaction targets
|
||||
3. **Reference Sharing**: Non-redacted properties maintain exact same object references
|
||||
4. **Setup Optimization**: Path parsing happens once during redactor creation, not per redaction
|
||||
|
||||
### Example: Reference Sharing in Action
|
||||
|
||||
```js
|
||||
const largeConfig = {
|
||||
database: { /* large config object */ },
|
||||
api: { /* another large config */ },
|
||||
secrets: { password: 'hidden', apiKey: 'secret' }
|
||||
}
|
||||
|
||||
const redact = slowRedact({ 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**.
|
||||
|
||||
## Remove Option
|
||||
|
||||
The `remove: true` option provides full compatibility with fast-redact's key removal functionality:
|
||||
|
||||
```js
|
||||
const redact = slowRedact({
|
||||
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"}]}
|
||||
```
|
||||
|
||||
### Remove vs Redact Behavior
|
||||
|
||||
| Option | Behavior | Output Example |
|
||||
|--------|----------|----------------|
|
||||
| Default (redact) | Replaces values with censor | `{"password":"[REDACTED]"}` |
|
||||
| `remove: true` | Completely omits keys | `{}` |
|
||||
|
||||
### Compatibility Notes
|
||||
|
||||
- **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)
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# 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
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- **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
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests welcome! Please ensure all tests pass and add tests for new features.
|
||||
184
node_modules/@pinojs/redact/benchmarks/basic.js
generated
vendored
Normal file
184
node_modules/@pinojs/redact/benchmarks/basic.js
generated
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
const { bench, group, run } = require('mitata')
|
||||
const slowRedact = require('../index.js')
|
||||
const fastRedact = require('fast-redact')
|
||||
|
||||
// Test objects
|
||||
const smallObj = {
|
||||
user: { name: 'john', password: 'secret123' },
|
||||
headers: { cookie: 'session-token', authorization: 'Bearer abc123' }
|
||||
}
|
||||
|
||||
const largeObj = {
|
||||
users: [],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
secret: 'app-secret-key',
|
||||
database: {
|
||||
host: 'localhost',
|
||||
password: 'db-password'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate users array with for loop instead of Array.from
|
||||
for (let i = 0; i < 100; i++) {
|
||||
largeObj.users.push({
|
||||
id: i,
|
||||
name: `user${i}`,
|
||||
email: `user${i}@example.com`,
|
||||
password: `secret${i}`,
|
||||
profile: {
|
||||
age: 20 + (i % 50),
|
||||
preferences: {
|
||||
theme: 'dark',
|
||||
notifications: true,
|
||||
apiKey: `key-${i}-secret`
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Redaction configurations
|
||||
const basicSlowRedact = slowRedact({
|
||||
paths: ['user.password', 'headers.cookie']
|
||||
})
|
||||
|
||||
const basicFastRedact = fastRedact({
|
||||
paths: ['user.password', 'headers.cookie']
|
||||
})
|
||||
|
||||
const wildcardSlowRedact = slowRedact({
|
||||
paths: ['users.*.password', 'users.*.profile.preferences.apiKey']
|
||||
})
|
||||
|
||||
const wildcardFastRedact = fastRedact({
|
||||
paths: ['users.*.password', 'users.*.profile.preferences.apiKey']
|
||||
})
|
||||
|
||||
const deepSlowRedact = slowRedact({
|
||||
paths: ['metadata.secret', 'metadata.database.password']
|
||||
})
|
||||
|
||||
const deepFastRedact = fastRedact({
|
||||
paths: ['metadata.secret', 'metadata.database.password']
|
||||
})
|
||||
|
||||
group('Small Object Redaction - @pinojs/redact', () => {
|
||||
bench('basic paths', () => {
|
||||
basicSlowRedact(smallObj)
|
||||
})
|
||||
|
||||
bench('serialize: false', () => {
|
||||
const redact = slowRedact({
|
||||
paths: ['user.password'],
|
||||
serialize: false
|
||||
})
|
||||
redact(smallObj)
|
||||
})
|
||||
|
||||
bench('custom censor function', () => {
|
||||
const redact = slowRedact({
|
||||
paths: ['user.password'],
|
||||
censor: (value, path) => `HIDDEN:${path}`
|
||||
})
|
||||
redact(smallObj)
|
||||
})
|
||||
})
|
||||
|
||||
group('Small Object Redaction - fast-redact', () => {
|
||||
bench('basic paths', () => {
|
||||
basicFastRedact(smallObj)
|
||||
})
|
||||
|
||||
bench('serialize: false', () => {
|
||||
const redact = fastRedact({
|
||||
paths: ['user.password'],
|
||||
serialize: false
|
||||
})
|
||||
redact(smallObj)
|
||||
})
|
||||
|
||||
bench('custom censor function', () => {
|
||||
const redact = fastRedact({
|
||||
paths: ['user.password'],
|
||||
censor: (value, path) => `HIDDEN:${path}`
|
||||
})
|
||||
redact(smallObj)
|
||||
})
|
||||
})
|
||||
|
||||
group('Large Object Redaction - @pinojs/redact', () => {
|
||||
bench('wildcard patterns', () => {
|
||||
wildcardSlowRedact(largeObj)
|
||||
})
|
||||
|
||||
bench('deep nested paths', () => {
|
||||
deepSlowRedact(largeObj)
|
||||
})
|
||||
|
||||
bench('multiple wildcards', () => {
|
||||
const redact = slowRedact({
|
||||
paths: ['users.*.password', 'users.*.profile.preferences.*']
|
||||
})
|
||||
redact(largeObj)
|
||||
})
|
||||
})
|
||||
|
||||
group('Large Object Redaction - fast-redact', () => {
|
||||
bench('wildcard patterns', () => {
|
||||
wildcardFastRedact(largeObj)
|
||||
})
|
||||
|
||||
bench('deep nested paths', () => {
|
||||
deepFastRedact(largeObj)
|
||||
})
|
||||
|
||||
bench('multiple wildcards', () => {
|
||||
const redact = fastRedact({
|
||||
paths: ['users.*.password', 'users.*.profile.preferences.*']
|
||||
})
|
||||
redact(largeObj)
|
||||
})
|
||||
})
|
||||
|
||||
group('Direct Performance Comparison', () => {
|
||||
bench('@pinojs/redact - basic paths', () => {
|
||||
basicSlowRedact(smallObj)
|
||||
})
|
||||
|
||||
bench('fast-redact - basic paths', () => {
|
||||
basicFastRedact(smallObj)
|
||||
})
|
||||
|
||||
bench('@pinojs/redact - wildcards', () => {
|
||||
wildcardSlowRedact(largeObj)
|
||||
})
|
||||
|
||||
bench('fast-redact - wildcards', () => {
|
||||
wildcardFastRedact(largeObj)
|
||||
})
|
||||
})
|
||||
|
||||
group('Object Cloning Overhead', () => {
|
||||
bench('@pinojs/redact - no redaction (clone only)', () => {
|
||||
const redact = slowRedact({ paths: [] })
|
||||
redact(smallObj)
|
||||
})
|
||||
|
||||
bench('fast-redact - no redaction', () => {
|
||||
const redact = fastRedact({ paths: [] })
|
||||
redact(smallObj)
|
||||
})
|
||||
|
||||
bench('@pinojs/redact - large object clone', () => {
|
||||
const redact = slowRedact({ paths: [] })
|
||||
redact(largeObj)
|
||||
})
|
||||
|
||||
bench('fast-redact - large object', () => {
|
||||
const redact = fastRedact({ paths: [] })
|
||||
redact(largeObj)
|
||||
})
|
||||
})
|
||||
|
||||
run()
|
||||
1
node_modules/@pinojs/redact/eslint.config.js
generated
vendored
Normal file
1
node_modules/@pinojs/redact/eslint.config.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('neostandard')()
|
||||
52
node_modules/@pinojs/redact/index.d.ts
generated
vendored
Normal file
52
node_modules/@pinojs/redact/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
export = F;
|
||||
|
||||
/**
|
||||
* When called without any options, or with a zero length paths array, @pinojs/redact will return JSON.stringify or the serialize option, if set.
|
||||
* @param redactOptions
|
||||
* @param redactOptions.paths An array of strings describing the nested location of a key in an object.
|
||||
* @param redactOptions.censor This is the value which overwrites redacted properties.
|
||||
* @param redactOptions.remove The remove option, when set to true will cause keys to be removed from the serialized output.
|
||||
* @param redactOptions.serialize The serialize option may either be a function or a boolean. If a function is supplied, this will be used to serialize the redacted object.
|
||||
* @param redactOptions.strict The strict option, when set to true, will cause the redactor function to throw if instead of an object it finds a primitive.
|
||||
* @returns Redacted value from input
|
||||
*/
|
||||
declare function F(
|
||||
redactOptions: F.RedactOptionsNoSerialize
|
||||
): F.redactFnNoSerialize;
|
||||
declare function F(redactOptions?: F.RedactOptions): F.redactFn;
|
||||
|
||||
declare namespace F {
|
||||
/** Redacts input */
|
||||
type redactFn = <T>(input: T) => string | T;
|
||||
|
||||
/** Redacts input without serialization */
|
||||
type redactFnNoSerialize = redactFn & {
|
||||
/** Method that allowing the redacted keys to be restored with the original data. Supplied only when serialize option set to false. */
|
||||
restore<T>(input: T): T;
|
||||
};
|
||||
|
||||
interface RedactOptions {
|
||||
/** An array of strings describing the nested location of a key in an object. */
|
||||
paths?: string[] | undefined;
|
||||
|
||||
/** This is the value which overwrites redacted properties. */
|
||||
censor?: string | ((v: any) => any) | undefined;
|
||||
|
||||
/** The remove option, when set to true will cause keys to be removed from the serialized output. */
|
||||
remove?: boolean | undefined;
|
||||
|
||||
/**
|
||||
* The serialize option may either be a function or a boolean. If a function is supplied, this will be used to serialize the redacted object.
|
||||
* The default serialize is the function JSON.stringify
|
||||
*/
|
||||
serialize?: boolean | ((v: any) => any) | undefined;
|
||||
|
||||
/** The strict option, when set to true, will cause the redactor function to throw if instead of an object it finds a primitive. */
|
||||
strict?: boolean | undefined;
|
||||
}
|
||||
|
||||
/** RedactOptions without serialization. Instead of the serialized object, the output of the redactor function will be the mutated object itself. */
|
||||
interface RedactOptionsNoSerialize extends RedactOptions {
|
||||
serialize: false;
|
||||
}
|
||||
}
|
||||
529
node_modules/@pinojs/redact/index.js
generated
vendored
Normal file
529
node_modules/@pinojs/redact/index.js
generated
vendored
Normal file
@@ -0,0 +1,529 @@
|
||||
'use strict'
|
||||
|
||||
function deepClone (obj) {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime())
|
||||
}
|
||||
|
||||
if (obj instanceof Array) {
|
||||
const cloned = []
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
cloned[i] = deepClone(obj[i])
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const cloned = Object.create(Object.getPrototypeOf(obj))
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
cloned[key] = deepClone(obj[key])
|
||||
}
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
function parsePath (path) {
|
||||
const parts = []
|
||||
let current = ''
|
||||
let inBrackets = false
|
||||
let inQuotes = false
|
||||
let quoteChar = ''
|
||||
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const char = path[i]
|
||||
|
||||
if (!inBrackets && char === '.') {
|
||||
if (current) {
|
||||
parts.push(current)
|
||||
current = ''
|
||||
}
|
||||
} else if (char === '[') {
|
||||
if (current) {
|
||||
parts.push(current)
|
||||
current = ''
|
||||
}
|
||||
inBrackets = true
|
||||
} else if (char === ']' && inBrackets) {
|
||||
// Always push the current value when closing brackets, even if it's an empty string
|
||||
parts.push(current)
|
||||
current = ''
|
||||
inBrackets = false
|
||||
inQuotes = false
|
||||
} else if ((char === '"' || char === "'") && inBrackets) {
|
||||
if (!inQuotes) {
|
||||
inQuotes = true
|
||||
quoteChar = char
|
||||
} else if (char === quoteChar) {
|
||||
inQuotes = false
|
||||
quoteChar = ''
|
||||
} else {
|
||||
current += char
|
||||
}
|
||||
} else {
|
||||
current += char
|
||||
}
|
||||
}
|
||||
|
||||
if (current) {
|
||||
parts.push(current)
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
function setValue (obj, parts, value) {
|
||||
let current = obj
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const key = parts[i]
|
||||
// Type safety: Check if current is an object before using 'in' operator
|
||||
if (typeof current !== 'object' || current === null || !(key in current)) {
|
||||
return false // Path doesn't exist, don't create it
|
||||
}
|
||||
if (typeof current[key] !== 'object' || current[key] === null) {
|
||||
return false // Path doesn't exist properly
|
||||
}
|
||||
current = current[key]
|
||||
}
|
||||
|
||||
const lastKey = parts[parts.length - 1]
|
||||
if (lastKey === '*') {
|
||||
if (Array.isArray(current)) {
|
||||
for (let i = 0; i < current.length; i++) {
|
||||
current[i] = value
|
||||
}
|
||||
} else if (typeof current === 'object' && current !== null) {
|
||||
for (const key in current) {
|
||||
if (Object.prototype.hasOwnProperty.call(current, key)) {
|
||||
current[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Type safety: Check if current is an object before using 'in' operator
|
||||
if (typeof current === 'object' && current !== null && lastKey in current && Object.prototype.hasOwnProperty.call(current, lastKey)) {
|
||||
current[lastKey] = value
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function removeKey (obj, parts) {
|
||||
let current = obj
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const key = parts[i]
|
||||
// Type safety: Check if current is an object before using 'in' operator
|
||||
if (typeof current !== 'object' || current === null || !(key in current)) {
|
||||
return false // Path doesn't exist, don't create it
|
||||
}
|
||||
if (typeof current[key] !== 'object' || current[key] === null) {
|
||||
return false // Path doesn't exist properly
|
||||
}
|
||||
current = current[key]
|
||||
}
|
||||
|
||||
const lastKey = parts[parts.length - 1]
|
||||
if (lastKey === '*') {
|
||||
if (Array.isArray(current)) {
|
||||
// For arrays, we can't really "remove" all items as that would change indices
|
||||
// Instead, we set them to undefined which will be omitted by JSON.stringify
|
||||
for (let i = 0; i < current.length; i++) {
|
||||
current[i] = undefined
|
||||
}
|
||||
} else if (typeof current === 'object' && current !== null) {
|
||||
for (const key in current) {
|
||||
if (Object.prototype.hasOwnProperty.call(current, key)) {
|
||||
delete current[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Type safety: Check if current is an object before using 'in' operator
|
||||
if (typeof current === 'object' && current !== null && lastKey in current && Object.prototype.hasOwnProperty.call(current, lastKey)) {
|
||||
delete current[lastKey]
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Sentinel object to distinguish between undefined value and non-existent path
|
||||
const PATH_NOT_FOUND = Symbol('PATH_NOT_FOUND')
|
||||
|
||||
function getValueIfExists (obj, parts) {
|
||||
let current = obj
|
||||
|
||||
for (const part of parts) {
|
||||
if (current === null || current === undefined) {
|
||||
return PATH_NOT_FOUND
|
||||
}
|
||||
// Type safety: Check if current is an object before property access
|
||||
if (typeof current !== 'object' || current === null) {
|
||||
return PATH_NOT_FOUND
|
||||
}
|
||||
// Check if the property exists before accessing it
|
||||
if (!(part in current)) {
|
||||
return PATH_NOT_FOUND
|
||||
}
|
||||
current = current[part]
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
function getValue (obj, parts) {
|
||||
let current = obj
|
||||
|
||||
for (const part of parts) {
|
||||
if (current === null || current === undefined) {
|
||||
return undefined
|
||||
}
|
||||
// Type safety: Check if current is an object before property access
|
||||
if (typeof current !== 'object' || current === null) {
|
||||
return undefined
|
||||
}
|
||||
current = current[part]
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
function redactPaths (obj, paths, censor, remove = false) {
|
||||
for (const path of paths) {
|
||||
const parts = parsePath(path)
|
||||
|
||||
if (parts.includes('*')) {
|
||||
redactWildcardPath(obj, parts, censor, path, remove)
|
||||
} else {
|
||||
if (remove) {
|
||||
removeKey(obj, parts)
|
||||
} else {
|
||||
// Get value only if path exists - single traversal
|
||||
const value = getValueIfExists(obj, parts)
|
||||
if (value === PATH_NOT_FOUND) {
|
||||
continue
|
||||
}
|
||||
|
||||
const actualCensor = typeof censor === 'function'
|
||||
? censor(value, parts)
|
||||
: censor
|
||||
setValue(obj, parts, actualCensor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function redactWildcardPath (obj, parts, censor, originalPath, remove = false) {
|
||||
const wildcardIndex = parts.indexOf('*')
|
||||
|
||||
if (wildcardIndex === parts.length - 1) {
|
||||
const parentParts = parts.slice(0, -1)
|
||||
let current = obj
|
||||
|
||||
for (const part of parentParts) {
|
||||
if (current === null || current === undefined) return
|
||||
// Type safety: Check if current is an object before property access
|
||||
if (typeof current !== 'object' || current === null) return
|
||||
current = current[part]
|
||||
}
|
||||
|
||||
if (Array.isArray(current)) {
|
||||
if (remove) {
|
||||
// For arrays, set all items to undefined which will be omitted by JSON.stringify
|
||||
for (let i = 0; i < current.length; i++) {
|
||||
current[i] = undefined
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < current.length; i++) {
|
||||
const indexPath = [...parentParts, i.toString()]
|
||||
const actualCensor = typeof censor === 'function'
|
||||
? censor(current[i], indexPath)
|
||||
: censor
|
||||
current[i] = actualCensor
|
||||
}
|
||||
}
|
||||
} else if (typeof current === 'object' && current !== null) {
|
||||
if (remove) {
|
||||
// Collect keys to delete to avoid issues with deleting during iteration
|
||||
const keysToDelete = []
|
||||
for (const key in current) {
|
||||
if (Object.prototype.hasOwnProperty.call(current, key)) {
|
||||
keysToDelete.push(key)
|
||||
}
|
||||
}
|
||||
for (const key of keysToDelete) {
|
||||
delete current[key]
|
||||
}
|
||||
} else {
|
||||
for (const key in current) {
|
||||
const keyPath = [...parentParts, key]
|
||||
const actualCensor = typeof censor === 'function'
|
||||
? censor(current[key], keyPath)
|
||||
: censor
|
||||
current[key] = actualCensor
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
redactIntermediateWildcard(obj, parts, censor, wildcardIndex, originalPath, remove)
|
||||
}
|
||||
}
|
||||
|
||||
function redactIntermediateWildcard (obj, parts, censor, wildcardIndex, originalPath, remove = false) {
|
||||
const beforeWildcard = parts.slice(0, wildcardIndex)
|
||||
const afterWildcard = parts.slice(wildcardIndex + 1)
|
||||
const pathArray = [] // Cached array to avoid allocations
|
||||
|
||||
function traverse (current, pathLength) {
|
||||
if (pathLength === beforeWildcard.length) {
|
||||
if (Array.isArray(current)) {
|
||||
for (let i = 0; i < current.length; i++) {
|
||||
pathArray[pathLength] = i.toString()
|
||||
traverse(current[i], pathLength + 1)
|
||||
}
|
||||
} else if (typeof current === 'object' && current !== null) {
|
||||
for (const key in current) {
|
||||
pathArray[pathLength] = key
|
||||
traverse(current[key], pathLength + 1)
|
||||
}
|
||||
}
|
||||
} else if (pathLength < beforeWildcard.length) {
|
||||
const nextKey = beforeWildcard[pathLength]
|
||||
// Type safety: Check if current is an object before using 'in' operator
|
||||
if (current && typeof current === 'object' && current !== null && nextKey in current) {
|
||||
pathArray[pathLength] = nextKey
|
||||
traverse(current[nextKey], pathLength + 1)
|
||||
}
|
||||
} else {
|
||||
// Check if afterWildcard contains more wildcards
|
||||
if (afterWildcard.includes('*')) {
|
||||
// Recursively handle remaining wildcards
|
||||
// Wrap censor to prepend current path context
|
||||
const wrappedCensor = typeof censor === 'function'
|
||||
? (value, path) => {
|
||||
const fullPath = [...pathArray.slice(0, pathLength), ...path]
|
||||
return censor(value, fullPath)
|
||||
}
|
||||
: censor
|
||||
redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove)
|
||||
} else {
|
||||
// No more wildcards, apply the redaction directly
|
||||
if (remove) {
|
||||
removeKey(current, afterWildcard)
|
||||
} else {
|
||||
const actualCensor = typeof censor === 'function'
|
||||
? censor(getValue(current, afterWildcard), [...pathArray.slice(0, pathLength), ...afterWildcard])
|
||||
: censor
|
||||
setValue(current, afterWildcard, actualCensor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (beforeWildcard.length === 0) {
|
||||
traverse(obj, 0)
|
||||
} else {
|
||||
let current = obj
|
||||
for (let i = 0; i < beforeWildcard.length; i++) {
|
||||
const part = beforeWildcard[i]
|
||||
if (current === null || current === undefined) return
|
||||
// Type safety: Check if current is an object before property access
|
||||
if (typeof current !== 'object' || current === null) return
|
||||
current = current[part]
|
||||
pathArray[i] = part
|
||||
}
|
||||
if (current !== null && current !== undefined) {
|
||||
traverse(current, beforeWildcard.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildPathStructure (pathsToClone) {
|
||||
if (pathsToClone.length === 0) {
|
||||
return null // No paths to redact
|
||||
}
|
||||
|
||||
// Parse all paths and organize by depth
|
||||
const pathStructure = new Map()
|
||||
for (const path of pathsToClone) {
|
||||
const parts = parsePath(path)
|
||||
let current = pathStructure
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
if (!current.has(part)) {
|
||||
current.set(part, new Map())
|
||||
}
|
||||
current = current.get(part)
|
||||
}
|
||||
}
|
||||
return pathStructure
|
||||
}
|
||||
|
||||
function selectiveClone (obj, pathStructure) {
|
||||
if (!pathStructure) {
|
||||
return obj // No paths to redact, return original
|
||||
}
|
||||
|
||||
function cloneSelectively (source, pathMap, depth = 0) {
|
||||
if (!pathMap || pathMap.size === 0) {
|
||||
return source // No more paths to clone, return reference
|
||||
}
|
||||
|
||||
if (source === null || typeof source !== 'object') {
|
||||
return source
|
||||
}
|
||||
|
||||
if (source instanceof Date) {
|
||||
return new Date(source.getTime())
|
||||
}
|
||||
|
||||
if (Array.isArray(source)) {
|
||||
const cloned = []
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const indexStr = i.toString()
|
||||
if (pathMap.has(indexStr) || pathMap.has('*')) {
|
||||
cloned[i] = cloneSelectively(source[i], pathMap.get(indexStr) || pathMap.get('*'))
|
||||
} else {
|
||||
cloned[i] = source[i] // Share reference for non-redacted items
|
||||
}
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
// Handle objects
|
||||
const cloned = Object.create(Object.getPrototypeOf(source))
|
||||
for (const key in source) {
|
||||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||||
if (pathMap.has(key) || pathMap.has('*')) {
|
||||
cloned[key] = cloneSelectively(source[key], pathMap.get(key) || pathMap.get('*'))
|
||||
} else {
|
||||
cloned[key] = source[key] // Share reference for non-redacted properties
|
||||
}
|
||||
}
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
return cloneSelectively(obj, pathStructure)
|
||||
}
|
||||
|
||||
function validatePath (path) {
|
||||
if (typeof path !== 'string') {
|
||||
throw new Error('Paths must be (non-empty) strings')
|
||||
}
|
||||
|
||||
if (path === '') {
|
||||
throw new Error('Invalid redaction path ()')
|
||||
}
|
||||
|
||||
// Check for double dots
|
||||
if (path.includes('..')) {
|
||||
throw new Error(`Invalid redaction path (${path})`)
|
||||
}
|
||||
|
||||
// Check for comma-separated paths (invalid syntax)
|
||||
if (path.includes(',')) {
|
||||
throw new Error(`Invalid redaction path (${path})`)
|
||||
}
|
||||
|
||||
// Check for unmatched brackets
|
||||
let bracketCount = 0
|
||||
let inQuotes = false
|
||||
let quoteChar = ''
|
||||
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const char = path[i]
|
||||
|
||||
if ((char === '"' || char === "'") && bracketCount > 0) {
|
||||
if (!inQuotes) {
|
||||
inQuotes = true
|
||||
quoteChar = char
|
||||
} else if (char === quoteChar) {
|
||||
inQuotes = false
|
||||
quoteChar = ''
|
||||
}
|
||||
} else if (char === '[' && !inQuotes) {
|
||||
bracketCount++
|
||||
} else if (char === ']' && !inQuotes) {
|
||||
bracketCount--
|
||||
if (bracketCount < 0) {
|
||||
throw new Error(`Invalid redaction path (${path})`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bracketCount !== 0) {
|
||||
throw new Error(`Invalid redaction path (${path})`)
|
||||
}
|
||||
}
|
||||
|
||||
function validatePaths (paths) {
|
||||
if (!Array.isArray(paths)) {
|
||||
throw new TypeError('paths must be an array')
|
||||
}
|
||||
|
||||
for (const path of paths) {
|
||||
validatePath(path)
|
||||
}
|
||||
}
|
||||
|
||||
function slowRedact (options = {}) {
|
||||
const {
|
||||
paths = [],
|
||||
censor = '[REDACTED]',
|
||||
serialize = JSON.stringify,
|
||||
strict = true,
|
||||
remove = false
|
||||
} = options
|
||||
|
||||
// Validate paths upfront to match fast-redact behavior
|
||||
validatePaths(paths)
|
||||
|
||||
// Build path structure once during setup, not on every call
|
||||
const pathStructure = buildPathStructure(paths)
|
||||
|
||||
return function redact (obj) {
|
||||
if (strict && (obj === null || typeof obj !== 'object')) {
|
||||
if (obj === null || obj === undefined) {
|
||||
return serialize ? serialize(obj) : obj
|
||||
}
|
||||
if (typeof obj !== 'object') {
|
||||
return serialize ? serialize(obj) : obj
|
||||
}
|
||||
}
|
||||
|
||||
// Only clone paths that need redaction
|
||||
const cloned = selectiveClone(obj, pathStructure)
|
||||
const original = obj // Keep reference to original for restore
|
||||
|
||||
let actualCensor = censor
|
||||
if (typeof censor === 'function') {
|
||||
actualCensor = censor
|
||||
}
|
||||
|
||||
redactPaths(cloned, paths, actualCensor, remove)
|
||||
|
||||
if (serialize === false) {
|
||||
cloned.restore = function () {
|
||||
return deepClone(original) // Full clone only when restore is called
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
if (typeof serialize === 'function') {
|
||||
return serialize(cloned)
|
||||
}
|
||||
|
||||
return JSON.stringify(cloned)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = slowRedact
|
||||
22
node_modules/@pinojs/redact/index.test-d.ts
generated
vendored
Normal file
22
node_modules/@pinojs/redact/index.test-d.ts
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import { expectType, expectAssignable } from "tsd";
|
||||
import slowRedact from ".";
|
||||
import type { redactFn, redactFnNoSerialize } from ".";
|
||||
|
||||
// should return redactFn
|
||||
expectType<redactFn>(slowRedact());
|
||||
expectType<redactFn>(slowRedact({ paths: [] }));
|
||||
expectType<redactFn>(slowRedact({ paths: ["some.path"] }));
|
||||
expectType<redactFn>(slowRedact({ paths: [], censor: "[REDACTED]" }));
|
||||
expectType<redactFn>(slowRedact({ paths: [], strict: true }));
|
||||
expectType<redactFn>(slowRedact({ paths: [], serialize: JSON.stringify }));
|
||||
expectType<redactFn>(slowRedact({ paths: [], serialize: true }));
|
||||
expectType<redactFnNoSerialize>(slowRedact({ paths: [], serialize: false }));
|
||||
expectType<redactFn>(slowRedact({ paths: [], remove: true }));
|
||||
|
||||
// should return string
|
||||
expectType<string>(slowRedact()(""));
|
||||
|
||||
// should return string or T
|
||||
expectAssignable<string | { someField: string }>(
|
||||
slowRedact()({ someField: "someValue" })
|
||||
);
|
||||
37
node_modules/@pinojs/redact/package.json
generated
vendored
Normal file
37
node_modules/@pinojs/redact/package.json
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@pinojs/redact",
|
||||
"version": "0.4.0",
|
||||
"description": "Redact JS objects",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"scripts": {
|
||||
"test": "node --test && npm run test:types",
|
||||
"test:integration": "node --test test/integration.test.js",
|
||||
"test:types": "tsd",
|
||||
"test:all": "node --test test/*.test.js",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"bench": "node benchmarks/basic.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/pinojs/redact.git"
|
||||
},
|
||||
"keywords": [
|
||||
"redact"
|
||||
],
|
||||
"author": "Matteo Collina <hello@matteocollina.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pinojs/redact/issues"
|
||||
},
|
||||
"homepage": "https://github.com/pinojs/redact#readme",
|
||||
"devDependencies": {
|
||||
"eslint": "^9.36.0",
|
||||
"fast-redact": "^3.5.0",
|
||||
"mitata": "^1.0.34",
|
||||
"neostandard": "^0.12.2",
|
||||
"tsd": "^0.33.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
20
node_modules/@pinojs/redact/scripts/sync-version.mjs
generated
vendored
Normal file
20
node_modules/@pinojs/redact/scripts/sync-version.mjs
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const packageJsonPath = path.resolve(import.meta.dirname, '../package.json')
|
||||
let { version } = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
|
||||
|
||||
let passedVersion = process.argv[2]
|
||||
|
||||
if (passedVersion) {
|
||||
passedVersion = passedVersion.trim().replace(/^v/, '')
|
||||
if (version !== passedVersion) {
|
||||
console.log(`Syncing version from ${version} to ${passedVersion}`)
|
||||
version = passedVersion
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
|
||||
packageJson.version = version
|
||||
fs.writeFileSync(path.resolve('./package.json'), JSON.stringify(packageJson, null, 2) + '\n', { encoding: 'utf-8' })
|
||||
}
|
||||
} else {
|
||||
throw new Error('Version argument is required')
|
||||
}
|
||||
211
node_modules/@pinojs/redact/test/actual-redact-comparison.test.js
generated
vendored
Normal file
211
node_modules/@pinojs/redact/test/actual-redact-comparison.test.js
generated
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
'use strict'
|
||||
|
||||
// Node.js test comparing @pinojs/redact vs fast-redact for multiple wildcard patterns
|
||||
// This test validates that @pinojs/redact correctly handles 3+ consecutive wildcards
|
||||
// matching the behavior of fast-redact
|
||||
|
||||
const { test } = require('node:test')
|
||||
const { strict: assert } = require('node:assert')
|
||||
const fastRedact = require('fast-redact')
|
||||
const slowRedact = require('../index.js')
|
||||
|
||||
// Helper function to test redaction and track which values were censored
|
||||
function testRedactDirect (library, pattern, testData = {}) {
|
||||
const matches = []
|
||||
const redactor = library === '@pinojs/redact' ? slowRedact : fastRedact
|
||||
|
||||
try {
|
||||
const redact = redactor({
|
||||
paths: [pattern],
|
||||
censor: (value, path) => {
|
||||
if (
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
typeof value === 'string' &&
|
||||
value.includes('secret')
|
||||
) {
|
||||
matches.push({
|
||||
value,
|
||||
path: path ? path.join('.') : 'unknown'
|
||||
})
|
||||
}
|
||||
return '[REDACTED]'
|
||||
}
|
||||
})
|
||||
|
||||
redact(JSON.parse(JSON.stringify(testData)))
|
||||
|
||||
return {
|
||||
library,
|
||||
pattern,
|
||||
matches,
|
||||
success: true,
|
||||
testData
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
library,
|
||||
pattern,
|
||||
matches: [],
|
||||
success: false,
|
||||
error: error.message,
|
||||
testData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function testSlowRedactDirect (pattern, testData) {
|
||||
return testRedactDirect('@pinojs/redact', pattern, testData)
|
||||
}
|
||||
|
||||
function testFastRedactDirect (pattern, testData) {
|
||||
return testRedactDirect('fast-redact', pattern, testData)
|
||||
}
|
||||
|
||||
test('@pinojs/redact: *.password (2 levels)', () => {
|
||||
const result = testSlowRedactDirect('*.password', {
|
||||
simple: { password: 'secret-2-levels' }
|
||||
})
|
||||
|
||||
assert.strictEqual(result.success, true)
|
||||
assert.strictEqual(result.matches.length, 1)
|
||||
assert.strictEqual(result.matches[0].value, 'secret-2-levels')
|
||||
})
|
||||
|
||||
test('@pinojs/redact: *.*.password (3 levels)', () => {
|
||||
const result = testSlowRedactDirect('*.*.password', {
|
||||
simple: { password: 'secret-2-levels' },
|
||||
user: { auth: { password: 'secret-3-levels' } }
|
||||
})
|
||||
|
||||
assert.strictEqual(result.success, true)
|
||||
assert.strictEqual(result.matches.length, 1)
|
||||
assert.strictEqual(result.matches[0].value, 'secret-3-levels')
|
||||
})
|
||||
|
||||
test('@pinojs/redact: *.*.*.password (4 levels)', () => {
|
||||
const result = testSlowRedactDirect('*.*.*.password', {
|
||||
simple: { password: 'secret-2-levels' },
|
||||
user: { auth: { password: 'secret-3-levels' } },
|
||||
nested: { deep: { auth: { password: 'secret-4-levels' } } }
|
||||
})
|
||||
|
||||
assert.strictEqual(result.success, true)
|
||||
assert.strictEqual(result.matches.length, 1)
|
||||
assert.strictEqual(result.matches[0].value, 'secret-4-levels')
|
||||
})
|
||||
|
||||
test('@pinojs/redact: *.*.*.*.password (5 levels)', () => {
|
||||
const result = testSlowRedactDirect('*.*.*.*.password', {
|
||||
simple: { password: 'secret-2-levels' },
|
||||
user: { auth: { password: 'secret-3-levels' } },
|
||||
nested: { deep: { auth: { password: 'secret-4-levels' } } },
|
||||
config: {
|
||||
user: { auth: { settings: { password: 'secret-5-levels' } } }
|
||||
}
|
||||
})
|
||||
|
||||
assert.strictEqual(result.success, true)
|
||||
assert.strictEqual(result.matches.length, 1)
|
||||
assert.strictEqual(result.matches[0].value, 'secret-5-levels')
|
||||
})
|
||||
|
||||
test('@pinojs/redact: *.*.*.*.*.password (6 levels)', () => {
|
||||
const result = testSlowRedactDirect('*.*.*.*.*.password', {
|
||||
simple: { password: 'secret-2-levels' },
|
||||
user: { auth: { password: 'secret-3-levels' } },
|
||||
nested: { deep: { auth: { password: 'secret-4-levels' } } },
|
||||
config: {
|
||||
user: { auth: { settings: { password: 'secret-5-levels' } } }
|
||||
},
|
||||
data: {
|
||||
reqConfig: {
|
||||
data: {
|
||||
credentials: {
|
||||
settings: {
|
||||
password: 'real-secret-6-levels'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
assert.strictEqual(result.success, true)
|
||||
assert.strictEqual(result.matches.length, 1)
|
||||
assert.strictEqual(result.matches[0].value, 'real-secret-6-levels')
|
||||
})
|
||||
|
||||
test('fast-redact: *.password (2 levels)', () => {
|
||||
const result = testFastRedactDirect('*.password', {
|
||||
simple: { password: 'secret-2-levels' }
|
||||
})
|
||||
|
||||
assert.strictEqual(result.success, true)
|
||||
assert.strictEqual(result.matches.length, 1)
|
||||
assert.strictEqual(result.matches[0].value, 'secret-2-levels')
|
||||
})
|
||||
|
||||
test('fast-redact: *.*.password (3 levels)', () => {
|
||||
const result = testFastRedactDirect('*.*.password', {
|
||||
simple: { password: 'secret-2-levels' },
|
||||
user: { auth: { password: 'secret-3-levels' } }
|
||||
})
|
||||
|
||||
assert.strictEqual(result.success, true)
|
||||
assert.strictEqual(result.matches.length, 1)
|
||||
assert.strictEqual(result.matches[0].value, 'secret-3-levels')
|
||||
})
|
||||
|
||||
test('fast-redact: *.*.*.password (4 levels)', () => {
|
||||
const result = testFastRedactDirect('*.*.*.password', {
|
||||
simple: { password: 'secret-2-levels' },
|
||||
user: { auth: { password: 'secret-3-levels' } },
|
||||
nested: { deep: { auth: { password: 'secret-4-levels' } } }
|
||||
})
|
||||
|
||||
assert.strictEqual(result.success, true)
|
||||
assert.strictEqual(result.matches.length, 1)
|
||||
assert.strictEqual(result.matches[0].value, 'secret-4-levels')
|
||||
})
|
||||
|
||||
test('fast-redact: *.*.*.*.password (5 levels)', () => {
|
||||
const result = testFastRedactDirect('*.*.*.*.password', {
|
||||
simple: { password: 'secret-2-levels' },
|
||||
user: { auth: { password: 'secret-3-levels' } },
|
||||
nested: { deep: { auth: { password: 'secret-4-levels' } } },
|
||||
config: {
|
||||
user: { auth: { settings: { password: 'secret-5-levels' } } }
|
||||
}
|
||||
})
|
||||
|
||||
assert.strictEqual(result.success, true)
|
||||
assert.strictEqual(result.matches.length, 1)
|
||||
assert.strictEqual(result.matches[0].value, 'secret-5-levels')
|
||||
})
|
||||
|
||||
test('fast-redact: *.*.*.*.*.password (6 levels)', () => {
|
||||
const result = testFastRedactDirect('*.*.*.*.*.password', {
|
||||
simple: { password: 'secret-2-levels' },
|
||||
user: { auth: { password: 'secret-3-levels' } },
|
||||
nested: { deep: { auth: { password: 'secret-4-levels' } } },
|
||||
config: {
|
||||
user: { auth: { settings: { password: 'secret-5-levels' } } }
|
||||
},
|
||||
data: {
|
||||
reqConfig: {
|
||||
data: {
|
||||
credentials: {
|
||||
settings: {
|
||||
password: 'real-secret-6-levels'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
assert.strictEqual(result.success, true)
|
||||
assert.strictEqual(result.matches.length, 1)
|
||||
assert.strictEqual(result.matches[0].value, 'real-secret-6-levels')
|
||||
})
|
||||
824
node_modules/@pinojs/redact/test/index.test.js
generated
vendored
Normal file
824
node_modules/@pinojs/redact/test/index.test.js
generated
vendored
Normal file
@@ -0,0 +1,824 @@
|
||||
const { test } = require('node:test')
|
||||
const { strict: assert } = require('node:assert')
|
||||
const slowRedact = require('../index.js')
|
||||
|
||||
test('basic path redaction', () => {
|
||||
const obj = {
|
||||
headers: {
|
||||
cookie: 'secret-cookie',
|
||||
authorization: 'Bearer token'
|
||||
},
|
||||
body: { message: 'hello' }
|
||||
}
|
||||
|
||||
const redact = slowRedact({ paths: ['headers.cookie'] })
|
||||
const result = redact(obj)
|
||||
|
||||
// Original object should remain unchanged
|
||||
assert.strictEqual(obj.headers.cookie, 'secret-cookie')
|
||||
|
||||
// Result should have redacted path
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.headers.cookie, '[REDACTED]')
|
||||
assert.strictEqual(parsed.headers.authorization, 'Bearer token')
|
||||
assert.strictEqual(parsed.body.message, 'hello')
|
||||
})
|
||||
|
||||
test('multiple paths redaction', () => {
|
||||
const obj = {
|
||||
user: { name: 'john', password: 'secret' },
|
||||
session: { token: 'abc123' }
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['user.password', 'session.token']
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
// Original unchanged
|
||||
assert.strictEqual(obj.user.password, 'secret')
|
||||
assert.strictEqual(obj.session.token, 'abc123')
|
||||
|
||||
// Result redacted
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.user.password, '[REDACTED]')
|
||||
assert.strictEqual(parsed.session.token, '[REDACTED]')
|
||||
assert.strictEqual(parsed.user.name, 'john')
|
||||
})
|
||||
|
||||
test('custom censor value', () => {
|
||||
const obj = { secret: 'hidden' }
|
||||
const redact = slowRedact({
|
||||
paths: ['secret'],
|
||||
censor: '***'
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.secret, '***')
|
||||
})
|
||||
|
||||
test('serialize: false returns object with restore method', () => {
|
||||
const obj = { secret: 'hidden' }
|
||||
const redact = slowRedact({
|
||||
paths: ['secret'],
|
||||
serialize: false
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
// Should be object, not string
|
||||
assert.strictEqual(typeof result, 'object')
|
||||
assert.strictEqual(result.secret, '[REDACTED]')
|
||||
|
||||
// Should have restore method
|
||||
assert.strictEqual(typeof result.restore, 'function')
|
||||
|
||||
const restored = result.restore()
|
||||
assert.strictEqual(restored.secret, 'hidden')
|
||||
})
|
||||
|
||||
test('bracket notation paths', () => {
|
||||
const obj = {
|
||||
'weird-key': { 'another-weird': 'secret' },
|
||||
normal: 'public'
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['["weird-key"]["another-weird"]']
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed['weird-key']['another-weird'], '[REDACTED]')
|
||||
assert.strictEqual(parsed.normal, 'public')
|
||||
})
|
||||
|
||||
test('array paths', () => {
|
||||
const obj = {
|
||||
users: [
|
||||
{ name: 'john', password: 'secret1' },
|
||||
{ name: 'jane', password: 'secret2' }
|
||||
]
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['users[0].password', 'users[1].password']
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.users[0].password, '[REDACTED]')
|
||||
assert.strictEqual(parsed.users[1].password, '[REDACTED]')
|
||||
assert.strictEqual(parsed.users[0].name, 'john')
|
||||
assert.strictEqual(parsed.users[1].name, 'jane')
|
||||
})
|
||||
|
||||
test('wildcard at end of path', () => {
|
||||
const obj = {
|
||||
secrets: {
|
||||
key1: 'secret1',
|
||||
key2: 'secret2'
|
||||
},
|
||||
public: 'data'
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['secrets.*']
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.secrets.key1, '[REDACTED]')
|
||||
assert.strictEqual(parsed.secrets.key2, '[REDACTED]')
|
||||
assert.strictEqual(parsed.public, 'data')
|
||||
})
|
||||
|
||||
test('wildcard with arrays', () => {
|
||||
const obj = {
|
||||
items: ['secret1', 'secret2', 'secret3']
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['items.*']
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.items[0], '[REDACTED]')
|
||||
assert.strictEqual(parsed.items[1], '[REDACTED]')
|
||||
assert.strictEqual(parsed.items[2], '[REDACTED]')
|
||||
})
|
||||
|
||||
test('intermediate wildcard', () => {
|
||||
const obj = {
|
||||
users: {
|
||||
user1: { password: 'secret1' },
|
||||
user2: { password: 'secret2' }
|
||||
}
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['users.*.password']
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.users.user1.password, '[REDACTED]')
|
||||
assert.strictEqual(parsed.users.user2.password, '[REDACTED]')
|
||||
})
|
||||
|
||||
test('censor function', () => {
|
||||
const obj = { secret: 'hidden' }
|
||||
const redact = slowRedact({
|
||||
paths: ['secret'],
|
||||
censor: (value, path) => `REDACTED:${path.join('.')}`
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.secret, 'REDACTED:secret')
|
||||
})
|
||||
|
||||
test('custom serialize function', () => {
|
||||
const obj = { secret: 'hidden', public: 'data' }
|
||||
const redact = slowRedact({
|
||||
paths: ['secret'],
|
||||
serialize: (obj) => `custom:${JSON.stringify(obj)}`
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
assert(result.startsWith('custom:'))
|
||||
const parsed = JSON.parse(result.slice(7))
|
||||
assert.strictEqual(parsed.secret, '[REDACTED]')
|
||||
assert.strictEqual(parsed.public, 'data')
|
||||
})
|
||||
|
||||
test('nested paths', () => {
|
||||
const obj = {
|
||||
level1: {
|
||||
level2: {
|
||||
level3: {
|
||||
secret: 'hidden'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['level1.level2.level3.secret']
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.level1.level2.level3.secret, '[REDACTED]')
|
||||
})
|
||||
|
||||
test('non-existent paths are ignored', () => {
|
||||
const obj = { existing: 'value' }
|
||||
const redact = slowRedact({
|
||||
paths: ['nonexistent.path']
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.existing, 'value')
|
||||
assert.strictEqual(parsed.nonexistent, undefined)
|
||||
})
|
||||
|
||||
test('null and undefined handling', () => {
|
||||
const obj = {
|
||||
nullValue: null,
|
||||
undefinedValue: undefined,
|
||||
nested: {
|
||||
nullValue: null
|
||||
}
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['nullValue', 'nested.nullValue']
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.nullValue, '[REDACTED]')
|
||||
assert.strictEqual(parsed.nested.nullValue, '[REDACTED]')
|
||||
})
|
||||
|
||||
test('original object remains unchanged', () => {
|
||||
const original = {
|
||||
secret: 'hidden',
|
||||
nested: { secret: 'hidden2' }
|
||||
}
|
||||
const copy = JSON.parse(JSON.stringify(original))
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['secret', 'nested.secret']
|
||||
})
|
||||
redact(original)
|
||||
|
||||
// Original should be completely unchanged
|
||||
assert.deepStrictEqual(original, copy)
|
||||
})
|
||||
|
||||
test('strict mode with primitives', () => {
|
||||
const redact = slowRedact({
|
||||
paths: ['test'],
|
||||
strict: true
|
||||
})
|
||||
|
||||
const stringResult = redact('primitive')
|
||||
assert.strictEqual(stringResult, '"primitive"')
|
||||
|
||||
const numberResult = redact(42)
|
||||
assert.strictEqual(numberResult, '42')
|
||||
})
|
||||
|
||||
// Path validation tests to match fast-redact behavior
|
||||
test('path validation - non-string paths should throw', () => {
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: [123] })
|
||||
}, {
|
||||
message: 'Paths must be (non-empty) strings'
|
||||
})
|
||||
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: [null] })
|
||||
}, {
|
||||
message: 'Paths must be (non-empty) strings'
|
||||
})
|
||||
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: [undefined] })
|
||||
}, {
|
||||
message: 'Paths must be (non-empty) strings'
|
||||
})
|
||||
})
|
||||
|
||||
test('path validation - empty string should throw', () => {
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: [''] })
|
||||
}, {
|
||||
message: 'Invalid redaction path ()'
|
||||
})
|
||||
})
|
||||
|
||||
test('path validation - double dots should throw', () => {
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: ['invalid..path'] })
|
||||
}, {
|
||||
message: 'Invalid redaction path (invalid..path)'
|
||||
})
|
||||
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: ['a..b..c'] })
|
||||
}, {
|
||||
message: 'Invalid redaction path (a..b..c)'
|
||||
})
|
||||
})
|
||||
|
||||
test('path validation - unmatched brackets should throw', () => {
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: ['invalid[unclosed'] })
|
||||
}, {
|
||||
message: 'Invalid redaction path (invalid[unclosed)'
|
||||
})
|
||||
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: ['invalid]unopened'] })
|
||||
}, {
|
||||
message: 'Invalid redaction path (invalid]unopened)'
|
||||
})
|
||||
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: ['nested[a[b]'] })
|
||||
}, {
|
||||
message: 'Invalid redaction path (nested[a[b])'
|
||||
})
|
||||
})
|
||||
|
||||
test('path validation - comma-separated paths should throw', () => {
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: ['req,headers.cookie'] })
|
||||
}, {
|
||||
message: 'Invalid redaction path (req,headers.cookie)'
|
||||
})
|
||||
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: ['user,profile,name'] })
|
||||
}, {
|
||||
message: 'Invalid redaction path (user,profile,name)'
|
||||
})
|
||||
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: ['a,b'] })
|
||||
}, {
|
||||
message: 'Invalid redaction path (a,b)'
|
||||
})
|
||||
})
|
||||
|
||||
test('path validation - mixed valid and invalid should throw', () => {
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: ['valid.path', 123, 'another.valid'] })
|
||||
}, {
|
||||
message: 'Paths must be (non-empty) strings'
|
||||
})
|
||||
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: ['valid.path', 'invalid..path'] })
|
||||
}, {
|
||||
message: 'Invalid redaction path (invalid..path)'
|
||||
})
|
||||
|
||||
assert.throws(() => {
|
||||
slowRedact({ paths: ['valid.path', 'req,headers.cookie'] })
|
||||
}, {
|
||||
message: 'Invalid redaction path (req,headers.cookie)'
|
||||
})
|
||||
})
|
||||
|
||||
test('path validation - valid paths should work', () => {
|
||||
// These should not throw
|
||||
assert.doesNotThrow(() => {
|
||||
slowRedact({ paths: [] })
|
||||
})
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
slowRedact({ paths: ['valid.path'] })
|
||||
})
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
slowRedact({ paths: ['user.password', 'data[0].secret'] })
|
||||
})
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
slowRedact({ paths: ['["quoted-key"].value'] })
|
||||
})
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
slowRedact({ paths: ["['single-quoted'].value"] })
|
||||
})
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
slowRedact({ paths: ['array[0]', 'object.property', 'wildcard.*'] })
|
||||
})
|
||||
})
|
||||
|
||||
// fast-redact compatibility tests
|
||||
test('censor function receives path as array (fast-redact compatibility)', () => {
|
||||
const obj = {
|
||||
headers: {
|
||||
authorization: 'Bearer token',
|
||||
'x-api-key': 'secret-key'
|
||||
}
|
||||
}
|
||||
|
||||
const pathsReceived = []
|
||||
const redact = slowRedact({
|
||||
paths: ['headers.authorization', 'headers["x-api-key"]'],
|
||||
censor: (value, path) => {
|
||||
pathsReceived.push(path)
|
||||
assert(Array.isArray(path), 'Path should be an array')
|
||||
return '[REDACTED]'
|
||||
}
|
||||
})
|
||||
|
||||
redact(obj)
|
||||
|
||||
// Verify paths are arrays
|
||||
assert.strictEqual(pathsReceived.length, 2)
|
||||
assert.deepStrictEqual(pathsReceived[0], ['headers', 'authorization'])
|
||||
assert.deepStrictEqual(pathsReceived[1], ['headers', 'x-api-key'])
|
||||
})
|
||||
|
||||
test('censor function with nested paths receives correct array', () => {
|
||||
const obj = {
|
||||
user: {
|
||||
profile: {
|
||||
credentials: {
|
||||
password: 'secret123'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let receivedPath
|
||||
const redact = slowRedact({
|
||||
paths: ['user.profile.credentials.password'],
|
||||
censor: (value, path) => {
|
||||
receivedPath = path
|
||||
assert.strictEqual(value, 'secret123')
|
||||
assert(Array.isArray(path))
|
||||
return '[REDACTED]'
|
||||
}
|
||||
})
|
||||
|
||||
redact(obj)
|
||||
|
||||
assert.deepStrictEqual(receivedPath, ['user', 'profile', 'credentials', 'password'])
|
||||
})
|
||||
|
||||
test('censor function with wildcards receives correct array paths', () => {
|
||||
const obj = {
|
||||
users: {
|
||||
user1: { password: 'secret1' },
|
||||
user2: { password: 'secret2' }
|
||||
}
|
||||
}
|
||||
|
||||
const pathsReceived = []
|
||||
const redact = slowRedact({
|
||||
paths: ['users.*.password'],
|
||||
censor: (value, path) => {
|
||||
pathsReceived.push([...path]) // copy the array
|
||||
assert(Array.isArray(path))
|
||||
return '[REDACTED]'
|
||||
}
|
||||
})
|
||||
|
||||
redact(obj)
|
||||
|
||||
assert.strictEqual(pathsReceived.length, 2)
|
||||
assert.deepStrictEqual(pathsReceived[0], ['users', 'user1', 'password'])
|
||||
assert.deepStrictEqual(pathsReceived[1], ['users', 'user2', 'password'])
|
||||
})
|
||||
|
||||
test('censor function with array wildcard receives correct array paths', () => {
|
||||
const obj = {
|
||||
items: [
|
||||
{ secret: 'value1' },
|
||||
{ secret: 'value2' }
|
||||
]
|
||||
}
|
||||
|
||||
const pathsReceived = []
|
||||
const redact = slowRedact({
|
||||
paths: ['items.*.secret'],
|
||||
censor: (value, path) => {
|
||||
pathsReceived.push([...path])
|
||||
assert(Array.isArray(path))
|
||||
return '[REDACTED]'
|
||||
}
|
||||
})
|
||||
|
||||
redact(obj)
|
||||
|
||||
assert.strictEqual(pathsReceived.length, 2)
|
||||
assert.deepStrictEqual(pathsReceived[0], ['items', '0', 'secret'])
|
||||
assert.deepStrictEqual(pathsReceived[1], ['items', '1', 'secret'])
|
||||
})
|
||||
|
||||
test('censor function with end wildcard receives correct array paths', () => {
|
||||
const obj = {
|
||||
secrets: {
|
||||
key1: 'secret1',
|
||||
key2: 'secret2'
|
||||
}
|
||||
}
|
||||
|
||||
const pathsReceived = []
|
||||
const redact = slowRedact({
|
||||
paths: ['secrets.*'],
|
||||
censor: (value, path) => {
|
||||
pathsReceived.push([...path])
|
||||
assert(Array.isArray(path))
|
||||
return '[REDACTED]'
|
||||
}
|
||||
})
|
||||
|
||||
redact(obj)
|
||||
|
||||
assert.strictEqual(pathsReceived.length, 2)
|
||||
// Sort paths for consistent testing since object iteration order isn't guaranteed
|
||||
pathsReceived.sort((a, b) => a[1].localeCompare(b[1]))
|
||||
assert.deepStrictEqual(pathsReceived[0], ['secrets', 'key1'])
|
||||
assert.deepStrictEqual(pathsReceived[1], ['secrets', 'key2'])
|
||||
})
|
||||
|
||||
test('type safety: accessing properties on primitive values should not throw', () => {
|
||||
// Test case from GitHub issue #5
|
||||
const redactor = slowRedact({ paths: ['headers.authorization'] })
|
||||
const data = {
|
||||
headers: 123 // primitive value
|
||||
}
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
const result = redactor(data)
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.headers, 123) // Should remain unchanged
|
||||
})
|
||||
|
||||
// Test wildcards with primitives
|
||||
const redactor2 = slowRedact({ paths: ['data.*.nested'] })
|
||||
const data2 = {
|
||||
data: {
|
||||
item1: 123, // primitive, trying to access .nested on it
|
||||
item2: { nested: 'secret' }
|
||||
}
|
||||
}
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
const result2 = redactor2(data2)
|
||||
const parsed2 = JSON.parse(result2)
|
||||
assert.strictEqual(parsed2.data.item1, 123) // Primitive unchanged
|
||||
assert.strictEqual(parsed2.data.item2.nested, '[REDACTED]') // Object property redacted
|
||||
})
|
||||
|
||||
// Test deep nested access on primitives
|
||||
const redactor3 = slowRedact({ paths: ['user.name.first.charAt'] })
|
||||
const data3 = {
|
||||
user: {
|
||||
name: 'John' // string primitive
|
||||
}
|
||||
}
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
const result3 = redactor3(data3)
|
||||
const parsed3 = JSON.parse(result3)
|
||||
assert.strictEqual(parsed3.user.name, 'John') // Should remain unchanged
|
||||
})
|
||||
})
|
||||
|
||||
// Remove option tests
|
||||
test('remove option: basic key removal', () => {
|
||||
const obj = { username: 'john', password: 'secret123' }
|
||||
const redact = slowRedact({ paths: ['password'], remove: true })
|
||||
const result = redact(obj)
|
||||
|
||||
// Original object should remain unchanged
|
||||
assert.strictEqual(obj.password, 'secret123')
|
||||
|
||||
// Result should have password completely removed
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.username, 'john')
|
||||
assert.strictEqual('password' in parsed, false)
|
||||
assert.strictEqual(parsed.password, undefined)
|
||||
})
|
||||
|
||||
test('remove option: multiple paths removal', () => {
|
||||
const obj = {
|
||||
user: { name: 'john', password: 'secret' },
|
||||
session: { token: 'abc123', id: 'session1' }
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['user.password', 'session.token'],
|
||||
remove: true
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
// Original unchanged
|
||||
assert.strictEqual(obj.user.password, 'secret')
|
||||
assert.strictEqual(obj.session.token, 'abc123')
|
||||
|
||||
// Result has keys completely removed
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.user.name, 'john')
|
||||
assert.strictEqual(parsed.session.id, 'session1')
|
||||
assert.strictEqual('password' in parsed.user, false)
|
||||
assert.strictEqual('token' in parsed.session, false)
|
||||
})
|
||||
|
||||
test('remove option: wildcard removal', () => {
|
||||
const obj = {
|
||||
secrets: {
|
||||
key1: 'secret1',
|
||||
key2: 'secret2'
|
||||
},
|
||||
public: 'data'
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['secrets.*'],
|
||||
remove: true
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.public, 'data')
|
||||
assert.deepStrictEqual(parsed.secrets, {}) // All keys removed
|
||||
})
|
||||
|
||||
test('remove option: array wildcard removal', () => {
|
||||
const obj = {
|
||||
items: ['secret1', 'secret2', 'secret3'],
|
||||
meta: 'data'
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['items.*'],
|
||||
remove: true
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.meta, 'data')
|
||||
// Array items set to undefined are omitted by JSON.stringify
|
||||
assert.deepStrictEqual(parsed.items, [null, null, null])
|
||||
})
|
||||
|
||||
test('remove option: intermediate wildcard removal', () => {
|
||||
const obj = {
|
||||
users: {
|
||||
user1: { password: 'secret1', name: 'john' },
|
||||
user2: { password: 'secret2', name: 'jane' }
|
||||
}
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['users.*.password'],
|
||||
remove: true
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.users.user1.name, 'john')
|
||||
assert.strictEqual(parsed.users.user2.name, 'jane')
|
||||
assert.strictEqual('password' in parsed.users.user1, false)
|
||||
assert.strictEqual('password' in parsed.users.user2, false)
|
||||
})
|
||||
|
||||
test('remove option: serialize false returns object with removed keys', () => {
|
||||
const obj = { secret: 'hidden', public: 'data' }
|
||||
const redact = slowRedact({
|
||||
paths: ['secret'],
|
||||
remove: true,
|
||||
serialize: false
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
// Should be object, not string
|
||||
assert.strictEqual(typeof result, 'object')
|
||||
assert.strictEqual(result.public, 'data')
|
||||
assert.strictEqual('secret' in result, false)
|
||||
|
||||
// Should have restore method
|
||||
assert.strictEqual(typeof result.restore, 'function')
|
||||
|
||||
const restored = result.restore()
|
||||
assert.strictEqual(restored.secret, 'hidden')
|
||||
})
|
||||
|
||||
test('remove option: non-existent paths are ignored', () => {
|
||||
const obj = { existing: 'value' }
|
||||
const redact = slowRedact({
|
||||
paths: ['nonexistent.path'],
|
||||
remove: true
|
||||
})
|
||||
const result = redact(obj)
|
||||
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed.existing, 'value')
|
||||
assert.strictEqual(parsed.nonexistent, undefined)
|
||||
})
|
||||
|
||||
// Test for Issue #13: Empty string bracket notation paths not being redacted correctly
|
||||
test('empty string bracket notation path', () => {
|
||||
const obj = { '': { c: 'sensitive-data' } }
|
||||
const redact = slowRedact({ paths: ["[''].c"] })
|
||||
const result = redact(obj)
|
||||
|
||||
// Original object should remain unchanged
|
||||
assert.strictEqual(obj[''].c, 'sensitive-data')
|
||||
|
||||
// Result should have redacted path
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed[''].c, '[REDACTED]')
|
||||
})
|
||||
|
||||
test('empty string bracket notation with double quotes', () => {
|
||||
const obj = { '': { c: 'sensitive-data' } }
|
||||
const redact = slowRedact({ paths: ['[""].c'] })
|
||||
const result = redact(obj)
|
||||
|
||||
// Original object should remain unchanged
|
||||
assert.strictEqual(obj[''].c, 'sensitive-data')
|
||||
|
||||
// Result should have redacted path
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed[''].c, '[REDACTED]')
|
||||
})
|
||||
|
||||
test('empty string key with nested bracket notation', () => {
|
||||
const obj = { '': { '': { secret: 'value' } } }
|
||||
const redact = slowRedact({ paths: ["[''][''].secret"] })
|
||||
const result = redact(obj)
|
||||
|
||||
// Original object should remain unchanged
|
||||
assert.strictEqual(obj[''][''].secret, 'value')
|
||||
|
||||
// Result should have redacted path
|
||||
const parsed = JSON.parse(result)
|
||||
assert.strictEqual(parsed[''][''].secret, '[REDACTED]')
|
||||
})
|
||||
|
||||
// Test for Pino issue #2313: censor should only be called when path exists
|
||||
test('censor function not called for non-existent paths', () => {
|
||||
let censorCallCount = 0
|
||||
const censorCalls = []
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['a.b.c', 'req.authorization', 'url'],
|
||||
serialize: false,
|
||||
censor (value, path) {
|
||||
censorCallCount++
|
||||
censorCalls.push({ value, path: path.slice() })
|
||||
return '***'
|
||||
}
|
||||
})
|
||||
|
||||
// Test case 1: { req: { id: 'test' } }
|
||||
// req.authorization doesn't exist, censor should not be called for it
|
||||
censorCallCount = 0
|
||||
censorCalls.length = 0
|
||||
redact({ req: { id: 'test' } })
|
||||
|
||||
// Should not have been called for any path since none exist
|
||||
assert.strictEqual(censorCallCount, 0, 'censor should not be called when paths do not exist')
|
||||
|
||||
// Test case 2: { a: { d: 'test' } }
|
||||
// a.b.c doesn't exist (a.d exists, but not a.b.c)
|
||||
censorCallCount = 0
|
||||
redact({ a: { d: 'test' } })
|
||||
assert.strictEqual(censorCallCount, 0)
|
||||
|
||||
// Test case 3: paths that do exist should still call censor
|
||||
censorCallCount = 0
|
||||
censorCalls.length = 0
|
||||
const result = redact({ req: { authorization: 'bearer token' } })
|
||||
assert.strictEqual(censorCallCount, 1, 'censor should be called when path exists')
|
||||
assert.deepStrictEqual(censorCalls[0].path, ['req', 'authorization'])
|
||||
assert.strictEqual(censorCalls[0].value, 'bearer token')
|
||||
assert.strictEqual(result.req.authorization, '***')
|
||||
})
|
||||
|
||||
test('censor function not called for non-existent nested paths', () => {
|
||||
let censorCallCount = 0
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['headers.authorization'],
|
||||
serialize: false,
|
||||
censor (value, path) {
|
||||
censorCallCount++
|
||||
return '[REDACTED]'
|
||||
}
|
||||
})
|
||||
|
||||
// headers exists but authorization doesn't
|
||||
censorCallCount = 0
|
||||
const result1 = redact({ headers: { 'content-type': 'application/json' } })
|
||||
assert.strictEqual(censorCallCount, 0)
|
||||
assert.deepStrictEqual(result1.headers, { 'content-type': 'application/json' })
|
||||
|
||||
// headers doesn't exist at all
|
||||
censorCallCount = 0
|
||||
const result2 = redact({ body: 'data' })
|
||||
assert.strictEqual(censorCallCount, 0)
|
||||
assert.strictEqual(result2.body, 'data')
|
||||
assert.strictEqual(typeof result2.restore, 'function')
|
||||
|
||||
// headers.authorization exists - should call censor
|
||||
censorCallCount = 0
|
||||
const result3 = redact({ headers: { authorization: 'Bearer token' } })
|
||||
assert.strictEqual(censorCallCount, 1)
|
||||
assert.strictEqual(result3.headers.authorization, '[REDACTED]')
|
||||
})
|
||||
390
node_modules/@pinojs/redact/test/integration.test.js
generated
vendored
Normal file
390
node_modules/@pinojs/redact/test/integration.test.js
generated
vendored
Normal file
@@ -0,0 +1,390 @@
|
||||
const { test } = require('node:test')
|
||||
const { strict: assert } = require('node:assert')
|
||||
const slowRedact = require('../index.js')
|
||||
const fastRedact = require('fast-redact')
|
||||
|
||||
test('integration: basic path redaction matches fast-redact', () => {
|
||||
const obj = {
|
||||
headers: {
|
||||
cookie: 'secret-cookie',
|
||||
authorization: 'Bearer token'
|
||||
},
|
||||
body: { message: 'hello' }
|
||||
}
|
||||
|
||||
const slowResult = slowRedact({ paths: ['headers.cookie'] })(obj)
|
||||
const fastResult = fastRedact({ paths: ['headers.cookie'] })(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: multiple paths match fast-redact', () => {
|
||||
const obj = {
|
||||
user: { name: 'john', password: 'secret' },
|
||||
session: { token: 'abc123' }
|
||||
}
|
||||
|
||||
const paths = ['user.password', 'session.token']
|
||||
const slowResult = slowRedact({ paths })(obj)
|
||||
const fastResult = fastRedact({ paths })(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: custom censor value matches fast-redact', () => {
|
||||
const obj = { secret: 'hidden' }
|
||||
const options = { paths: ['secret'], censor: '***' }
|
||||
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: bracket notation matches fast-redact', () => {
|
||||
const obj = {
|
||||
'weird-key': { 'another-weird': 'secret' },
|
||||
normal: 'public'
|
||||
}
|
||||
|
||||
const options = { paths: ['["weird-key"]["another-weird"]'] }
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: array paths match fast-redact', () => {
|
||||
const obj = {
|
||||
users: [
|
||||
{ name: 'john', password: 'secret1' },
|
||||
{ name: 'jane', password: 'secret2' }
|
||||
]
|
||||
}
|
||||
|
||||
const options = { paths: ['users[0].password', 'users[1].password'] }
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: wildcard at end matches fast-redact', () => {
|
||||
const obj = {
|
||||
secrets: {
|
||||
key1: 'secret1',
|
||||
key2: 'secret2'
|
||||
},
|
||||
public: 'data'
|
||||
}
|
||||
|
||||
const options = { paths: ['secrets.*'] }
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: wildcard with arrays matches fast-redact', () => {
|
||||
const obj = {
|
||||
items: ['secret1', 'secret2', 'secret3']
|
||||
}
|
||||
|
||||
const options = { paths: ['items.*'] }
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: intermediate wildcard matches fast-redact', () => {
|
||||
const obj = {
|
||||
users: {
|
||||
user1: { password: 'secret1' },
|
||||
user2: { password: 'secret2' }
|
||||
}
|
||||
}
|
||||
|
||||
const options = { paths: ['users.*.password'] }
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: custom serialize function matches fast-redact', () => {
|
||||
const obj = { secret: 'hidden', public: 'data' }
|
||||
const options = {
|
||||
paths: ['secret'],
|
||||
serialize: (obj) => `custom:${JSON.stringify(obj)}`
|
||||
}
|
||||
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: nested paths match fast-redact', () => {
|
||||
const obj = {
|
||||
level1: {
|
||||
level2: {
|
||||
level3: {
|
||||
secret: 'hidden'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const options = { paths: ['level1.level2.level3.secret'] }
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: non-existent paths match fast-redact', () => {
|
||||
const obj = { existing: 'value' }
|
||||
const options = { paths: ['nonexistent.path'] }
|
||||
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: null and undefined handling - legitimate difference', () => {
|
||||
const obj = {
|
||||
nullValue: null,
|
||||
undefinedValue: undefined,
|
||||
nested: {
|
||||
nullValue: null
|
||||
}
|
||||
}
|
||||
|
||||
const options = { paths: ['nullValue', 'nested.nullValue'] }
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
// This is a legitimate behavioral difference:
|
||||
// @pinojs/redact redacts null values, fast-redact doesn't
|
||||
const slowParsed = JSON.parse(slowResult)
|
||||
const fastParsed = JSON.parse(fastResult)
|
||||
|
||||
// @pinojs/redact redacts nulls
|
||||
assert.strictEqual(slowParsed.nullValue, '[REDACTED]')
|
||||
assert.strictEqual(slowParsed.nested.nullValue, '[REDACTED]')
|
||||
|
||||
// fast-redact preserves nulls
|
||||
assert.strictEqual(fastParsed.nullValue, null)
|
||||
assert.strictEqual(fastParsed.nested.nullValue, null)
|
||||
})
|
||||
|
||||
test('integration: strict mode with primitives - different error handling', () => {
|
||||
const options = { paths: ['test'], strict: true }
|
||||
|
||||
const slowRedactFn = slowRedact(options)
|
||||
const fastRedactFn = fastRedact(options)
|
||||
|
||||
// @pinojs/redact handles primitives gracefully
|
||||
const stringSlowResult = slowRedactFn('primitive')
|
||||
assert.strictEqual(stringSlowResult, '"primitive"')
|
||||
|
||||
const numberSlowResult = slowRedactFn(42)
|
||||
assert.strictEqual(numberSlowResult, '42')
|
||||
|
||||
// fast-redact throws an error for primitives in strict mode
|
||||
assert.throws(() => {
|
||||
fastRedactFn('primitive')
|
||||
}, /primitives cannot be redacted/)
|
||||
|
||||
assert.throws(() => {
|
||||
fastRedactFn(42)
|
||||
}, /primitives cannot be redacted/)
|
||||
})
|
||||
|
||||
test('integration: serialize false behavior difference', () => {
|
||||
const slowObj = { secret: 'hidden' }
|
||||
const fastObj = { secret: 'hidden' }
|
||||
const options = { paths: ['secret'], serialize: false }
|
||||
|
||||
const slowResult = slowRedact(options)(slowObj)
|
||||
const fastResult = fastRedact(options)(fastObj)
|
||||
|
||||
// Both should redact the secret
|
||||
assert.strictEqual(slowResult.secret, '[REDACTED]')
|
||||
assert.strictEqual(fastResult.secret, '[REDACTED]')
|
||||
|
||||
// @pinojs/redact always has restore method
|
||||
assert.strictEqual(typeof slowResult.restore, 'function')
|
||||
|
||||
// @pinojs/redact should restore to original value
|
||||
assert.strictEqual(slowResult.restore().secret, 'hidden')
|
||||
|
||||
// Key difference: original object state
|
||||
// fast-redact mutates the original, @pinojs/redact doesn't
|
||||
assert.strictEqual(slowObj.secret, 'hidden') // @pinojs/redact preserves original
|
||||
assert.strictEqual(fastObj.secret, '[REDACTED]') // fast-redact mutates original
|
||||
})
|
||||
|
||||
test('integration: censor function behavior', () => {
|
||||
const obj = { secret: 'hidden' }
|
||||
const options = {
|
||||
paths: ['secret'],
|
||||
censor: (value, path) => `REDACTED:${path}`
|
||||
}
|
||||
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: complex object with mixed patterns', () => {
|
||||
const obj = {
|
||||
users: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'john',
|
||||
credentials: { password: 'secret1', apiKey: 'key1' }
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'jane',
|
||||
credentials: { password: 'secret2', apiKey: 'key2' }
|
||||
}
|
||||
],
|
||||
config: {
|
||||
database: { password: 'db-secret' },
|
||||
api: { keys: ['key1', 'key2', 'key3'] }
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
paths: [
|
||||
'users.*.credentials.password',
|
||||
'users.*.credentials.apiKey',
|
||||
'config.database.password',
|
||||
'config.api.keys.*'
|
||||
]
|
||||
}
|
||||
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
// Remove option integration tests - comparing with fast-redact
|
||||
test('integration: remove option basic comparison with fast-redact', () => {
|
||||
const obj = { username: 'john', password: 'secret123' }
|
||||
const options = { paths: ['password'], remove: true }
|
||||
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
|
||||
// Verify the key is actually removed
|
||||
const parsed = JSON.parse(slowResult)
|
||||
assert.strictEqual(parsed.username, 'john')
|
||||
assert.strictEqual('password' in parsed, false)
|
||||
})
|
||||
|
||||
test('integration: remove option multiple paths comparison with fast-redact', () => {
|
||||
const obj = {
|
||||
user: { name: 'john', password: 'secret' },
|
||||
session: { token: 'abc123', id: 'session1' }
|
||||
}
|
||||
|
||||
const options = {
|
||||
paths: ['user.password', 'session.token'],
|
||||
remove: true
|
||||
}
|
||||
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: remove option wildcard comparison with fast-redact', () => {
|
||||
const obj = {
|
||||
secrets: {
|
||||
key1: 'secret1',
|
||||
key2: 'secret2'
|
||||
},
|
||||
public: 'data'
|
||||
}
|
||||
|
||||
const options = {
|
||||
paths: ['secrets.*'],
|
||||
remove: true
|
||||
}
|
||||
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: remove option intermediate wildcard comparison with fast-redact', () => {
|
||||
const obj = {
|
||||
users: {
|
||||
user1: { password: 'secret1', name: 'john' },
|
||||
user2: { password: 'secret2', name: 'jane' }
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
paths: ['users.*.password'],
|
||||
remove: true
|
||||
}
|
||||
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
})
|
||||
|
||||
test('integration: remove option with custom censor comparison with fast-redact', () => {
|
||||
const obj = { secret: 'hidden', public: 'data' }
|
||||
const options = {
|
||||
paths: ['secret'],
|
||||
censor: '***',
|
||||
remove: true
|
||||
}
|
||||
|
||||
const slowResult = slowRedact(options)(obj)
|
||||
const fastResult = fastRedact(options)(obj)
|
||||
|
||||
assert.strictEqual(slowResult, fastResult)
|
||||
|
||||
// With remove: true, censor value should be ignored
|
||||
const parsed = JSON.parse(slowResult)
|
||||
assert.strictEqual('secret' in parsed, false)
|
||||
assert.strictEqual(parsed.public, 'data')
|
||||
})
|
||||
|
||||
test('integration: remove option serialize false behavior - @pinojs/redact only', () => {
|
||||
// fast-redact doesn't support remove option with serialize: false
|
||||
// so we test @pinojs/redact's behavior only
|
||||
const obj = { secret: 'hidden', public: 'data' }
|
||||
const options = { paths: ['secret'], remove: true, serialize: false }
|
||||
|
||||
const result = slowRedact(options)(obj)
|
||||
|
||||
// Should have the key removed
|
||||
assert.strictEqual('secret' in result, false)
|
||||
assert.strictEqual(result.public, 'data')
|
||||
|
||||
// Should have restore method
|
||||
assert.strictEqual(typeof result.restore, 'function')
|
||||
|
||||
// Original object should be preserved
|
||||
assert.strictEqual(obj.secret, 'hidden')
|
||||
|
||||
// Restore should bring back the removed key
|
||||
const restored = result.restore()
|
||||
assert.strictEqual(restored.secret, 'hidden')
|
||||
})
|
||||
227
node_modules/@pinojs/redact/test/multiple-wildcards.test.js
generated
vendored
Normal file
227
node_modules/@pinojs/redact/test/multiple-wildcards.test.js
generated
vendored
Normal file
@@ -0,0 +1,227 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('node:test')
|
||||
const { strict: assert } = require('node:assert')
|
||||
const slowRedact = require('../index.js')
|
||||
|
||||
// Tests for Issue #2319: @pinojs/redact fails to redact patterns with 3+ consecutive wildcards
|
||||
test('three consecutive wildcards: *.*.*.password (4 levels deep)', () => {
|
||||
const obj = {
|
||||
simple: { password: 'secret-2-levels' },
|
||||
user: { auth: { password: 'secret-3-levels' } },
|
||||
nested: { deep: { auth: { password: 'secret-4-levels' } } }
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['*.*.*.password']
|
||||
})
|
||||
const result = redact(obj)
|
||||
const parsed = JSON.parse(result)
|
||||
|
||||
// Only the 4-level deep password should be redacted
|
||||
assert.strictEqual(parsed.simple.password, 'secret-2-levels', '2-level password should NOT be redacted')
|
||||
assert.strictEqual(parsed.user.auth.password, 'secret-3-levels', '3-level password should NOT be redacted')
|
||||
assert.strictEqual(parsed.nested.deep.auth.password, '[REDACTED]', '4-level password SHOULD be redacted')
|
||||
})
|
||||
|
||||
test('four consecutive wildcards: *.*.*.*.password (5 levels deep)', () => {
|
||||
const obj = {
|
||||
simple: { password: 'secret-2-levels' },
|
||||
user: { auth: { password: 'secret-3-levels' } },
|
||||
nested: { deep: { auth: { password: 'secret-4-levels' } } },
|
||||
config: { user: { auth: { settings: { password: 'secret-5-levels' } } } }
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['*.*.*.*.password']
|
||||
})
|
||||
const result = redact(obj)
|
||||
const parsed = JSON.parse(result)
|
||||
|
||||
// Only the 5-level deep password should be redacted
|
||||
assert.strictEqual(parsed.simple.password, 'secret-2-levels', '2-level password should NOT be redacted')
|
||||
assert.strictEqual(parsed.user.auth.password, 'secret-3-levels', '3-level password should NOT be redacted')
|
||||
assert.strictEqual(parsed.nested.deep.auth.password, 'secret-4-levels', '4-level password should NOT be redacted')
|
||||
assert.strictEqual(parsed.config.user.auth.settings.password, '[REDACTED]', '5-level password SHOULD be redacted')
|
||||
})
|
||||
|
||||
test('five consecutive wildcards: *.*.*.*.*.password (6 levels deep)', () => {
|
||||
const obj = {
|
||||
simple: { password: 'secret-2-levels' },
|
||||
user: { auth: { password: 'secret-3-levels' } },
|
||||
nested: { deep: { auth: { password: 'secret-4-levels' } } },
|
||||
config: { user: { auth: { settings: { password: 'secret-5-levels' } } } },
|
||||
data: {
|
||||
reqConfig: {
|
||||
data: {
|
||||
credentials: {
|
||||
settings: {
|
||||
password: 'secret-6-levels'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['*.*.*.*.*.password']
|
||||
})
|
||||
const result = redact(obj)
|
||||
const parsed = JSON.parse(result)
|
||||
|
||||
// Only the 6-level deep password should be redacted
|
||||
assert.strictEqual(parsed.simple.password, 'secret-2-levels', '2-level password should NOT be redacted')
|
||||
assert.strictEqual(parsed.user.auth.password, 'secret-3-levels', '3-level password should NOT be redacted')
|
||||
assert.strictEqual(parsed.nested.deep.auth.password, 'secret-4-levels', '4-level password should NOT be redacted')
|
||||
assert.strictEqual(parsed.config.user.auth.settings.password, 'secret-5-levels', '5-level password should NOT be redacted')
|
||||
assert.strictEqual(parsed.data.reqConfig.data.credentials.settings.password, '[REDACTED]', '6-level password SHOULD be redacted')
|
||||
})
|
||||
|
||||
test('three wildcards with censor function receives correct values', () => {
|
||||
const obj = {
|
||||
nested: { deep: { auth: { password: 'secret-value' } } }
|
||||
}
|
||||
|
||||
const censorCalls = []
|
||||
const redact = slowRedact({
|
||||
paths: ['*.*.*.password'],
|
||||
censor: (value, path) => {
|
||||
censorCalls.push({ value, path: [...path] })
|
||||
return '[REDACTED]'
|
||||
}
|
||||
})
|
||||
|
||||
const result = redact(obj)
|
||||
const parsed = JSON.parse(result)
|
||||
|
||||
// Should have been called exactly once with the correct value
|
||||
assert.strictEqual(censorCalls.length, 1, 'censor should be called once')
|
||||
assert.strictEqual(censorCalls[0].value, 'secret-value', 'censor should receive the actual value')
|
||||
assert.deepStrictEqual(censorCalls[0].path, ['nested', 'deep', 'auth', 'password'], 'censor should receive correct path')
|
||||
assert.strictEqual(parsed.nested.deep.auth.password, '[REDACTED]')
|
||||
})
|
||||
|
||||
test('three wildcards with multiple matches', () => {
|
||||
const obj = {
|
||||
api1: { v1: { auth: { token: 'token1' } } },
|
||||
api2: { v2: { auth: { token: 'token2' } } },
|
||||
api3: { v1: { auth: { token: 'token3' } } }
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['*.*.*.token']
|
||||
})
|
||||
const result = redact(obj)
|
||||
const parsed = JSON.parse(result)
|
||||
|
||||
// All three tokens should be redacted
|
||||
assert.strictEqual(parsed.api1.v1.auth.token, '[REDACTED]')
|
||||
assert.strictEqual(parsed.api2.v2.auth.token, '[REDACTED]')
|
||||
assert.strictEqual(parsed.api3.v1.auth.token, '[REDACTED]')
|
||||
})
|
||||
|
||||
test('three wildcards with remove option', () => {
|
||||
const obj = {
|
||||
nested: { deep: { auth: { password: 'secret', username: 'admin' } } }
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['*.*.*.password'],
|
||||
remove: true
|
||||
})
|
||||
const result = redact(obj)
|
||||
const parsed = JSON.parse(result)
|
||||
|
||||
// Password should be removed entirely
|
||||
assert.strictEqual('password' in parsed.nested.deep.auth, false, 'password key should be removed')
|
||||
assert.strictEqual(parsed.nested.deep.auth.username, 'admin', 'username should remain')
|
||||
})
|
||||
|
||||
test('mixed: two and three wildcards in same redactor', () => {
|
||||
const obj = {
|
||||
user: { auth: { password: 'secret-3-levels' } },
|
||||
config: { deep: { auth: { password: 'secret-4-levels' } } }
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['*.*.password', '*.*.*.password']
|
||||
})
|
||||
const result = redact(obj)
|
||||
const parsed = JSON.parse(result)
|
||||
|
||||
// Both should be redacted
|
||||
assert.strictEqual(parsed.user.auth.password, '[REDACTED]', '3-level should be redacted by *.*.password')
|
||||
assert.strictEqual(parsed.config.deep.auth.password, '[REDACTED]', '4-level should be redacted by *.*.*.password')
|
||||
})
|
||||
|
||||
test('three wildcards should not call censor for non-existent paths', () => {
|
||||
const obj = {
|
||||
shallow: { data: 'value' },
|
||||
nested: { deep: { auth: { password: 'secret' } } }
|
||||
}
|
||||
|
||||
let censorCallCount = 0
|
||||
const redact = slowRedact({
|
||||
paths: ['*.*.*.password'],
|
||||
censor: (value, path) => {
|
||||
censorCallCount++
|
||||
return '[REDACTED]'
|
||||
}
|
||||
})
|
||||
|
||||
redact(obj)
|
||||
|
||||
// Should only be called once for the path that exists
|
||||
assert.strictEqual(censorCallCount, 1, 'censor should only be called for existing paths')
|
||||
})
|
||||
|
||||
test('three wildcards with arrays', () => {
|
||||
const obj = {
|
||||
users: [
|
||||
{ auth: { password: 'secret1' } },
|
||||
{ auth: { password: 'secret2' } }
|
||||
]
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['*.*.*.password']
|
||||
})
|
||||
const result = redact(obj)
|
||||
const parsed = JSON.parse(result)
|
||||
|
||||
// Both passwords should be redacted (users[0].auth.password is 4 levels)
|
||||
assert.strictEqual(parsed.users[0].auth.password, '[REDACTED]')
|
||||
assert.strictEqual(parsed.users[1].auth.password, '[REDACTED]')
|
||||
})
|
||||
|
||||
test('four wildcards with authorization header (real-world case)', () => {
|
||||
const obj = {
|
||||
requests: {
|
||||
api1: {
|
||||
config: {
|
||||
headers: {
|
||||
authorization: 'Bearer secret-token'
|
||||
}
|
||||
}
|
||||
},
|
||||
api2: {
|
||||
config: {
|
||||
headers: {
|
||||
authorization: 'Bearer another-token'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['*.*.*.*.authorization']
|
||||
})
|
||||
const result = redact(obj)
|
||||
const parsed = JSON.parse(result)
|
||||
|
||||
// Both authorization headers should be redacted
|
||||
assert.strictEqual(parsed.requests.api1.config.headers.authorization, '[REDACTED]')
|
||||
assert.strictEqual(parsed.requests.api2.config.headers.authorization, '[REDACTED]')
|
||||
})
|
||||
223
node_modules/@pinojs/redact/test/prototype-pollution.test.js
generated
vendored
Normal file
223
node_modules/@pinojs/redact/test/prototype-pollution.test.js
generated
vendored
Normal file
@@ -0,0 +1,223 @@
|
||||
const { test } = require('node:test')
|
||||
const { strict: assert } = require('node:assert')
|
||||
const slowRedact = require('../index.js')
|
||||
|
||||
/* eslint-disable no-proto */
|
||||
|
||||
test('prototype pollution: __proto__ path should not pollute Object prototype', () => {
|
||||
const obj = {
|
||||
user: { name: 'john' },
|
||||
__proto__: { isAdmin: true }
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['__proto__.isAdmin'],
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const result = redact(obj)
|
||||
|
||||
// Should not pollute Object.prototype
|
||||
assert.strictEqual(Object.prototype.isAdmin, undefined)
|
||||
assert.strictEqual({}.isAdmin, undefined)
|
||||
|
||||
// Should redact the __proto__ property if it exists as a regular property
|
||||
assert.strictEqual(result.__proto__.isAdmin, '[REDACTED]')
|
||||
})
|
||||
|
||||
test('prototype pollution: constructor.prototype path should not pollute', () => {
|
||||
const obj = {
|
||||
user: { name: 'john' },
|
||||
constructor: {
|
||||
prototype: { isAdmin: true }
|
||||
}
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['constructor.prototype.isAdmin'],
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const result = redact(obj)
|
||||
|
||||
// Should not pollute Object.prototype
|
||||
assert.strictEqual(Object.prototype.isAdmin, undefined)
|
||||
assert.strictEqual({}.isAdmin, undefined)
|
||||
|
||||
// Should redact the constructor.prototype property if it exists as a regular property
|
||||
assert.strictEqual(result.constructor.prototype.isAdmin, '[REDACTED]')
|
||||
})
|
||||
|
||||
test('prototype pollution: nested __proto__ should not pollute', () => {
|
||||
const obj = {
|
||||
user: {
|
||||
settings: {
|
||||
__proto__: { isAdmin: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['user.settings.__proto__.isAdmin'],
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const result = redact(obj)
|
||||
|
||||
// Should not pollute Object.prototype
|
||||
assert.strictEqual(Object.prototype.isAdmin, undefined)
|
||||
assert.strictEqual({}.isAdmin, undefined)
|
||||
|
||||
// Should redact the nested __proto__ property
|
||||
assert.strictEqual(result.user.settings.__proto__.isAdmin, '[REDACTED]')
|
||||
})
|
||||
|
||||
test('prototype pollution: bracket notation __proto__ should not pollute', () => {
|
||||
const obj = {
|
||||
user: { name: 'john' },
|
||||
__proto__: { isAdmin: true }
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['["__proto__"]["isAdmin"]'],
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const result = redact(obj)
|
||||
|
||||
// Should not pollute Object.prototype
|
||||
assert.strictEqual(Object.prototype.isAdmin, undefined)
|
||||
assert.strictEqual({}.isAdmin, undefined)
|
||||
|
||||
// Should redact the __proto__ property when accessed via bracket notation
|
||||
assert.strictEqual(result.__proto__.isAdmin, '[REDACTED]')
|
||||
})
|
||||
|
||||
test('prototype pollution: wildcard with __proto__ should not pollute', () => {
|
||||
const obj = {
|
||||
users: {
|
||||
__proto__: { isAdmin: true },
|
||||
user1: { name: 'john' },
|
||||
user2: { name: 'jane' }
|
||||
}
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['users.*'],
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const result = redact(obj)
|
||||
|
||||
// Should not pollute Object.prototype
|
||||
assert.strictEqual(Object.prototype.isAdmin, undefined)
|
||||
assert.strictEqual({}.isAdmin, undefined)
|
||||
|
||||
// Should redact only own properties
|
||||
assert.strictEqual(result.users.user1, '[REDACTED]')
|
||||
assert.strictEqual(result.users.user2, '[REDACTED]')
|
||||
|
||||
// __proto__ should only be redacted if it's an own property, not inherited
|
||||
if (Object.prototype.hasOwnProperty.call(obj.users, '__proto__')) {
|
||||
assert.strictEqual(result.users.__proto__, '[REDACTED]')
|
||||
}
|
||||
})
|
||||
|
||||
test('prototype pollution: malicious JSON payload should not pollute', () => {
|
||||
// Simulate a malicious payload that might come from JSON.parse
|
||||
const maliciousObj = JSON.parse('{"user": {"name": "john"}, "__proto__": {"isAdmin": true}}')
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['__proto__.isAdmin'],
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const result = redact(maliciousObj)
|
||||
|
||||
// Should not pollute Object.prototype
|
||||
assert.strictEqual(Object.prototype.isAdmin, undefined)
|
||||
assert.strictEqual({}.isAdmin, undefined)
|
||||
|
||||
// The malicious payload should have been redacted
|
||||
assert.strictEqual(result.__proto__.isAdmin, '[REDACTED]')
|
||||
})
|
||||
|
||||
test('prototype pollution: verify prototype chain is preserved', () => {
|
||||
function CustomClass () {
|
||||
this.data = 'test'
|
||||
}
|
||||
CustomClass.prototype.method = function () { return 'original' }
|
||||
|
||||
const obj = new CustomClass()
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['data'],
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const result = redact(obj)
|
||||
|
||||
// Should redact the data property
|
||||
assert.strictEqual(result.data, '[REDACTED]')
|
||||
|
||||
// Should preserve the original prototype chain
|
||||
assert.strictEqual(result.method(), 'original')
|
||||
assert.strictEqual(Object.getPrototypeOf(result), CustomClass.prototype)
|
||||
})
|
||||
|
||||
test('prototype pollution: setValue should not create prototype pollution', () => {
|
||||
const obj = { user: { name: 'john' } }
|
||||
|
||||
// Try to pollute via non-existent path that could create __proto__
|
||||
const redact = slowRedact({
|
||||
paths: ['__proto__.isAdmin'],
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const result = redact(obj)
|
||||
|
||||
// Should not pollute Object.prototype
|
||||
assert.strictEqual(Object.prototype.isAdmin, undefined)
|
||||
assert.strictEqual({}.isAdmin, undefined)
|
||||
|
||||
// Should not create the path if it doesn't exist
|
||||
// The __proto__ property may exist due to Object.create, but should not contain our redacted value
|
||||
if (result.__proto__) {
|
||||
assert.strictEqual(result.__proto__.isAdmin, undefined)
|
||||
}
|
||||
})
|
||||
|
||||
test('prototype pollution: deep nested prototype properties should not pollute', () => {
|
||||
const obj = {
|
||||
level1: {
|
||||
level2: {
|
||||
level3: {
|
||||
__proto__: { isAdmin: true },
|
||||
constructor: {
|
||||
prototype: { isEvil: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: [
|
||||
'level1.level2.level3.__proto__.isAdmin',
|
||||
'level1.level2.level3.constructor.prototype.isEvil'
|
||||
],
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const result = redact(obj)
|
||||
|
||||
// Should not pollute Object.prototype
|
||||
assert.strictEqual(Object.prototype.isAdmin, undefined)
|
||||
assert.strictEqual(Object.prototype.isEvil, undefined)
|
||||
assert.strictEqual({}.isAdmin, undefined)
|
||||
assert.strictEqual({}.isEvil, undefined)
|
||||
|
||||
// Should redact the deep nested properties
|
||||
assert.strictEqual(result.level1.level2.level3.__proto__.isAdmin, '[REDACTED]')
|
||||
assert.strictEqual(result.level1.level2.level3.constructor.prototype.isEvil, '[REDACTED]')
|
||||
})
|
||||
115
node_modules/@pinojs/redact/test/selective-clone.test.js
generated
vendored
Normal file
115
node_modules/@pinojs/redact/test/selective-clone.test.js
generated
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
const { test } = require('node:test')
|
||||
const { strict: assert } = require('node:assert')
|
||||
const slowRedact = require('../index.js')
|
||||
|
||||
test('selective cloning shares references for non-redacted paths', () => {
|
||||
const sharedObject = { unchanged: 'data' }
|
||||
const obj = {
|
||||
toRedact: 'secret',
|
||||
shared: sharedObject,
|
||||
nested: {
|
||||
toRedact: 'secret2',
|
||||
shared: sharedObject
|
||||
}
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['toRedact', 'nested.toRedact'],
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const result = redact(obj)
|
||||
|
||||
// Redacted values should be different
|
||||
assert.strictEqual(result.toRedact, '[REDACTED]')
|
||||
assert.strictEqual(result.nested.toRedact, '[REDACTED]')
|
||||
|
||||
// Non-redacted references should be shared (same object reference)
|
||||
assert.strictEqual(result.shared, obj.shared)
|
||||
assert.strictEqual(result.nested.shared, obj.nested.shared)
|
||||
|
||||
// The shared object should be the exact same reference
|
||||
assert.strictEqual(result.shared, sharedObject)
|
||||
assert.strictEqual(result.nested.shared, sharedObject)
|
||||
})
|
||||
|
||||
test('selective cloning works with arrays', () => {
|
||||
const sharedItem = { unchanged: 'data' }
|
||||
const obj = {
|
||||
items: [
|
||||
{ secret: 'hidden1', shared: sharedItem },
|
||||
{ secret: 'hidden2', shared: sharedItem },
|
||||
sharedItem
|
||||
]
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['items.*.secret'],
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const result = redact(obj)
|
||||
|
||||
// Secrets should be redacted
|
||||
assert.strictEqual(result.items[0].secret, '[REDACTED]')
|
||||
assert.strictEqual(result.items[1].secret, '[REDACTED]')
|
||||
|
||||
// Shared references should be preserved where possible
|
||||
// Note: array items with secrets will be cloned, but their shared properties should still reference the original
|
||||
assert.strictEqual(result.items[0].shared, sharedItem)
|
||||
assert.strictEqual(result.items[1].shared, sharedItem)
|
||||
|
||||
// The third item gets cloned due to wildcard, but should have the same content
|
||||
assert.deepStrictEqual(result.items[2], sharedItem)
|
||||
// Note: Due to wildcard '*', all array items are cloned, even if they don't need redaction
|
||||
// This is still a significant optimization for object properties that aren't in wildcard paths
|
||||
})
|
||||
|
||||
test('selective cloning with no paths returns original object', () => {
|
||||
const obj = { data: 'unchanged' }
|
||||
const redact = slowRedact({
|
||||
paths: [],
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const result = redact(obj)
|
||||
|
||||
// Should return the exact same object reference
|
||||
assert.strictEqual(result, obj)
|
||||
})
|
||||
|
||||
test('selective cloning performance - large objects with minimal redaction', () => {
|
||||
// Create a large object with mostly shared data
|
||||
const sharedData = { large: 'data'.repeat(1000) }
|
||||
const obj = {
|
||||
secret: 'hidden',
|
||||
shared1: sharedData,
|
||||
shared2: sharedData,
|
||||
nested: {
|
||||
secret: 'hidden2',
|
||||
shared3: sharedData,
|
||||
deep: {
|
||||
shared4: sharedData,
|
||||
moreShared: sharedData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const redact = slowRedact({
|
||||
paths: ['secret', 'nested.secret'],
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const result = redact(obj)
|
||||
|
||||
// Verify redaction worked
|
||||
assert.strictEqual(result.secret, '[REDACTED]')
|
||||
assert.strictEqual(result.nested.secret, '[REDACTED]')
|
||||
|
||||
// Verify shared references are preserved
|
||||
assert.strictEqual(result.shared1, sharedData)
|
||||
assert.strictEqual(result.shared2, sharedData)
|
||||
assert.strictEqual(result.nested.shared3, sharedData)
|
||||
assert.strictEqual(result.nested.deep.shared4, sharedData)
|
||||
assert.strictEqual(result.nested.deep.moreShared, sharedData)
|
||||
})
|
||||
19
node_modules/@pinojs/redact/tsconfig.json
generated
vendored
Normal file
19
node_modules/@pinojs/redact/tsconfig.json
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"lib": [
|
||||
"es6"
|
||||
],
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true,
|
||||
"types": [],
|
||||
"noEmit": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"files": [
|
||||
"index.d.ts",
|
||||
"index.test-d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user