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:
Bernt
2026-07-14 15:27:33 +00:00
parent 3c522e39f0
commit 1a12fb870b
6296 changed files with 911440 additions and 55607 deletions
@@ -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
View File
@@ -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
View File
@@ -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)
@@ -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))
@@ -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))
@@ -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))
@@ -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))
@@ -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))
@@ -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)
@@ -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 }))
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,282 @@
# fast-redact
very fast object redaction
[![Build Status](https://travis-ci.org/davidmarkclements/fast-redact.svg?branch=master)](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
View File
@@ -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()
})