From 1947556fc65b1a58890d3aceaf0b6f86c39dc9aa Mon Sep 17 00:00:00 2001 From: Conrad Lang Date: Tue, 29 Aug 2023 10:41:02 +1000 Subject: [PATCH 1/8] feat: Redact or remove paths Pino has a limitation where you can either redact or remove specified `redact.paths` by setting `redact.remove` to `true` or `false`. A logger option has been added to allow you to redact some paths and remove other paths. Example: ```typescript const logger = createLogger({ name: 'my-app', redact: { paths: ['req.headers["x-redact-this"]'], removePaths: ['req.headers["x-remove-this"]'], }, }); ``` --- README.md | 21 +++++++++++++ src/index.test.ts | 72 +++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 4 ++- src/redact/index.ts | 46 ++++++++++++++++++++++------- 4 files changed, 132 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 269e70e..fcb8020 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,26 @@ logger.error( Bearer tokens are redacted regardless of their placement in the log object. +#### Pino Redaction + +To redact other properties, use the `redact` logger options as per [pino redaction] docs. + +Pino has a limitation where you can either redact or remove specified `redact.paths` by setting `redact.remove` to `true` or `false`. + +A logger option has been added to allow you to redact some paths and remove other paths. + +Example: + +```typescript +const logger = createLogger({ + name: 'my-app', + redact: { + paths: ['req.headers["x-redact-this"]'], + removePaths: ['req.headers["x-remove-this"]'], + }, +}); +``` + ### Trimming The following trimming rules apply to all logging data: @@ -162,3 +182,4 @@ const logger = createLogger({ [pino]: https://github.com/pinojs/pino [pino options]: https://github.com/pinojs/pino/blob/master/docs/api.md#options [pino-pretty]: https://github.com/pinojs/pino-pretty +[pino redaction]: https://github.com/pinojs/pino/blob/master/docs/api.md#redact-array--object diff --git a/src/index.test.ts b/src/index.test.ts index 5a3fe36..c52c066 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -38,6 +38,7 @@ function testLog( output: any, method?: 'error' | 'info', loggerOptions?: LoggerOptions, + shouldNotHavePropertyPaths?: string[], ) { // eslint-disable-next-line jest/valid-title test(testName, async () => { @@ -50,6 +51,9 @@ function testLog( expect(log).toMatchObject(output); expect(inputString).toEqual(JSON.stringify(input)); expect(log).toHaveProperty('timestamp'); + shouldNotHavePropertyPaths?.forEach((path) => { + expect(log).not.toHaveProperty(path); + }); }); } @@ -422,6 +426,74 @@ testLog( }, ); +testLog( + 'should redact specified paths', + { + msg: 'allowed', + req: { + headers: { + ['x-leave-me']: 'Should be present', + secret: 'Should be redacted', + }, + }, + }, + { + msg: 'allowed', + req: { + headers: { ['x-leave-me']: 'Should be present', secret: '[Redacted]' }, + }, + }, + 'info', + { + redact: ['req.headers.secret'], + }, + ['req.headers.x-remove-me'], +); + +testLog( + 'should redact or remove specified paths', + { + msg: 'allowed', + req: { + headers: { + ['x-remove-me']: 'Should be removed', + secret: 'Should be redacted', + }, + }, + }, + { msg: 'allowed', req: { headers: { secret: '[Redacted]' } } }, + 'info', + { + redact: { + paths: ['req.headers.secret'], + removePaths: ['req.headers["x-remove-me"]'], + }, + }, + ['req.headers.x-remove-me'], +); + +testLog( + 'should remove specified paths', + { + msg: 'allowed', + req: { + headers: { + ['x-remove-me']: 'Should be removed', + secret: 'Should be removed', + }, + }, + }, + { msg: 'allowed', req: { headers: {} } }, + 'info', + { + redact: { + paths: [], + removePaths: ['req.headers.secret', 'req.headers["x-remove-me"]'], + }, + }, + ['req.headers.secret', 'req.headers.x-remove-me'], +); + interface ExampleMessageContext { activity: string; err?: { diff --git a/src/index.ts b/src/index.ts index be65d6a..825b22c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,8 @@ import serializers from './serializers'; export { pino }; -export type LoggerOptions = pino.LoggerOptions & FormatterOptions; +export type LoggerOptions = Omit & + FormatterOptions & { redact?: redact.ExtendedRedactOptions }; export type Logger = pino.Logger; /** @@ -25,6 +26,7 @@ export default ( }), ): Logger => { opts.redact = redact.addDefaultRedactPathStrings(opts.redact); + opts.redact = redact.addRemovePathsCensor(opts.redact); opts.serializers = { ...serializers, ...opts.serializers, diff --git a/src/redact/index.ts b/src/redact/index.ts index c2ebc40..8ffd73e 100644 --- a/src/redact/index.ts +++ b/src/redact/index.ts @@ -1,18 +1,21 @@ +import type * as pino from 'pino'; + // TODO: Redact cookies? export const defaultRedact = []; -/** - * Private interface vendored from `pino` - */ -interface redactOptions { - paths: string[]; - censor?: string | ((value: unknown, path: string[]) => unknown); - remove?: boolean; -} +export type ExtendedRedactOptions = pino.LoggerOptions['redact'] & { + /** + * A list of paths to remove from the logged object instead of redacting them. + * Note that if you are only removing, rather use the `paths` property and set `remove` to `true`. + */ + removePaths?: string[]; +}; + +type ExtendedRedact = string[] | ExtendedRedactOptions | undefined; export const addDefaultRedactPathStrings = ( - redact: string[] | redactOptions | undefined, -) => { + redact: ExtendedRedact, +): ExtendedRedact => { if (!redact) { return defaultRedact; } @@ -21,3 +24,26 @@ export const addDefaultRedactPathStrings = ( } return redact; }; + +export const addRemovePathsCensor = ( + redact: ExtendedRedact, +): ExtendedRedact => { + if ( + !redact || + Array.isArray(redact) || + !redact.removePaths || + redact.removePaths.length === 0 + ) { + return redact; + } + + const { paths: redactPaths, removePaths } = redact; + + return redactPaths.length === 0 + ? { paths: removePaths, remove: true } + : { + paths: [...redactPaths, ...removePaths], + censor: (_value, path) => + redactPaths.includes(path.join('.')) ? '[Redacted]' : undefined, + }; +}; From 147072909a847b397eb5d4fbf2b6896a6ce9fd7d Mon Sep 17 00:00:00 2001 From: Conrad Lang Date: Tue, 29 Aug 2023 12:58:57 +1000 Subject: [PATCH 2/8] refactor: Use map instead of includes Property access should be more performant than string compare. --- src/redact/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/redact/index.ts b/src/redact/index.ts index 8ffd73e..5ca8984 100644 --- a/src/redact/index.ts +++ b/src/redact/index.ts @@ -39,11 +39,16 @@ export const addRemovePathsCensor = ( const { paths: redactPaths, removePaths } = redact; + const redactMap = redactPaths.reduce( + (previous, current) => ({ ...previous, [current]: true }), + {} as Record, + ); + return redactPaths.length === 0 ? { paths: removePaths, remove: true } : { paths: [...redactPaths, ...removePaths], censor: (_value, path) => - redactPaths.includes(path.join('.')) ? '[Redacted]' : undefined, + redactMap[path.join('.')] ? '[Redacted]' : undefined, }; }; From 19b77a5860da219109afe54c6344a3ba7afe60da Mon Sep 17 00:00:00 2001 From: Conrad Lang Date: Wed, 30 Aug 2023 11:46:39 +1000 Subject: [PATCH 3/8] refactor: Handle non-standard property names --- src/index.test.ts | 33 ++++++++++++++++++++++++++++++++- src/redact/index.test.ts | 15 +++++++++++++++ src/redact/index.ts | 20 +++++++++++++++++++- 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 src/redact/index.test.ts diff --git a/src/index.test.ts b/src/index.test.ts index c52c066..befea1f 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -38,7 +38,7 @@ function testLog( output: any, method?: 'error' | 'info', loggerOptions?: LoggerOptions, - shouldNotHavePropertyPaths?: string[], + shouldNotHavePropertyPaths?: Array, ) { // eslint-disable-next-line jest/valid-title test(testName, async () => { @@ -472,6 +472,37 @@ testLog( ['req.headers.x-remove-me'], ); +testLog( + 'should redact or remove specified paths where property names contain dots', + { + msg: 'allowed', + data: { + top: { + prop1: 'Should be redacted', + prop2: 'Should be removed', + }, + ['top.prop1']: 'Should be removed', + ['top.prop2']: 'Should be redacted', + }, + }, + { + msg: 'allowed', + data: { + top: { prop1: '[Redacted]' }, + ['top.prop2']: '[Redacted]', + }, + }, + 'info', + { + maxObjectDepth: 20, + redact: { + paths: ['data.top.prop1', 'data["top.prop2"]'], + removePaths: ['data.top.prop2', 'data["top.prop1"]'], + }, + }, + ['data.top.prop2', ['data', 'top.prop1']], +); + testLog( 'should remove specified paths', { diff --git a/src/redact/index.test.ts b/src/redact/index.test.ts new file mode 100644 index 0000000..c25a3af --- /dev/null +++ b/src/redact/index.test.ts @@ -0,0 +1,15 @@ +import { keyFromPath } from '.'; + +it.each` + paths | path + ${['data', 'top', 'prop1']} | ${'data.top.prop1'} + ${['data', 'top.prop1']} | ${'data["top.prop1"]'} + ${['headers', 'x-request-id']} | ${'headers["x-request-id"]'} + ${['_start', '$with', 'allowed', 'Char']} | ${'_start.$with.allowed.Char'} + ${['-start', '.with', '4notAllowed', '~Char']} | ${'["-start"][".with"]["4notAllowed"]["~Char"]'} + ${['con-tain', 'not!', 'allow∑∂', 'Chår']} | ${'["con-tain"]["not!"]["allow∑∂"]["Chår"]'} +`(`should convert '$paths' to '$path'`, ({ paths, path }) => { + const result = keyFromPath(paths); + + expect(result).toBe(path); +}); diff --git a/src/redact/index.ts b/src/redact/index.ts index 5ca8984..7d7e829 100644 --- a/src/redact/index.ts +++ b/src/redact/index.ts @@ -25,6 +25,24 @@ export const addDefaultRedactPathStrings = ( return redact; }; +const STARTS_WITH_INVALID_CHARS = '^[^$_a-zA-Z]'; +const CONTAINS_INVALID_CHARS = '[^a-zA-Z0-9_$]+'; +const nonStandardIdentifierRegex = new RegExp( + `(${STARTS_WITH_INVALID_CHARS}|${CONTAINS_INVALID_CHARS})`, +); + +export const keyFromPath = (paths: string[]): string => { + const path = paths.reduce((previous, current) => { + const dotDelimiter = previous === '' ? '' : '.'; + const escapedPath = nonStandardIdentifierRegex.test(current) + ? `["${current}"]` + : `${dotDelimiter}${current}`; + return `${previous}${escapedPath}`; + }, ''); + + return path; +}; + export const addRemovePathsCensor = ( redact: ExtendedRedact, ): ExtendedRedact => { @@ -49,6 +67,6 @@ export const addRemovePathsCensor = ( : { paths: [...redactPaths, ...removePaths], censor: (_value, path) => - redactMap[path.join('.')] ? '[Redacted]' : undefined, + redactMap[keyFromPath(path)] ? '[Redacted]' : undefined, }; }; From 150481ee3acf4106c4b96a3625b50002cf0cec58 Mon Sep 17 00:00:00 2001 From: Conrad Lang Date: Wed, 30 Aug 2023 11:49:46 +1000 Subject: [PATCH 4/8] refactor: Rename addRemovePathsCensor Renamed to configureRedactCensor. --- src/index.ts | 2 +- src/redact/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 825b22c..ec6c57a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,7 +26,7 @@ export default ( }), ): Logger => { opts.redact = redact.addDefaultRedactPathStrings(opts.redact); - opts.redact = redact.addRemovePathsCensor(opts.redact); + opts.redact = redact.configureRedactCensor(opts.redact); opts.serializers = { ...serializers, ...opts.serializers, diff --git a/src/redact/index.ts b/src/redact/index.ts index 7d7e829..bcabe1e 100644 --- a/src/redact/index.ts +++ b/src/redact/index.ts @@ -43,7 +43,7 @@ export const keyFromPath = (paths: string[]): string => { return path; }; -export const addRemovePathsCensor = ( +export const configureRedactCensor = ( redact: ExtendedRedact, ): ExtendedRedact => { if ( From aa16fcb51d3a481f333d65225ecaaab28e5a86a4 Mon Sep 17 00:00:00 2001 From: Conrad Lang Date: Wed, 30 Aug 2023 11:56:20 +1000 Subject: [PATCH 5/8] refactor: Use set instead of map --- src/redact/index.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/redact/index.ts b/src/redact/index.ts index bcabe1e..8afe5cc 100644 --- a/src/redact/index.ts +++ b/src/redact/index.ts @@ -56,17 +56,13 @@ export const configureRedactCensor = ( } const { paths: redactPaths, removePaths } = redact; - - const redactMap = redactPaths.reduce( - (previous, current) => ({ ...previous, [current]: true }), - {} as Record, - ); + const redactSet = new Set(redactPaths); return redactPaths.length === 0 ? { paths: removePaths, remove: true } : { paths: [...redactPaths, ...removePaths], censor: (_value, path) => - redactMap[keyFromPath(path)] ? '[Redacted]' : undefined, + redactSet.has(keyFromPath(path)) ? '[Redacted]' : undefined, }; }; From 91a9f1361d26718eb5e0dacdf1ba72e5110edd61 Mon Sep 17 00:00:00 2001 From: Conrad Lang Date: Wed, 30 Aug 2023 12:08:31 +1000 Subject: [PATCH 6/8] refactor: Orchestrate redact configuration Default redact paths should always be added before configuring the censor. --- src/index.ts | 3 +-- src/redact/index.ts | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index ec6c57a..4e548fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,8 +25,7 @@ export default ( sync: true, }), ): Logger => { - opts.redact = redact.addDefaultRedactPathStrings(opts.redact); - opts.redact = redact.configureRedactCensor(opts.redact); + opts.redact = redact.configureRedact(opts.redact); opts.serializers = { ...serializers, ...opts.serializers, diff --git a/src/redact/index.ts b/src/redact/index.ts index 8afe5cc..69534bb 100644 --- a/src/redact/index.ts +++ b/src/redact/index.ts @@ -13,7 +13,7 @@ export type ExtendedRedactOptions = pino.LoggerOptions['redact'] & { type ExtendedRedact = string[] | ExtendedRedactOptions | undefined; -export const addDefaultRedactPathStrings = ( +const addDefaultRedactPathStrings = ( redact: ExtendedRedact, ): ExtendedRedact => { if (!redact) { @@ -43,9 +43,7 @@ export const keyFromPath = (paths: string[]): string => { return path; }; -export const configureRedactCensor = ( - redact: ExtendedRedact, -): ExtendedRedact => { +const configureRedactCensor = (redact: ExtendedRedact): ExtendedRedact => { if ( !redact || Array.isArray(redact) || @@ -66,3 +64,6 @@ export const configureRedactCensor = ( redactSet.has(keyFromPath(path)) ? '[Redacted]' : undefined, }; }; + +export const configureRedact = (redact: ExtendedRedact): ExtendedRedact => + configureRedactCensor(addDefaultRedactPathStrings(redact)); From 8bcb486262000bf5572c145f9f5ebefcbc17714e Mon Sep 17 00:00:00 2001 From: Conrad Lang Date: Wed, 30 Aug 2023 18:23:35 +1000 Subject: [PATCH 7/8] Feat: Enable configuring default remove paths Also corrected `ExtendedRedact` definition as `removePaths` should only have been on the pino `redactOptions` type. --- README.md | 18 ++++++++++++ src/index.test.ts | 68 +++++++++++++++++++++++++++++++++------------ src/index.ts | 2 +- src/redact/index.ts | 40 ++++++++++++++++++-------- 4 files changed, 98 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index fcb8020..4951592 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,24 @@ const logger = createLogger({ }); ``` +#### Removal of Default Properties + +The library will remove a set of default properties. +To bypass this, set `ignoreDefaultRemovePaths` to `true` on the `redact` options. + +Example: + +```typescript +const logger = createLogger({ + name: 'my-app', + redact: { + paths: ['req.headers["x-redact-this"]'], + removePaths: ['req.headers["x-remove-this"]'], + ignoreDefaultRemovePaths: true, + }, +}); +``` + ### Trimming The following trimming rules apply to all logging data: diff --git a/src/index.test.ts b/src/index.test.ts index befea1f..6775c1c 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,5 +1,7 @@ import split from 'split2'; +import { defaultRemovePaths } from './redact'; + import createLogger, { type LoggerOptions } from '.'; const bearerToken = @@ -472,35 +474,67 @@ testLog( ['req.headers.x-remove-me'], ); +const buildObjectFromPath = (path: string): Record => + path + .split(/[.\[]/) + .reverse() + .reduce( + (previous, current) => { + const key = current.replace(/["\[\]]/g, ''); + if (!previous) { + return { [key]: 'Default path property' }; + } + + return { [key]: previous }; + }, + undefined as unknown as Record, + ); + +const buildObjectFromDefaultRemovePaths = (): Record => + !defaultRemovePaths?.[0] ? {} : buildObjectFromPath(defaultRemovePaths[0]); + testLog( - 'should redact or remove specified paths where property names contain dots', + 'should remove default paths when ignoreDefaultRemovePaths is missing', { - msg: 'allowed', - data: { - top: { - prop1: 'Should be redacted', - prop2: 'Should be removed', - }, - ['top.prop1']: 'Should be removed', - ['top.prop2']: 'Should be redacted', - }, + redact: 'Should be redacted', + remove: 'Should be removed', + ...buildObjectFromDefaultRemovePaths(), }, { - msg: 'allowed', - data: { - top: { prop1: '[Redacted]' }, - ['top.prop2']: '[Redacted]', + redact: '[Redacted]', + }, + 'info', + { + maxObjectDepth: 20, + redact: { + paths: ['redact'], + removePaths: ['remove'], }, }, + ['remove', ...defaultRemovePaths], +); + +testLog( + 'should not remove default paths when ignoreDefaultRemovePaths is true', + { + redact: 'Should be redacted', + remove: 'Should be removed', + ...buildObjectFromDefaultRemovePaths(), + }, + { + redact: '[Redacted]', + ...buildObjectFromDefaultRemovePaths(), + }, 'info', { maxObjectDepth: 20, redact: { - paths: ['data.top.prop1', 'data["top.prop2"]'], - removePaths: ['data.top.prop2', 'data["top.prop1"]'], + paths: ['redact'], + removePaths: ['remove'], + ignoreDefaultRemovePaths: true, }, }, - ['data.top.prop2', ['data', 'top.prop1']], + ['remove'], ); testLog( diff --git a/src/index.ts b/src/index.ts index 4e548fe..0474612 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ import serializers from './serializers'; export { pino }; export type LoggerOptions = Omit & - FormatterOptions & { redact?: redact.ExtendedRedactOptions }; + FormatterOptions & { redact?: redact.ExtendedRedact }; export type Logger = pino.Logger; /** diff --git a/src/redact/index.ts b/src/redact/index.ts index 69534bb..344c022 100644 --- a/src/redact/index.ts +++ b/src/redact/index.ts @@ -3,26 +3,42 @@ import type * as pino from 'pino'; // TODO: Redact cookies? export const defaultRedact = []; -export type ExtendedRedactOptions = pino.LoggerOptions['redact'] & { +export const defaultRemovePaths: string[] = []; + +type redactOptions = Extract; + +type extendedRedactOptions = redactOptions & { /** * A list of paths to remove from the logged object instead of redacting them. * Note that if you are only removing, rather use the `paths` property and set `remove` to `true`. */ removePaths?: string[]; + + /** + * When `true`, the `defaultRemovePaths` will not be appended to the `removePaths` list. + */ + ignoreDefaultRemovePaths?: true; }; -type ExtendedRedact = string[] | ExtendedRedactOptions | undefined; +export type ExtendedRedact = string[] | extendedRedactOptions | undefined; -const addDefaultRedactPathStrings = ( +const appendDefaultRedactAndRemove = ( redact: ExtendedRedact, -): ExtendedRedact => { - if (!redact) { - return defaultRedact; - } - if (Array.isArray(redact)) { - return redact.concat(defaultRedact); - } - return redact; +): extendedRedactOptions | undefined => { + const inputRedact = + typeof redact !== 'undefined' && !Array.isArray(redact) + ? redact + : { paths: redact ?? [] }; + + const paths = inputRedact.paths.concat(defaultRedact); + const inputRemovePaths = inputRedact.removePaths ?? []; + const removePaths = inputRedact.ignoreDefaultRemovePaths + ? inputRemovePaths + : inputRemovePaths.concat(defaultRemovePaths); + + return paths.length === 0 && removePaths.length === 0 + ? undefined + : { ...inputRedact, paths, removePaths }; }; const STARTS_WITH_INVALID_CHARS = '^[^$_a-zA-Z]'; @@ -66,4 +82,4 @@ const configureRedactCensor = (redact: ExtendedRedact): ExtendedRedact => { }; export const configureRedact = (redact: ExtendedRedact): ExtendedRedact => - configureRedactCensor(addDefaultRedactPathStrings(redact)); + configureRedactCensor(appendDefaultRedactAndRemove(redact)); From a4cc18b0527bbdf6cc77e6fdc70bd3e9f0ccb266 Mon Sep 17 00:00:00 2001 From: Conrad Lang Date: Thu, 31 Aug 2023 11:05:11 +1000 Subject: [PATCH 8/8] fix: Input censor is always preserved for redact --- src/index.test.ts | 92 +++++++++++++++++++++++++++++++++++++++++++++ src/redact/index.ts | 15 +++++--- 2 files changed, 102 insertions(+), 5 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 6775c1c..9b53565 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -537,6 +537,98 @@ testLog( ['remove'], ); +testLog( + 'should use input censor when redacting', + { + redact: 'Should be redacted', + }, + { + redact: '[SHHH]', + }, + 'info', + { + redact: { + paths: ['redact'], + censor: (_value, _path) => '[SHHH]', + }, + }, +); + +testLog( + 'should not use input censor when removing', + { + remove: 'Should be "removed"', + }, + {}, + 'info', + { + redact: { + paths: ['remove'], + remove: true, + censor: (_value, _path) => 'You have been erased!', + }, + }, + ['remove'], +); + +testLog( + 'should not use input censor when redacting and removing with no redact paths', + { + remove: 'Should be "removed"', + }, + {}, + 'info', + { + redact: { + paths: [], + removePaths: ['remove'], + censor: (_value, _path) => 'You have been erased!', + }, + }, + ['remove'], +); + +testLog( + 'should use input censor function to redact when redacting and removing', + { + redact: 'Should be redacted', + remove: 'Should be "removed"', + }, + { + redact: '[SHHH]', + }, + 'info', + { + redact: { + paths: ['redact'], + removePaths: ['remove'], + censor: (_value, path) => + path.includes('redact') ? '[SHHH]' : 'You have been erased!', + }, + }, + ['remove'], +); + +testLog( + 'should use input censor text to redact when redacting and removing', + { + redact: 'Should be redacted', + remove: 'Should be "removed"', + }, + { + redact: '[SHHH]', + }, + 'info', + { + redact: { + paths: ['redact'], + removePaths: ['remove'], + censor: '[SHHH]', + }, + }, + ['remove'], +); + testLog( 'should remove specified paths', { diff --git a/src/redact/index.ts b/src/redact/index.ts index 344c022..44acf44 100644 --- a/src/redact/index.ts +++ b/src/redact/index.ts @@ -69,15 +69,20 @@ const configureRedactCensor = (redact: ExtendedRedact): ExtendedRedact => { return redact; } - const { paths: redactPaths, removePaths } = redact; - const redactSet = new Set(redactPaths); + const { paths: redactPaths, removePaths, censor } = redact; + const removeSet = new Set(removePaths); + const censorText = typeof censor === 'string' ? censor : '[Redacted]'; + const censorPath = (value: unknown, path: string[]): unknown => + typeof censor === 'function' ? censor(value, path) : censorText; return redactPaths.length === 0 - ? { paths: removePaths, remove: true } + ? { paths: removePaths, remove: true, censor } : { paths: [...redactPaths, ...removePaths], - censor: (_value, path) => - redactSet.has(keyFromPath(path)) ? '[Redacted]' : undefined, + censor: (value, path) => + removeSet.has(keyFromPath(path)) + ? undefined + : censorPath(value, path), }; };