dev-api: add developer portal, OpenAPI spec, and Flatenbadet case study
- New /developers/ page with API docs, SDKs, pricing, use cases - OpenAPI 3.0 spec for Orders, Missions, Photos, Analytics - Case study: Glasskiosken i Flatenbadet — complete ROI analysis - Updated /order/ with recurring missions and frequency dropdown
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
||||
name: ci
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
# This allows a subsequently queued workflow run to interrupt previous runs
|
||||
concurrency:
|
||||
group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [6.x, 8.x, 10.x, 12.x, 14.x, 16.x, 18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install
|
||||
run: |
|
||||
npm install
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
npm run test
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2020 David Mark Clements
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
'use strict'
|
||||
const bench = require('fastbench')
|
||||
const fastRedact = require('..')
|
||||
|
||||
const censorFn = (v) => v + '.'
|
||||
const censorFnWithPath = (v, p) => v + '.' + p
|
||||
|
||||
const noir = require('pino-noir')(['aa.b.c'])
|
||||
const redactNoSerialize = fastRedact({ paths: ['ab.b.c'], serialize: false })
|
||||
const redactNoSerializeRestore = fastRedact({ paths: ['ac.b.c'], serialize: false })
|
||||
const noirWild = require('pino-noir')(['ad.b.*'])
|
||||
const redactWildNoSerialize = fastRedact({ paths: ['ae.b.*'], serialize: false })
|
||||
const redactWildNoSerializeRestore = fastRedact({ paths: ['af.b.*'], serialize: false })
|
||||
const redactIntermediateWildNoSerialize = fastRedact({ paths: ['ag.*.c'], serialize: false })
|
||||
const redactIntermediateWildNoSerializeRestore = fastRedact({ paths: ['ah.*.c'], serialize: false })
|
||||
const noirJSONSerialize = require('pino-noir')(['aj.b.c']) // `ai` used in pure JSON test.
|
||||
const redact = fastRedact({ paths: ['ak.b.c'] })
|
||||
const noirWildJSONSerialize = require('pino-noir')(['al.b.c'])
|
||||
const redactWild = fastRedact({ paths: ['am.b.*'] })
|
||||
const redactIntermediateWild = fastRedact({ paths: ['an.*.c'] })
|
||||
const redactIntermediateWildMatchWildOutcome = fastRedact({ paths: ['ao.*.c', 'ao.*.b', 'ao.*.a'] })
|
||||
const redactStaticMatchWildOutcome = fastRedact({ paths: ['ap.b.c', 'ap.d.a', 'ap.d.b', 'ap.d.c'] })
|
||||
const noirCensorFunction = require('pino-noir')(['aq.b.*'], censorFn)
|
||||
const redactCensorFunction = fastRedact({ paths: ['ar.b.*'], censor: censorFn, serialize: false })
|
||||
const redactIntermediateWildCensorFunction = fastRedact({ paths: ['as.*.c'], censor: censorFn, serialize: false })
|
||||
const redactCensorFunctionWithPath = fastRedact({ paths: ['at.d.b'], censor: censorFn, serialize: false })
|
||||
const redactWildCensorFunctionWithPath = fastRedact({ paths: ['au.d.*'], censor: censorFnWithPath, serialize: false })
|
||||
const redactIntermediateWildCensorFunctionWithPath = fastRedact({ paths: ['av.*.c'], censorFnWithPath, serialize: false })
|
||||
const redactMultiWild = fastRedact({ paths: ['aw.*.*'] })
|
||||
const redactMultiWildCensorFunction = fastRedact({ paths: ['ax.*.*'], censor: censorFn, serialize: false })
|
||||
|
||||
const getObj = (outerKey) => ({
|
||||
[outerKey]: {
|
||||
b: {
|
||||
c: 's'
|
||||
},
|
||||
d: {
|
||||
a: 's',
|
||||
b: 's',
|
||||
c: 's'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const max = 500
|
||||
|
||||
var run = bench([
|
||||
function benchNoirV2 (cb) {
|
||||
const obj = getObj('aa')
|
||||
for (var i = 0; i < max; i++) {
|
||||
noir.aa(obj.aa)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedact (cb) {
|
||||
const obj = getObj('ab')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactNoSerialize(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactRestore (cb) {
|
||||
const obj = getObj('ac')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactNoSerializeRestore(obj)
|
||||
redactNoSerializeRestore.restore(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchNoirV2Wild (cb) {
|
||||
const obj = getObj('ad')
|
||||
for (var i = 0; i < max; i++) {
|
||||
noirWild.ad(obj.ad)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactWild (cb) {
|
||||
const obj = getObj('ae')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactWildNoSerialize(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactWildRestore (cb) {
|
||||
const obj = getObj('af')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactWildNoSerializeRestore(obj)
|
||||
redactWildNoSerializeRestore.restore(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactIntermediateWild (cb) {
|
||||
const obj = getObj('ag')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactIntermediateWildNoSerialize(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactIntermediateWildRestore (cb) {
|
||||
const obj = getObj('ah')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactIntermediateWildNoSerializeRestore(obj)
|
||||
redactIntermediateWildNoSerializeRestore.restore(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchJSONStringify (cb) {
|
||||
const obj = getObj('ai')
|
||||
for (var i = 0; i < max; i++) {
|
||||
JSON.stringify(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchNoirV2Serialize (cb) {
|
||||
const obj = getObj('aj')
|
||||
for (var i = 0; i < max; i++) {
|
||||
noirJSONSerialize.aj(obj.aj)
|
||||
JSON.stringify(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactSerialize (cb) {
|
||||
const obj = getObj('ak')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redact(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchNoirV2WildSerialize (cb) {
|
||||
const obj = getObj('al')
|
||||
for (var i = 0; i < max; i++) {
|
||||
noirWildJSONSerialize.al(obj.al)
|
||||
JSON.stringify(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactWildSerialize (cb) {
|
||||
const obj = getObj('am')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactWild(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactIntermediateWildSerialize (cb) {
|
||||
const obj = getObj('an')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactIntermediateWild(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactIntermediateWildMatchWildOutcomeSerialize (cb) {
|
||||
const obj = getObj('ao')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactIntermediateWildMatchWildOutcome(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactStaticMatchWildOutcomeSerialize (cb) {
|
||||
const obj = getObj('ap')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactStaticMatchWildOutcome(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchNoirV2CensorFunction (cb) {
|
||||
const obj = getObj('aq')
|
||||
for (var i = 0; i < max; i++) {
|
||||
noirCensorFunction.aq(obj.aq)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactCensorFunction (cb) {
|
||||
const obj = getObj('ar')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactCensorFunction(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactCensorFunctionIntermediateWild (cb) {
|
||||
const obj = getObj('as')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactIntermediateWildCensorFunction(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactCensorFunctionWithPath (cb) {
|
||||
const obj = getObj('at')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactCensorFunctionWithPath(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactWildCensorFunctionWithPath (cb) {
|
||||
const obj = getObj('au')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactWildCensorFunctionWithPath(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactIntermediateWildCensorFunctionWithPath (cb) {
|
||||
const obj = getObj('av')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactIntermediateWildCensorFunctionWithPath(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactMultiWild (cb) {
|
||||
const obj = getObj('aw')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactMultiWild(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
},
|
||||
function benchFastRedactMultiWildCensorFunction (cb) {
|
||||
const obj = getObj('ax')
|
||||
for (var i = 0; i < max; i++) {
|
||||
redactMultiWildCensorFunction(obj)
|
||||
}
|
||||
setImmediate(cb)
|
||||
}
|
||||
], 500)
|
||||
|
||||
run(run)
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
'use strict'
|
||||
const fastRedact = require('..')
|
||||
const fauxRequest = {
|
||||
headers: {
|
||||
host: 'http://example.com',
|
||||
cookie: `oh oh we don't want this exposed in logs in etc.`,
|
||||
referer: `if we're cool maybe we'll even redact this`
|
||||
}
|
||||
}
|
||||
const redact = fastRedact({
|
||||
paths: ['headers.cookie', 'headers.referer']
|
||||
})
|
||||
|
||||
console.log(redact(fauxRequest))
|
||||
Generated
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
'use strict'
|
||||
const fastRedact = require('..')
|
||||
const redact = fastRedact({ paths: ['a[*].c.d'] })
|
||||
const obj = {
|
||||
a: [
|
||||
{ c: { d: 'hide me', e: 'leave me be' } },
|
||||
{ c: { d: 'and me', f: 'I want to live' } },
|
||||
{ c: { d: 'and also I', g: 'I want to run in a stream' } }
|
||||
]
|
||||
}
|
||||
console.log(redact(obj))
|
||||
Generated
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
'use strict'
|
||||
const fastRedact = require('..')
|
||||
const redact = fastRedact({ paths: ['a[*].c.d[*].i'] })
|
||||
const obj = {
|
||||
a: [
|
||||
{ c: { d: [ { i: 'redact me', j: 'not me' } ], e: 'leave me be' } },
|
||||
{ c: { d: [ { i: 'redact me too', j: 'not me' }, { i: 'redact me too', j: 'not me' } ], f: 'I want to live' } },
|
||||
{ c: { d: [ { i: 'redact me 3', j: 'not me' } ], g: 'I want to run in a stream' } }
|
||||
]
|
||||
}
|
||||
console.log(redact(obj))
|
||||
Generated
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
'use strict'
|
||||
const fastRedact = require('..')
|
||||
const redact = fastRedact({ paths: ['a[*].c.d[*]'] })
|
||||
const obj = {
|
||||
a: [
|
||||
{ c: { d: ['hide me', '2'], e: 'leave me be' } },
|
||||
{ c: { d: ['and me'], f: 'I want to live' } },
|
||||
{ c: { d: ['and also I'], g: 'I want to run in a stream' } }
|
||||
]
|
||||
}
|
||||
console.log(redact(obj))
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
'use strict'
|
||||
const fastRedact = require('..')
|
||||
const redact = fastRedact({ paths: ['a[*].c[*].d'] })
|
||||
const obj = {
|
||||
a: [
|
||||
{ c: [{ d: 'hide me', e: 'leave me be' }, { d: 'hide me too', e: 'leave me be' }, { d: 'hide me 3', e: 'leave me be' }] },
|
||||
{ c: [{ d: 'and me', f: 'I want to live' }] },
|
||||
{ c: [{ d: 'and also I', g: 'I want to run in a stream' }] }
|
||||
]
|
||||
}
|
||||
console.log(redact(obj))
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
'use strict'
|
||||
const fastRedact = require('..')
|
||||
const redact = fastRedact({
|
||||
paths: ['a'],
|
||||
serialize: false
|
||||
})
|
||||
const o = { a: 1, b: 2 }
|
||||
console.log(redact(o) === o)
|
||||
console.log(o)
|
||||
console.log(redact.restore(o) === o)
|
||||
console.log(o)
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
'use strict'
|
||||
const fastRedact = require('..')
|
||||
const redact = fastRedact({ paths: ['a'], serialize: (o) => JSON.stringify(o, 0, 2) })
|
||||
console.log(redact({ a: 1, b: 2 }))
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
'use strict'
|
||||
const fastRedact = require('..')
|
||||
const redact = fastRedact({ paths: ['*.c.d'] })
|
||||
const obj = {
|
||||
x: { c: { d: 'hide me', e: 'leave me be' } },
|
||||
y: { c: { d: 'and me', f: 'I want to live' } },
|
||||
z: { c: { d: 'and also I', g: 'I want to run in a stream' } }
|
||||
}
|
||||
console.log(redact(obj))
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
'use strict'
|
||||
|
||||
const validator = require('./lib/validator')
|
||||
const parse = require('./lib/parse')
|
||||
const redactor = require('./lib/redactor')
|
||||
const restorer = require('./lib/restorer')
|
||||
const { groupRedact, nestedRedact } = require('./lib/modifiers')
|
||||
const state = require('./lib/state')
|
||||
const rx = require('./lib/rx')
|
||||
const validate = validator()
|
||||
const noop = (o) => o
|
||||
noop.restore = noop
|
||||
|
||||
const DEFAULT_CENSOR = '[REDACTED]'
|
||||
fastRedact.rx = rx
|
||||
fastRedact.validator = validator
|
||||
|
||||
module.exports = fastRedact
|
||||
|
||||
function fastRedact (opts = {}) {
|
||||
const paths = Array.from(new Set(opts.paths || []))
|
||||
const serialize = 'serialize' in opts ? (
|
||||
opts.serialize === false ? opts.serialize
|
||||
: (typeof opts.serialize === 'function' ? opts.serialize : JSON.stringify)
|
||||
) : JSON.stringify
|
||||
const remove = opts.remove
|
||||
if (remove === true && serialize !== JSON.stringify) {
|
||||
throw Error('fast-redact – remove option may only be set when serializer is JSON.stringify')
|
||||
}
|
||||
const censor = remove === true
|
||||
? undefined
|
||||
: 'censor' in opts ? opts.censor : DEFAULT_CENSOR
|
||||
|
||||
const isCensorFct = typeof censor === 'function'
|
||||
const censorFctTakesPath = isCensorFct && censor.length > 1
|
||||
|
||||
if (paths.length === 0) return serialize || noop
|
||||
|
||||
validate({ paths, serialize, censor })
|
||||
|
||||
const { wildcards, wcLen, secret } = parse({ paths, censor })
|
||||
|
||||
const compileRestore = restorer()
|
||||
const strict = 'strict' in opts ? opts.strict : true
|
||||
|
||||
return redactor({ secret, wcLen, serialize, strict, isCensorFct, censorFctTakesPath }, state({
|
||||
secret,
|
||||
censor,
|
||||
compileRestore,
|
||||
serialize,
|
||||
groupRedact,
|
||||
nestedRedact,
|
||||
wildcards,
|
||||
wcLen
|
||||
}))
|
||||
}
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
groupRedact,
|
||||
groupRestore,
|
||||
nestedRedact,
|
||||
nestedRestore
|
||||
}
|
||||
|
||||
function groupRestore ({ keys, values, target }) {
|
||||
if (target == null || typeof target === 'string') return
|
||||
const length = keys.length
|
||||
for (var i = 0; i < length; i++) {
|
||||
const k = keys[i]
|
||||
target[k] = values[i]
|
||||
}
|
||||
}
|
||||
|
||||
function groupRedact (o, path, censor, isCensorFct, censorFctTakesPath) {
|
||||
const target = get(o, path)
|
||||
if (target == null || typeof target === 'string') return { keys: null, values: null, target, flat: true }
|
||||
const keys = Object.keys(target)
|
||||
const keysLength = keys.length
|
||||
const pathLength = path.length
|
||||
const pathWithKey = censorFctTakesPath ? [...path] : undefined
|
||||
const values = new Array(keysLength)
|
||||
|
||||
for (var i = 0; i < keysLength; i++) {
|
||||
const key = keys[i]
|
||||
values[i] = target[key]
|
||||
|
||||
if (censorFctTakesPath) {
|
||||
pathWithKey[pathLength] = key
|
||||
target[key] = censor(target[key], pathWithKey)
|
||||
} else if (isCensorFct) {
|
||||
target[key] = censor(target[key])
|
||||
} else {
|
||||
target[key] = censor
|
||||
}
|
||||
}
|
||||
return { keys, values, target, flat: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RestoreInstruction[]} instructions a set of instructions for restoring values to objects
|
||||
*/
|
||||
function nestedRestore (instructions) {
|
||||
for (let i = 0; i < instructions.length; i++) {
|
||||
const { target, path, value } = instructions[i]
|
||||
let current = target
|
||||
for (let i = path.length - 1; i > 0; i--) {
|
||||
current = current[path[i]]
|
||||
}
|
||||
current[path[0]] = value
|
||||
}
|
||||
}
|
||||
|
||||
function nestedRedact (store, o, path, ns, censor, isCensorFct, censorFctTakesPath) {
|
||||
const target = get(o, path)
|
||||
if (target == null) return
|
||||
const keys = Object.keys(target)
|
||||
const keysLength = keys.length
|
||||
for (var i = 0; i < keysLength; i++) {
|
||||
const key = keys[i]
|
||||
specialSet(store, target, key, path, ns, censor, isCensorFct, censorFctTakesPath)
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
function has (obj, prop) {
|
||||
return obj !== undefined && obj !== null
|
||||
? ('hasOwn' in Object ? Object.hasOwn(obj, prop) : Object.prototype.hasOwnProperty.call(obj, prop))
|
||||
: false
|
||||
}
|
||||
|
||||
function specialSet (store, o, k, path, afterPath, censor, isCensorFct, censorFctTakesPath) {
|
||||
const afterPathLen = afterPath.length
|
||||
const lastPathIndex = afterPathLen - 1
|
||||
const originalKey = k
|
||||
var i = -1
|
||||
var n
|
||||
var nv
|
||||
var ov
|
||||
var oov = null
|
||||
var wc = null
|
||||
var kIsWc
|
||||
var wcov
|
||||
var consecutive = false
|
||||
var level = 0
|
||||
// need to track depth of the `redactPath` tree
|
||||
var depth = 0
|
||||
var redactPathCurrent = tree()
|
||||
ov = n = o[k]
|
||||
if (typeof n !== 'object') return
|
||||
while (n != null && ++i < afterPathLen) {
|
||||
depth += 1
|
||||
k = afterPath[i]
|
||||
oov = ov
|
||||
if (k !== '*' && !wc && !(typeof n === 'object' && k in n)) {
|
||||
break
|
||||
}
|
||||
if (k === '*') {
|
||||
if (wc === '*') {
|
||||
consecutive = true
|
||||
}
|
||||
wc = k
|
||||
if (i !== lastPathIndex) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (wc) {
|
||||
const wcKeys = Object.keys(n)
|
||||
for (var j = 0; j < wcKeys.length; j++) {
|
||||
const wck = wcKeys[j]
|
||||
wcov = n[wck]
|
||||
kIsWc = k === '*'
|
||||
if (consecutive) {
|
||||
redactPathCurrent = node(redactPathCurrent, wck, depth)
|
||||
level = i
|
||||
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)
|
||||
} else {
|
||||
if (kIsWc || (typeof wcov === 'object' && wcov !== null && k in wcov)) {
|
||||
if (kIsWc) {
|
||||
ov = wcov
|
||||
} else {
|
||||
ov = wcov[k]
|
||||
}
|
||||
nv = (i !== lastPathIndex)
|
||||
? ov
|
||||
: (isCensorFct
|
||||
? (censorFctTakesPath ? censor(ov, [...path, originalKey, ...afterPath]) : censor(ov))
|
||||
: censor)
|
||||
if (kIsWc) {
|
||||
const rv = restoreInstr(node(redactPathCurrent, wck, depth), ov, o[originalKey])
|
||||
store.push(rv)
|
||||
n[wck] = nv
|
||||
} else {
|
||||
if (wcov[k] === nv) {
|
||||
// pass
|
||||
} else if ((nv === undefined && censor !== undefined) || (has(wcov, k) && nv === ov)) {
|
||||
redactPathCurrent = node(redactPathCurrent, wck, depth)
|
||||
} else {
|
||||
redactPathCurrent = node(redactPathCurrent, wck, depth)
|
||||
const rv = restoreInstr(node(redactPathCurrent, k, depth + 1), ov, o[originalKey])
|
||||
store.push(rv)
|
||||
wcov[k] = nv
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
wc = null
|
||||
} else {
|
||||
ov = n[k]
|
||||
redactPathCurrent = node(redactPathCurrent, k, depth)
|
||||
nv = (i !== lastPathIndex)
|
||||
? ov
|
||||
: (isCensorFct
|
||||
? (censorFctTakesPath ? censor(ov, [...path, originalKey, ...afterPath]) : censor(ov))
|
||||
: censor)
|
||||
if ((has(n, k) && nv === ov) || (nv === undefined && censor !== undefined)) {
|
||||
// pass
|
||||
} else {
|
||||
const rv = restoreInstr(redactPathCurrent, ov, o[originalKey])
|
||||
store.push(rv)
|
||||
n[k] = nv
|
||||
}
|
||||
n = n[k]
|
||||
}
|
||||
if (typeof n !== 'object') break
|
||||
// prevent circular structure, see https://github.com/pinojs/pino/issues/1513
|
||||
if (ov === oov || typeof ov === 'undefined') {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function get (o, p) {
|
||||
var i = -1
|
||||
var l = p.length
|
||||
var n = o
|
||||
while (n != null && ++i < l) {
|
||||
n = n[p[i]]
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
function iterateNthLevel (wcov, level, k, path, afterPath, censor, isCensorFct, censorFctTakesPath, originalKey, n, nv, ov, kIsWc, wck, i, lastPathIndex, redactPathCurrent, store, parent, depth) {
|
||||
if (level === 0) {
|
||||
if (kIsWc || (typeof wcov === 'object' && wcov !== null && k in wcov)) {
|
||||
if (kIsWc) {
|
||||
ov = wcov
|
||||
} else {
|
||||
ov = wcov[k]
|
||||
}
|
||||
nv = (i !== lastPathIndex)
|
||||
? ov
|
||||
: (isCensorFct
|
||||
? (censorFctTakesPath ? censor(ov, [...path, originalKey, ...afterPath]) : censor(ov))
|
||||
: censor)
|
||||
if (kIsWc) {
|
||||
const rv = restoreInstr(redactPathCurrent, ov, parent)
|
||||
store.push(rv)
|
||||
n[wck] = nv
|
||||
} else {
|
||||
if (wcov[k] === nv) {
|
||||
// pass
|
||||
} else if ((nv === undefined && censor !== undefined) || (has(wcov, k) && nv === ov)) {
|
||||
// pass
|
||||
} else {
|
||||
const rv = restoreInstr(node(redactPathCurrent, k, depth + 1), ov, parent)
|
||||
store.push(rv)
|
||||
wcov[k] = nv
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key in wcov) {
|
||||
if (typeof wcov[key] === 'object') {
|
||||
redactPathCurrent = node(redactPathCurrent, key, depth)
|
||||
iterateNthLevel(wcov[key], level - 1, k, path, afterPath, censor, isCensorFct, censorFctTakesPath, originalKey, n, nv, ov, kIsWc, wck, i, lastPathIndex, redactPathCurrent, store, parent, depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} TreeNode
|
||||
* @prop {TreeNode} [parent] reference to the parent of this node in the tree, or `null` if there is no parent
|
||||
* @prop {string} key the key that this node represents (key here being part of the path being redacted
|
||||
* @prop {TreeNode[]} children the child nodes of this node
|
||||
* @prop {number} depth the depth of this node in the tree
|
||||
*/
|
||||
|
||||
/**
|
||||
* instantiate a new, empty tree
|
||||
* @returns {TreeNode}
|
||||
*/
|
||||
function tree () {
|
||||
return { parent: null, key: null, children: [], depth: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a new node in the tree, attaching it as a child of the provided parent node
|
||||
* if the specified depth matches the parent depth, adds the new node as a _sibling_ of the parent instead
|
||||
* @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
|
||||
* @param {string} key the key that the new node represents (key here being part of the path being redacted)
|
||||
* @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
|
||||
* @returns {TreeNode} a reference to the newly created node in the tree
|
||||
*/
|
||||
function node (parent, key, depth) {
|
||||
if (parent.depth === depth) {
|
||||
return node(parent.parent, key, depth)
|
||||
}
|
||||
|
||||
var child = {
|
||||
parent,
|
||||
key,
|
||||
depth,
|
||||
children: []
|
||||
}
|
||||
|
||||
parent.children.push(child)
|
||||
|
||||
return child
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} RestoreInstruction
|
||||
* @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
|
||||
* @prop {*} value the value to restore
|
||||
* @prop {object} target the object to restore the `value` in
|
||||
*/
|
||||
|
||||
/**
|
||||
* create a restore instruction for the given redactPath node
|
||||
* generates a path in reverse order by walking up the redactPath tree
|
||||
* @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
|
||||
* @param {*} value the value to restore
|
||||
* @param {object} target a reference to the parent object to apply the restore instruction to
|
||||
* @returns {RestoreInstruction} an instruction used to restore a nested value for a specific object
|
||||
*/
|
||||
function restoreInstr (node, value, target) {
|
||||
let current = node
|
||||
const path = []
|
||||
do {
|
||||
path.push(current.key)
|
||||
current = current.parent
|
||||
} while (current.parent != null)
|
||||
|
||||
return { path, value, target }
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
'use strict'
|
||||
|
||||
const rx = require('./rx')
|
||||
|
||||
module.exports = parse
|
||||
|
||||
function parse ({ paths }) {
|
||||
const wildcards = []
|
||||
var wcLen = 0
|
||||
const secret = paths.reduce(function (o, strPath, ix) {
|
||||
var path = strPath.match(rx).map((p) => p.replace(/'|"|`/g, ''))
|
||||
const leadingBracket = strPath[0] === '['
|
||||
path = path.map((p) => {
|
||||
if (p[0] === '[') return p.substr(1, p.length - 2)
|
||||
else return p
|
||||
})
|
||||
const star = path.indexOf('*')
|
||||
if (star > -1) {
|
||||
const before = path.slice(0, star)
|
||||
const beforeStr = before.join('.')
|
||||
const after = path.slice(star + 1, path.length)
|
||||
const nested = after.length > 0
|
||||
wcLen++
|
||||
wildcards.push({
|
||||
before,
|
||||
beforeStr,
|
||||
after,
|
||||
nested
|
||||
})
|
||||
} else {
|
||||
o[strPath] = {
|
||||
path: path,
|
||||
val: undefined,
|
||||
precensored: false,
|
||||
circle: '',
|
||||
escPath: JSON.stringify(strPath),
|
||||
leadingBracket: leadingBracket
|
||||
}
|
||||
}
|
||||
return o
|
||||
}, {})
|
||||
|
||||
return { wildcards, wcLen, secret }
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
'use strict'
|
||||
|
||||
const rx = require('./rx')
|
||||
|
||||
module.exports = redactor
|
||||
|
||||
function redactor ({ secret, serialize, wcLen, strict, isCensorFct, censorFctTakesPath }, state) {
|
||||
/* eslint-disable-next-line */
|
||||
const redact = Function('o', `
|
||||
if (typeof o !== 'object' || o == null) {
|
||||
${strictImpl(strict, serialize)}
|
||||
}
|
||||
const { censor, secret } = this
|
||||
const originalSecret = {}
|
||||
const secretKeys = Object.keys(secret)
|
||||
for (var i = 0; i < secretKeys.length; i++) {
|
||||
originalSecret[secretKeys[i]] = secret[secretKeys[i]]
|
||||
}
|
||||
|
||||
${redactTmpl(secret, isCensorFct, censorFctTakesPath)}
|
||||
this.compileRestore()
|
||||
${dynamicRedactTmpl(wcLen > 0, isCensorFct, censorFctTakesPath)}
|
||||
this.secret = originalSecret
|
||||
${resultTmpl(serialize)}
|
||||
`).bind(state)
|
||||
|
||||
redact.state = state
|
||||
|
||||
if (serialize === false) {
|
||||
redact.restore = (o) => state.restore(o)
|
||||
}
|
||||
|
||||
return redact
|
||||
}
|
||||
|
||||
function redactTmpl (secret, isCensorFct, censorFctTakesPath) {
|
||||
return Object.keys(secret).map((path) => {
|
||||
const { escPath, leadingBracket, path: arrPath } = secret[path]
|
||||
const skip = leadingBracket ? 1 : 0
|
||||
const delim = leadingBracket ? '' : '.'
|
||||
const hops = []
|
||||
var match
|
||||
while ((match = rx.exec(path)) !== null) {
|
||||
const [ , ix ] = match
|
||||
const { index, input } = match
|
||||
if (index > skip) hops.push(input.substring(0, index - (ix ? 0 : 1)))
|
||||
}
|
||||
var existence = hops.map((p) => `o${delim}${p}`).join(' && ')
|
||||
if (existence.length === 0) existence += `o${delim}${path} != null`
|
||||
else existence += ` && o${delim}${path} != null`
|
||||
|
||||
const circularDetection = `
|
||||
switch (true) {
|
||||
${hops.reverse().map((p) => `
|
||||
case o${delim}${p} === censor:
|
||||
secret[${escPath}].circle = ${JSON.stringify(p)}
|
||||
break
|
||||
`).join('\n')}
|
||||
}
|
||||
`
|
||||
|
||||
const censorArgs = censorFctTakesPath
|
||||
? `val, ${JSON.stringify(arrPath)}`
|
||||
: `val`
|
||||
|
||||
return `
|
||||
if (${existence}) {
|
||||
const val = o${delim}${path}
|
||||
if (val === censor) {
|
||||
secret[${escPath}].precensored = true
|
||||
} else {
|
||||
secret[${escPath}].val = val
|
||||
o${delim}${path} = ${isCensorFct ? `censor(${censorArgs})` : 'censor'}
|
||||
${circularDetection}
|
||||
}
|
||||
}
|
||||
`
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
function dynamicRedactTmpl (hasWildcards, isCensorFct, censorFctTakesPath) {
|
||||
return hasWildcards === true ? `
|
||||
{
|
||||
const { wildcards, wcLen, groupRedact, nestedRedact } = this
|
||||
for (var i = 0; i < wcLen; i++) {
|
||||
const { before, beforeStr, after, nested } = wildcards[i]
|
||||
if (nested === true) {
|
||||
secret[beforeStr] = secret[beforeStr] || []
|
||||
nestedRedact(secret[beforeStr], o, before, after, censor, ${isCensorFct}, ${censorFctTakesPath})
|
||||
} else secret[beforeStr] = groupRedact(o, before, censor, ${isCensorFct}, ${censorFctTakesPath})
|
||||
}
|
||||
}
|
||||
` : ''
|
||||
}
|
||||
|
||||
function resultTmpl (serialize) {
|
||||
return serialize === false ? `return o` : `
|
||||
var s = this.serialize(o)
|
||||
this.restore(o)
|
||||
return s
|
||||
`
|
||||
}
|
||||
|
||||
function strictImpl (strict, serialize) {
|
||||
return strict === true
|
||||
? `throw Error('fast-redact: primitives cannot be redacted')`
|
||||
: serialize === false ? `return o` : `return this.serialize(o)`
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
'use strict'
|
||||
|
||||
const { groupRestore, nestedRestore } = require('./modifiers')
|
||||
|
||||
module.exports = restorer
|
||||
|
||||
function restorer () {
|
||||
return function compileRestore () {
|
||||
if (this.restore) {
|
||||
this.restore.state.secret = this.secret
|
||||
return
|
||||
}
|
||||
const { secret, wcLen } = this
|
||||
const paths = Object.keys(secret)
|
||||
const resetters = resetTmpl(secret, paths)
|
||||
const hasWildcards = wcLen > 0
|
||||
const state = hasWildcards ? { secret, groupRestore, nestedRestore } : { secret }
|
||||
/* eslint-disable-next-line */
|
||||
this.restore = Function(
|
||||
'o',
|
||||
restoreTmpl(resetters, paths, hasWildcards)
|
||||
).bind(state)
|
||||
this.restore.state = state
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutates the original object to be censored by restoring its original values
|
||||
* prior to censoring.
|
||||
*
|
||||
* @param {object} secret Compiled object describing which target fields should
|
||||
* be censored and the field states.
|
||||
* @param {string[]} paths The list of paths to censor as provided at
|
||||
* initialization time.
|
||||
*
|
||||
* @returns {string} String of JavaScript to be used by `Function()`. The
|
||||
* string compiles to the function that does the work in the description.
|
||||
*/
|
||||
function resetTmpl (secret, paths) {
|
||||
return paths.map((path) => {
|
||||
const { circle, escPath, leadingBracket } = secret[path]
|
||||
const delim = leadingBracket ? '' : '.'
|
||||
const reset = circle
|
||||
? `o.${circle} = secret[${escPath}].val`
|
||||
: `o${delim}${path} = secret[${escPath}].val`
|
||||
const clear = `secret[${escPath}].val = undefined`
|
||||
return `
|
||||
if (secret[${escPath}].val !== undefined) {
|
||||
try { ${reset} } catch (e) {}
|
||||
${clear}
|
||||
}
|
||||
`
|
||||
}).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the body of the restore function
|
||||
*
|
||||
* Restoration of the redacted object happens
|
||||
* backwards, in reverse order of redactions,
|
||||
* so that repeated redactions on the same object
|
||||
* property can be eventually rolled back to the
|
||||
* original value.
|
||||
*
|
||||
* This way dynamic redactions are restored first,
|
||||
* starting from the last one working backwards and
|
||||
* followed by the static ones.
|
||||
*
|
||||
* @returns {string} the body of the restore function
|
||||
*/
|
||||
function restoreTmpl (resetters, paths, hasWildcards) {
|
||||
const dynamicReset = hasWildcards === true ? `
|
||||
const keys = Object.keys(secret)
|
||||
const len = keys.length
|
||||
for (var i = len - 1; i >= ${paths.length}; i--) {
|
||||
const k = keys[i]
|
||||
const o = secret[k]
|
||||
if (o) {
|
||||
if (o.flat === true) this.groupRestore(o)
|
||||
else this.nestedRestore(o)
|
||||
secret[k] = null
|
||||
}
|
||||
}
|
||||
` : ''
|
||||
|
||||
return `
|
||||
const secret = this.secret
|
||||
${dynamicReset}
|
||||
${resetters}
|
||||
return o
|
||||
`
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = /[^.[\]]+|\[((?:.)*?)\]/g
|
||||
|
||||
/*
|
||||
Regular expression explanation:
|
||||
|
||||
Alt 1: /[^.[\]]+/ - Match one or more characters that are *not* a dot (.)
|
||||
opening square bracket ([) or closing square bracket (])
|
||||
|
||||
Alt 2: /\[((?:.)*?)\]/ - If the char IS dot or square bracket, then create a capture
|
||||
group (which will be capture group $1) that matches anything
|
||||
within square brackets. Expansion is lazy so it will
|
||||
stop matching as soon as the first closing bracket is met `]`
|
||||
(rather than continuing to match until the final closing bracket).
|
||||
*/
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = state
|
||||
|
||||
function state (o) {
|
||||
const {
|
||||
secret,
|
||||
censor,
|
||||
compileRestore,
|
||||
serialize,
|
||||
groupRedact,
|
||||
nestedRedact,
|
||||
wildcards,
|
||||
wcLen
|
||||
} = o
|
||||
const builder = [{ secret, censor, compileRestore }]
|
||||
if (serialize !== false) builder.push({ serialize })
|
||||
if (wcLen > 0) builder.push({ groupRedact, nestedRedact, wildcards, wcLen })
|
||||
return Object.assign(...builder)
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = validator
|
||||
|
||||
function validator (opts = {}) {
|
||||
const {
|
||||
ERR_PATHS_MUST_BE_STRINGS = () => 'fast-redact - Paths must be (non-empty) strings',
|
||||
ERR_INVALID_PATH = (s) => `fast-redact – Invalid path (${s})`
|
||||
} = opts
|
||||
|
||||
return function validate ({ paths }) {
|
||||
paths.forEach((s) => {
|
||||
if (typeof s !== 'string') {
|
||||
throw Error(ERR_PATHS_MUST_BE_STRINGS())
|
||||
}
|
||||
try {
|
||||
if (/〇/.test(s)) throw Error()
|
||||
const expr = (s[0] === '[' ? '' : '.') + s.replace(/^\*/, '〇').replace(/\.\*/g, '.〇').replace(/\[\*\]/g, '[〇]')
|
||||
if (/\n|\r|;/.test(expr)) throw Error()
|
||||
if (/\/\*/.test(expr)) throw Error()
|
||||
/* eslint-disable-next-line */
|
||||
Function(`
|
||||
'use strict'
|
||||
const o = new Proxy({}, { get: () => o, set: () => { throw Error() } });
|
||||
const 〇 = null;
|
||||
o${expr}
|
||||
if ([o${expr}].length !== 1) throw Error()`)()
|
||||
} catch (e) {
|
||||
throw Error(ERR_INVALID_PATH(s))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "fast-redact",
|
||||
"version": "3.5.0",
|
||||
"description": "very fast object redaction",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "tap test",
|
||||
"posttest": "standard index.js 'lib/*.js' 'example/*.js' benchmark/index.js test/index.js | snazzy",
|
||||
"cov": "tap --cov test",
|
||||
"cov-ui": "tap --coverage-report=html test",
|
||||
"ci": "tap --cov --100 test",
|
||||
"bench": "node benchmark"
|
||||
},
|
||||
"keywords": [
|
||||
"redact",
|
||||
"censor",
|
||||
"performance",
|
||||
"performant",
|
||||
"gdpr",
|
||||
"fast",
|
||||
"speed",
|
||||
"serialize",
|
||||
"stringify"
|
||||
],
|
||||
"author": "David Mark Clements <david.clements@nearform.com>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"fastbench": "^1.0.1",
|
||||
"pino-noir": "^2.2.1",
|
||||
"snazzy": "^8.0.0",
|
||||
"standard": "^12.0.1",
|
||||
"tap": "^12.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"directories": {
|
||||
"example": "example",
|
||||
"lib": "lib",
|
||||
"test": "test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/davidmarkclements/fast-redact.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/davidmarkclements/fast-redact/issues"
|
||||
},
|
||||
"homepage": "https://github.com/davidmarkclements/fast-redact#readme"
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
# fast-redact
|
||||
|
||||
very fast object redaction
|
||||
|
||||
[](https://travis-ci.org/davidmarkclements/fast-redact)
|
||||
|
||||
## Default Usage
|
||||
|
||||
By default, `fast-redact` serializes an object with `JSON.stringify`, censoring any
|
||||
data at paths specified:
|
||||
|
||||
```js
|
||||
const fastRedact = require('fast-redact')
|
||||
const fauxRequest = {
|
||||
headers: {
|
||||
host: 'http://example.com',
|
||||
cookie: `oh oh we don't want this exposed in logs in etc.`,
|
||||
referer: `if we're cool maybe we'll even redact this`,
|
||||
// Note: headers often contain hyphens and require bracket notation
|
||||
'X-Forwarded-For': `192.168.0.1`
|
||||
}
|
||||
}
|
||||
const redact = fastRedact({
|
||||
paths: ['headers.cookie', 'headers.referer', 'headers["X-Forwarded-For"]']
|
||||
})
|
||||
|
||||
console.log(redact(fauxRequest))
|
||||
// {"headers":{"host":"http://example.com","cookie":"[REDACTED]","referer":"[REDACTED]","X-Forwarded-For": "[REDACTED]"}}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `require('fast-redact')({paths, censor, serialize}) => Function`
|
||||
|
||||
When called without any options, or with a zero length `paths` array,
|
||||
`fast-redact` will return `JSON.stringify` or the `serialize` option, if set.
|
||||
|
||||
#### `paths` – `Array`
|
||||
|
||||
An array of strings describing the nested location of a key in an object.
|
||||
|
||||
The syntax follows that of the EcmaScript specification, that is any JavaScript
|
||||
path is accepted – both bracket and dot notation is supported. For instance in
|
||||
each of the following cases, the `c` property will be redacted: `a.b.c`,`a['b'].c`,
|
||||
`a["b"].c`, `a[``b``].c`. Since bracket notation is supported, array indices are also
|
||||
supported `a[0].b` would redact the `b` key in the first object of the `a` array.
|
||||
|
||||
Leading brackets are also allowed, for instance `["a"].b.c` will work.
|
||||
|
||||
##### Wildcards
|
||||
|
||||
In addition to static paths, asterisk wildcards are also supported.
|
||||
|
||||
When an asterisk is place in the final position it will redact all keys within the
|
||||
parent object. For instance `a.b.*` will redact all keys in the `b` object. Similarly
|
||||
for arrays `a.b[*]` will redact all elements of an array (in truth it actually doesn't matter
|
||||
whether `b` is in an object or array in either case, both notation styles will work).
|
||||
|
||||
When an asterisk is in an intermediate or first position, the paths following the asterisk will
|
||||
be redacted for every object within the parent.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
const fastRedact = require('fast-redact')
|
||||
const redact = fastRedact({paths: ['*.c.d']})
|
||||
const obj = {
|
||||
x: {c: {d: 'hide me', e: 'leave me be'}},
|
||||
y: {c: {d: 'and me', f: 'I want to live'}},
|
||||
z: {c: {d: 'and also I', g: 'I want to run in a stream'}}
|
||||
}
|
||||
console.log(redact(obj))
|
||||
// {"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"}}}
|
||||
```
|
||||
|
||||
Another example with a nested array:
|
||||
|
||||
```js
|
||||
const fastRedact = require('..')
|
||||
const redact = fastRedact({paths: ['a[*].c.d']})
|
||||
const obj = {
|
||||
a: [
|
||||
{c: {d: 'hide me', e: 'leave me be'}},
|
||||
{c: {d: 'and me', f: 'I want to live'}},
|
||||
{c: {d: 'and also I', g: 'I want to run in a stream'}}
|
||||
]
|
||||
}
|
||||
console.log(redact(obj))
|
||||
// {"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"}}]}
|
||||
```
|
||||
|
||||
#### `remove` - `Boolean` - `[false]`
|
||||
|
||||
The `remove` option, when set to `true` will cause keys to be removed from the
|
||||
serialized output.
|
||||
|
||||
Since the implementation exploits the fact that `undefined` keys are ignored
|
||||
by `JSON.stringify` the `remove` option may *only* be used when `JSON.stringify`
|
||||
is the serializer (this is the default) – otherwise `fast-redact` will throw.
|
||||
|
||||
If supplying a custom serializer that has the same behavior (removing keys
|
||||
with `undefined` values), this restriction can be bypassed by explicitly setting
|
||||
the `censor` to `undefined`.
|
||||
|
||||
|
||||
#### `censor` – `<Any type>` – `('[REDACTED]')`
|
||||
|
||||
This is the value which overwrites redacted properties.
|
||||
|
||||
Setting `censor` to `undefined` will cause properties to removed as long as this is
|
||||
the behavior of the `serializer` – which defaults to `JSON.stringify`, which does
|
||||
remove `undefined` properties.
|
||||
|
||||
Setting `censor` to a function will cause `fast-redact` to invoke it with the original
|
||||
value. The output of the `censor` function sets the redacted value.
|
||||
Please note that asynchronous functions are not supported.
|
||||
|
||||
#### `serialize` – `Function | Boolean` – `(JSON.stringify)`
|
||||
|
||||
The `serialize` option may either be a function or a boolean. If a function is supplied, this
|
||||
will be used to `serialize` the redacted object. It's important to understand that for
|
||||
performance reasons `fast-redact` *mutates* the original object, then serializes, then
|
||||
restores the original values. So the object passed to the serializer is the exact same
|
||||
object passed to the redacting function.
|
||||
|
||||
The `serialize` option as a function example:
|
||||
|
||||
```js
|
||||
const fastRedact = require('fast-redact')
|
||||
const redact = fastRedact({
|
||||
paths: ['a'],
|
||||
serialize: (o) => JSON.stringify(o, 0, 2)
|
||||
})
|
||||
console.log(redact({a: 1, b: 2}))
|
||||
// {
|
||||
// "a": "[REDACTED]",
|
||||
// "b": 2
|
||||
// }
|
||||
```
|
||||
|
||||
For advanced usage the `serialize` option can be set to `false`. When `serialize` is set to `false`,
|
||||
instead of the serialized object, the output of the redactor function will be the mutated object
|
||||
itself (this is the exact same as the object passed in). In addition a `restore` method is supplied
|
||||
on the redactor function allowing the redacted keys to be restored with the original data.
|
||||
|
||||
```js
|
||||
const fastRedact = require('fast-redact')
|
||||
const redact = fastRedact({
|
||||
paths: ['a'],
|
||||
serialize: false
|
||||
})
|
||||
const o = {a: 1, b: 2}
|
||||
console.log(redact(o) === o) // true
|
||||
console.log(o) // { a: '[REDACTED]', b: 2 }
|
||||
console.log(redact.restore(o) === o) // true
|
||||
console.log(o) // { a: 1, b: 2 }
|
||||
```
|
||||
|
||||
#### `strict` – `Boolean` - `[true]`
|
||||
The `strict` option, when set to `true`, will cause the redactor function to throw if instead
|
||||
of an object it finds a primitive. When `strict` is set to `false`, the redactor function
|
||||
will treat the primitive value as having already been redacted, and return it serialized (with
|
||||
`JSON.stringify` or the user's custom `serialize` function), or as-is if the `serialize` option
|
||||
was set to false.
|
||||
|
||||
## Approach
|
||||
|
||||
In order to achieve lowest cost/highest performance redaction `fast-redact`
|
||||
creates and compiles a function (using the `Function` constructor) on initialization.
|
||||
It's important to distinguish this from the dangers of a runtime eval, no user input
|
||||
is involved in creating the string that compiles into the function. This is as safe
|
||||
as writing code normally and having it compiled by V8 in the usual way.
|
||||
|
||||
Thanks to changes in V8 in recent years, state can be injected into compiled functions
|
||||
using `bind` at very low cost (whereas `bind` used to be expensive, and getting state
|
||||
into a compiled function by any means was difficult without a performance penalty).
|
||||
|
||||
For static paths, this function simply checks that the path exists and then overwrites
|
||||
with the censor. Wildcard paths are processed with normal functions that iterate over
|
||||
the object redacting values as necessary.
|
||||
|
||||
It's important to note, that the original object is mutated – for performance reasons
|
||||
a copy is not made. See [rfdc](https://github.com/davidmarkclements/rfdc) (Really Fast
|
||||
Deep Clone) for the fastest known way to clone – it's not nearly close enough in speed
|
||||
to editing the original object, serializing and then restoring values.
|
||||
|
||||
A `restore` function is also created and compiled to put the original state back on
|
||||
to the object after redaction. This means that in the default usage case, the operation
|
||||
is essentially atomic - the object is mutated, serialized and restored internally which
|
||||
avoids any state management issues.
|
||||
|
||||
## Caveat
|
||||
|
||||
As mentioned in approach, the `paths` array input is dynamically compiled into a function
|
||||
at initialization time. While the `paths` array is vigourously tested for any developer
|
||||
errors, it's strongly recommended against allowing user input to directly supply any
|
||||
paths to redact. It can't be guaranteed that allowing user input for `paths` couldn't
|
||||
feasibly expose an attack vector.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
The fastest known predecessor to `fast-redact` is the non-generic [`pino-noir`](http://npm.im/pino-noir)
|
||||
library (which was also written by myself).
|
||||
|
||||
In the direct calling case, `fast-redact` is ~30x faster than `pino-noir`, however a more realistic
|
||||
comparison is overhead on `JSON.stringify`.
|
||||
|
||||
For a static redaction case (no wildcards) `pino-noir` adds ~25% overhead on top of `JSON.stringify`
|
||||
whereas `fast-redact` adds ~1% overhead.
|
||||
|
||||
In the basic last-position wildcard case,`fast-redact` is ~12% faster than `pino-noir`.
|
||||
|
||||
The `pino-noir` module does not support intermediate wildcards, but `fast-redact` does,
|
||||
the cost of an intermediate wildcard that results in two keys over two nested objects
|
||||
being redacted is about 25% overhead on `JSON.stringify`. The cost of an intermediate
|
||||
wildcard that results in four keys across two objects being redacted is about 55% overhead
|
||||
on `JSON.stringify` and ~50% more expensive that explicitly declaring the keys.
|
||||
|
||||
```sh
|
||||
npm run bench
|
||||
```
|
||||
|
||||
```
|
||||
benchNoirV2*500: 59.108ms
|
||||
benchFastRedact*500: 2.483ms
|
||||
benchFastRedactRestore*500: 10.904ms
|
||||
benchNoirV2Wild*500: 91.399ms
|
||||
benchFastRedactWild*500: 21.200ms
|
||||
benchFastRedactWildRestore*500: 27.304ms
|
||||
benchFastRedactIntermediateWild*500: 92.304ms
|
||||
benchFastRedactIntermediateWildRestore*500: 107.047ms
|
||||
benchJSONStringify*500: 210.573ms
|
||||
benchNoirV2Serialize*500: 281.148ms
|
||||
benchFastRedactSerialize*500: 215.845ms
|
||||
benchNoirV2WildSerialize*500: 281.168ms
|
||||
benchFastRedactWildSerialize*500: 247.140ms
|
||||
benchFastRedactIntermediateWildSerialize*500: 333.722ms
|
||||
benchFastRedactIntermediateWildMatchWildOutcomeSerialize*500: 463.667ms
|
||||
benchFastRedactStaticMatchWildOutcomeSerialize*500: 239.293ms
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
```
|
||||
224 passing (499.544ms)
|
||||
```
|
||||
|
||||
### Coverage
|
||||
|
||||
```
|
||||
npm run cov
|
||||
```
|
||||
|
||||
```
|
||||
-----------------|----------|----------|----------|----------|-------------------|
|
||||
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
|
||||
-----------------|----------|----------|----------|----------|-------------------|
|
||||
All files | 100 | 100 | 100 | 100 | |
|
||||
fast-redact | 100 | 100 | 100 | 100 | |
|
||||
index.js | 100 | 100 | 100 | 100 | |
|
||||
fast-redact/lib | 100 | 100 | 100 | 100 | |
|
||||
modifiers.js | 100 | 100 | 100 | 100 | |
|
||||
parse.js | 100 | 100 | 100 | 100 | |
|
||||
redactor.js | 100 | 100 | 100 | 100 | |
|
||||
restorer.js | 100 | 100 | 100 | 100 | |
|
||||
rx.js | 100 | 100 | 100 | 100 | |
|
||||
state.js | 100 | 100 | 100 | 100 | |
|
||||
validator.js | 100 | 100 | 100 | 100 | |
|
||||
-----------------|----------|----------|----------|----------|-------------------|
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Sponsored by [nearForm](http://www.nearform.com)
|
||||
+1502
@@ -0,0 +1,1502 @@
|
||||
'use strict'
|
||||
|
||||
const { test } = require('tap')
|
||||
const fastRedact = require('..')
|
||||
|
||||
const censor = '[REDACTED]'
|
||||
const censorFct = value => !value ? value : 'xxx' + value.substr(-2)
|
||||
const censorWithPath = (v, p) => p.join('.') + ' ' + censorFct(v)
|
||||
|
||||
test('returns no-op when passed no paths [serialize: false]', ({ end, doesNotThrow }) => {
|
||||
const redact = fastRedact({ paths: [], serialize: false })
|
||||
doesNotThrow(() => redact({}))
|
||||
doesNotThrow(() => {
|
||||
const o = redact({})
|
||||
redact.restore(o)
|
||||
})
|
||||
end()
|
||||
})
|
||||
|
||||
test('returns serializer when passed no paths [serialize: default]', ({ end, is }) => {
|
||||
is(fastRedact({ paths: [] }), JSON.stringify)
|
||||
is(fastRedact(), JSON.stringify)
|
||||
end()
|
||||
})
|
||||
|
||||
test('throws when passed non-object using defaults', ({ end, throws }) => {
|
||||
const redact = fastRedact({ paths: ['a.b.c'] })
|
||||
throws(() => redact(1))
|
||||
end()
|
||||
})
|
||||
|
||||
test('throws when passed non-object number using [strict: true]', ({ end, throws }) => {
|
||||
const redact = fastRedact({ paths: ['a.b.c'], strict: true })
|
||||
throws(() => redact(1))
|
||||
end()
|
||||
})
|
||||
|
||||
test('returns JSON.stringified value when passed non-object using [strict: false] and no serialize option', ({ end, is, doesNotThrow }) => {
|
||||
const redactDefaultSerialize = fastRedact({ paths: ['a.b.c'], strict: false })
|
||||
|
||||
// expectedOutputs holds `JSON.stringify`ied versions of each primitive.
|
||||
// We write them out explicitly though to make the test a bit clearer.
|
||||
const primitives = [null, undefined, 'A', 1, false]
|
||||
const expectedOutputs = ['null', undefined, '"A"', '1', 'false']
|
||||
|
||||
primitives.forEach((it, i) => {
|
||||
doesNotThrow(() => redactDefaultSerialize(it))
|
||||
const res = redactDefaultSerialize(it)
|
||||
is(res, expectedOutputs[i])
|
||||
})
|
||||
|
||||
end()
|
||||
})
|
||||
|
||||
test('returns custom serialized value when passed non-object using [strict: false, serialize: fn]', ({ end, is, doesNotThrow }) => {
|
||||
const customSerialize = (v) => `Hello ${v}!`
|
||||
const redactCustomSerialize = fastRedact({
|
||||
paths: ['a.b.c'],
|
||||
strict: false,
|
||||
serialize: customSerialize
|
||||
})
|
||||
|
||||
const primitives = [null, undefined, 'A', 1, false]
|
||||
|
||||
primitives.forEach((it) => {
|
||||
doesNotThrow(() => redactCustomSerialize(it))
|
||||
const res = redactCustomSerialize(it)
|
||||
is(res, customSerialize(it))
|
||||
})
|
||||
|
||||
end()
|
||||
})
|
||||
|
||||
test('returns original value when passed non-object using [strict: false, serialize: false]', ({ end, is, doesNotThrow }) => {
|
||||
const redactSerializeFalse = fastRedact({
|
||||
paths: ['a.b.c'],
|
||||
strict: false,
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const primitives = [null, undefined, 'A', 1, false]
|
||||
|
||||
primitives.forEach((it) => {
|
||||
doesNotThrow(() => redactSerializeFalse(it))
|
||||
const res = redactSerializeFalse(it)
|
||||
is(res, it)
|
||||
})
|
||||
|
||||
end()
|
||||
})
|
||||
|
||||
test('returns original value when passed non-object at wildcard key', ({ end, doesNotThrow, strictSame }) => {
|
||||
const redactSerializeFalse = fastRedact({
|
||||
paths: ['a.*'],
|
||||
strict: false,
|
||||
serialize: false
|
||||
})
|
||||
|
||||
const primitives = [null, undefined, 'A', 1, false]
|
||||
|
||||
primitives.forEach((a) => {
|
||||
doesNotThrow(() => redactSerializeFalse({ a }))
|
||||
const res = redactSerializeFalse({ a })
|
||||
strictSame(res, { a })
|
||||
})
|
||||
|
||||
end()
|
||||
})
|
||||
|
||||
test('returns censored values when passed array at wildcard key', ({ end, strictSame }) => {
|
||||
const redactSerializeFalse = fastRedact({
|
||||
paths: ['a.*'],
|
||||
strict: false,
|
||||
serialize: false
|
||||
})
|
||||
const res = redactSerializeFalse({ a: ['redact', 'me'] })
|
||||
strictSame(res.a, [censor, censor])
|
||||
end()
|
||||
})
|
||||
|
||||
test('throws if a path is not a string', ({ end, throws }) => {
|
||||
const invalidTypeMsg = 'fast-redact - Paths must be (non-empty) strings'
|
||||
throws((e) => {
|
||||
fastRedact({ paths: [1] })
|
||||
}, Error(invalidTypeMsg))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: [null] })
|
||||
}, Error(invalidTypeMsg))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: [undefined] })
|
||||
}, Error(invalidTypeMsg))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: [{}] })
|
||||
}, Error(invalidTypeMsg))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: [[null]] })
|
||||
}, Error(invalidTypeMsg))
|
||||
end()
|
||||
})
|
||||
|
||||
test('throws when passed illegal paths', ({ end, throws }) => {
|
||||
const err = (s) => Error(`fast-redact – Invalid path (${s})`)
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['@'] })
|
||||
}, err('@'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['0'] })
|
||||
}, err('0'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['〇'] })
|
||||
}, err('〇'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a.1.c'] })
|
||||
}, err('a.1.c'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a..c'] })
|
||||
}, err('a..c'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['1..c'] })
|
||||
}, err('1..c'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a = b'] })
|
||||
}, err('a = b'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a(b)'] })
|
||||
}, err('a(b)'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['//a.b.c'] })
|
||||
}, err('//a.b.c'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['\\a.b.c'] })
|
||||
}, err('\\a.b.c'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a.#.c'] })
|
||||
}, err('a.#.c'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['~~a.b.c'] })
|
||||
}, err('~~a.b.c'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['^a.b.c'] })
|
||||
}, err('^a.b.c'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a + b'] })
|
||||
}, err('a + b'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['return a + b'] })
|
||||
}, err('return a + b'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a / b'] })
|
||||
}, err('a / b'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a * b'] })
|
||||
}, err('a * b'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a - b'] })
|
||||
}, err('a - b'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a ** b'] })
|
||||
}, err('a ** b'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a % b'] })
|
||||
}, err('a % b'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a.b*.c'] })
|
||||
}, err('a.b*.c'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a;global.foo = "bar"'] })
|
||||
}, err('a;global.foo = "bar"'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a;while(1){}'] })
|
||||
}, err('a;while(1){}'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a//'] })
|
||||
}, err('a//'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a/*foo*/'] })
|
||||
}, err('a/*foo*/'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a,o.b'] })
|
||||
}, err('a,o.b'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a = o.b'] })
|
||||
}, err('a = o.b'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a\n'] })
|
||||
}, err('a\n'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['a\r'] })
|
||||
}, err('a\r'))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: [''] })
|
||||
}, err(''))
|
||||
throws((e) => {
|
||||
fastRedact({ paths: ['[""""]'] })
|
||||
}, err('[""""]'))
|
||||
end()
|
||||
})
|
||||
|
||||
test('throws if a custom serializer is used and remove is true', ({ end, throws }) => {
|
||||
throws(() => {
|
||||
fastRedact({ paths: ['a'], serialize: (o) => o, remove: true })
|
||||
}, Error('fast-redact – remove option may only be set when serializer is JSON.stringify'))
|
||||
end()
|
||||
})
|
||||
|
||||
test('throws if serialize is false and remove is true', ({ end, throws }) => {
|
||||
throws(() => {
|
||||
fastRedact({ paths: ['a'], serialize: false, remove: true })
|
||||
}, Error('fast-redact – remove option may only be set when serializer is JSON.stringify'))
|
||||
end()
|
||||
})
|
||||
|
||||
test('supports path segments that aren\'t identifiers if bracketed', ({ end, strictSame }) => {
|
||||
const redactSerializeFalse = fastRedact({
|
||||
paths: ['a[""]', 'a["x-y"]', 'a[\'"y"\']', "a['\\'x\\'']"],
|
||||
serialize: false,
|
||||
censor: 'X'
|
||||
})
|
||||
|
||||
const res = redactSerializeFalse({ a: { '': 'Hi!', 'x-y': 'Hi!', '"y"': 'Hi!', "'x'": 'Hi!' } })
|
||||
strictSame(res, { a: { '': 'X', 'x-y': 'X', '"y"': 'X', "'x'": 'X' } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('supports consecutive bracketed path segments', ({ end, strictSame }) => {
|
||||
const redactSerializeFalse = fastRedact({
|
||||
paths: ['a[""]["y"]'],
|
||||
serialize: false,
|
||||
censor: 'X'
|
||||
})
|
||||
|
||||
const res = redactSerializeFalse({ a: { '': { 'y': 'Hi!' } } })
|
||||
strictSame(res, { a: { '': { 'y': 'X' } } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('supports leading bracketed widcard', ({ end, strictSame }) => {
|
||||
const redactSerializeFalse = fastRedact({
|
||||
paths: ['[*]["y"]'],
|
||||
serialize: false,
|
||||
censor: 'X'
|
||||
})
|
||||
|
||||
const res = redactSerializeFalse({ 'x': { 'y': 'Hi!' } })
|
||||
strictSame(res, { 'x': { 'y': 'X' } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('masks according to supplied censor', ({ end, is }) => {
|
||||
const censor = 'test'
|
||||
const redact = fastRedact({ paths: ['a'], censor, serialize: false })
|
||||
is(redact({ a: 'a' }).a, censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('redact.restore function is available when serialize is false', ({ end, is }) => {
|
||||
const censor = 'test'
|
||||
const redact = fastRedact({ paths: ['a'], censor, serialize: false })
|
||||
is(typeof redact.restore, 'function')
|
||||
end()
|
||||
})
|
||||
|
||||
test('redact.restore function places original values back in place', ({ end, is }) => {
|
||||
const censor = 'test'
|
||||
const redact = fastRedact({ paths: ['a'], censor, serialize: false })
|
||||
const o = { a: 'a' }
|
||||
redact(o)
|
||||
is(o.a, censor)
|
||||
redact.restore(o)
|
||||
is(o.a, 'a')
|
||||
end()
|
||||
})
|
||||
|
||||
test('redact.restore function places original values back in place when called twice and the first call is precensored', ({ end, is }) => {
|
||||
const censor = 'test'
|
||||
const redact = fastRedact({ paths: ['a'], censor, serialize: false })
|
||||
const o1 = { a: censor }
|
||||
const o2 = { a: 'a' }
|
||||
redact(o1)
|
||||
is(o1.a, censor)
|
||||
redact.restore(o1)
|
||||
is(o1.a, censor)
|
||||
redact(o2)
|
||||
is(o2.a, censor)
|
||||
redact.restore(o2)
|
||||
is(o2.a, 'a')
|
||||
end()
|
||||
})
|
||||
|
||||
test('masks according to supplied censor function', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a'], censor: censorFct, serialize: false })
|
||||
is(redact({ a: '0123456' }).a, 'xxx56')
|
||||
end()
|
||||
})
|
||||
|
||||
test('masks according to supplied censor function with wildcards', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: '*', censor: censorFct, serialize: false })
|
||||
is(redact({ a: '0123456' }).a, 'xxx56')
|
||||
end()
|
||||
})
|
||||
|
||||
test('masks according to supplied censor function with nested wildcards', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['*.b'], censor: censorFct, serialize: false })
|
||||
is(redact({ a: { b: '0123456' } }).a.b, 'xxx56')
|
||||
is(redact({ c: { b: '0123456', d: 'pristine' } }).c.b, 'xxx56')
|
||||
is(redact({ c: { b: '0123456', d: 'pristine' } }).c.d, 'pristine')
|
||||
end()
|
||||
})
|
||||
|
||||
test('does not increment secret size', ({ end, is, same }) => {
|
||||
const redact = fastRedact({ paths: ['x', '*.b'], censor: censorFct, serialize: false })
|
||||
is(redact({ a: { b: '0123456' } }).a.b, 'xxx56')
|
||||
same(Object.keys(redact.state.secret), ['x'])
|
||||
const intialSecretSize = JSON.stringify(redact.state.secret).length
|
||||
is(redact({ c: { b: '0123456', d: 'pristine' } }).c.b, 'xxx56')
|
||||
same(Object.keys(redact.state.secret), ['x'])
|
||||
is(JSON.stringify(redact.state.secret).length, intialSecretSize)
|
||||
is(redact({ c: { b: '0123456', d: 'pristine' } }).c.d, 'pristine')
|
||||
same(Object.keys(redact.state.secret), ['x'])
|
||||
is(JSON.stringify(redact.state.secret).length, intialSecretSize)
|
||||
end()
|
||||
})
|
||||
|
||||
test('masks according to supplied censor-with-path function', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a'], censor: censorWithPath, serialize: false })
|
||||
is(redact({ a: '0123456' }).a, 'a xxx56')
|
||||
end()
|
||||
})
|
||||
|
||||
test('masks according to supplied censor-with-path function with wildcards', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: '*', censor: censorWithPath, serialize: false })
|
||||
is(redact({ a: '0123456' }).a, 'a xxx56')
|
||||
end()
|
||||
})
|
||||
|
||||
test('masks according to supplied censor-with-path function with nested wildcards', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['*.b'], censor: censorWithPath, serialize: false })
|
||||
is(redact({ a: { b: '0123456' } }).a.b, 'a.b xxx56')
|
||||
is(redact({ c: { b: '0123456', d: 'pristine' } }).c.b, 'c.b xxx56')
|
||||
is(redact({ c: { b: '0123456', d: 'pristine' } }).c.d, 'pristine')
|
||||
end()
|
||||
})
|
||||
|
||||
test('redact.restore function places original values back in place with censor function', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a'], censor: censorFct, serialize: false })
|
||||
const o = { a: 'qwerty' }
|
||||
redact(o)
|
||||
is(o.a, 'xxxty')
|
||||
redact.restore(o)
|
||||
is(o.a, 'qwerty')
|
||||
end()
|
||||
})
|
||||
|
||||
test('serializes with JSON.stringify by default', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a'] })
|
||||
const o = { a: 'a' }
|
||||
is(redact(o), `{"a":"${censor}"}`)
|
||||
is(o.a, 'a')
|
||||
end()
|
||||
})
|
||||
|
||||
test('removes during serialization instead of redacting when remove option is true', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a'], remove: true })
|
||||
const o = { a: 'a', b: 'b' }
|
||||
is(redact(o), `{"b":"b"}`)
|
||||
is(o.a, 'a')
|
||||
end()
|
||||
})
|
||||
|
||||
test('serializes with JSON.stringify if serialize is true', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a'], serialize: true })
|
||||
const o = { a: 'a' }
|
||||
is(redact(o), `{"a":"${censor}"}`)
|
||||
is(o.a, 'a')
|
||||
end()
|
||||
})
|
||||
|
||||
test('serializes with JSON.stringify if serialize is not a function', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a'], serialize: {} })
|
||||
const o = { a: 'a' }
|
||||
is(redact(o), `{"a":"${censor}"}`)
|
||||
is(o.a, 'a')
|
||||
end()
|
||||
})
|
||||
|
||||
test('serializes with custom serializer if supplied', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a'], serialize: (o) => JSON.stringify(o, 0, 2) })
|
||||
const o = { a: 'a' }
|
||||
is(redact(o), `{\n "a": "${censor}"\n}`)
|
||||
is(o.a, 'a')
|
||||
end()
|
||||
})
|
||||
|
||||
test('redacts parent keys', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.b.c'], serialize: false })
|
||||
const result = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result.a.b.c, censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('supports paths with array indexes', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['insideArray.like[3].this'], serialize: false })
|
||||
same(redact({ insideArray: { like: ['a', 'b', 'c', { this: { foo: 'meow' } }] } }), { insideArray: { like: ['a', 'b', 'c', { this: censor }] } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('censor may be any type, including function', ({ end, same }) => {
|
||||
const redactToString = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: 'censor', serialize: false })
|
||||
const redactToUndefined = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: undefined, serialize: false })
|
||||
const sym = Symbol('sym')
|
||||
const redactToSymbol = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: sym, serialize: false })
|
||||
const redactToNumber = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: 0, serialize: false })
|
||||
const redactToBoolean = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: false, serialize: false })
|
||||
const redactToNull = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: null, serialize: false })
|
||||
const redactToObject = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: { redacted: true }, serialize: false })
|
||||
const redactToArray = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: ['redacted'], serialize: false })
|
||||
const redactToBuffer = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: Buffer.from('redacted'), serialize: false })
|
||||
const redactToError = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: Error('redacted'), serialize: false })
|
||||
const redactToFunction = fastRedact({ paths: ['a.b.c', 'a.b.d.*'], censor: () => 'redacted', serialize: false })
|
||||
same(redactToString({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: 'censor', d: { x: 'censor', y: 'censor' } } } })
|
||||
same(redactToUndefined({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: undefined, d: { x: undefined, y: undefined } } } })
|
||||
same(redactToSymbol({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: sym, d: { x: sym, y: sym } } } })
|
||||
same(redactToNumber({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: 0, d: { x: 0, y: 0 } } } })
|
||||
same(redactToBoolean({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: false, d: { x: false, y: false } } } })
|
||||
same(redactToNull({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: null, d: { x: null, y: null } } } })
|
||||
same(redactToObject({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: { redacted: true }, d: { x: { redacted: true }, y: { redacted: true } } } } })
|
||||
same(redactToArray({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: ['redacted'], d: { x: ['redacted'], y: ['redacted'] } } } })
|
||||
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') } } } })
|
||||
same(redactToError({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: Error('redacted'), d: { x: Error('redacted'), y: Error('redacted') } } } })
|
||||
same(redactToFunction({ a: { b: { c: 's', d: { x: 's', y: 's' } } } }), { a: { b: { c: 'redacted', d: { x: 'redacted', y: 'redacted' } } } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('supports multiple paths from the same root', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['deep.bar.shoe', 'deep.baz.shoe', 'deep.foo', 'deep.not.there.sooo', 'deep.fum.shoe'], serialize: false })
|
||||
same(redact({ deep: { bar: 'hmm', baz: { shoe: { k: 1 } }, foo: {}, fum: { shoe: 'moo' } } }), { deep: { bar: 'hmm', baz: { shoe: censor }, foo: censor, fum: { shoe: censor } } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('supports strings in bracket notation paths (single quote)', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: [`a['@#!='].c`], serialize: false })
|
||||
const result = redact({ a: { '@#!=': { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result.a['@#!='].c, censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('supports strings in bracket notation paths (double quote)', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: [`a["@#!="].c`], serialize: false })
|
||||
const result = redact({ a: { '@#!=': { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result.a['@#!='].c, censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('supports strings in bracket notation paths (backtick quote)', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a[`@#!=`].c'], serialize: false })
|
||||
const result = redact({ a: { '@#!=': { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result.a['@#!='].c, censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('allows * within a bracket notation string', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a["*"].c'], serialize: false })
|
||||
const result = redact({ a: { '*': { c: 's', x: 1 } } })
|
||||
is(result.a['*'].c, censor)
|
||||
is(result.a['*'].x, 1)
|
||||
end()
|
||||
})
|
||||
|
||||
test('redacts parent keys – restore', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.b.c'], serialize: false })
|
||||
const result = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result.a.b.c, censor)
|
||||
redact.restore(result)
|
||||
is(result.a.b.c, 's')
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles null proto objects', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.b.c'], serialize: false })
|
||||
const result = redact({ __proto__: null, a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result.a.b.c, censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles null proto objects – restore', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.b.c'], serialize: false })
|
||||
const result = redact({ __proto__: null, a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result.a.b.c, censor)
|
||||
redact.restore(result, 's')
|
||||
is(result.a.b.c, 's')
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles paths that do not match object structure', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['x.y.z'], serialize: false })
|
||||
same(redact({ a: { b: { c: 's' } } }), { a: { b: { c: 's' } } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('ignores missing paths in object', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['a.b.c', 'a.z.d', 'a.b.z'], serialize: false })
|
||||
same(redact({ a: { b: { c: 's' } } }), { a: { b: { c: censor } } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('ignores missing paths in object – restore', ({ end, doesNotThrow }) => {
|
||||
const redact = fastRedact({ paths: ['a.b.c', 'a.z.d', 'a.b.z'], serialize: false })
|
||||
const o = { a: { b: { c: 's' } } }
|
||||
redact(o)
|
||||
doesNotThrow(() => {
|
||||
redact.restore(o)
|
||||
})
|
||||
|
||||
end()
|
||||
})
|
||||
|
||||
test('gracefully handles primitives that match intermediate keys in paths', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['a.b.c', 'a.b.c.d'], serialize: false })
|
||||
same(redact({ a: { b: null } }), { a: { b: null } })
|
||||
same(redact({ a: { b: 's' } }), { a: { b: 's' } })
|
||||
same(redact({ a: { b: 1 } }), { a: { b: 1 } })
|
||||
same(redact({ a: { b: undefined } }), { a: { b: undefined } })
|
||||
same(redact({ a: { b: true } }), { a: { b: true } })
|
||||
const sym = Symbol('sym')
|
||||
same(redact({ a: { b: sym } }), { a: { b: sym } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles circulars', ({ end, is, same }) => {
|
||||
const redact = fastRedact({ paths: ['bar.baz.baz'], serialize: false })
|
||||
const bar = { b: 2 }
|
||||
const o = { a: 1, bar }
|
||||
bar.baz = bar
|
||||
o.bar.baz = o.bar
|
||||
same(redact(o), { a: 1, bar: { b: 2, baz: censor } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles circulars – restore', ({ end, is, same }) => {
|
||||
const redact = fastRedact({ paths: ['bar.baz.baz'], serialize: false })
|
||||
const bar = { b: 2 }
|
||||
const o = { a: 1, bar }
|
||||
bar.baz = bar
|
||||
o.bar.baz = o.bar
|
||||
is(o.bar.baz, bar)
|
||||
redact(o)
|
||||
is(o.bar.baz, censor)
|
||||
redact.restore(o)
|
||||
is(o.bar.baz, bar)
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles circulars and cross references – restore', ({ end, is, same }) => {
|
||||
const redact = fastRedact({ paths: ['bar.baz.baz', 'cf.bar'], serialize: false })
|
||||
const bar = { b: 2 }
|
||||
const o = { a: 1, bar, cf: { bar } }
|
||||
bar.baz = bar
|
||||
o.bar.baz = o.bar
|
||||
is(o.bar.baz, bar)
|
||||
is(o.cf.bar, bar)
|
||||
redact(o)
|
||||
is(o.bar.baz, censor)
|
||||
is(o.cf.bar, censor)
|
||||
redact.restore(o)
|
||||
is(o.bar.baz, bar)
|
||||
is(o.cf.bar, bar)
|
||||
end()
|
||||
})
|
||||
|
||||
test('ultimate wildcards – shallow', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['test.*'], serialize: false })
|
||||
same(redact({ test: { baz: 1, bar: 'private' } }), { test: { baz: censor, bar: censor } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('ultimate wildcards – deep', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['deep.bar.baz.ding.*'], serialize: false })
|
||||
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 } } } } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('ultimate wildcards - array – shallow', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['array[*]'], serialize: false })
|
||||
same(redact({ array: ['a', 'b', 'c', 'd'] }), { array: [censor, censor, censor, censor] })
|
||||
end()
|
||||
})
|
||||
|
||||
test('ultimate wildcards – array – deep', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['deepArray.down.here[*]'], serialize: false })
|
||||
same(redact({ deepArray: { down: { here: ['a', 'b', 'c'] } } }), { deepArray: { down: { here: [censor, censor, censor] } } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('ultimate wildcards – array – single index', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['insideArray.like[3].this.*'], serialize: false })
|
||||
same(redact({ insideArray: { like: ['a', 'b', 'c', { this: { foo: 'meow' } }] } }), { insideArray: { like: ['a', 'b', 'c', { this: { foo: censor } }] } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('ultimate wildcards - handles null proto objects', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.b.c'], serialize: false })
|
||||
const result = redact({ __proto__: null, a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result.a.b.c, censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('ultimate wildcards - handles paths that do not match object structure', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['x.y.z'], serialize: false })
|
||||
same(redact({ a: { b: { c: 's' } } }), { a: { b: { c: 's' } } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('ultimate wildcards - gracefully handles primitives that match intermediate keys in paths', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['a.b.c', 'a.b.c.d'], serialize: false })
|
||||
same(redact({ a: { b: null } }), { a: { b: null } })
|
||||
same(redact({ a: { b: 's' } }), { a: { b: 's' } })
|
||||
same(redact({ a: { b: 1 } }), { a: { b: 1 } })
|
||||
same(redact({ a: { b: undefined } }), { a: { b: undefined } })
|
||||
same(redact({ a: { b: true } }), { a: { b: true } })
|
||||
const sym = Symbol('sym')
|
||||
same(redact({ a: { b: sym } }), { a: { b: sym } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('ultimate wildcards – handles circulars', ({ end, is, same }) => {
|
||||
const redact = fastRedact({ paths: ['bar.baz.*'], serialize: false })
|
||||
const bar = { b: 2 }
|
||||
const o = { a: 1, bar }
|
||||
bar.baz = bar
|
||||
o.bar.baz = o.bar
|
||||
same(redact(o), { a: 1, bar: { b: censor, baz: censor } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('ultimate wildcards – handles circulars – restore', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['bar.baz.*'], serialize: false })
|
||||
const bar = { b: 2 }
|
||||
const o = { a: 1, bar }
|
||||
bar.baz = bar
|
||||
o.bar.baz = o.bar
|
||||
is(o.bar.baz, bar)
|
||||
redact(o)
|
||||
is(o.bar.baz, censor)
|
||||
redact.restore(o)
|
||||
is(o.bar.baz, bar)
|
||||
end()
|
||||
})
|
||||
|
||||
test('ultimate multi wildcards – handles circulars – restore', ({ end, is, same }) => {
|
||||
const redact = fastRedact({ paths: ['bar.*.baz.*.b'], serialize: false })
|
||||
const bar = { b: 2 }
|
||||
const o = { a: 1, bar }
|
||||
bar.baz = bar
|
||||
o.bar.baz = o.bar
|
||||
is(o.bar.baz, bar)
|
||||
redact(o)
|
||||
is(o.bar.baz.b, censor)
|
||||
redact.restore(o)
|
||||
same(o.bar.baz, bar)
|
||||
end()
|
||||
})
|
||||
|
||||
test('ultimate wildcards – handles circulars and cross references – restore', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['bar.baz.*', 'cf.*'], serialize: false })
|
||||
const bar = { b: 2 }
|
||||
const o = { a: 1, bar, cf: { bar } }
|
||||
bar.baz = bar
|
||||
o.bar.baz = o.bar
|
||||
is(o.bar.baz, bar)
|
||||
is(o.cf.bar, bar)
|
||||
redact(o)
|
||||
is(o.bar.baz, censor)
|
||||
is(o.cf.bar, censor)
|
||||
redact.restore(o)
|
||||
is(o.bar.baz, bar)
|
||||
is(o.cf.bar, bar)
|
||||
end()
|
||||
})
|
||||
|
||||
test('static + wildcards', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.b.c', 'a.d.*', 'a.b.z.*'], serialize: false })
|
||||
const result = redact({ a: { b: { c: 's', z: { x: 's', y: 's' } }, d: { a: 's', b: 's', c: 's' } } })
|
||||
|
||||
is(result.a.b.c, censor)
|
||||
is(result.a.d.a, censor)
|
||||
is(result.a.d.b, censor)
|
||||
is(result.a.d.c, censor)
|
||||
is(result.a.b.z.x, censor)
|
||||
is(result.a.b.z.y, censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('static + wildcards reuse', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.b.c', 'a.d.*'], serialize: false })
|
||||
const result = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
|
||||
is(result.a.b.c, censor)
|
||||
is(result.a.d.a, censor)
|
||||
is(result.a.d.b, censor)
|
||||
is(result.a.d.c, censor)
|
||||
|
||||
redact.restore(result)
|
||||
|
||||
const result2 = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result2.a.b.c, censor)
|
||||
is(result2.a.d.a, censor)
|
||||
is(result2.a.d.b, censor)
|
||||
is(result2.a.d.c, censor)
|
||||
|
||||
redact.restore(result2)
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcard – first position', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['*.c'], serialize: false })
|
||||
const result = redact({ b: { c: 's' }, d: { a: 's', b: 's', c: 's' } })
|
||||
is(result.b.c, censor)
|
||||
is(result.d.a, 's')
|
||||
is(result.d.b, 's')
|
||||
is(result.d.c, censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcard – one following key', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.*.c'], serialize: false })
|
||||
const result = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result.a.b.c, censor)
|
||||
is(result.a.d.a, 's')
|
||||
is(result.a.d.b, 's')
|
||||
is(result.a.d.c, censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('restore parent wildcard – one following key', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.*.c'], serialize: false })
|
||||
const result = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
redact.restore(result)
|
||||
is(result.a.b.c, 's')
|
||||
is(result.a.d.a, 's')
|
||||
is(result.a.d.b, 's')
|
||||
is(result.a.d.c, 's')
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcard – one following key – reuse', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.*.c'], serialize: false })
|
||||
const result = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result.a.b.c, censor)
|
||||
is(result.a.d.a, 's')
|
||||
is(result.a.d.b, 's')
|
||||
is(result.a.d.c, censor)
|
||||
const result2 = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result2.a.b.c, censor)
|
||||
is(result2.a.d.a, 's')
|
||||
is(result2.a.d.b, 's')
|
||||
is(result2.a.d.c, censor)
|
||||
redact.restore(result2)
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcard – two following keys', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.*.x.c'], serialize: false })
|
||||
const result = redact({ a: { b: { x: { c: 's' } }, d: { x: { a: 's', b: 's', c: 's' } } } })
|
||||
is(result.a.b.x.c, censor)
|
||||
is(result.a.d.x.a, 's')
|
||||
is(result.a.d.x.b, 's')
|
||||
is(result.a.d.x.c, censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('multi object wildcard', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.*.x.*.c'], serialize: false })
|
||||
const result = redact({ a: { b: { x: { z: { c: 's' } } }, d: { x: { u: { a: 's', b: 's', c: 's' } } } } })
|
||||
is(result.a.b.x.z.c, censor)
|
||||
is(result.a.d.x.u.a, 's')
|
||||
is(result.a.d.x.u.b, 's')
|
||||
is(result.a.d.x.u.c, censor)
|
||||
redact.restore(result)
|
||||
is(result.a.b.x.z.c, 's')
|
||||
is(result.a.d.x.u.a, 's')
|
||||
is(result.a.d.x.u.b, 's')
|
||||
is(result.a.d.x.u.c, 's')
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcard – two following keys – reuse', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.*.x.c'], serialize: false })
|
||||
const result = redact({ a: { b: { x: { c: 's' } }, d: { x: { a: 's', b: 's', c: 's' } } } })
|
||||
is(result.a.b.x.c, censor)
|
||||
is(result.a.d.x.a, 's')
|
||||
is(result.a.d.x.b, 's')
|
||||
is(result.a.d.x.c, censor)
|
||||
redact.restore(result)
|
||||
const result2 = redact({ a: { b: { x: { c: 's' } }, d: { x: { a: 's', b: 's', c: 's' } } } })
|
||||
is(result2.a.b.x.c, censor)
|
||||
is(result2.a.d.x.a, 's')
|
||||
is(result2.a.d.x.b, 's')
|
||||
is(result2.a.d.x.c, censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('restore parent wildcard – two following keys', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.*.x.c'], serialize: false })
|
||||
const result = redact({ a: { b: { x: { c: 's' } }, d: { x: { a: 's', b: 's', c: 's' } } } })
|
||||
redact.restore(result)
|
||||
is(result.a.b.x.c, 's')
|
||||
is(result.a.d.x.a, 's')
|
||||
is(result.a.d.x.b, 's')
|
||||
is(result.a.d.x.c, 's')
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcard - array', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.b[*].x'], serialize: false })
|
||||
const result = redact({ a: { b: [{ x: 1 }, { a: 2 }], d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result.a.b[0].x, censor)
|
||||
is(result.a.b[1].a, 2)
|
||||
is(result.a.d.a, 's')
|
||||
is(result.a.d.b, 's')
|
||||
end()
|
||||
})
|
||||
|
||||
test('multiple wildcards', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a[*].c[*].d'], serialize: false })
|
||||
const obj = {
|
||||
a: [
|
||||
{ c: [{ d: '1', e: '2' }, { d: '1', e: '3' }, { d: '1', e: '4' }] },
|
||||
{ c: [{ d: '1', f: '5' }] },
|
||||
{ c: [{ d: '2', g: '6' }] }
|
||||
]
|
||||
}
|
||||
const result = redact(obj)
|
||||
is(result.a[0].c[0].d, censor)
|
||||
is(result.a[0].c[0].e, '2')
|
||||
is(result.a[0].c[1].d, censor)
|
||||
is(result.a[0].c[1].e, '3')
|
||||
is(result.a[0].c[2].d, censor)
|
||||
is(result.a[0].c[2].e, '4')
|
||||
is(result.a[1].c[0].d, censor)
|
||||
is(result.a[1].c[0].f, '5')
|
||||
is(result.a[2].c[0].d, censor)
|
||||
is(result.a[2].c[0].g, '6')
|
||||
redact.restore(result)
|
||||
is(result.a[0].c[0].d, '1')
|
||||
is(result.a[0].c[0].e, '2')
|
||||
is(result.a[0].c[1].d, '1')
|
||||
is(result.a[0].c[1].e, '3')
|
||||
is(result.a[0].c[2].d, '1')
|
||||
is(result.a[0].c[2].e, '4')
|
||||
is(result.a[1].c[0].d, '1')
|
||||
is(result.a[1].c[0].f, '5')
|
||||
is(result.a[2].c[0].d, '2')
|
||||
is(result.a[2].c[0].g, '6')
|
||||
end()
|
||||
})
|
||||
|
||||
test('multiple wildcards - censor function', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a[*].c[*].d'], censor: censorFct, serialize: false })
|
||||
const obj = {
|
||||
a: [
|
||||
{ c: [{ d: '1', e: '2' }, { d: '1', e: '3' }, { d: '1', e: '4' }] },
|
||||
{ c: [{ d: '1', f: '5' }] },
|
||||
{ c: [{ d: '2', g: '6' }] }
|
||||
]
|
||||
}
|
||||
const result = redact(obj)
|
||||
is(result.a[0].c[0].d, 'xxx1')
|
||||
is(result.a[0].c[0].e, '2')
|
||||
is(result.a[0].c[1].d, 'xxx1')
|
||||
is(result.a[0].c[1].e, '3')
|
||||
is(result.a[0].c[2].d, 'xxx1')
|
||||
is(result.a[0].c[2].e, '4')
|
||||
is(result.a[1].c[0].d, 'xxx1')
|
||||
is(result.a[1].c[0].f, '5')
|
||||
is(result.a[2].c[0].d, 'xxx2')
|
||||
is(result.a[2].c[0].g, '6')
|
||||
end()
|
||||
})
|
||||
|
||||
test('multiple wildcards end', ({ end, is, same }) => {
|
||||
const redact = fastRedact({ paths: ['a[*].c.d[*]'], serialize: false })
|
||||
const obj = {
|
||||
a: [
|
||||
{ c: { d: [ '1', '2' ], e: '3' } },
|
||||
{ c: { d: [ '1' ], f: '4' } },
|
||||
{ c: { d: [ '1' ], g: '5' } }
|
||||
]
|
||||
}
|
||||
const result = redact(obj)
|
||||
same(result.a[0].c.d, [censor, censor])
|
||||
is(result.a[0].c.e, '3')
|
||||
same(result.a[1].c.d, [censor])
|
||||
is(result.a[1].c.f, '4')
|
||||
same(result.a[2].c.d, [censor])
|
||||
is(result.a[2].c.g, '5')
|
||||
end()
|
||||
})
|
||||
|
||||
test('multiple wildcards depth after n wildcard', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a[*].c.d[*].i'], serialize: false })
|
||||
const obj = {
|
||||
a: [
|
||||
{ c: { d: [ { i: '1', j: '2' } ], e: '3' } },
|
||||
{ c: { d: [ { i: '1', j: '2' }, { i: '1', j: '3' } ], f: '4' } },
|
||||
{ c: { d: [ { i: '1', j: '2' } ], g: '5' } }
|
||||
]
|
||||
}
|
||||
const result = redact(obj)
|
||||
is(result.a[0].c.d[0].i, censor)
|
||||
is(result.a[0].c.d[0].j, '2')
|
||||
is(result.a[0].c.e, '3')
|
||||
is(result.a[1].c.d[0].i, censor)
|
||||
is(result.a[1].c.d[0].j, '2')
|
||||
is(result.a[1].c.d[1].i, censor)
|
||||
is(result.a[1].c.d[1].j, '3')
|
||||
is(result.a[1].c.f, '4')
|
||||
is(result.a[2].c.d[0].i, censor)
|
||||
is(result.a[2].c.d[0].j, '2')
|
||||
is(result.a[2].c.g, '5')
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcards – array – single index', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['insideArray.like[3].*.foo'], serialize: false })
|
||||
same(redact({ insideArray: { like: ['a', 'b', 'c', { this: { foo: 'meow' } }] } }), { insideArray: { like: ['a', 'b', 'c', { this: { foo: censor } }] } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcards - handles null proto objects', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.*.c'], serialize: false })
|
||||
const result = redact({ __proto__: null, a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result.a.b.c, censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcards - handles paths that do not match object structure', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['a.*.y.z'], serialize: false })
|
||||
same(redact({ a: { b: { c: 's' } } }), { a: { b: { c: 's' } } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcards - gracefully handles primitives that match intermediate keys in paths', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['a.*.c'], serialize: false })
|
||||
same(redact({ a: { b: null } }), { a: { b: null } })
|
||||
same(redact({ a: { b: 's' } }), { a: { b: 's' } })
|
||||
same(redact({ a: { b: 1 } }), { a: { b: 1 } })
|
||||
same(redact({ a: { b: undefined } }), { a: { b: undefined } })
|
||||
same(redact({ a: { b: true } }), { a: { b: true } })
|
||||
const sym = Symbol('sym')
|
||||
same(redact({ a: { b: sym } }), { a: { b: sym } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcards – handles circulars', ({ end, same }) => {
|
||||
const redact = fastRedact({ paths: ['x.*.baz'], serialize: false })
|
||||
const bar = { b: 2 }
|
||||
const o = { x: { a: 1, bar } }
|
||||
bar.baz = bar
|
||||
o.x.bar.baz = o.x.bar
|
||||
same(redact(o), { x: { a: 1, bar: { b: 2, baz: censor } } })
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcards – handles circulars – restore', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['x.*.baz'], serialize: false })
|
||||
const bar = { b: 2 }
|
||||
const o = { x: { a: 1, bar } }
|
||||
bar.baz = bar
|
||||
o.x.bar.baz = o.x.bar
|
||||
is(o.x.bar.baz, bar)
|
||||
redact(o)
|
||||
is(o.x.a, 1)
|
||||
is(o.x.bar.baz, censor)
|
||||
redact.restore(o)
|
||||
is(o.x.bar.baz, bar)
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcards – handles circulars and cross references – restore', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['x.*.baz', 'x.*.cf.bar'], serialize: false })
|
||||
const bar = { b: 2 }
|
||||
const o = { x: { a: 1, bar, y: { cf: { bar } } } }
|
||||
bar.baz = bar
|
||||
o.x.bar.baz = o.x.bar
|
||||
is(o.x.bar.baz, bar)
|
||||
is(o.x.y.cf.bar, bar)
|
||||
redact(o)
|
||||
is(o.x.bar.baz, censor)
|
||||
is(o.x.y.cf.bar, censor)
|
||||
redact.restore(o)
|
||||
is(o.x.bar.baz, bar)
|
||||
is(o.x.y.cf.bar, bar)
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcards – handles missing paths', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['z.*.baz'] })
|
||||
const o = { a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }
|
||||
is(redact(o), JSON.stringify(o))
|
||||
end()
|
||||
})
|
||||
|
||||
test('ultimate wildcards – handles missing paths', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['z.*'] })
|
||||
const o = { a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }
|
||||
is(redact(o), JSON.stringify(o))
|
||||
end()
|
||||
})
|
||||
|
||||
test('parent wildcards – removes during serialization instead of redacting when remove option is true', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.*.c'], remove: true })
|
||||
const o = { a: { b: { c: 'c' }, x: { c: 1 } } }
|
||||
is(redact(o), `{"a":{"b":{},"x":{}}}`)
|
||||
end()
|
||||
})
|
||||
|
||||
test('ultimate wildcards – removes during serialization instead of redacting when remove option is true', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.b.*'], remove: true })
|
||||
const o = { a: { b: { c: 'c' }, x: { c: 1 } } }
|
||||
is(redact(o), `{"a":{"b":{},"x":{"c":1}}}`)
|
||||
end()
|
||||
})
|
||||
|
||||
test('supports leading bracket notation', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['["a"].b.c'] })
|
||||
const o = { a: { b: { c: 'd' } } }
|
||||
is(redact(o), `{"a":{"b":{"c":"${censor}"}}}`)
|
||||
end()
|
||||
})
|
||||
|
||||
test('supports leading bracket notation containing non-legal keyword characters', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['["a-x"].b.c'] })
|
||||
const o = { 'a-x': { b: { c: 'd' } } }
|
||||
is(redact(o), `{"a-x":{"b":{"c":"${censor}"}}}`)
|
||||
end()
|
||||
})
|
||||
|
||||
test('supports single leading bracket', ({ end, is }) => {
|
||||
const censor = 'test'
|
||||
const redact = fastRedact({ paths: ['["a"]'], censor, serialize: false })
|
||||
is(redact({ a: 'a' }).a, censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('supports single leading bracket containing non-legal keyword characters', ({ end, is }) => {
|
||||
const censor = 'test'
|
||||
const redact = fastRedact({ paths: ['["a-x"]'], censor, serialize: false })
|
||||
is(redact({ 'a-x': 'a' })['a-x'], censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('(leading brackets) ultimate wildcards – handles circulars and cross references – restore', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['bar.baz.*', 'cf.*'], serialize: false })
|
||||
const bar = { b: 2 }
|
||||
const o = { a: 1, bar, cf: { bar } }
|
||||
bar.baz = bar
|
||||
o.bar.baz = o.bar
|
||||
is(o.bar.baz, bar)
|
||||
is(o.cf.bar, bar)
|
||||
redact(o)
|
||||
is(o.bar.baz, censor)
|
||||
is(o.cf.bar, censor)
|
||||
redact.restore(o)
|
||||
is(o.bar.baz, bar)
|
||||
is(o.cf.bar, bar)
|
||||
end()
|
||||
})
|
||||
|
||||
test('(leading brackets) parent wildcards – handles circulars and cross references – restore', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['["x"].*.baz', '["x"].*.cf.bar'], serialize: false })
|
||||
const bar = { b: 2 }
|
||||
const o = { x: { a: 1, bar, y: { cf: { bar } } } }
|
||||
bar.baz = bar
|
||||
o.x.bar.baz = o.x.bar
|
||||
is(o.x.bar.baz, bar)
|
||||
is(o.x.y.cf.bar, bar)
|
||||
redact(o)
|
||||
is(o.x.bar.baz, censor)
|
||||
is(o.x.y.cf.bar, censor)
|
||||
redact.restore(o)
|
||||
is(o.x.bar.baz, bar)
|
||||
is(o.x.y.cf.bar, bar)
|
||||
end()
|
||||
})
|
||||
|
||||
test('(leading brackets) ultimate wildcards – handles missing paths', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['["z"].*'] })
|
||||
const o = { a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } }
|
||||
is(redact(o), JSON.stringify(o))
|
||||
end()
|
||||
})
|
||||
|
||||
test('(leading brackets) static + wildcards reuse', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['["a"].b.c', '["a"].d.*'], serialize: false })
|
||||
const result = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
|
||||
is(result.a.b.c, censor)
|
||||
is(result.a.d.a, censor)
|
||||
is(result.a.d.b, censor)
|
||||
is(result.a.d.c, censor)
|
||||
|
||||
redact.restore(result)
|
||||
|
||||
const result2 = redact({ a: { b: { c: 's' }, d: { a: 's', b: 's', c: 's' } } })
|
||||
is(result2.a.b.c, censor)
|
||||
is(result2.a.d.a, censor)
|
||||
is(result2.a.d.b, censor)
|
||||
is(result2.a.d.c, censor)
|
||||
|
||||
redact.restore(result2)
|
||||
end()
|
||||
})
|
||||
|
||||
test('correctly restores original object when a path does not match object', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['foo.bar'], strict: false })
|
||||
const o = {}
|
||||
is(redact({ foo: o }), '{"foo":{}}')
|
||||
is(o.hasOwnProperty('bar'), false)
|
||||
end()
|
||||
})
|
||||
|
||||
test('correctly restores original object when a matching path has value of `undefined`', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['foo.bar'], strict: false })
|
||||
const o = { bar: undefined }
|
||||
is(redact({ foo: o }), '{"foo":{}}')
|
||||
is(o.hasOwnProperty('bar'), true)
|
||||
is(o.bar, undefined)
|
||||
end()
|
||||
})
|
||||
|
||||
test('correctly restores keys matching a static path and a wildcard', ({ end, is }) => {
|
||||
const redact = fastRedact({
|
||||
paths: ['a', '*.b', 'x.b'],
|
||||
serialize: false
|
||||
})
|
||||
const o = { x: { a: 'a', b: 'b' } }
|
||||
redact(o)
|
||||
is(o.x.a, 'a')
|
||||
is(o.x.b, '[REDACTED]')
|
||||
redact.restore(o)
|
||||
is(o.x.a, 'a')
|
||||
is(o.x.b, 'b')
|
||||
end()
|
||||
})
|
||||
|
||||
test('correctly restores keys matching multiple wildcards', ({ end, is }) => {
|
||||
const redact = fastRedact({
|
||||
paths: ['a', '*.b', 'x.*', 'y.*.z'],
|
||||
serialize: false
|
||||
})
|
||||
const o = { x: { a: 'a', b: 'b' }, y: { f: { z: 'z' } } }
|
||||
redact(o)
|
||||
is(o.x.a, '[REDACTED]')
|
||||
is(o.x.b, '[REDACTED]')
|
||||
is(o.y.f.z, '[REDACTED]')
|
||||
redact.restore(o)
|
||||
is(o.x.a, 'a')
|
||||
is(o.x.b, 'b')
|
||||
is(o.y.f.z, 'z')
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles multiple paths with leading brackets', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['["x-y"]', '["y-x"]'] })
|
||||
const o = { 'x-y': 'test', 'y-x': 'test2' }
|
||||
is(redact(o), '{"x-y":"[REDACTED]","y-x":"[REDACTED]"}')
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles objects with and then without target paths', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['test'] })
|
||||
const o1 = { test: 'check' }
|
||||
const o2 = {}
|
||||
is(redact(o1), '{"test":"[REDACTED]"}')
|
||||
is(redact(o2), '{}')
|
||||
// run each check twice to ensure no mutations
|
||||
is(redact(o1), '{"test":"[REDACTED]"}')
|
||||
is(redact(o2), '{}')
|
||||
is('test' in o1, true)
|
||||
is('test' in o2, false)
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles leading wildcards and null values', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['*.test'] })
|
||||
const o = { prop: null }
|
||||
is(redact(o), '{"prop":null}')
|
||||
is(o.prop, null)
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles keys with dots', ({ end, is }) => {
|
||||
const redactSingleQ = fastRedact({ paths: [`a['b.c']`], serialize: false })
|
||||
const redactDoubleQ = fastRedact({ paths: [`a["b.c"]`], serialize: false })
|
||||
const redactBacktickQ = fastRedact({ paths: ['a[`b.c`]'], serialize: false })
|
||||
const redactNum = fastRedact({ paths: [`a[-1.2]`], serialize: false })
|
||||
const redactLeading = fastRedact({ paths: [`["b.c"]`], serialize: false })
|
||||
is(redactSingleQ({ a: { 'b.c': 'x', '-1.2': 'x' } }).a['b.c'], censor)
|
||||
is(redactDoubleQ({ a: { 'b.c': 'x', '-1.2': 'x' } }).a['b.c'], censor)
|
||||
is(redactBacktickQ({ a: { 'b.c': 'x', '-1.2': 'x' } }).a['b.c'], censor)
|
||||
is(redactNum({ a: { 'b.c': 'x', '-1.2': 'x' } }).a['-1.2'], censor)
|
||||
is(redactLeading({ 'b.c': 'x', '-1.2': 'x' })['b.c'], censor)
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles multi wildcards within arrays', ({ end, is }) => {
|
||||
const redact = fastRedact({
|
||||
paths: ['a[*].x.d[*].i.*']
|
||||
})
|
||||
const o = {
|
||||
a: [ { x: { d: [ { j: { i: 'R' } }, { i: 'R', j: 'NR' } ] } } ]
|
||||
}
|
||||
is(redact(o), '{"a":[{"x":{"d":["[REDACTED]","[REDACTED]"]}}]}')
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles multi wildcards within arrays with a censorFct', ({ end, is }) => {
|
||||
const redact = fastRedact({
|
||||
paths: ['a[*].x.d[*].i.*.i'],
|
||||
censor: censorWithPath
|
||||
})
|
||||
const o = {
|
||||
a: [
|
||||
{ x: { d: [ { i: 'R', j: 'NR' } ] } }
|
||||
]
|
||||
}
|
||||
is(redact(o), '{"a":[{"x":{"d":[{"i":"a.0.x.d.*.i.*.i xxxR","j":"NR"}]}}]}')
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles multi wildcards within arrays with undefined values', ({ end, is }) => {
|
||||
const redact = fastRedact({
|
||||
paths: ['a[*].x.d[*].i.*.i']
|
||||
})
|
||||
const o = {
|
||||
a: [
|
||||
{ x: { d: [ { i: undefined, j: 'NR' } ] } }
|
||||
]
|
||||
}
|
||||
is(redact(o), '{"a":[{"x":{"d":[{"i":"[REDACTED]","j":"NR"}]}}]}')
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles multi wildcards with objects containing nulls', ({ end, is }) => {
|
||||
const redact = fastRedact({
|
||||
paths: ['*.*.x'],
|
||||
serialize: false,
|
||||
censor: '[REDACTED]'
|
||||
})
|
||||
const o = { a: { b: null } }
|
||||
is(redact(o), o)
|
||||
end()
|
||||
})
|
||||
|
||||
test('handles multi wildcards with pattern repetition', ({ end, is }) => {
|
||||
const redact = fastRedact({
|
||||
paths: ['*.d', '*.*.d', '*.*.*.d']
|
||||
})
|
||||
const o = {
|
||||
x: { c: { d: 'hide me', e: 'leave me be' } },
|
||||
y: { c: { d: 'and me', f: 'I want to live' } },
|
||||
z: { c: { d: 'and also I', g: 'I want to run in a stream' } }
|
||||
}
|
||||
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"}}}')
|
||||
end()
|
||||
})
|
||||
|
||||
test('restores multi wildcards with pattern repetition', ({ end, is }) => {
|
||||
const redact = fastRedact({
|
||||
paths: ['*.d', '*.*.d', '*.*.*.d']
|
||||
})
|
||||
const o = {
|
||||
x: { c: { d: 'hide me', e: 'leave me be' } },
|
||||
y: { c: { d: 'and me', f: 'I want to live' } },
|
||||
z: { c: { d: 'and also I', g: 'I want to run in a stream' } }
|
||||
}
|
||||
redact(o)
|
||||
is(o.x.c.d, 'hide me')
|
||||
is(o.y.c.d, 'and me')
|
||||
is(o.z.c.d, 'and also I')
|
||||
end()
|
||||
})
|
||||
|
||||
test('multi level wildcards with level > 3', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['*.*.*.c', '*.*.*.*.c'] })
|
||||
const o = {
|
||||
a: {
|
||||
b: {
|
||||
x: {
|
||||
c: 's'
|
||||
}
|
||||
},
|
||||
d: {
|
||||
x: {
|
||||
u: {
|
||||
a: 's',
|
||||
b: 's',
|
||||
c: 's'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is(redact(o), '{"a":{"b":{"x":{"c":"[REDACTED]"}},"d":{"x":{"u":{"a":"s","b":"s","c":"[REDACTED]"}}}}}')
|
||||
end()
|
||||
})
|
||||
|
||||
test('multi level wildcards at nested level inside object', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.*.*.c', 'a.d.*.*.c'] })
|
||||
const o = {
|
||||
a: {
|
||||
b: {
|
||||
x: {
|
||||
c: 's'
|
||||
}
|
||||
},
|
||||
d: {
|
||||
x: {
|
||||
u: {
|
||||
a: 's',
|
||||
b: 's',
|
||||
c: 's'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is(redact(o), '{"a":{"b":{"x":{"c":"[REDACTED]"}},"d":{"x":{"u":{"a":"s","b":"s","c":"[REDACTED]"}}}}}')
|
||||
end()
|
||||
})
|
||||
|
||||
test('multi level wildcards with level > 3 with serialize false', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['*.*.*.c', '*.*.*.*.c'], serialize: false })
|
||||
const result = redact({ a: { b: { x: { c: 's' } }, d: { x: { u: { a: 's', b: 's', c: 's' } } } } })
|
||||
is(result.a.b.x.c, censor)
|
||||
is(result.a.d.x.u.a, 's')
|
||||
is(result.a.d.x.u.b, 's')
|
||||
is(result.a.d.x.u.c, censor)
|
||||
redact.restore(result)
|
||||
is(result.a.b.x.c, 's')
|
||||
is(result.a.d.x.u.a, 's')
|
||||
is(result.a.d.x.u.b, 's')
|
||||
is(result.a.d.x.u.c, 's')
|
||||
end()
|
||||
})
|
||||
|
||||
test('multi level wildcards at nested level inside object with serialize false', ({ end, is }) => {
|
||||
const redact = fastRedact({ paths: ['a.*.*.c', 'a.d.*.*.c'], serialize: false })
|
||||
const result = redact({ a: { b: { x: { c: 's' } }, d: { x: { u: { a: 's', b: 's', c: 's' } } } } })
|
||||
is(result.a.b.x.c, censor)
|
||||
is(result.a.d.x.u.a, 's')
|
||||
is(result.a.d.x.u.b, 's')
|
||||
is(result.a.d.x.u.c, censor)
|
||||
redact.restore(result)
|
||||
is(result.a.b.x.c, 's')
|
||||
is(result.a.d.x.u.a, 's')
|
||||
is(result.a.d.x.u.b, 's')
|
||||
is(result.a.d.x.u.c, 's')
|
||||
end()
|
||||
})
|
||||
|
||||
test('restores nested wildcard values', ({ end, is }) => {
|
||||
const o = { a: { b: [{ c: [
|
||||
{ d: '123' },
|
||||
{ d: '456' }
|
||||
] }] } }
|
||||
|
||||
const censor = 'censor'
|
||||
const paths = ['a.b[*].c[*].d']
|
||||
const redact = fastRedact({ paths, censor, serialize: false })
|
||||
|
||||
redact(o)
|
||||
is(o.a.b[0].c[0].d, censor)
|
||||
is(o.a.b[0].c[1].d, censor)
|
||||
redact.restore(o)
|
||||
is(o.a.b[0].c[0].d, '123')
|
||||
is(o.a.b[0].c[1].d, '456')
|
||||
end()
|
||||
})
|
||||
|
||||
test('restores multi nested wildcard values', ({ end, is }) => {
|
||||
const o = {
|
||||
a: {
|
||||
b1: {
|
||||
c1: {
|
||||
d1: { e: '123' },
|
||||
d2: { e: '456' }
|
||||
},
|
||||
c2: {
|
||||
d1: { e: '789' },
|
||||
d2: { e: '012' }
|
||||
}
|
||||
},
|
||||
b2: {
|
||||
c1: {
|
||||
d1: { e: '345' },
|
||||
d2: { e: '678' }
|
||||
},
|
||||
c2: {
|
||||
d1: { e: '901' },
|
||||
d2: { e: '234' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const o2 = {
|
||||
a: {
|
||||
b1: {
|
||||
c1: {
|
||||
d1: { e: '123' },
|
||||
d2: { e: '456' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const censor = 'censor'
|
||||
const paths = ['a.*.*.*.e']
|
||||
const redact = fastRedact({ paths, censor, serialize: false })
|
||||
|
||||
redact(o)
|
||||
is(o.a.b1.c1.d1.e, censor)
|
||||
is(o.a.b1.c1.d2.e, censor)
|
||||
is(o.a.b1.c2.d1.e, censor)
|
||||
is(o.a.b1.c2.d2.e, censor)
|
||||
is(o.a.b2.c1.d1.e, censor)
|
||||
is(o.a.b2.c1.d2.e, censor)
|
||||
is(o.a.b2.c2.d1.e, censor)
|
||||
is(o.a.b2.c2.d2.e, censor)
|
||||
redact.restore(o)
|
||||
is(o.a.b1.c1.d1.e, '123')
|
||||
is(o.a.b1.c1.d2.e, '456')
|
||||
is(o.a.b1.c2.d1.e, '789')
|
||||
is(o.a.b1.c2.d2.e, '012')
|
||||
is(o.a.b2.c1.d1.e, '345')
|
||||
is(o.a.b2.c1.d2.e, '678')
|
||||
is(o.a.b2.c2.d1.e, '901')
|
||||
is(o.a.b2.c2.d2.e, '234')
|
||||
|
||||
redact(o2)
|
||||
is(o2.a.b1.c1.d1.e, censor)
|
||||
is(o2.a.b1.c1.d2.e, censor)
|
||||
redact.restore(o2)
|
||||
is(o2.a.b1.c1.d1.e, '123')
|
||||
is(o2.a.b1.c1.d2.e, '456')
|
||||
|
||||
end()
|
||||
})
|
||||
|
||||
test('redact multi trailing wildcard', ({ end, is }) => {
|
||||
const o = { a: { b: { c: 'value' } } }
|
||||
|
||||
const censor = 'censor'
|
||||
const paths = ['a.*.*']
|
||||
const redact = fastRedact({ paths, censor, serialize: false })
|
||||
|
||||
redact(o)
|
||||
is(o.a.b.c, censor)
|
||||
redact.restore(o)
|
||||
is(o.a.b.c, 'value')
|
||||
end()
|
||||
})
|
||||
Reference in New Issue
Block a user