├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── benchmark └── index.js ├── example ├── default-usage.js ├── intermediate-wildcard-array.js ├── multi-wildcard-array-depth.js ├── multi-wildcard-array-end.js ├── multi-wildcard-array.js ├── serialize-false.js ├── serialize-function.js └── top-wildcard-object.js ├── index.js ├── lib ├── modifiers.js ├── parse.js ├── redactor.js ├── restorer.js ├── rx.js ├── state.js └── validator.js ├── package.json ├── readme.md └── test └── index.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | # This allows a subsequently queued workflow run to interrupt previous runs 6 | concurrency: 7 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | contents: read 16 | 17 | strategy: 18 | matrix: 19 | node-version: [6.x, 8.x, 10.x, 12.x, 14.x, 16.x, 18.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | with: 24 | persist-credentials: false 25 | 26 | - name: Use Node.js 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | 31 | - name: Install 32 | run: | 33 | npm install 34 | 35 | - name: Run tests 36 | run: | 37 | npm run test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | package-lock.json 4 | coverage 5 | .idea 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2020 David Mark Clements 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const bench = require('fastbench') 3 | const fastRedact = require('..') 4 | 5 | const censorFn = (v) => v + '.' 6 | const censorFnWithPath = (v, p) => v + '.' + p 7 | 8 | const noir = require('pino-noir')(['aa.b.c']) 9 | const redactNoSerialize = fastRedact({ paths: ['ab.b.c'], serialize: false }) 10 | const redactNoSerializeRestore = fastRedact({ paths: ['ac.b.c'], serialize: false }) 11 | const noirWild = require('pino-noir')(['ad.b.*']) 12 | const redactWildNoSerialize = fastRedact({ paths: ['ae.b.*'], serialize: false }) 13 | const redactWildNoSerializeRestore = fastRedact({ paths: ['af.b.*'], serialize: false }) 14 | const redactIntermediateWildNoSerialize = fastRedact({ paths: ['ag.*.c'], serialize: false }) 15 | const redactIntermediateWildNoSerializeRestore = fastRedact({ paths: ['ah.*.c'], serialize: false }) 16 | const noirJSONSerialize = require('pino-noir')(['aj.b.c']) // `ai` used in pure JSON test. 17 | const redact = fastRedact({ paths: ['ak.b.c'] }) 18 | const noirWildJSONSerialize = require('pino-noir')(['al.b.c']) 19 | const redactWild = fastRedact({ paths: ['am.b.*'] }) 20 | const redactIntermediateWild = fastRedact({ paths: ['an.*.c'] }) 21 | const redactIntermediateWildMatchWildOutcome = fastRedact({ paths: ['ao.*.c', 'ao.*.b', 'ao.*.a'] }) 22 | const redactStaticMatchWildOutcome = fastRedact({ paths: ['ap.b.c', 'ap.d.a', 'ap.d.b', 'ap.d.c'] }) 23 | const noirCensorFunction = require('pino-noir')(['aq.b.*'], censorFn) 24 | const redactCensorFunction = fastRedact({ paths: ['ar.b.*'], censor: censorFn, serialize: false }) 25 | const redactIntermediateWildCensorFunction = fastRedact({ paths: ['as.*.c'], censor: censorFn, serialize: false }) 26 | const redactCensorFunctionWithPath = fastRedact({ paths: ['at.d.b'], censor: censorFn, serialize: false }) 27 | const redactWildCensorFunctionWithPath = fastRedact({ paths: ['au.d.*'], censor: censorFnWithPath, serialize: false }) 28 | const redactIntermediateWildCensorFunctionWithPath = fastRedact({ paths: ['av.*.c'], censorFnWithPath, serialize: false }) 29 | const redactMultiWild = fastRedact({ paths: ['aw.*.*'] }) 30 | const redactMultiWildCensorFunction = fastRedact({ paths: ['ax.*.*'], censor: censorFn, serialize: false }) 31 | 32 | const getObj = (outerKey) => ({ 33 | [outerKey]: { 34 | b: { 35 | c: 's' 36 | }, 37 | d: { 38 | a: 's', 39 | b: 's', 40 | c: 's' 41 | } 42 | } 43 | }) 44 | 45 | const max = 500 46 | 47 | var run = bench([ 48 | function benchNoirV2 (cb) { 49 | const obj = getObj('aa') 50 | for (var i = 0; i < max; i++) { 51 | noir.aa(obj.aa) 52 | } 53 | setImmediate(cb) 54 | }, 55 | function benchFastRedact (cb) { 56 | const obj = getObj('ab') 57 | for (var i = 0; i < max; i++) { 58 | redactNoSerialize(obj) 59 | } 60 | setImmediate(cb) 61 | }, 62 | function benchFastRedactRestore (cb) { 63 | const obj = getObj('ac') 64 | for (var i = 0; i < max; i++) { 65 | redactNoSerializeRestore(obj) 66 | redactNoSerializeRestore.restore(obj) 67 | } 68 | setImmediate(cb) 69 | }, 70 | function benchNoirV2Wild (cb) { 71 | const obj = getObj('ad') 72 | for (var i = 0; i < max; i++) { 73 | noirWild.ad(obj.ad) 74 | } 75 | setImmediate(cb) 76 | }, 77 | function benchFastRedactWild (cb) { 78 | const obj = getObj('ae') 79 | for (var i = 0; i < max; i++) { 80 | redactWildNoSerialize(obj) 81 | } 82 | setImmediate(cb) 83 | }, 84 | function benchFastRedactWildRestore (cb) { 85 | const obj = getObj('af') 86 | for (var i = 0; i < max; i++) { 87 | redactWildNoSerializeRestore(obj) 88 | redactWildNoSerializeRestore.restore(obj) 89 | } 90 | setImmediate(cb) 91 | }, 92 | function benchFastRedactIntermediateWild (cb) { 93 | const obj = getObj('ag') 94 | for (var i = 0; i < max; i++) { 95 | redactIntermediateWildNoSerialize(obj) 96 | } 97 | setImmediate(cb) 98 | }, 99 | function benchFastRedactIntermediateWildRestore (cb) { 100 | const obj = getObj('ah') 101 | for (var i = 0; i < max; i++) { 102 | redactIntermediateWildNoSerializeRestore(obj) 103 | redactIntermediateWildNoSerializeRestore.restore(obj) 104 | } 105 | setImmediate(cb) 106 | }, 107 | function benchJSONStringify (cb) { 108 | const obj = getObj('ai') 109 | for (var i = 0; i < max; i++) { 110 | JSON.stringify(obj) 111 | } 112 | setImmediate(cb) 113 | }, 114 | function benchNoirV2Serialize (cb) { 115 | const obj = getObj('aj') 116 | for (var i = 0; i < max; i++) { 117 | noirJSONSerialize.aj(obj.aj) 118 | JSON.stringify(obj) 119 | } 120 | setImmediate(cb) 121 | }, 122 | function benchFastRedactSerialize (cb) { 123 | const obj = getObj('ak') 124 | for (var i = 0; i < max; i++) { 125 | redact(obj) 126 | } 127 | setImmediate(cb) 128 | }, 129 | function benchNoirV2WildSerialize (cb) { 130 | const obj = getObj('al') 131 | for (var i = 0; i < max; i++) { 132 | noirWildJSONSerialize.al(obj.al) 133 | JSON.stringify(obj) 134 | } 135 | setImmediate(cb) 136 | }, 137 | function benchFastRedactWildSerialize (cb) { 138 | const obj = getObj('am') 139 | for (var i = 0; i < max; i++) { 140 | redactWild(obj) 141 | } 142 | setImmediate(cb) 143 | }, 144 | function benchFastRedactIntermediateWildSerialize (cb) { 145 | const obj = getObj('an') 146 | for (var i = 0; i < max; i++) { 147 | redactIntermediateWild(obj) 148 | } 149 | setImmediate(cb) 150 | }, 151 | function benchFastRedactIntermediateWildMatchWildOutcomeSerialize (cb) { 152 | const obj = getObj('ao') 153 | for (var i = 0; i < max; i++) { 154 | redactIntermediateWildMatchWildOutcome(obj) 155 | } 156 | setImmediate(cb) 157 | }, 158 | function benchFastRedactStaticMatchWildOutcomeSerialize (cb) { 159 | const obj = getObj('ap') 160 | for (var i = 0; i < max; i++) { 161 | redactStaticMatchWildOutcome(obj) 162 | } 163 | setImmediate(cb) 164 | }, 165 | function benchNoirV2CensorFunction (cb) { 166 | const obj = getObj('aq') 167 | for (var i = 0; i < max; i++) { 168 | noirCensorFunction.aq(obj.aq) 169 | } 170 | setImmediate(cb) 171 | }, 172 | function benchFastRedactCensorFunction (cb) { 173 | const obj = getObj('ar') 174 | for (var i = 0; i < max; i++) { 175 | redactCensorFunction(obj) 176 | } 177 | setImmediate(cb) 178 | }, 179 | function benchFastRedactCensorFunctionIntermediateWild (cb) { 180 | const obj = getObj('as') 181 | for (var i = 0; i < max; i++) { 182 | redactIntermediateWildCensorFunction(obj) 183 | } 184 | setImmediate(cb) 185 | }, 186 | function benchFastRedactCensorFunctionWithPath (cb) { 187 | const obj = getObj('at') 188 | for (var i = 0; i < max; i++) { 189 | redactCensorFunctionWithPath(obj) 190 | } 191 | setImmediate(cb) 192 | }, 193 | function benchFastRedactWildCensorFunctionWithPath (cb) { 194 | const obj = getObj('au') 195 | for (var i = 0; i < max; i++) { 196 | redactWildCensorFunctionWithPath(obj) 197 | } 198 | setImmediate(cb) 199 | }, 200 | function benchFastRedactIntermediateWildCensorFunctionWithPath (cb) { 201 | const obj = getObj('av') 202 | for (var i = 0; i < max; i++) { 203 | redactIntermediateWildCensorFunctionWithPath(obj) 204 | } 205 | setImmediate(cb) 206 | }, 207 | function benchFastRedactMultiWild (cb) { 208 | const obj = getObj('aw') 209 | for (var i = 0; i < max; i++) { 210 | redactMultiWild(obj) 211 | } 212 | setImmediate(cb) 213 | }, 214 | function benchFastRedactMultiWildCensorFunction (cb) { 215 | const obj = getObj('ax') 216 | for (var i = 0; i < max; i++) { 217 | redactMultiWildCensorFunction(obj) 218 | } 219 | setImmediate(cb) 220 | } 221 | ], 500) 222 | 223 | run(run) 224 | -------------------------------------------------------------------------------- /example/default-usage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fastRedact = require('..') 3 | const fauxRequest = { 4 | headers: { 5 | host: 'http://example.com', 6 | cookie: `oh oh we don't want this exposed in logs in etc.`, 7 | referer: `if we're cool maybe we'll even redact this` 8 | } 9 | } 10 | const redact = fastRedact({ 11 | paths: ['headers.cookie', 'headers.referer'] 12 | }) 13 | 14 | console.log(redact(fauxRequest)) 15 | -------------------------------------------------------------------------------- /example/intermediate-wildcard-array.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fastRedact = require('..') 3 | const redact = fastRedact({ paths: ['a[*].c.d'] }) 4 | const obj = { 5 | a: [ 6 | { c: { d: 'hide me', e: 'leave me be' } }, 7 | { c: { d: 'and me', f: 'I want to live' } }, 8 | { c: { d: 'and also I', g: 'I want to run in a stream' } } 9 | ] 10 | } 11 | console.log(redact(obj)) 12 | -------------------------------------------------------------------------------- /example/multi-wildcard-array-depth.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fastRedact = require('..') 3 | const redact = fastRedact({ paths: ['a[*].c.d[*].i'] }) 4 | const obj = { 5 | a: [ 6 | { c: { d: [ { i: 'redact me', j: 'not me' } ], e: 'leave me be' } }, 7 | { c: { d: [ { i: 'redact me too', j: 'not me' }, { i: 'redact me too', j: 'not me' } ], f: 'I want to live' } }, 8 | { c: { d: [ { i: 'redact me 3', j: 'not me' } ], g: 'I want to run in a stream' } } 9 | ] 10 | } 11 | console.log(redact(obj)) 12 | -------------------------------------------------------------------------------- /example/multi-wildcard-array-end.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fastRedact = require('..') 3 | const redact = fastRedact({ paths: ['a[*].c.d[*]'] }) 4 | const obj = { 5 | a: [ 6 | { c: { d: ['hide me', '2'], e: 'leave me be' } }, 7 | { c: { d: ['and me'], f: 'I want to live' } }, 8 | { c: { d: ['and also I'], g: 'I want to run in a stream' } } 9 | ] 10 | } 11 | console.log(redact(obj)) 12 | -------------------------------------------------------------------------------- /example/multi-wildcard-array.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fastRedact = require('..') 3 | const redact = fastRedact({ paths: ['a[*].c[*].d'] }) 4 | const obj = { 5 | a: [ 6 | { c: [{ d: 'hide me', e: 'leave me be' }, { d: 'hide me too', e: 'leave me be' }, { d: 'hide me 3', e: 'leave me be' }] }, 7 | { c: [{ d: 'and me', f: 'I want to live' }] }, 8 | { c: [{ d: 'and also I', g: 'I want to run in a stream' }] } 9 | ] 10 | } 11 | console.log(redact(obj)) 12 | -------------------------------------------------------------------------------- /example/serialize-false.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fastRedact = require('..') 3 | const redact = fastRedact({ 4 | paths: ['a'], 5 | serialize: false 6 | }) 7 | const o = { a: 1, b: 2 } 8 | console.log(redact(o) === o) 9 | console.log(o) 10 | console.log(redact.restore(o) === o) 11 | console.log(o) 12 | -------------------------------------------------------------------------------- /example/serialize-function.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fastRedact = require('..') 3 | const redact = fastRedact({ paths: ['a'], serialize: (o) => JSON.stringify(o, 0, 2) }) 4 | console.log(redact({ a: 1, b: 2 })) 5 | -------------------------------------------------------------------------------- /example/top-wildcard-object.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fastRedact = require('..') 3 | const redact = fastRedact({ paths: ['*.c.d'] }) 4 | const obj = { 5 | x: { c: { d: 'hide me', e: 'leave me be' } }, 6 | y: { c: { d: 'and me', f: 'I want to live' } }, 7 | z: { c: { d: 'and also I', g: 'I want to run in a stream' } } 8 | } 9 | console.log(redact(obj)) 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const validator = require('./lib/validator') 4 | const parse = require('./lib/parse') 5 | const redactor = require('./lib/redactor') 6 | const restorer = require('./lib/restorer') 7 | const { groupRedact, nestedRedact } = require('./lib/modifiers') 8 | const state = require('./lib/state') 9 | const rx = require('./lib/rx') 10 | const validate = validator() 11 | const noop = (o) => o 12 | noop.restore = noop 13 | 14 | const DEFAULT_CENSOR = '[REDACTED]' 15 | fastRedact.rx = rx 16 | fastRedact.validator = validator 17 | 18 | module.exports = fastRedact 19 | 20 | function fastRedact (opts = {}) { 21 | const paths = Array.from(new Set(opts.paths || [])) 22 | const serialize = 'serialize' in opts ? ( 23 | opts.serialize === false ? opts.serialize 24 | : (typeof opts.serialize === 'function' ? opts.serialize : JSON.stringify) 25 | ) : JSON.stringify 26 | const remove = opts.remove 27 | if (remove === true && serialize !== JSON.stringify) { 28 | throw Error('fast-redact – remove option may only be set when serializer is JSON.stringify') 29 | } 30 | const censor = remove === true 31 | ? undefined 32 | : 'censor' in opts ? opts.censor : DEFAULT_CENSOR 33 | 34 | const isCensorFct = typeof censor === 'function' 35 | const censorFctTakesPath = isCensorFct && censor.length > 1 36 | 37 | if (paths.length === 0) return serialize || noop 38 | 39 | validate({ paths, serialize, censor }) 40 | 41 | const { wildcards, wcLen, secret } = parse({ paths, censor }) 42 | 43 | const compileRestore = restorer() 44 | const strict = 'strict' in opts ? opts.strict : true 45 | 46 | return redactor({ secret, wcLen, serialize, strict, isCensorFct, censorFctTakesPath }, state({ 47 | secret, 48 | censor, 49 | compileRestore, 50 | serialize, 51 | groupRedact, 52 | nestedRedact, 53 | wildcards, 54 | wcLen 55 | })) 56 | } 57 | -------------------------------------------------------------------------------- /lib/modifiers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | groupRedact, 5 | groupRestore, 6 | nestedRedact, 7 | nestedRestore 8 | } 9 | 10 | function groupRestore ({ keys, values, target }) { 11 | if (target == null || typeof target === 'string') return 12 | const length = keys.length 13 | for (var i = 0; i < length; i++) { 14 | const k = keys[i] 15 | target[k] = values[i] 16 | } 17 | } 18 | 19 | function groupRedact (o, path, censor, isCensorFct, censorFctTakesPath) { 20 | const target = get(o, path) 21 | if (target == null || typeof target === 'string') return { keys: null, values: null, target, flat: true } 22 | const keys = Object.keys(target) 23 | const keysLength = keys.length 24 | const pathLength = path.length 25 | const pathWithKey = censorFctTakesPath ? [...path] : undefined 26 | const values = new Array(keysLength) 27 | 28 | for (var i = 0; i < keysLength; i++) { 29 | const key = keys[i] 30 | values[i] = target[key] 31 | 32 | if (censorFctTakesPath) { 33 | pathWithKey[pathLength] = key 34 | target[key] = censor(target[key], pathWithKey) 35 | } else if (isCensorFct) { 36 | target[key] = censor(target[key]) 37 | } else { 38 | target[key] = censor 39 | } 40 | } 41 | return { keys, values, target, flat: true } 42 | } 43 | 44 | /** 45 | * @param {RestoreInstruction[]} instructions a set of instructions for restoring values to objects 46 | */ 47 | function nestedRestore (instructions) { 48 | for (let i = 0; i < instructions.length; i++) { 49 | const { target, path, value } = instructions[i] 50 | let current = target 51 | for (let i = path.length - 1; i > 0; i--) { 52 | current = current[path[i]] 53 | } 54 | current[path[0]] = value 55 | } 56 | } 57 | 58 | function nestedRedact (store, o, path, ns, censor, isCensorFct, censorFctTakesPath) { 59 | const target = get(o, path) 60 | if (target == null) return 61 | const keys = Object.keys(target) 62 | const keysLength = keys.length 63 | for (var i = 0; i < keysLength; i++) { 64 | const key = keys[i] 65 | specialSet(store, target, key, path, ns, censor, isCensorFct, censorFctTakesPath) 66 | } 67 | return store 68 | } 69 | 70 | function has (obj, prop) { 71 | return obj !== undefined && obj !== null 72 | ? ('hasOwn' in Object ? Object.hasOwn(obj, prop) : Object.prototype.hasOwnProperty.call(obj, prop)) 73 | : false 74 | } 75 | 76 | function specialSet (store, o, k, path, afterPath, censor, isCensorFct, censorFctTakesPath) { 77 | const afterPathLen = afterPath.length 78 | const lastPathIndex = afterPathLen - 1 79 | const originalKey = k 80 | var i = -1 81 | var n 82 | var nv 83 | var ov 84 | var oov = null 85 | var wc = null 86 | var kIsWc 87 | var wcov 88 | var consecutive = false 89 | var level = 0 90 | // need to track depth of the `redactPath` tree 91 | var depth = 0 92 | var redactPathCurrent = tree() 93 | ov = n = o[k] 94 | if (typeof n !== 'object') return 95 | while (n != null && ++i < afterPathLen) { 96 | depth += 1 97 | k = afterPath[i] 98 | oov = ov 99 | if (k !== '*' && !wc && !(typeof n === 'object' && k in n)) { 100 | break 101 | } 102 | if (k === '*') { 103 | if (wc === '*') { 104 | consecutive = true 105 | } 106 | wc = k 107 | if (i !== lastPathIndex) { 108 | continue 109 | } 110 | } 111 | if (wc) { 112 | const wcKeys = Object.keys(n) 113 | for (var j = 0; j < wcKeys.length; j++) { 114 | const wck = wcKeys[j] 115 | wcov = n[wck] 116 | kIsWc = k === '*' 117 | if (consecutive) { 118 | redactPathCurrent = node(redactPathCurrent, wck, depth) 119 | level = i 120 | ov = iterateNthLevel(wcov, level - 1, k, path, afterPath, censor, isCensorFct, censorFctTakesPath, originalKey, n, nv, ov, kIsWc, wck, i, lastPathIndex, redactPathCurrent, store, o[originalKey], depth + 1) 121 | } else { 122 | if (kIsWc || (typeof wcov === 'object' && wcov !== null && k in wcov)) { 123 | if (kIsWc) { 124 | ov = wcov 125 | } else { 126 | ov = wcov[k] 127 | } 128 | nv = (i !== lastPathIndex) 129 | ? ov 130 | : (isCensorFct 131 | ? (censorFctTakesPath ? censor(ov, [...path, originalKey, ...afterPath]) : censor(ov)) 132 | : censor) 133 | if (kIsWc) { 134 | const rv = restoreInstr(node(redactPathCurrent, wck, depth), ov, o[originalKey]) 135 | store.push(rv) 136 | n[wck] = nv 137 | } else { 138 | if (wcov[k] === nv) { 139 | // pass 140 | } else if ((nv === undefined && censor !== undefined) || (has(wcov, k) && nv === ov)) { 141 | redactPathCurrent = node(redactPathCurrent, wck, depth) 142 | } else { 143 | redactPathCurrent = node(redactPathCurrent, wck, depth) 144 | const rv = restoreInstr(node(redactPathCurrent, k, depth + 1), ov, o[originalKey]) 145 | store.push(rv) 146 | wcov[k] = nv 147 | } 148 | } 149 | } 150 | } 151 | } 152 | wc = null 153 | } else { 154 | ov = n[k] 155 | redactPathCurrent = node(redactPathCurrent, k, depth) 156 | nv = (i !== lastPathIndex) 157 | ? ov 158 | : (isCensorFct 159 | ? (censorFctTakesPath ? censor(ov, [...path, originalKey, ...afterPath]) : censor(ov)) 160 | : censor) 161 | if ((has(n, k) && nv === ov) || (nv === undefined && censor !== undefined)) { 162 | // pass 163 | } else { 164 | const rv = restoreInstr(redactPathCurrent, ov, o[originalKey]) 165 | store.push(rv) 166 | n[k] = nv 167 | } 168 | n = n[k] 169 | } 170 | if (typeof n !== 'object') break 171 | // prevent circular structure, see https://github.com/pinojs/pino/issues/1513 172 | if (ov === oov || typeof ov === 'undefined') { 173 | // pass 174 | } 175 | } 176 | } 177 | 178 | function get (o, p) { 179 | var i = -1 180 | var l = p.length 181 | var n = o 182 | while (n != null && ++i < l) { 183 | n = n[p[i]] 184 | } 185 | return n 186 | } 187 | 188 | function iterateNthLevel (wcov, level, k, path, afterPath, censor, isCensorFct, censorFctTakesPath, originalKey, n, nv, ov, kIsWc, wck, i, lastPathIndex, redactPathCurrent, store, parent, depth) { 189 | if (level === 0) { 190 | if (kIsWc || (typeof wcov === 'object' && wcov !== null && k in wcov)) { 191 | if (kIsWc) { 192 | ov = wcov 193 | } else { 194 | ov = wcov[k] 195 | } 196 | nv = (i !== lastPathIndex) 197 | ? ov 198 | : (isCensorFct 199 | ? (censorFctTakesPath ? censor(ov, [...path, originalKey, ...afterPath]) : censor(ov)) 200 | : censor) 201 | if (kIsWc) { 202 | const rv = restoreInstr(redactPathCurrent, ov, parent) 203 | store.push(rv) 204 | n[wck] = nv 205 | } else { 206 | if (wcov[k] === nv) { 207 | // pass 208 | } else if ((nv === undefined && censor !== undefined) || (has(wcov, k) && nv === ov)) { 209 | // pass 210 | } else { 211 | const rv = restoreInstr(node(redactPathCurrent, k, depth + 1), ov, parent) 212 | store.push(rv) 213 | wcov[k] = nv 214 | } 215 | } 216 | } 217 | } 218 | for (const key in wcov) { 219 | if (typeof wcov[key] === 'object') { 220 | redactPathCurrent = node(redactPathCurrent, key, depth) 221 | iterateNthLevel(wcov[key], level - 1, k, path, afterPath, censor, isCensorFct, censorFctTakesPath, originalKey, n, nv, ov, kIsWc, wck, i, lastPathIndex, redactPathCurrent, store, parent, depth + 1) 222 | } 223 | } 224 | } 225 | 226 | /** 227 | * @typedef {object} TreeNode 228 | * @prop {TreeNode} [parent] reference to the parent of this node in the tree, or `null` if there is no parent 229 | * @prop {string} key the key that this node represents (key here being part of the path being redacted 230 | * @prop {TreeNode[]} children the child nodes of this node 231 | * @prop {number} depth the depth of this node in the tree 232 | */ 233 | 234 | /** 235 | * instantiate a new, empty tree 236 | * @returns {TreeNode} 237 | */ 238 | function tree () { 239 | return { parent: null, key: null, children: [], depth: 0 } 240 | } 241 | 242 | /** 243 | * creates a new node in the tree, attaching it as a child of the provided parent node 244 | * if the specified depth matches the parent depth, adds the new node as a _sibling_ of the parent instead 245 | * @param {TreeNode} parent the parent node to add a new node to (if the parent depth matches the provided `depth` value, will instead add as a sibling of this 246 | * @param {string} key the key that the new node represents (key here being part of the path being redacted) 247 | * @param {number} depth the depth of the new node in the tree - used to determing whether to add the new node as a child or sibling of the provided `parent` node 248 | * @returns {TreeNode} a reference to the newly created node in the tree 249 | */ 250 | function node (parent, key, depth) { 251 | if (parent.depth === depth) { 252 | return node(parent.parent, key, depth) 253 | } 254 | 255 | var child = { 256 | parent, 257 | key, 258 | depth, 259 | children: [] 260 | } 261 | 262 | parent.children.push(child) 263 | 264 | return child 265 | } 266 | 267 | /** 268 | * @typedef {object} RestoreInstruction 269 | * @prop {string[]} path a reverse-order path that can be used to find the correct insertion point to restore a `value` for the given `parent` object 270 | * @prop {*} value the value to restore 271 | * @prop {object} target the object to restore the `value` in 272 | */ 273 | 274 | /** 275 | * create a restore instruction for the given redactPath node 276 | * generates a path in reverse order by walking up the redactPath tree 277 | * @param {TreeNode} node a tree node that should be at the bottom of the redact path (i.e. have no children) - this will be used to walk up the redact path tree to construct the path needed to restore 278 | * @param {*} value the value to restore 279 | * @param {object} target a reference to the parent object to apply the restore instruction to 280 | * @returns {RestoreInstruction} an instruction used to restore a nested value for a specific object 281 | */ 282 | function restoreInstr (node, value, target) { 283 | let current = node 284 | const path = [] 285 | do { 286 | path.push(current.key) 287 | current = current.parent 288 | } while (current.parent != null) 289 | 290 | return { path, value, target } 291 | } 292 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rx = require('./rx') 4 | 5 | module.exports = parse 6 | 7 | function parse ({ paths }) { 8 | const wildcards = [] 9 | var wcLen = 0 10 | const secret = paths.reduce(function (o, strPath, ix) { 11 | var path = strPath.match(rx).map((p) => p.replace(/'|"|`/g, '')) 12 | const leadingBracket = strPath[0] === '[' 13 | path = path.map((p) => { 14 | if (p[0] === '[') return p.substr(1, p.length - 2) 15 | else return p 16 | }) 17 | const star = path.indexOf('*') 18 | if (star > -1) { 19 | const before = path.slice(0, star) 20 | const beforeStr = before.join('.') 21 | const after = path.slice(star + 1, path.length) 22 | const nested = after.length > 0 23 | wcLen++ 24 | wildcards.push({ 25 | before, 26 | beforeStr, 27 | after, 28 | nested 29 | }) 30 | } else { 31 | o[strPath] = { 32 | path: path, 33 | val: undefined, 34 | precensored: false, 35 | circle: '', 36 | escPath: JSON.stringify(strPath), 37 | leadingBracket: leadingBracket 38 | } 39 | } 40 | return o 41 | }, {}) 42 | 43 | return { wildcards, wcLen, secret } 44 | } 45 | -------------------------------------------------------------------------------- /lib/redactor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rx = require('./rx') 4 | 5 | module.exports = redactor 6 | 7 | function redactor ({ secret, serialize, wcLen, strict, isCensorFct, censorFctTakesPath }, state) { 8 | /* eslint-disable-next-line */ 9 | const redact = Function('o', ` 10 | if (typeof o !== 'object' || o == null) { 11 | ${strictImpl(strict, serialize)} 12 | } 13 | const { censor, secret } = this 14 | const originalSecret = {} 15 | const secretKeys = Object.keys(secret) 16 | for (var i = 0; i < secretKeys.length; i++) { 17 | originalSecret[secretKeys[i]] = secret[secretKeys[i]] 18 | } 19 | 20 | ${redactTmpl(secret, isCensorFct, censorFctTakesPath)} 21 | this.compileRestore() 22 | ${dynamicRedactTmpl(wcLen > 0, isCensorFct, censorFctTakesPath)} 23 | this.secret = originalSecret 24 | ${resultTmpl(serialize)} 25 | `).bind(state) 26 | 27 | redact.state = state 28 | 29 | if (serialize === false) { 30 | redact.restore = (o) => state.restore(o) 31 | } 32 | 33 | return redact 34 | } 35 | 36 | function redactTmpl (secret, isCensorFct, censorFctTakesPath) { 37 | return Object.keys(secret).map((path) => { 38 | const { escPath, leadingBracket, path: arrPath } = secret[path] 39 | const skip = leadingBracket ? 1 : 0 40 | const delim = leadingBracket ? '' : '.' 41 | const hops = [] 42 | var match 43 | while ((match = rx.exec(path)) !== null) { 44 | const [ , ix ] = match 45 | const { index, input } = match 46 | if (index > skip) hops.push(input.substring(0, index - (ix ? 0 : 1))) 47 | } 48 | var existence = hops.map((p) => `o${delim}${p}`).join(' && ') 49 | if (existence.length === 0) existence += `o${delim}${path} != null` 50 | else existence += ` && o${delim}${path} != null` 51 | 52 | const circularDetection = ` 53 | switch (true) { 54 | ${hops.reverse().map((p) => ` 55 | case o${delim}${p} === censor: 56 | secret[${escPath}].circle = ${JSON.stringify(p)} 57 | break 58 | `).join('\n')} 59 | } 60 | ` 61 | 62 | const censorArgs = censorFctTakesPath 63 | ? `val, ${JSON.stringify(arrPath)}` 64 | : `val` 65 | 66 | return ` 67 | if (${existence}) { 68 | const val = o${delim}${path} 69 | if (val === censor) { 70 | secret[${escPath}].precensored = true 71 | } else { 72 | secret[${escPath}].val = val 73 | o${delim}${path} = ${isCensorFct ? `censor(${censorArgs})` : 'censor'} 74 | ${circularDetection} 75 | } 76 | } 77 | ` 78 | }).join('\n') 79 | } 80 | 81 | function dynamicRedactTmpl (hasWildcards, isCensorFct, censorFctTakesPath) { 82 | return hasWildcards === true ? ` 83 | { 84 | const { wildcards, wcLen, groupRedact, nestedRedact } = this 85 | for (var i = 0; i < wcLen; i++) { 86 | const { before, beforeStr, after, nested } = wildcards[i] 87 | if (nested === true) { 88 | secret[beforeStr] = secret[beforeStr] || [] 89 | nestedRedact(secret[beforeStr], o, before, after, censor, ${isCensorFct}, ${censorFctTakesPath}) 90 | } else secret[beforeStr] = groupRedact(o, before, censor, ${isCensorFct}, ${censorFctTakesPath}) 91 | } 92 | } 93 | ` : '' 94 | } 95 | 96 | function resultTmpl (serialize) { 97 | return serialize === false ? `return o` : ` 98 | var s = this.serialize(o) 99 | this.restore(o) 100 | return s 101 | ` 102 | } 103 | 104 | function strictImpl (strict, serialize) { 105 | return strict === true 106 | ? `throw Error('fast-redact: primitives cannot be redacted')` 107 | : serialize === false ? `return o` : `return this.serialize(o)` 108 | } 109 | -------------------------------------------------------------------------------- /lib/restorer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { groupRestore, nestedRestore } = require('./modifiers') 4 | 5 | module.exports = restorer 6 | 7 | function restorer () { 8 | return function compileRestore () { 9 | if (this.restore) { 10 | this.restore.state.secret = this.secret 11 | return 12 | } 13 | const { secret, wcLen } = this 14 | const paths = Object.keys(secret) 15 | const resetters = resetTmpl(secret, paths) 16 | const hasWildcards = wcLen > 0 17 | const state = hasWildcards ? { secret, groupRestore, nestedRestore } : { secret } 18 | /* eslint-disable-next-line */ 19 | this.restore = Function( 20 | 'o', 21 | restoreTmpl(resetters, paths, hasWildcards) 22 | ).bind(state) 23 | this.restore.state = state 24 | } 25 | } 26 | 27 | /** 28 | * Mutates the original object to be censored by restoring its original values 29 | * prior to censoring. 30 | * 31 | * @param {object} secret Compiled object describing which target fields should 32 | * be censored and the field states. 33 | * @param {string[]} paths The list of paths to censor as provided at 34 | * initialization time. 35 | * 36 | * @returns {string} String of JavaScript to be used by `Function()`. The 37 | * string compiles to the function that does the work in the description. 38 | */ 39 | function resetTmpl (secret, paths) { 40 | return paths.map((path) => { 41 | const { circle, escPath, leadingBracket } = secret[path] 42 | const delim = leadingBracket ? '' : '.' 43 | const reset = circle 44 | ? `o.${circle} = secret[${escPath}].val` 45 | : `o${delim}${path} = secret[${escPath}].val` 46 | const clear = `secret[${escPath}].val = undefined` 47 | return ` 48 | if (secret[${escPath}].val !== undefined) { 49 | try { ${reset} } catch (e) {} 50 | ${clear} 51 | } 52 | ` 53 | }).join('') 54 | } 55 | 56 | /** 57 | * Creates the body of the restore function 58 | * 59 | * Restoration of the redacted object happens 60 | * backwards, in reverse order of redactions, 61 | * so that repeated redactions on the same object 62 | * property can be eventually rolled back to the 63 | * original value. 64 | * 65 | * This way dynamic redactions are restored first, 66 | * starting from the last one working backwards and 67 | * followed by the static ones. 68 | * 69 | * @returns {string} the body of the restore function 70 | */ 71 | function restoreTmpl (resetters, paths, hasWildcards) { 72 | const dynamicReset = hasWildcards === true ? ` 73 | const keys = Object.keys(secret) 74 | const len = keys.length 75 | for (var i = len - 1; i >= ${paths.length}; i--) { 76 | const k = keys[i] 77 | const o = secret[k] 78 | if (o) { 79 | if (o.flat === true) this.groupRestore(o) 80 | else this.nestedRestore(o) 81 | secret[k] = null 82 | } 83 | } 84 | ` : '' 85 | 86 | return ` 87 | const secret = this.secret 88 | ${dynamicReset} 89 | ${resetters} 90 | return o 91 | ` 92 | } 93 | -------------------------------------------------------------------------------- /lib/rx.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = /[^.[\]]+|\[((?:.)*?)\]/g 4 | 5 | /* 6 | Regular expression explanation: 7 | 8 | Alt 1: /[^.[\]]+/ - Match one or more characters that are *not* a dot (.) 9 | opening square bracket ([) or closing square bracket (]) 10 | 11 | Alt 2: /\[((?:.)*?)\]/ - If the char IS dot or square bracket, then create a capture 12 | group (which will be capture group $1) that matches anything 13 | within square brackets. Expansion is lazy so it will 14 | stop matching as soon as the first closing bracket is met `]` 15 | (rather than continuing to match until the final closing bracket). 16 | */ 17 | -------------------------------------------------------------------------------- /lib/state.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = state 4 | 5 | function state (o) { 6 | const { 7 | secret, 8 | censor, 9 | compileRestore, 10 | serialize, 11 | groupRedact, 12 | nestedRedact, 13 | wildcards, 14 | wcLen 15 | } = o 16 | const builder = [{ secret, censor, compileRestore }] 17 | if (serialize !== false) builder.push({ serialize }) 18 | if (wcLen > 0) builder.push({ groupRedact, nestedRedact, wildcards, wcLen }) 19 | return Object.assign(...builder) 20 | } 21 | -------------------------------------------------------------------------------- /lib/validator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = validator 4 | 5 | function validator (opts = {}) { 6 | const { 7 | ERR_PATHS_MUST_BE_STRINGS = () => 'fast-redact - Paths must be (non-empty) strings', 8 | ERR_INVALID_PATH = (s) => `fast-redact – Invalid path (${s})` 9 | } = opts 10 | 11 | return function validate ({ paths }) { 12 | paths.forEach((s) => { 13 | if (typeof s !== 'string') { 14 | throw Error(ERR_PATHS_MUST_BE_STRINGS()) 15 | } 16 | try { 17 | if (/〇/.test(s)) throw Error() 18 | const expr = (s[0] === '[' ? '' : '.') + s.replace(/^\*/, '〇').replace(/\.\*/g, '.〇').replace(/\[\*\]/g, '[〇]') 19 | if (/\n|\r|;/.test(expr)) throw Error() 20 | if (/\/\*/.test(expr)) throw Error() 21 | /* eslint-disable-next-line */ 22 | Function(` 23 | 'use strict' 24 | const o = new Proxy({}, { get: () => o, set: () => { throw Error() } }); 25 | const 〇 = null; 26 | o${expr} 27 | if ([o${expr}].length !== 1) throw Error()`)() 28 | } catch (e) { 29 | throw Error(ERR_INVALID_PATH(s)) 30 | } 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-redact", 3 | "version": "3.5.0", 4 | "description": "very fast object redaction", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tap test", 8 | "posttest": "standard index.js 'lib/*.js' 'example/*.js' benchmark/index.js test/index.js | snazzy", 9 | "cov": "tap --cov test", 10 | "cov-ui": "tap --coverage-report=html test", 11 | "ci": "tap --cov --100 test", 12 | "bench": "node benchmark" 13 | }, 14 | "keywords": [ 15 | "redact", 16 | "censor", 17 | "performance", 18 | "performant", 19 | "gdpr", 20 | "fast", 21 | "speed", 22 | "serialize", 23 | "stringify" 24 | ], 25 | "author": "David Mark Clements ", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "fastbench": "^1.0.1", 29 | "pino-noir": "^2.2.1", 30 | "snazzy": "^8.0.0", 31 | "standard": "^12.0.1", 32 | "tap": "^12.5.2" 33 | }, 34 | "engines": { 35 | "node": ">=6" 36 | }, 37 | "directories": { 38 | "example": "example", 39 | "lib": "lib", 40 | "test": "test" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/davidmarkclements/fast-redact.git" 45 | }, 46 | "bugs": { 47 | "url": "https://github.com/davidmarkclements/fast-redact/issues" 48 | }, 49 | "homepage": "https://github.com/davidmarkclements/fast-redact#readme" 50 | } 51 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # fast-redact 2 | 3 | very fast object redaction 4 | 5 | [![Build Status](https://travis-ci.org/davidmarkclements/fast-redact.svg?branch=master)](https://travis-ci.org/davidmarkclements/fast-redact) 6 | 7 | ## Default Usage 8 | 9 | By default, `fast-redact` serializes an object with `JSON.stringify`, censoring any 10 | data at paths specified: 11 | 12 | ```js 13 | const fastRedact = require('fast-redact') 14 | const fauxRequest = { 15 | headers: { 16 | host: 'http://example.com', 17 | cookie: `oh oh we don't want this exposed in logs in etc.`, 18 | referer: `if we're cool maybe we'll even redact this`, 19 | // Note: headers often contain hyphens and require bracket notation 20 | 'X-Forwarded-For': `192.168.0.1` 21 | } 22 | } 23 | const redact = fastRedact({ 24 | paths: ['headers.cookie', 'headers.referer', 'headers["X-Forwarded-For"]'] 25 | }) 26 | 27 | console.log(redact(fauxRequest)) 28 | // {"headers":{"host":"http://example.com","cookie":"[REDACTED]","referer":"[REDACTED]","X-Forwarded-For": "[REDACTED]"}} 29 | ``` 30 | 31 | ## API 32 | 33 | ### `require('fast-redact')({paths, censor, serialize}) => Function` 34 | 35 | When called without any options, or with a zero length `paths` array, 36 | `fast-redact` will return `JSON.stringify` or the `serialize` option, if set. 37 | 38 | #### `paths` – `Array` 39 | 40 | An array of strings describing the nested location of a key in an object. 41 | 42 | The syntax follows that of the EcmaScript specification, that is any JavaScript 43 | path is accepted – both bracket and dot notation is supported. For instance in 44 | each of the following cases, the `c` property will be redacted: `a.b.c`,`a['b'].c`, 45 | `a["b"].c`, `a[``b``].c`. Since bracket notation is supported, array indices are also 46 | supported `a[0].b` would redact the `b` key in the first object of the `a` array. 47 | 48 | Leading brackets are also allowed, for instance `["a"].b.c` will work. 49 | 50 | ##### Wildcards 51 | 52 | In addition to static paths, asterisk wildcards are also supported. 53 | 54 | When an asterisk is place in the final position it will redact all keys within the 55 | parent object. For instance `a.b.*` will redact all keys in the `b` object. Similarly 56 | for arrays `a.b[*]` will redact all elements of an array (in truth it actually doesn't matter 57 | whether `b` is in an object or array in either case, both notation styles will work). 58 | 59 | When an asterisk is in an intermediate or first position, the paths following the asterisk will 60 | be redacted for every object within the parent. 61 | 62 | For example: 63 | 64 | ```js 65 | const fastRedact = require('fast-redact') 66 | const redact = fastRedact({paths: ['*.c.d']}) 67 | const obj = { 68 | x: {c: {d: 'hide me', e: 'leave me be'}}, 69 | y: {c: {d: 'and me', f: 'I want to live'}}, 70 | z: {c: {d: 'and also I', g: 'I want to run in a stream'}} 71 | } 72 | console.log(redact(obj)) 73 | // {"x":{"c":{"d":"[REDACTED]","e":"leave me be"}},"y":{"c":{"d":"[REDACTED]","f":"I want to live"}},"z":{"c":{"d":"[REDACTED]","g":"I want to run in a stream"}}} 74 | ``` 75 | 76 | Another example with a nested array: 77 | 78 | ```js 79 | const fastRedact = require('..') 80 | const redact = fastRedact({paths: ['a[*].c.d']}) 81 | const obj = { 82 | a: [ 83 | {c: {d: 'hide me', e: 'leave me be'}}, 84 | {c: {d: 'and me', f: 'I want to live'}}, 85 | {c: {d: 'and also I', g: 'I want to run in a stream'}} 86 | ] 87 | } 88 | console.log(redact(obj)) 89 | // {"a":[{"c":{"d":"[REDACTED]","e":"leave me be"}},{"c":{"d":"[REDACTED]","f":"I want to live"}},{"c":{"d":"[REDACTED]","g":"I want to run in a stream"}}]} 90 | ``` 91 | 92 | #### `remove` - `Boolean` - `[false]` 93 | 94 | The `remove` option, when set to `true` will cause keys to be removed from the 95 | serialized output. 96 | 97 | Since the implementation exploits the fact that `undefined` keys are ignored 98 | by `JSON.stringify` the `remove` option may *only* be used when `JSON.stringify` 99 | is the serializer (this is the default) – otherwise `fast-redact` will throw. 100 | 101 | If supplying a custom serializer that has the same behavior (removing keys 102 | with `undefined` values), this restriction can be bypassed by explicitly setting 103 | the `censor` to `undefined`. 104 | 105 | 106 | #### `censor` – `` – `('[REDACTED]')` 107 | 108 | This is the value which overwrites redacted properties. 109 | 110 | Setting `censor` to `undefined` will cause properties to removed as long as this is 111 | the behavior of the `serializer` – which defaults to `JSON.stringify`, which does 112 | remove `undefined` properties. 113 | 114 | Setting `censor` to a function will cause `fast-redact` to invoke it with the original 115 | value. The output of the `censor` function sets the redacted value. 116 | Please note that asynchronous functions are not supported. 117 | 118 | #### `serialize` – `Function | Boolean` – `(JSON.stringify)` 119 | 120 | The `serialize` option may either be a function or a boolean. If a function is supplied, this 121 | will be used to `serialize` the redacted object. It's important to understand that for 122 | performance reasons `fast-redact` *mutates* the original object, then serializes, then 123 | restores the original values. So the object passed to the serializer is the exact same 124 | object passed to the redacting function. 125 | 126 | The `serialize` option as a function example: 127 | 128 | ```js 129 | const fastRedact = require('fast-redact') 130 | const redact = fastRedact({ 131 | paths: ['a'], 132 | serialize: (o) => JSON.stringify(o, 0, 2) 133 | }) 134 | console.log(redact({a: 1, b: 2})) 135 | // { 136 | // "a": "[REDACTED]", 137 | // "b": 2 138 | // } 139 | ``` 140 | 141 | For advanced usage the `serialize` option can be set to `false`. When `serialize` is set to `false`, 142 | instead of the serialized object, the output of the redactor function will be the mutated object 143 | itself (this is the exact same as the object passed in). In addition a `restore` method is supplied 144 | on the redactor function allowing the redacted keys to be restored with the original data. 145 | 146 | ```js 147 | const fastRedact = require('fast-redact') 148 | const redact = fastRedact({ 149 | paths: ['a'], 150 | serialize: false 151 | }) 152 | const o = {a: 1, b: 2} 153 | console.log(redact(o) === o) // true 154 | console.log(o) // { a: '[REDACTED]', b: 2 } 155 | console.log(redact.restore(o) === o) // true 156 | console.log(o) // { a: 1, b: 2 } 157 | ``` 158 | 159 | #### `strict` – `Boolean` - `[true]` 160 | The `strict` option, when set to `true`, will cause the redactor function to throw if instead 161 | of an object it finds a primitive. When `strict` is set to `false`, the redactor function 162 | will treat the primitive value as having already been redacted, and return it serialized (with 163 | `JSON.stringify` or the user's custom `serialize` function), or as-is if the `serialize` option 164 | was set to false. 165 | 166 | ## Approach 167 | 168 | In order to achieve lowest cost/highest performance redaction `fast-redact` 169 | creates and compiles a function (using the `Function` constructor) on initialization. 170 | It's important to distinguish this from the dangers of a runtime eval, no user input 171 | is involved in creating the string that compiles into the function. This is as safe 172 | as writing code normally and having it compiled by V8 in the usual way. 173 | 174 | Thanks to changes in V8 in recent years, state can be injected into compiled functions 175 | using `bind` at very low cost (whereas `bind` used to be expensive, and getting state 176 | into a compiled function by any means was difficult without a performance penalty). 177 | 178 | For static paths, this function simply checks that the path exists and then overwrites 179 | with the censor. Wildcard paths are processed with normal functions that iterate over 180 | the object redacting values as necessary. 181 | 182 | It's important to note, that the original object is mutated – for performance reasons 183 | a copy is not made. See [rfdc](https://github.com/davidmarkclements/rfdc) (Really Fast 184 | Deep Clone) for the fastest known way to clone – it's not nearly close enough in speed 185 | to editing the original object, serializing and then restoring values. 186 | 187 | A `restore` function is also created and compiled to put the original state back on 188 | to the object after redaction. This means that in the default usage case, the operation 189 | is essentially atomic - the object is mutated, serialized and restored internally which 190 | avoids any state management issues. 191 | 192 | ## Caveat 193 | 194 | As mentioned in approach, the `paths` array input is dynamically compiled into a function 195 | at initialization time. While the `paths` array is vigourously tested for any developer 196 | errors, it's strongly recommended against allowing user input to directly supply any 197 | paths to redact. It can't be guaranteed that allowing user input for `paths` couldn't 198 | feasibly expose an attack vector. 199 | 200 | ## Benchmarks 201 | 202 | The fastest known predecessor to `fast-redact` is the non-generic [`pino-noir`](http://npm.im/pino-noir) 203 | library (which was also written by myself). 204 | 205 | In the direct calling case, `fast-redact` is ~30x faster than `pino-noir`, however a more realistic 206 | comparison is overhead on `JSON.stringify`. 207 | 208 | For a static redaction case (no wildcards) `pino-noir` adds ~25% overhead on top of `JSON.stringify` 209 | whereas `fast-redact` adds ~1% overhead. 210 | 211 | In the basic last-position wildcard case,`fast-redact` is ~12% faster than `pino-noir`. 212 | 213 | The `pino-noir` module does not support intermediate wildcards, but `fast-redact` does, 214 | the cost of an intermediate wildcard that results in two keys over two nested objects 215 | being redacted is about 25% overhead on `JSON.stringify`. The cost of an intermediate 216 | wildcard that results in four keys across two objects being redacted is about 55% overhead 217 | on `JSON.stringify` and ~50% more expensive that explicitly declaring the keys. 218 | 219 | ```sh 220 | npm run bench 221 | ``` 222 | 223 | ``` 224 | benchNoirV2*500: 59.108ms 225 | benchFastRedact*500: 2.483ms 226 | benchFastRedactRestore*500: 10.904ms 227 | benchNoirV2Wild*500: 91.399ms 228 | benchFastRedactWild*500: 21.200ms 229 | benchFastRedactWildRestore*500: 27.304ms 230 | benchFastRedactIntermediateWild*500: 92.304ms 231 | benchFastRedactIntermediateWildRestore*500: 107.047ms 232 | benchJSONStringify*500: 210.573ms 233 | benchNoirV2Serialize*500: 281.148ms 234 | benchFastRedactSerialize*500: 215.845ms 235 | benchNoirV2WildSerialize*500: 281.168ms 236 | benchFastRedactWildSerialize*500: 247.140ms 237 | benchFastRedactIntermediateWildSerialize*500: 333.722ms 238 | benchFastRedactIntermediateWildMatchWildOutcomeSerialize*500: 463.667ms 239 | benchFastRedactStaticMatchWildOutcomeSerialize*500: 239.293ms 240 | ``` 241 | 242 | ## Tests 243 | 244 | ``` 245 | npm test 246 | ``` 247 | 248 | ``` 249 | 224 passing (499.544ms) 250 | ``` 251 | 252 | ### Coverage 253 | 254 | ``` 255 | npm run cov 256 | ``` 257 | 258 | ``` 259 | -----------------|----------|----------|----------|----------|-------------------| 260 | File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | 261 | -----------------|----------|----------|----------|----------|-------------------| 262 | All files | 100 | 100 | 100 | 100 | | 263 | fast-redact | 100 | 100 | 100 | 100 | | 264 | index.js | 100 | 100 | 100 | 100 | | 265 | fast-redact/lib | 100 | 100 | 100 | 100 | | 266 | modifiers.js | 100 | 100 | 100 | 100 | | 267 | parse.js | 100 | 100 | 100 | 100 | | 268 | redactor.js | 100 | 100 | 100 | 100 | | 269 | restorer.js | 100 | 100 | 100 | 100 | | 270 | rx.js | 100 | 100 | 100 | 100 | | 271 | state.js | 100 | 100 | 100 | 100 | | 272 | validator.js | 100 | 100 | 100 | 100 | | 273 | -----------------|----------|----------|----------|----------|-------------------| 274 | ``` 275 | 276 | ## License 277 | 278 | MIT 279 | 280 | ## Acknowledgements 281 | 282 | Sponsored by [nearForm](http://www.nearform.com) 283 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const fastRedact = require('..') 5 | 6 | const censor = '[REDACTED]' 7 | const censorFct = value => !value ? value : 'xxx' + value.substr(-2) 8 | const censorWithPath = (v, p) => p.join('.') + ' ' + censorFct(v) 9 | 10 | test('returns no-op when passed no paths [serialize: false]', ({ end, doesNotThrow }) => { 11 | const redact = fastRedact({ paths: [], serialize: false }) 12 | doesNotThrow(() => redact({})) 13 | doesNotThrow(() => { 14 | const o = redact({}) 15 | redact.restore(o) 16 | }) 17 | end() 18 | }) 19 | 20 | test('returns serializer when passed no paths [serialize: default]', ({ end, is }) => { 21 | is(fastRedact({ paths: [] }), JSON.stringify) 22 | is(fastRedact(), JSON.stringify) 23 | end() 24 | }) 25 | 26 | test('throws when passed non-object using defaults', ({ end, throws }) => { 27 | const redact = fastRedact({ paths: ['a.b.c'] }) 28 | throws(() => redact(1)) 29 | end() 30 | }) 31 | 32 | test('throws when passed non-object number using [strict: true]', ({ end, throws }) => { 33 | const redact = fastRedact({ paths: ['a.b.c'], strict: true }) 34 | throws(() => redact(1)) 35 | end() 36 | }) 37 | 38 | test('returns JSON.stringified value when passed non-object using [strict: false] and no serialize option', ({ end, is, doesNotThrow }) => { 39 | const redactDefaultSerialize = fastRedact({ paths: ['a.b.c'], strict: false }) 40 | 41 | // expectedOutputs holds `JSON.stringify`ied versions of each primitive. 42 | // We write them out explicitly though to make the test a bit clearer. 43 | const primitives = [null, undefined, 'A', 1, false] 44 | const expectedOutputs = ['null', undefined, '"A"', '1', 'false'] 45 | 46 | primitives.forEach((it, i) => { 47 | doesNotThrow(() => redactDefaultSerialize(it)) 48 | const res = redactDefaultSerialize(it) 49 | is(res, expectedOutputs[i]) 50 | }) 51 | 52 | end() 53 | }) 54 | 55 | test('returns custom serialized value when passed non-object using [strict: false, serialize: fn]', ({ end, is, doesNotThrow }) => { 56 | const customSerialize = (v) => `Hello ${v}!` 57 | const redactCustomSerialize = fastRedact({ 58 | paths: ['a.b.c'], 59 | strict: false, 60 | serialize: customSerialize 61 | }) 62 | 63 | const primitives = [null, undefined, 'A', 1, false] 64 | 65 | primitives.forEach((it) => { 66 | doesNotThrow(() => redactCustomSerialize(it)) 67 | const res = redactCustomSerialize(it) 68 | is(res, customSerialize(it)) 69 | }) 70 | 71 | end() 72 | }) 73 | 74 | test('returns original value when passed non-object using [strict: false, serialize: false]', ({ end, is, doesNotThrow }) => { 75 | const redactSerializeFalse = fastRedact({ 76 | paths: ['a.b.c'], 77 | strict: false, 78 | serialize: false 79 | }) 80 | 81 | const primitives = [null, undefined, 'A', 1, false] 82 | 83 | primitives.forEach((it) => { 84 | doesNotThrow(() => redactSerializeFalse(it)) 85 | const res = redactSerializeFalse(it) 86 | is(res, it) 87 | }) 88 | 89 | end() 90 | }) 91 | 92 | test('returns original value when passed non-object at wildcard key', ({ end, doesNotThrow, strictSame }) => { 93 | const redactSerializeFalse = fastRedact({ 94 | paths: ['a.*'], 95 | strict: false, 96 | serialize: false 97 | }) 98 | 99 | const primitives = [null, undefined, 'A', 1, false] 100 | 101 | primitives.forEach((a) => { 102 | doesNotThrow(() => redactSerializeFalse({ a })) 103 | const res = redactSerializeFalse({ a }) 104 | strictSame(res, { a }) 105 | }) 106 | 107 | end() 108 | }) 109 | 110 | test('returns censored values when passed array at wildcard key', ({ end, strictSame }) => { 111 | const redactSerializeFalse = fastRedact({ 112 | paths: ['a.*'], 113 | strict: false, 114 | serialize: false 115 | }) 116 | const res = redactSerializeFalse({ a: ['redact', 'me'] }) 117 | strictSame(res.a, [censor, censor]) 118 | end() 119 | }) 120 | 121 | test('throws if a path is not a string', ({ end, throws }) => { 122 | const invalidTypeMsg = 'fast-redact - Paths must be (non-empty) strings' 123 | throws((e) => { 124 | fastRedact({ paths: [1] }) 125 | }, Error(invalidTypeMsg)) 126 | throws((e) => { 127 | fastRedact({ paths: [null] }) 128 | }, Error(invalidTypeMsg)) 129 | throws((e) => { 130 | fastRedact({ paths: [undefined] }) 131 | }, Error(invalidTypeMsg)) 132 | throws((e) => { 133 | fastRedact({ paths: [{}] }) 134 | }, Error(invalidTypeMsg)) 135 | throws((e) => { 136 | fastRedact({ paths: [[null]] }) 137 | }, Error(invalidTypeMsg)) 138 | end() 139 | }) 140 | 141 | test('throws when passed illegal paths', ({ end, throws }) => { 142 | const err = (s) => Error(`fast-redact – Invalid path (${s})`) 143 | throws((e) => { 144 | fastRedact({ paths: ['@'] }) 145 | }, err('@')) 146 | throws((e) => { 147 | fastRedact({ paths: ['0'] }) 148 | }, err('0')) 149 | throws((e) => { 150 | fastRedact({ paths: ['〇'] }) 151 | }, err('〇')) 152 | throws((e) => { 153 | fastRedact({ paths: ['a.1.c'] }) 154 | }, err('a.1.c')) 155 | throws((e) => { 156 | fastRedact({ paths: ['a..c'] }) 157 | }, err('a..c')) 158 | throws((e) => { 159 | fastRedact({ paths: ['1..c'] }) 160 | }, err('1..c')) 161 | throws((e) => { 162 | fastRedact({ paths: ['a = b'] }) 163 | }, err('a = b')) 164 | throws((e) => { 165 | fastRedact({ paths: ['a(b)'] }) 166 | }, err('a(b)')) 167 | throws((e) => { 168 | fastRedact({ paths: ['//a.b.c'] }) 169 | }, err('//a.b.c')) 170 | throws((e) => { 171 | fastRedact({ paths: ['\\a.b.c'] }) 172 | }, err('\\a.b.c')) 173 | throws((e) => { 174 | fastRedact({ paths: ['a.#.c'] }) 175 | }, err('a.#.c')) 176 | throws((e) => { 177 | fastRedact({ paths: ['~~a.b.c'] }) 178 | }, err('~~a.b.c')) 179 | throws((e) => { 180 | fastRedact({ paths: ['^a.b.c'] }) 181 | }, err('^a.b.c')) 182 | throws((e) => { 183 | fastRedact({ paths: ['a + b'] }) 184 | }, err('a + b')) 185 | throws((e) => { 186 | fastRedact({ paths: ['return a + b'] }) 187 | }, err('return a + b')) 188 | throws((e) => { 189 | fastRedact({ paths: ['a / b'] }) 190 | }, err('a / b')) 191 | throws((e) => { 192 | fastRedact({ paths: ['a * b'] }) 193 | }, err('a * b')) 194 | throws((e) => { 195 | fastRedact({ paths: ['a - b'] }) 196 | }, err('a - b')) 197 | throws((e) => { 198 | fastRedact({ paths: ['a ** b'] }) 199 | }, err('a ** b')) 200 | throws((e) => { 201 | fastRedact({ paths: ['a % b'] }) 202 | }, err('a % b')) 203 | throws((e) => { 204 | fastRedact({ paths: ['a.b*.c'] }) 205 | }, err('a.b*.c')) 206 | throws((e) => { 207 | fastRedact({ paths: ['a;global.foo = "bar"'] }) 208 | }, err('a;global.foo = "bar"')) 209 | throws((e) => { 210 | fastRedact({ paths: ['a;while(1){}'] }) 211 | }, err('a;while(1){}')) 212 | throws((e) => { 213 | fastRedact({ paths: ['a//'] }) 214 | }, err('a//')) 215 | throws((e) => { 216 | fastRedact({ paths: ['a/*foo*/'] }) 217 | }, err('a/*foo*/')) 218 | throws((e) => { 219 | fastRedact({ paths: ['a,o.b'] }) 220 | }, err('a,o.b')) 221 | throws((e) => { 222 | fastRedact({ paths: ['a = o.b'] }) 223 | }, err('a = o.b')) 224 | throws((e) => { 225 | fastRedact({ paths: ['a\n'] }) 226 | }, err('a\n')) 227 | throws((e) => { 228 | fastRedact({ paths: ['a\r'] }) 229 | }, err('a\r')) 230 | throws((e) => { 231 | fastRedact({ paths: [''] }) 232 | }, err('')) 233 | throws((e) => { 234 | fastRedact({ paths: ['[""""]'] }) 235 | }, err('[""""]')) 236 | end() 237 | }) 238 | 239 | test('throws if a custom serializer is used and remove is true', ({ end, throws }) => { 240 | throws(() => { 241 | fastRedact({ paths: ['a'], serialize: (o) => o, remove: true }) 242 | }, Error('fast-redact – remove option may only be set when serializer is JSON.stringify')) 243 | end() 244 | }) 245 | 246 | test('throws if serialize is false and remove is true', ({ end, throws }) => { 247 | throws(() => { 248 | fastRedact({ paths: ['a'], serialize: false, remove: true }) 249 | }, Error('fast-redact – remove option may only be set when serializer is JSON.stringify')) 250 | end() 251 | }) 252 | 253 | test('supports path segments that aren\'t identifiers if bracketed', ({ end, strictSame }) => { 254 | const redactSerializeFalse = fastRedact({ 255 | paths: ['a[""]', 'a["x-y"]', 'a[\'"y"\']', "a['\\'x\\'']"], 256 | serialize: false, 257 | censor: 'X' 258 | }) 259 | 260 | const res = redactSerializeFalse({ a: { '': 'Hi!', 'x-y': 'Hi!', '"y"': 'Hi!', "'x'": 'Hi!' } }) 261 | strictSame(res, { a: { '': 'X', 'x-y': 'X', '"y"': 'X', "'x'": 'X' } }) 262 | end() 263 | }) 264 | 265 | test('supports consecutive bracketed path segments', ({ end, strictSame }) => { 266 | const redactSerializeFalse = fastRedact({ 267 | paths: ['a[""]["y"]'], 268 | serialize: false, 269 | censor: 'X' 270 | }) 271 | 272 | const res = redactSerializeFalse({ a: { '': { 'y': 'Hi!' } } }) 273 | strictSame(res, { a: { '': { 'y': 'X' } } }) 274 | end() 275 | }) 276 | 277 | test('supports leading bracketed widcard', ({ end, strictSame }) => { 278 | const redactSerializeFalse = fastRedact({ 279 | paths: ['[*]["y"]'], 280 | serialize: false, 281 | censor: 'X' 282 | }) 283 | 284 | const res = redactSerializeFalse({ 'x': { 'y': 'Hi!' } }) 285 | strictSame(res, { 'x': { 'y': 'X' } }) 286 | end() 287 | }) 288 | 289 | test('masks according to supplied censor', ({ end, is }) => { 290 | const censor = 'test' 291 | const redact = fastRedact({ paths: ['a'], censor, serialize: false }) 292 | is(redact({ a: 'a' }).a, censor) 293 | end() 294 | }) 295 | 296 | test('redact.restore function is available when serialize is false', ({ end, is }) => { 297 | const censor = 'test' 298 | const redact = fastRedact({ paths: ['a'], censor, serialize: false }) 299 | is(typeof redact.restore, 'function') 300 | end() 301 | }) 302 | 303 | test('redact.restore function places original values back in place', ({ end, is }) => { 304 | const censor = 'test' 305 | const redact = fastRedact({ paths: ['a'], censor, serialize: false }) 306 | const o = { a: 'a' } 307 | redact(o) 308 | is(o.a, censor) 309 | redact.restore(o) 310 | is(o.a, 'a') 311 | end() 312 | }) 313 | 314 | test('redact.restore function places original values back in place when called twice and the first call is precensored', ({ end, is }) => { 315 | const censor = 'test' 316 | const redact = fastRedact({ paths: ['a'], censor, serialize: false }) 317 | const o1 = { a: censor } 318 | const o2 = { a: 'a' } 319 | redact(o1) 320 | is(o1.a, censor) 321 | redact.restore(o1) 322 | is(o1.a, censor) 323 | redact(o2) 324 | is(o2.a, censor) 325 | redact.restore(o2) 326 | is(o2.a, 'a') 327 | end() 328 | }) 329 | 330 | test('masks according to supplied censor function', ({ end, is }) => { 331 | const redact = fastRedact({ paths: ['a'], censor: censorFct, serialize: false }) 332 | is(redact({ a: '0123456' }).a, 'xxx56') 333 | end() 334 | }) 335 | 336 | test('masks according to supplied censor function with wildcards', ({ end, is }) => { 337 | const redact = fastRedact({ paths: '*', censor: censorFct, serialize: false }) 338 | is(redact({ a: '0123456' }).a, 'xxx56') 339 | end() 340 | }) 341 | 342 | test('masks according to supplied censor function with nested wildcards', ({ end, is }) => { 343 | const redact = fastRedact({ paths: ['*.b'], censor: censorFct, serialize: false }) 344 | is(redact({ a: { b: '0123456' } }).a.b, 'xxx56') 345 | is(redact({ c: { b: '0123456', d: 'pristine' } }).c.b, 'xxx56') 346 | is(redact({ c: { b: '0123456', d: 'pristine' } }).c.d, 'pristine') 347 | end() 348 | }) 349 | 350 | test('does not increment secret size', ({ end, is, same }) => { 351 | const redact = fastRedact({ paths: ['x', '*.b'], censor: censorFct, serialize: false }) 352 | is(redact({ a: { b: '0123456' } }).a.b, 'xxx56') 353 | same(Object.keys(redact.state.secret), ['x']) 354 | const intialSecretSize = JSON.stringify(redact.state.secret).length 355 | is(redact({ c: { b: '0123456', d: 'pristine' } }).c.b, 'xxx56') 356 | same(Object.keys(redact.state.secret), ['x']) 357 | is(JSON.stringify(redact.state.secret).length, intialSecretSize) 358 | is(redact({ c: { b: '0123456', d: 'pristine' } }).c.d, 'pristine') 359 | same(Object.keys(redact.state.secret), ['x']) 360 | is(JSON.stringify(redact.state.secret).length, intialSecretSize) 361 | end() 362 | }) 363 | 364 | test('masks according to supplied censor-with-path function', ({ end, is }) => { 365 | const redact = fastRedact({ paths: ['a'], censor: censorWithPath, serialize: false }) 366 | is(redact({ a: '0123456' }).a, 'a xxx56') 367 | end() 368 | }) 369 | 370 | test('masks according to supplied censor-with-path function with wildcards', ({ end, is }) => { 371 | const redact = fastRedact({ paths: '*', censor: censorWithPath, serialize: false }) 372 | is(redact({ a: '0123456' }).a, 'a xxx56') 373 | end() 374 | }) 375 | 376 | test('masks according to supplied censor-with-path function with nested wildcards', ({ end, is }) => { 377 | const redact = fastRedact({ paths: ['*.b'], censor: censorWithPath, serialize: false }) 378 | is(redact({ a: { b: '0123456' } }).a.b, 'a.b xxx56') 379 | is(redact({ c: { b: '0123456', d: 'pristine' } }).c.b, 'c.b xxx56') 380 | is(redact({ c: { b: '0123456', d: 'pristine' } }).c.d, 'pristine') 381 | end() 382 | }) 383 | 384 | test('redact.restore function places original values back in place with censor function', ({ end, is }) => { 385 | const redact = fastRedact({ paths: ['a'], censor: censorFct, serialize: false }) 386 | const o = { a: 'qwerty' } 387 | redact(o) 388 | is(o.a, 'xxxty') 389 | redact.restore(o) 390 | is(o.a, 'qwerty') 391 | end() 392 | }) 393 | 394 | test('serializes with JSON.stringify by default', ({ end, is }) => { 395 | const redact = fastRedact({ paths: ['a'] }) 396 | const o = { a: 'a' } 397 | is(redact(o), `{"a":"${censor}"}`) 398 | is(o.a, 'a') 399 | end() 400 | }) 401 | 402 | test('removes during serialization instead of redacting when remove option is true', ({ end, is }) => { 403 | const redact = fastRedact({ paths: ['a'], remove: true }) 404 | const o = { a: 'a', b: 'b' } 405 | is(redact(o), `{"b":"b"}`) 406 | is(o.a, 'a') 407 | end() 408 | }) 409 | 410 | test('serializes with JSON.stringify if serialize is true', ({ end, is }) => { 411 | const redact = fastRedact({ paths: ['a'], serialize: true }) 412 | const o = { a: 'a' } 413 | is(redact(o), `{"a":"${censor}"}`) 414 | is(o.a, 'a') 415 | end() 416 | }) 417 | 418 | test('serializes with JSON.stringify if serialize is not a function', ({ end, is }) => { 419 | const redact = fastRedact({ paths: ['a'], serialize: {} }) 420 | const o = { a: 'a' } 421 | is(redact(o), `{"a":"${censor}"}`) 422 | is(o.a, 'a') 423 | end() 424 | }) 425 | 426 | test('serializes with custom serializer if supplied', ({ end, is }) => { 427 | const redact = fastRedact({ paths: ['a'], serialize: (o) => JSON.stringify(o, 0, 2) }) 428 | const o = { a: 'a' } 429 | is(redact(o), `{\n "a": "${censor}"\n}`) 430 | is(o.a, 'a') 431 | end() 432 | }) 433 | 434 | test('redacts parent keys', ({ end, is }) => { 435 | const redact = fastRedact({ paths: ['a.b.c'], serialize: false }) 436 | const result = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 437 | is(result.a.b.c, censor) 438 | end() 439 | }) 440 | 441 | test('supports paths with array indexes', ({ end, same }) => { 442 | const redact = fastRedact({ paths: ['insideArray.like[3].this'], serialize: false }) 443 | same(redact({ insideArray: { like: ['a', 'b', 'c', { this: { foo: 'meow' } }] } }), { insideArray: { like: ['a', 'b', 'c', { this: censor }] } }) 444 | end() 445 | }) 446 | 447 | test('censor may be any type, including function', ({ end, same }) => { 448 | const redactToString = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: 'censor', serialize: false }) 449 | const redactToUndefined = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: undefined, serialize: false }) 450 | const sym = Symbol('sym') 451 | const redactToSymbol = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: sym, serialize: false }) 452 | const redactToNumber = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: 0, serialize: false }) 453 | const redactToBoolean = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: false, serialize: false }) 454 | const redactToNull = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: null, serialize: false }) 455 | const redactToObject = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: { redacted: true }, serialize: false }) 456 | const redactToArray = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: ['redacted'], serialize: false }) 457 | const redactToBuffer = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: Buffer.from('redacted'), serialize: false }) 458 | const redactToError = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: Error('redacted'), serialize: false }) 459 | const redactToFunction = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: () => 'redacted', serialize: false }) 460 | same(redactToString({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: 'censor', d: { x: 'censor', y: 'censor' } } } }) 461 | same(redactToUndefined({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: undefined, d: { x: undefined, y: undefined } } } }) 462 | same(redactToSymbol({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: sym, d: { x: sym, y: sym } } } }) 463 | same(redactToNumber({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: 0, d: { x: 0, y: 0 } } } }) 464 | same(redactToBoolean({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: false, d: { x: false, y: false } } } }) 465 | same(redactToNull({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: null, d: { x: null, y: null } } } }) 466 | same(redactToObject({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: { redacted: true }, d: { x: { redacted: true }, y: { redacted: true } } } } }) 467 | same(redactToArray({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: ['redacted'], d: { x: ['redacted'], y: ['redacted'] } } } }) 468 | same(redactToBuffer({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: Buffer.from('redacted'), d: { x: Buffer.from('redacted'), y: Buffer.from('redacted') } } } }) 469 | same(redactToError({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: Error('redacted'), d: { x: Error('redacted'), y: Error('redacted') } } } }) 470 | same(redactToFunction({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: 'redacted', d: { x: 'redacted', y: 'redacted' } } } }) 471 | end() 472 | }) 473 | 474 | test('supports multiple paths from the same root', ({ end, same }) => { 475 | const redact = fastRedact({ paths: ['deep.bar.shoe', 'deep.baz.shoe', 'deep.foo', 'deep.not.there.sooo', 'deep.fum.shoe'], serialize: false }) 476 | same(redact({ deep: { bar: 'hmm', baz: { shoe: { k: 1 } }, foo: {}, fum: { shoe: 'moo' } } }), { deep: { bar: 'hmm', baz: { shoe: censor }, foo: censor, fum: { shoe: censor } } }) 477 | end() 478 | }) 479 | 480 | test('supports strings in bracket notation paths (single quote)', ({ end, is }) => { 481 | const redact = fastRedact({ paths: [`a['@#!='].c`], serialize: false }) 482 | const result = redact({ a: { '@#!=': { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 483 | is(result.a['@#!='].c, censor) 484 | end() 485 | }) 486 | 487 | test('supports strings in bracket notation paths (double quote)', ({ end, is }) => { 488 | const redact = fastRedact({ paths: [`a["@#!="].c`], serialize: false }) 489 | const result = redact({ a: { '@#!=': { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 490 | is(result.a['@#!='].c, censor) 491 | end() 492 | }) 493 | 494 | test('supports strings in bracket notation paths (backtick quote)', ({ end, is }) => { 495 | const redact = fastRedact({ paths: ['a[`@#!=`].c'], serialize: false }) 496 | const result = redact({ a: { '@#!=': { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 497 | is(result.a['@#!='].c, censor) 498 | end() 499 | }) 500 | 501 | test('allows * within a bracket notation string', ({ end, is }) => { 502 | const redact = fastRedact({ paths: ['a["*"].c'], serialize: false }) 503 | const result = redact({ a: { '*': { c: 's', x: 1 } } }) 504 | is(result.a['*'].c, censor) 505 | is(result.a['*'].x, 1) 506 | end() 507 | }) 508 | 509 | test('redacts parent keys – restore', ({ end, is }) => { 510 | const redact = fastRedact({ paths: ['a.b.c'], serialize: false }) 511 | const result = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 512 | is(result.a.b.c, censor) 513 | redact.restore(result) 514 | is(result.a.b.c, 's') 515 | end() 516 | }) 517 | 518 | test('handles null proto objects', ({ end, is }) => { 519 | const redact = fastRedact({ paths: ['a.b.c'], serialize: false }) 520 | const result = redact({ __proto__: null, a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 521 | is(result.a.b.c, censor) 522 | end() 523 | }) 524 | 525 | test('handles null proto objects – restore', ({ end, is }) => { 526 | const redact = fastRedact({ paths: ['a.b.c'], serialize: false }) 527 | const result = redact({ __proto__: null, a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 528 | is(result.a.b.c, censor) 529 | redact.restore(result, 's') 530 | is(result.a.b.c, 's') 531 | end() 532 | }) 533 | 534 | test('handles paths that do not match object structure', ({ end, same }) => { 535 | const redact = fastRedact({ paths: ['x.y.z'], serialize: false }) 536 | same(redact({ a: { b: { c: 's' } } }), { a: { b: { c: 's' } } }) 537 | end() 538 | }) 539 | 540 | test('ignores missing paths in object', ({ end, same }) => { 541 | const redact = fastRedact({ paths: ['a.b.c', 'a.z.d', 'a.b.z'], serialize: false }) 542 | same(redact({ a: { b: { c: 's' } } }), { a: { b: { c: censor } } }) 543 | end() 544 | }) 545 | 546 | test('ignores missing paths in object – restore', ({ end, doesNotThrow }) => { 547 | const redact = fastRedact({ paths: ['a.b.c', 'a.z.d', 'a.b.z'], serialize: false }) 548 | const o = { a: { b: { c: 's' } } } 549 | redact(o) 550 | doesNotThrow(() => { 551 | redact.restore(o) 552 | }) 553 | 554 | end() 555 | }) 556 | 557 | test('gracefully handles primitives that match intermediate keys in paths', ({ end, same }) => { 558 | const redact = fastRedact({ paths: ['a.b.c', 'a.b.c.d'], serialize: false }) 559 | same(redact({ a: { b: null } }), { a: { b: null } }) 560 | same(redact({ a: { b: 's' } }), { a: { b: 's' } }) 561 | same(redact({ a: { b: 1 } }), { a: { b: 1 } }) 562 | same(redact({ a: { b: undefined } }), { a: { b: undefined } }) 563 | same(redact({ a: { b: true } }), { a: { b: true } }) 564 | const sym = Symbol('sym') 565 | same(redact({ a: { b: sym } }), { a: { b: sym } }) 566 | end() 567 | }) 568 | 569 | test('handles circulars', ({ end, is, same }) => { 570 | const redact = fastRedact({ paths: ['bar.baz.baz'], serialize: false }) 571 | const bar = { b: 2 } 572 | const o = { a: 1, bar } 573 | bar.baz = bar 574 | o.bar.baz = o.bar 575 | same(redact(o), { a: 1, bar: { b: 2, baz: censor } }) 576 | end() 577 | }) 578 | 579 | test('handles circulars – restore', ({ end, is, same }) => { 580 | const redact = fastRedact({ paths: ['bar.baz.baz'], serialize: false }) 581 | const bar = { b: 2 } 582 | const o = { a: 1, bar } 583 | bar.baz = bar 584 | o.bar.baz = o.bar 585 | is(o.bar.baz, bar) 586 | redact(o) 587 | is(o.bar.baz, censor) 588 | redact.restore(o) 589 | is(o.bar.baz, bar) 590 | end() 591 | }) 592 | 593 | test('handles circulars and cross references – restore', ({ end, is, same }) => { 594 | const redact = fastRedact({ paths: ['bar.baz.baz', 'cf.bar'], serialize: false }) 595 | const bar = { b: 2 } 596 | const o = { a: 1, bar, cf: { bar } } 597 | bar.baz = bar 598 | o.bar.baz = o.bar 599 | is(o.bar.baz, bar) 600 | is(o.cf.bar, bar) 601 | redact(o) 602 | is(o.bar.baz, censor) 603 | is(o.cf.bar, censor) 604 | redact.restore(o) 605 | is(o.bar.baz, bar) 606 | is(o.cf.bar, bar) 607 | end() 608 | }) 609 | 610 | test('ultimate wildcards – shallow', ({ end, same }) => { 611 | const redact = fastRedact({ paths: ['test.*'], serialize: false }) 612 | same(redact({ test: { baz: 1, bar: 'private' } }), { test: { baz: censor, bar: censor } }) 613 | end() 614 | }) 615 | 616 | test('ultimate wildcards – deep', ({ end, same }) => { 617 | const redact = fastRedact({ paths: ['deep.bar.baz.ding.*'], serialize: false }) 618 | same(redact({ deep: { a: 1, bar: { b: 2, baz: { c: 3, ding: { d: 4, e: 5, f: 'six' } } } } }), { deep: { a: 1, bar: { b: 2, baz: { c: 3, ding: { d: censor, e: censor, f: censor } } } } }) 619 | end() 620 | }) 621 | 622 | test('ultimate wildcards - array – shallow', ({ end, same }) => { 623 | const redact = fastRedact({ paths: ['array[*]'], serialize: false }) 624 | same(redact({ array: ['a', 'b', 'c', 'd'] }), { array: [censor, censor, censor, censor] }) 625 | end() 626 | }) 627 | 628 | test('ultimate wildcards – array – deep', ({ end, same }) => { 629 | const redact = fastRedact({ paths: ['deepArray.down.here[*]'], serialize: false }) 630 | same(redact({ deepArray: { down: { here: ['a', 'b', 'c'] } } }), { deepArray: { down: { here: [censor, censor, censor] } } }) 631 | end() 632 | }) 633 | 634 | test('ultimate wildcards – array – single index', ({ end, same }) => { 635 | const redact = fastRedact({ paths: ['insideArray.like[3].this.*'], serialize: false }) 636 | same(redact({ insideArray: { like: ['a', 'b', 'c', { this: { foo: 'meow' } }] } }), { insideArray: { like: ['a', 'b', 'c', { this: { foo: censor } }] } }) 637 | end() 638 | }) 639 | 640 | test('ultimate wildcards - handles null proto objects', ({ end, is }) => { 641 | const redact = fastRedact({ paths: ['a.b.c'], serialize: false }) 642 | const result = redact({ __proto__: null, a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 643 | is(result.a.b.c, censor) 644 | end() 645 | }) 646 | 647 | test('ultimate wildcards - handles paths that do not match object structure', ({ end, same }) => { 648 | const redact = fastRedact({ paths: ['x.y.z'], serialize: false }) 649 | same(redact({ a: { b: { c: 's' } } }), { a: { b: { c: 's' } } }) 650 | end() 651 | }) 652 | 653 | test('ultimate wildcards - gracefully handles primitives that match intermediate keys in paths', ({ end, same }) => { 654 | const redact = fastRedact({ paths: ['a.b.c', 'a.b.c.d'], serialize: false }) 655 | same(redact({ a: { b: null } }), { a: { b: null } }) 656 | same(redact({ a: { b: 's' } }), { a: { b: 's' } }) 657 | same(redact({ a: { b: 1 } }), { a: { b: 1 } }) 658 | same(redact({ a: { b: undefined } }), { a: { b: undefined } }) 659 | same(redact({ a: { b: true } }), { a: { b: true } }) 660 | const sym = Symbol('sym') 661 | same(redact({ a: { b: sym } }), { a: { b: sym } }) 662 | end() 663 | }) 664 | 665 | test('ultimate wildcards – handles circulars', ({ end, is, same }) => { 666 | const redact = fastRedact({ paths: ['bar.baz.*'], serialize: false }) 667 | const bar = { b: 2 } 668 | const o = { a: 1, bar } 669 | bar.baz = bar 670 | o.bar.baz = o.bar 671 | same(redact(o), { a: 1, bar: { b: censor, baz: censor } }) 672 | end() 673 | }) 674 | 675 | test('ultimate wildcards – handles circulars – restore', ({ end, is }) => { 676 | const redact = fastRedact({ paths: ['bar.baz.*'], serialize: false }) 677 | const bar = { b: 2 } 678 | const o = { a: 1, bar } 679 | bar.baz = bar 680 | o.bar.baz = o.bar 681 | is(o.bar.baz, bar) 682 | redact(o) 683 | is(o.bar.baz, censor) 684 | redact.restore(o) 685 | is(o.bar.baz, bar) 686 | end() 687 | }) 688 | 689 | test('ultimate multi wildcards – handles circulars – restore', ({ end, is, same }) => { 690 | const redact = fastRedact({ paths: ['bar.*.baz.*.b'], serialize: false }) 691 | const bar = { b: 2 } 692 | const o = { a: 1, bar } 693 | bar.baz = bar 694 | o.bar.baz = o.bar 695 | is(o.bar.baz, bar) 696 | redact(o) 697 | is(o.bar.baz.b, censor) 698 | redact.restore(o) 699 | same(o.bar.baz, bar) 700 | end() 701 | }) 702 | 703 | test('ultimate wildcards – handles circulars and cross references – restore', ({ end, is }) => { 704 | const redact = fastRedact({ paths: ['bar.baz.*', 'cf.*'], serialize: false }) 705 | const bar = { b: 2 } 706 | const o = { a: 1, bar, cf: { bar } } 707 | bar.baz = bar 708 | o.bar.baz = o.bar 709 | is(o.bar.baz, bar) 710 | is(o.cf.bar, bar) 711 | redact(o) 712 | is(o.bar.baz, censor) 713 | is(o.cf.bar, censor) 714 | redact.restore(o) 715 | is(o.bar.baz, bar) 716 | is(o.cf.bar, bar) 717 | end() 718 | }) 719 | 720 | test('static + wildcards', ({ end, is }) => { 721 | const redact = fastRedact({ paths: ['a.b.c', 'a.d.*', 'a.b.z.*'], serialize: false }) 722 | const result = redact({ a: { b: { c: 's', z: { x: 's', y: 's' } }, d: { a: 's', b: 's', c: 's' } } }) 723 | 724 | is(result.a.b.c, censor) 725 | is(result.a.d.a, censor) 726 | is(result.a.d.b, censor) 727 | is(result.a.d.c, censor) 728 | is(result.a.b.z.x, censor) 729 | is(result.a.b.z.y, censor) 730 | end() 731 | }) 732 | 733 | test('static + wildcards reuse', ({ end, is }) => { 734 | const redact = fastRedact({ paths: ['a.b.c', 'a.d.*'], serialize: false }) 735 | const result = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 736 | 737 | is(result.a.b.c, censor) 738 | is(result.a.d.a, censor) 739 | is(result.a.d.b, censor) 740 | is(result.a.d.c, censor) 741 | 742 | redact.restore(result) 743 | 744 | const result2 = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 745 | is(result2.a.b.c, censor) 746 | is(result2.a.d.a, censor) 747 | is(result2.a.d.b, censor) 748 | is(result2.a.d.c, censor) 749 | 750 | redact.restore(result2) 751 | end() 752 | }) 753 | 754 | test('parent wildcard – first position', ({ end, is }) => { 755 | const redact = fastRedact({ paths: ['*.c'], serialize: false }) 756 | const result = redact({ b: { c: 's' }, d: { a: 's', b: 's', c: 's' } }) 757 | is(result.b.c, censor) 758 | is(result.d.a, 's') 759 | is(result.d.b, 's') 760 | is(result.d.c, censor) 761 | end() 762 | }) 763 | 764 | test('parent wildcard – one following key', ({ end, is }) => { 765 | const redact = fastRedact({ paths: ['a.*.c'], serialize: false }) 766 | const result = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 767 | is(result.a.b.c, censor) 768 | is(result.a.d.a, 's') 769 | is(result.a.d.b, 's') 770 | is(result.a.d.c, censor) 771 | end() 772 | }) 773 | 774 | test('restore parent wildcard – one following key', ({ end, is }) => { 775 | const redact = fastRedact({ paths: ['a.*.c'], serialize: false }) 776 | const result = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 777 | redact.restore(result) 778 | is(result.a.b.c, 's') 779 | is(result.a.d.a, 's') 780 | is(result.a.d.b, 's') 781 | is(result.a.d.c, 's') 782 | end() 783 | }) 784 | 785 | test('parent wildcard – one following key – reuse', ({ end, is }) => { 786 | const redact = fastRedact({ paths: ['a.*.c'], serialize: false }) 787 | const result = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 788 | is(result.a.b.c, censor) 789 | is(result.a.d.a, 's') 790 | is(result.a.d.b, 's') 791 | is(result.a.d.c, censor) 792 | const result2 = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 793 | is(result2.a.b.c, censor) 794 | is(result2.a.d.a, 's') 795 | is(result2.a.d.b, 's') 796 | is(result2.a.d.c, censor) 797 | redact.restore(result2) 798 | end() 799 | }) 800 | 801 | test('parent wildcard – two following keys', ({ end, is }) => { 802 | const redact = fastRedact({ paths: ['a.*.x.c'], serialize: false }) 803 | const result = redact({ a: { b: { x: { c: 's' } }, d: { x: { a: 's', b: 's', c: 's' } } } }) 804 | is(result.a.b.x.c, censor) 805 | is(result.a.d.x.a, 's') 806 | is(result.a.d.x.b, 's') 807 | is(result.a.d.x.c, censor) 808 | end() 809 | }) 810 | 811 | test('multi object wildcard', ({ end, is }) => { 812 | const redact = fastRedact({ paths: ['a.*.x.*.c'], serialize: false }) 813 | const result = redact({ a: { b: { x: { z: { c: 's' } } }, d: { x: { u: { a: 's', b: 's', c: 's' } } } } }) 814 | is(result.a.b.x.z.c, censor) 815 | is(result.a.d.x.u.a, 's') 816 | is(result.a.d.x.u.b, 's') 817 | is(result.a.d.x.u.c, censor) 818 | redact.restore(result) 819 | is(result.a.b.x.z.c, 's') 820 | is(result.a.d.x.u.a, 's') 821 | is(result.a.d.x.u.b, 's') 822 | is(result.a.d.x.u.c, 's') 823 | end() 824 | }) 825 | 826 | test('parent wildcard – two following keys – reuse', ({ end, is }) => { 827 | const redact = fastRedact({ paths: ['a.*.x.c'], serialize: false }) 828 | const result = redact({ a: { b: { x: { c: 's' } }, d: { x: { a: 's', b: 's', c: 's' } } } }) 829 | is(result.a.b.x.c, censor) 830 | is(result.a.d.x.a, 's') 831 | is(result.a.d.x.b, 's') 832 | is(result.a.d.x.c, censor) 833 | redact.restore(result) 834 | const result2 = redact({ a: { b: { x: { c: 's' } }, d: { x: { a: 's', b: 's', c: 's' } } } }) 835 | is(result2.a.b.x.c, censor) 836 | is(result2.a.d.x.a, 's') 837 | is(result2.a.d.x.b, 's') 838 | is(result2.a.d.x.c, censor) 839 | end() 840 | }) 841 | 842 | test('restore parent wildcard – two following keys', ({ end, is }) => { 843 | const redact = fastRedact({ paths: ['a.*.x.c'], serialize: false }) 844 | const result = redact({ a: { b: { x: { c: 's' } }, d: { x: { a: 's', b: 's', c: 's' } } } }) 845 | redact.restore(result) 846 | is(result.a.b.x.c, 's') 847 | is(result.a.d.x.a, 's') 848 | is(result.a.d.x.b, 's') 849 | is(result.a.d.x.c, 's') 850 | end() 851 | }) 852 | 853 | test('parent wildcard - array', ({ end, is }) => { 854 | const redact = fastRedact({ paths: ['a.b[*].x'], serialize: false }) 855 | const result = redact({ a: { b: [{ x: 1 }, { a: 2 }], d: { a: 's', b: 's', c: 's' } } }) 856 | is(result.a.b[0].x, censor) 857 | is(result.a.b[1].a, 2) 858 | is(result.a.d.a, 's') 859 | is(result.a.d.b, 's') 860 | end() 861 | }) 862 | 863 | test('multiple wildcards', ({ end, is }) => { 864 | const redact = fastRedact({ paths: ['a[*].c[*].d'], serialize: false }) 865 | const obj = { 866 | a: [ 867 | { c: [{ d: '1', e: '2' }, { d: '1', e: '3' }, { d: '1', e: '4' }] }, 868 | { c: [{ d: '1', f: '5' }] }, 869 | { c: [{ d: '2', g: '6' }] } 870 | ] 871 | } 872 | const result = redact(obj) 873 | is(result.a[0].c[0].d, censor) 874 | is(result.a[0].c[0].e, '2') 875 | is(result.a[0].c[1].d, censor) 876 | is(result.a[0].c[1].e, '3') 877 | is(result.a[0].c[2].d, censor) 878 | is(result.a[0].c[2].e, '4') 879 | is(result.a[1].c[0].d, censor) 880 | is(result.a[1].c[0].f, '5') 881 | is(result.a[2].c[0].d, censor) 882 | is(result.a[2].c[0].g, '6') 883 | redact.restore(result) 884 | is(result.a[0].c[0].d, '1') 885 | is(result.a[0].c[0].e, '2') 886 | is(result.a[0].c[1].d, '1') 887 | is(result.a[0].c[1].e, '3') 888 | is(result.a[0].c[2].d, '1') 889 | is(result.a[0].c[2].e, '4') 890 | is(result.a[1].c[0].d, '1') 891 | is(result.a[1].c[0].f, '5') 892 | is(result.a[2].c[0].d, '2') 893 | is(result.a[2].c[0].g, '6') 894 | end() 895 | }) 896 | 897 | test('multiple wildcards - censor function', ({ end, is }) => { 898 | const redact = fastRedact({ paths: ['a[*].c[*].d'], censor: censorFct, serialize: false }) 899 | const obj = { 900 | a: [ 901 | { c: [{ d: '1', e: '2' }, { d: '1', e: '3' }, { d: '1', e: '4' }] }, 902 | { c: [{ d: '1', f: '5' }] }, 903 | { c: [{ d: '2', g: '6' }] } 904 | ] 905 | } 906 | const result = redact(obj) 907 | is(result.a[0].c[0].d, 'xxx1') 908 | is(result.a[0].c[0].e, '2') 909 | is(result.a[0].c[1].d, 'xxx1') 910 | is(result.a[0].c[1].e, '3') 911 | is(result.a[0].c[2].d, 'xxx1') 912 | is(result.a[0].c[2].e, '4') 913 | is(result.a[1].c[0].d, 'xxx1') 914 | is(result.a[1].c[0].f, '5') 915 | is(result.a[2].c[0].d, 'xxx2') 916 | is(result.a[2].c[0].g, '6') 917 | end() 918 | }) 919 | 920 | test('multiple wildcards end', ({ end, is, same }) => { 921 | const redact = fastRedact({ paths: ['a[*].c.d[*]'], serialize: false }) 922 | const obj = { 923 | a: [ 924 | { c: { d: [ '1', '2' ], e: '3' } }, 925 | { c: { d: [ '1' ], f: '4' } }, 926 | { c: { d: [ '1' ], g: '5' } } 927 | ] 928 | } 929 | const result = redact(obj) 930 | same(result.a[0].c.d, [censor, censor]) 931 | is(result.a[0].c.e, '3') 932 | same(result.a[1].c.d, [censor]) 933 | is(result.a[1].c.f, '4') 934 | same(result.a[2].c.d, [censor]) 935 | is(result.a[2].c.g, '5') 936 | end() 937 | }) 938 | 939 | test('multiple wildcards depth after n wildcard', ({ end, is }) => { 940 | const redact = fastRedact({ paths: ['a[*].c.d[*].i'], serialize: false }) 941 | const obj = { 942 | a: [ 943 | { c: { d: [ { i: '1', j: '2' } ], e: '3' } }, 944 | { c: { d: [ { i: '1', j: '2' }, { i: '1', j: '3' } ], f: '4' } }, 945 | { c: { d: [ { i: '1', j: '2' } ], g: '5' } } 946 | ] 947 | } 948 | const result = redact(obj) 949 | is(result.a[0].c.d[0].i, censor) 950 | is(result.a[0].c.d[0].j, '2') 951 | is(result.a[0].c.e, '3') 952 | is(result.a[1].c.d[0].i, censor) 953 | is(result.a[1].c.d[0].j, '2') 954 | is(result.a[1].c.d[1].i, censor) 955 | is(result.a[1].c.d[1].j, '3') 956 | is(result.a[1].c.f, '4') 957 | is(result.a[2].c.d[0].i, censor) 958 | is(result.a[2].c.d[0].j, '2') 959 | is(result.a[2].c.g, '5') 960 | end() 961 | }) 962 | 963 | test('parent wildcards – array – single index', ({ end, same }) => { 964 | const redact = fastRedact({ paths: ['insideArray.like[3].*.foo'], serialize: false }) 965 | same(redact({ insideArray: { like: ['a', 'b', 'c', { this: { foo: 'meow' } }] } }), { insideArray: { like: ['a', 'b', 'c', { this: { foo: censor } }] } }) 966 | end() 967 | }) 968 | 969 | test('parent wildcards - handles null proto objects', ({ end, is }) => { 970 | const redact = fastRedact({ paths: ['a.*.c'], serialize: false }) 971 | const result = redact({ __proto__: null, a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 972 | is(result.a.b.c, censor) 973 | end() 974 | }) 975 | 976 | test('parent wildcards - handles paths that do not match object structure', ({ end, same }) => { 977 | const redact = fastRedact({ paths: ['a.*.y.z'], serialize: false }) 978 | same(redact({ a: { b: { c: 's' } } }), { a: { b: { c: 's' } } }) 979 | end() 980 | }) 981 | 982 | test('parent wildcards - gracefully handles primitives that match intermediate keys in paths', ({ end, same }) => { 983 | const redact = fastRedact({ paths: ['a.*.c'], serialize: false }) 984 | same(redact({ a: { b: null } }), { a: { b: null } }) 985 | same(redact({ a: { b: 's' } }), { a: { b: 's' } }) 986 | same(redact({ a: { b: 1 } }), { a: { b: 1 } }) 987 | same(redact({ a: { b: undefined } }), { a: { b: undefined } }) 988 | same(redact({ a: { b: true } }), { a: { b: true } }) 989 | const sym = Symbol('sym') 990 | same(redact({ a: { b: sym } }), { a: { b: sym } }) 991 | end() 992 | }) 993 | 994 | test('parent wildcards – handles circulars', ({ end, same }) => { 995 | const redact = fastRedact({ paths: ['x.*.baz'], serialize: false }) 996 | const bar = { b: 2 } 997 | const o = { x: { a: 1, bar } } 998 | bar.baz = bar 999 | o.x.bar.baz = o.x.bar 1000 | same(redact(o), { x: { a: 1, bar: { b: 2, baz: censor } } }) 1001 | end() 1002 | }) 1003 | 1004 | test('parent wildcards – handles circulars – restore', ({ end, is }) => { 1005 | const redact = fastRedact({ paths: ['x.*.baz'], serialize: false }) 1006 | const bar = { b: 2 } 1007 | const o = { x: { a: 1, bar } } 1008 | bar.baz = bar 1009 | o.x.bar.baz = o.x.bar 1010 | is(o.x.bar.baz, bar) 1011 | redact(o) 1012 | is(o.x.a, 1) 1013 | is(o.x.bar.baz, censor) 1014 | redact.restore(o) 1015 | is(o.x.bar.baz, bar) 1016 | end() 1017 | }) 1018 | 1019 | test('parent wildcards – handles circulars and cross references – restore', ({ end, is }) => { 1020 | const redact = fastRedact({ paths: ['x.*.baz', 'x.*.cf.bar'], serialize: false }) 1021 | const bar = { b: 2 } 1022 | const o = { x: { a: 1, bar, y: { cf: { bar } } } } 1023 | bar.baz = bar 1024 | o.x.bar.baz = o.x.bar 1025 | is(o.x.bar.baz, bar) 1026 | is(o.x.y.cf.bar, bar) 1027 | redact(o) 1028 | is(o.x.bar.baz, censor) 1029 | is(o.x.y.cf.bar, censor) 1030 | redact.restore(o) 1031 | is(o.x.bar.baz, bar) 1032 | is(o.x.y.cf.bar, bar) 1033 | end() 1034 | }) 1035 | 1036 | test('parent wildcards – handles missing paths', ({ end, is }) => { 1037 | const redact = fastRedact({ paths: ['z.*.baz'] }) 1038 | const o = { a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } } 1039 | is(redact(o), JSON.stringify(o)) 1040 | end() 1041 | }) 1042 | 1043 | test('ultimate wildcards – handles missing paths', ({ end, is }) => { 1044 | const redact = fastRedact({ paths: ['z.*'] }) 1045 | const o = { a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } } 1046 | is(redact(o), JSON.stringify(o)) 1047 | end() 1048 | }) 1049 | 1050 | test('parent wildcards – removes during serialization instead of redacting when remove option is true', ({ end, is }) => { 1051 | const redact = fastRedact({ paths: ['a.*.c'], remove: true }) 1052 | const o = { a: { b: { c: 'c' }, x: { c: 1 } } } 1053 | is(redact(o), `{"a":{"b":{},"x":{}}}`) 1054 | end() 1055 | }) 1056 | 1057 | test('ultimate wildcards – removes during serialization instead of redacting when remove option is true', ({ end, is }) => { 1058 | const redact = fastRedact({ paths: ['a.b.*'], remove: true }) 1059 | const o = { a: { b: { c: 'c' }, x: { c: 1 } } } 1060 | is(redact(o), `{"a":{"b":{},"x":{"c":1}}}`) 1061 | end() 1062 | }) 1063 | 1064 | test('supports leading bracket notation', ({ end, is }) => { 1065 | const redact = fastRedact({ paths: ['["a"].b.c'] }) 1066 | const o = { a: { b: { c: 'd' } } } 1067 | is(redact(o), `{"a":{"b":{"c":"${censor}"}}}`) 1068 | end() 1069 | }) 1070 | 1071 | test('supports leading bracket notation containing non-legal keyword characters', ({ end, is }) => { 1072 | const redact = fastRedact({ paths: ['["a-x"].b.c'] }) 1073 | const o = { 'a-x': { b: { c: 'd' } } } 1074 | is(redact(o), `{"a-x":{"b":{"c":"${censor}"}}}`) 1075 | end() 1076 | }) 1077 | 1078 | test('supports single leading bracket', ({ end, is }) => { 1079 | const censor = 'test' 1080 | const redact = fastRedact({ paths: ['["a"]'], censor, serialize: false }) 1081 | is(redact({ a: 'a' }).a, censor) 1082 | end() 1083 | }) 1084 | 1085 | test('supports single leading bracket containing non-legal keyword characters', ({ end, is }) => { 1086 | const censor = 'test' 1087 | const redact = fastRedact({ paths: ['["a-x"]'], censor, serialize: false }) 1088 | is(redact({ 'a-x': 'a' })['a-x'], censor) 1089 | end() 1090 | }) 1091 | 1092 | test('(leading brackets) ultimate wildcards – handles circulars and cross references – restore', ({ end, is }) => { 1093 | const redact = fastRedact({ paths: ['bar.baz.*', 'cf.*'], serialize: false }) 1094 | const bar = { b: 2 } 1095 | const o = { a: 1, bar, cf: { bar } } 1096 | bar.baz = bar 1097 | o.bar.baz = o.bar 1098 | is(o.bar.baz, bar) 1099 | is(o.cf.bar, bar) 1100 | redact(o) 1101 | is(o.bar.baz, censor) 1102 | is(o.cf.bar, censor) 1103 | redact.restore(o) 1104 | is(o.bar.baz, bar) 1105 | is(o.cf.bar, bar) 1106 | end() 1107 | }) 1108 | 1109 | test('(leading brackets) parent wildcards – handles circulars and cross references – restore', ({ end, is }) => { 1110 | const redact = fastRedact({ paths: ['["x"].*.baz', '["x"].*.cf.bar'], serialize: false }) 1111 | const bar = { b: 2 } 1112 | const o = { x: { a: 1, bar, y: { cf: { bar } } } } 1113 | bar.baz = bar 1114 | o.x.bar.baz = o.x.bar 1115 | is(o.x.bar.baz, bar) 1116 | is(o.x.y.cf.bar, bar) 1117 | redact(o) 1118 | is(o.x.bar.baz, censor) 1119 | is(o.x.y.cf.bar, censor) 1120 | redact.restore(o) 1121 | is(o.x.bar.baz, bar) 1122 | is(o.x.y.cf.bar, bar) 1123 | end() 1124 | }) 1125 | 1126 | test('(leading brackets) ultimate wildcards – handles missing paths', ({ end, is }) => { 1127 | const redact = fastRedact({ paths: ['["z"].*'] }) 1128 | const o = { a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } } 1129 | is(redact(o), JSON.stringify(o)) 1130 | end() 1131 | }) 1132 | 1133 | test('(leading brackets) static + wildcards reuse', ({ end, is }) => { 1134 | const redact = fastRedact({ paths: ['["a"].b.c', '["a"].d.*'], serialize: false }) 1135 | const result = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 1136 | 1137 | is(result.a.b.c, censor) 1138 | is(result.a.d.a, censor) 1139 | is(result.a.d.b, censor) 1140 | is(result.a.d.c, censor) 1141 | 1142 | redact.restore(result) 1143 | 1144 | const result2 = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }) 1145 | is(result2.a.b.c, censor) 1146 | is(result2.a.d.a, censor) 1147 | is(result2.a.d.b, censor) 1148 | is(result2.a.d.c, censor) 1149 | 1150 | redact.restore(result2) 1151 | end() 1152 | }) 1153 | 1154 | test('correctly restores original object when a path does not match object', ({ end, is }) => { 1155 | const redact = fastRedact({ paths: ['foo.bar'], strict: false }) 1156 | const o = {} 1157 | is(redact({ foo: o }), '{"foo":{}}') 1158 | is(o.hasOwnProperty('bar'), false) 1159 | end() 1160 | }) 1161 | 1162 | test('correctly restores original object when a matching path has value of `undefined`', ({ end, is }) => { 1163 | const redact = fastRedact({ paths: ['foo.bar'], strict: false }) 1164 | const o = { bar: undefined } 1165 | is(redact({ foo: o }), '{"foo":{}}') 1166 | is(o.hasOwnProperty('bar'), true) 1167 | is(o.bar, undefined) 1168 | end() 1169 | }) 1170 | 1171 | test('correctly restores keys matching a static path and a wildcard', ({ end, is }) => { 1172 | const redact = fastRedact({ 1173 | paths: ['a', '*.b', 'x.b'], 1174 | serialize: false 1175 | }) 1176 | const o = { x: { a: 'a', b: 'b' } } 1177 | redact(o) 1178 | is(o.x.a, 'a') 1179 | is(o.x.b, '[REDACTED]') 1180 | redact.restore(o) 1181 | is(o.x.a, 'a') 1182 | is(o.x.b, 'b') 1183 | end() 1184 | }) 1185 | 1186 | test('correctly restores keys matching multiple wildcards', ({ end, is }) => { 1187 | const redact = fastRedact({ 1188 | paths: ['a', '*.b', 'x.*', 'y.*.z'], 1189 | serialize: false 1190 | }) 1191 | const o = { x: { a: 'a', b: 'b' }, y: { f: { z: 'z' } } } 1192 | redact(o) 1193 | is(o.x.a, '[REDACTED]') 1194 | is(o.x.b, '[REDACTED]') 1195 | is(o.y.f.z, '[REDACTED]') 1196 | redact.restore(o) 1197 | is(o.x.a, 'a') 1198 | is(o.x.b, 'b') 1199 | is(o.y.f.z, 'z') 1200 | end() 1201 | }) 1202 | 1203 | test('handles multiple paths with leading brackets', ({ end, is }) => { 1204 | const redact = fastRedact({ paths: ['["x-y"]', '["y-x"]'] }) 1205 | const o = { 'x-y': 'test', 'y-x': 'test2' } 1206 | is(redact(o), '{"x-y":"[REDACTED]","y-x":"[REDACTED]"}') 1207 | end() 1208 | }) 1209 | 1210 | test('handles objects with and then without target paths', ({ end, is }) => { 1211 | const redact = fastRedact({ paths: ['test'] }) 1212 | const o1 = { test: 'check' } 1213 | const o2 = {} 1214 | is(redact(o1), '{"test":"[REDACTED]"}') 1215 | is(redact(o2), '{}') 1216 | // run each check twice to ensure no mutations 1217 | is(redact(o1), '{"test":"[REDACTED]"}') 1218 | is(redact(o2), '{}') 1219 | is('test' in o1, true) 1220 | is('test' in o2, false) 1221 | end() 1222 | }) 1223 | 1224 | test('handles leading wildcards and null values', ({ end, is }) => { 1225 | const redact = fastRedact({ paths: ['*.test'] }) 1226 | const o = { prop: null } 1227 | is(redact(o), '{"prop":null}') 1228 | is(o.prop, null) 1229 | end() 1230 | }) 1231 | 1232 | test('handles keys with dots', ({ end, is }) => { 1233 | const redactSingleQ = fastRedact({ paths: [`a['b.c']`], serialize: false }) 1234 | const redactDoubleQ = fastRedact({ paths: [`a["b.c"]`], serialize: false }) 1235 | const redactBacktickQ = fastRedact({ paths: ['a[`b.c`]'], serialize: false }) 1236 | const redactNum = fastRedact({ paths: [`a[-1.2]`], serialize: false }) 1237 | const redactLeading = fastRedact({ paths: [`["b.c"]`], serialize: false }) 1238 | is(redactSingleQ({ a: { 'b.c': 'x', '-1.2': 'x' } }).a['b.c'], censor) 1239 | is(redactDoubleQ({ a: { 'b.c': 'x', '-1.2': 'x' } }).a['b.c'], censor) 1240 | is(redactBacktickQ({ a: { 'b.c': 'x', '-1.2': 'x' } }).a['b.c'], censor) 1241 | is(redactNum({ a: { 'b.c': 'x', '-1.2': 'x' } }).a['-1.2'], censor) 1242 | is(redactLeading({ 'b.c': 'x', '-1.2': 'x' })['b.c'], censor) 1243 | end() 1244 | }) 1245 | 1246 | test('handles multi wildcards within arrays', ({ end, is }) => { 1247 | const redact = fastRedact({ 1248 | paths: ['a[*].x.d[*].i.*'] 1249 | }) 1250 | const o = { 1251 | a: [ { x: { d: [ { j: { i: 'R' } }, { i: 'R', j: 'NR' } ] } } ] 1252 | } 1253 | is(redact(o), '{"a":[{"x":{"d":["[REDACTED]","[REDACTED]"]}}]}') 1254 | end() 1255 | }) 1256 | 1257 | test('handles multi wildcards within arrays with a censorFct', ({ end, is }) => { 1258 | const redact = fastRedact({ 1259 | paths: ['a[*].x.d[*].i.*.i'], 1260 | censor: censorWithPath 1261 | }) 1262 | const o = { 1263 | a: [ 1264 | { x: { d: [ { i: 'R', j: 'NR' } ] } } 1265 | ] 1266 | } 1267 | is(redact(o), '{"a":[{"x":{"d":[{"i":"a.0.x.d.*.i.*.i xxxR","j":"NR"}]}}]}') 1268 | end() 1269 | }) 1270 | 1271 | test('handles multi wildcards within arrays with undefined values', ({ end, is }) => { 1272 | const redact = fastRedact({ 1273 | paths: ['a[*].x.d[*].i.*.i'] 1274 | }) 1275 | const o = { 1276 | a: [ 1277 | { x: { d: [ { i: undefined, j: 'NR' } ] } } 1278 | ] 1279 | } 1280 | is(redact(o), '{"a":[{"x":{"d":[{"i":"[REDACTED]","j":"NR"}]}}]}') 1281 | end() 1282 | }) 1283 | 1284 | test('handles multi wildcards with objects containing nulls', ({ end, is }) => { 1285 | const redact = fastRedact({ 1286 | paths: ['*.*.x'], 1287 | serialize: false, 1288 | censor: '[REDACTED]' 1289 | }) 1290 | const o = { a: { b: null } } 1291 | is(redact(o), o) 1292 | end() 1293 | }) 1294 | 1295 | test('handles multi wildcards with pattern repetition', ({ end, is }) => { 1296 | const redact = fastRedact({ 1297 | paths: ['*.d', '*.*.d', '*.*.*.d'] 1298 | }) 1299 | const o = { 1300 | x: { c: { d: 'hide me', e: 'leave me be' } }, 1301 | y: { c: { d: 'and me', f: 'I want to live' } }, 1302 | z: { c: { d: 'and also I', g: 'I want to run in a stream' } } 1303 | } 1304 | is(redact(o), '{"x":{"c":{"d":"[REDACTED]","e":"leave me be"}},"y":{"c":{"d":"[REDACTED]","f":"I want to live"}},"z":{"c":{"d":"[REDACTED]","g":"I want to run in a stream"}}}') 1305 | end() 1306 | }) 1307 | 1308 | test('restores multi wildcards with pattern repetition', ({ end, is }) => { 1309 | const redact = fastRedact({ 1310 | paths: ['*.d', '*.*.d', '*.*.*.d'] 1311 | }) 1312 | const o = { 1313 | x: { c: { d: 'hide me', e: 'leave me be' } }, 1314 | y: { c: { d: 'and me', f: 'I want to live' } }, 1315 | z: { c: { d: 'and also I', g: 'I want to run in a stream' } } 1316 | } 1317 | redact(o) 1318 | is(o.x.c.d, 'hide me') 1319 | is(o.y.c.d, 'and me') 1320 | is(o.z.c.d, 'and also I') 1321 | end() 1322 | }) 1323 | 1324 | test('multi level wildcards with level > 3', ({ end, is }) => { 1325 | const redact = fastRedact({ paths: ['*.*.*.c', '*.*.*.*.c'] }) 1326 | const o = { 1327 | a: { 1328 | b: { 1329 | x: { 1330 | c: 's' 1331 | } 1332 | }, 1333 | d: { 1334 | x: { 1335 | u: { 1336 | a: 's', 1337 | b: 's', 1338 | c: 's' 1339 | } 1340 | } 1341 | } 1342 | } 1343 | } 1344 | is(redact(o), '{"a":{"b":{"x":{"c":"[REDACTED]"}},"d":{"x":{"u":{"a":"s","b":"s","c":"[REDACTED]"}}}}}') 1345 | end() 1346 | }) 1347 | 1348 | test('multi level wildcards at nested level inside object', ({ end, is }) => { 1349 | const redact = fastRedact({ paths: ['a.*.*.c', 'a.d.*.*.c'] }) 1350 | const o = { 1351 | a: { 1352 | b: { 1353 | x: { 1354 | c: 's' 1355 | } 1356 | }, 1357 | d: { 1358 | x: { 1359 | u: { 1360 | a: 's', 1361 | b: 's', 1362 | c: 's' 1363 | } 1364 | } 1365 | } 1366 | } 1367 | } 1368 | is(redact(o), '{"a":{"b":{"x":{"c":"[REDACTED]"}},"d":{"x":{"u":{"a":"s","b":"s","c":"[REDACTED]"}}}}}') 1369 | end() 1370 | }) 1371 | 1372 | test('multi level wildcards with level > 3 with serialize false', ({ end, is }) => { 1373 | const redact = fastRedact({ paths: ['*.*.*.c', '*.*.*.*.c'], serialize: false }) 1374 | const result = redact({ a: { b: { x: { c: 's' } }, d: { x: { u: { a: 's', b: 's', c: 's' } } } } }) 1375 | is(result.a.b.x.c, censor) 1376 | is(result.a.d.x.u.a, 's') 1377 | is(result.a.d.x.u.b, 's') 1378 | is(result.a.d.x.u.c, censor) 1379 | redact.restore(result) 1380 | is(result.a.b.x.c, 's') 1381 | is(result.a.d.x.u.a, 's') 1382 | is(result.a.d.x.u.b, 's') 1383 | is(result.a.d.x.u.c, 's') 1384 | end() 1385 | }) 1386 | 1387 | test('multi level wildcards at nested level inside object with serialize false', ({ end, is }) => { 1388 | const redact = fastRedact({ paths: ['a.*.*.c', 'a.d.*.*.c'], serialize: false }) 1389 | const result = redact({ a: { b: { x: { c: 's' } }, d: { x: { u: { a: 's', b: 's', c: 's' } } } } }) 1390 | is(result.a.b.x.c, censor) 1391 | is(result.a.d.x.u.a, 's') 1392 | is(result.a.d.x.u.b, 's') 1393 | is(result.a.d.x.u.c, censor) 1394 | redact.restore(result) 1395 | is(result.a.b.x.c, 's') 1396 | is(result.a.d.x.u.a, 's') 1397 | is(result.a.d.x.u.b, 's') 1398 | is(result.a.d.x.u.c, 's') 1399 | end() 1400 | }) 1401 | 1402 | test('restores nested wildcard values', ({ end, is }) => { 1403 | const o = { a: { b: [{ c: [ 1404 | { d: '123' }, 1405 | { d: '456' } 1406 | ] }] } } 1407 | 1408 | const censor = 'censor' 1409 | const paths = ['a.b[*].c[*].d'] 1410 | const redact = fastRedact({ paths, censor, serialize: false }) 1411 | 1412 | redact(o) 1413 | is(o.a.b[0].c[0].d, censor) 1414 | is(o.a.b[0].c[1].d, censor) 1415 | redact.restore(o) 1416 | is(o.a.b[0].c[0].d, '123') 1417 | is(o.a.b[0].c[1].d, '456') 1418 | end() 1419 | }) 1420 | 1421 | test('restores multi nested wildcard values', ({ end, is }) => { 1422 | const o = { 1423 | a: { 1424 | b1: { 1425 | c1: { 1426 | d1: { e: '123' }, 1427 | d2: { e: '456' } 1428 | }, 1429 | c2: { 1430 | d1: { e: '789' }, 1431 | d2: { e: '012' } 1432 | } 1433 | }, 1434 | b2: { 1435 | c1: { 1436 | d1: { e: '345' }, 1437 | d2: { e: '678' } 1438 | }, 1439 | c2: { 1440 | d1: { e: '901' }, 1441 | d2: { e: '234' } 1442 | } 1443 | } 1444 | } 1445 | } 1446 | const o2 = { 1447 | a: { 1448 | b1: { 1449 | c1: { 1450 | d1: { e: '123' }, 1451 | d2: { e: '456' } 1452 | } 1453 | } 1454 | } 1455 | } 1456 | 1457 | const censor = 'censor' 1458 | const paths = ['a.*.*.*.e'] 1459 | const redact = fastRedact({ paths, censor, serialize: false }) 1460 | 1461 | redact(o) 1462 | is(o.a.b1.c1.d1.e, censor) 1463 | is(o.a.b1.c1.d2.e, censor) 1464 | is(o.a.b1.c2.d1.e, censor) 1465 | is(o.a.b1.c2.d2.e, censor) 1466 | is(o.a.b2.c1.d1.e, censor) 1467 | is(o.a.b2.c1.d2.e, censor) 1468 | is(o.a.b2.c2.d1.e, censor) 1469 | is(o.a.b2.c2.d2.e, censor) 1470 | redact.restore(o) 1471 | is(o.a.b1.c1.d1.e, '123') 1472 | is(o.a.b1.c1.d2.e, '456') 1473 | is(o.a.b1.c2.d1.e, '789') 1474 | is(o.a.b1.c2.d2.e, '012') 1475 | is(o.a.b2.c1.d1.e, '345') 1476 | is(o.a.b2.c1.d2.e, '678') 1477 | is(o.a.b2.c2.d1.e, '901') 1478 | is(o.a.b2.c2.d2.e, '234') 1479 | 1480 | redact(o2) 1481 | is(o2.a.b1.c1.d1.e, censor) 1482 | is(o2.a.b1.c1.d2.e, censor) 1483 | redact.restore(o2) 1484 | is(o2.a.b1.c1.d1.e, '123') 1485 | is(o2.a.b1.c1.d2.e, '456') 1486 | 1487 | end() 1488 | }) 1489 | 1490 | test('redact multi trailing wildcard', ({ end, is }) => { 1491 | const o = { a: { b: { c: 'value' } } } 1492 | 1493 | const censor = 'censor' 1494 | const paths = ['a.*.*'] 1495 | const redact = fastRedact({ paths, censor, serialize: false }) 1496 | 1497 | redact(o) 1498 | is(o.a.b.c, censor) 1499 | redact.restore(o) 1500 | is(o.a.b.c, 'value') 1501 | end() 1502 | }) 1503 | --------------------------------------------------------------------------------