@hackylabs/deep-redact 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # Deep Redact
2
2
 
3
- Faster than fast-redact <sup>1</sup> as well as being safer and more configurable than many other redaction libraries,
3
+ Faster than Fast Redact <sup>1</sup> as well as being safer and more configurable than many other redaction libraries,
4
4
  Deep Redact is a zero-dependency tool that redacts sensitive information from strings and objects. It is designed to be
5
5
  used in a production environment where sensitive information needs to be redacted from logs, error messages, files,
6
6
  and other outputs.
7
7
 
8
- Circular references and other unsupported are handled gracefully, and the library is designed to be as fast as possible
9
- while still being configurable.
8
+ Circular references and other unsupported values are handled gracefully, and the library is designed to be as fast as
9
+ possible while still being configurable.
10
10
 
11
11
  Supporting both CommonJS and ESM, with named and default exports, Deep Redact is designed to be versatile and easy to
12
12
  use in any modern JavaScript or TypeScript project in Node or the browser.
@@ -24,25 +24,33 @@ library outside of your global logging/error-reporting libraries.</h4>
24
24
 
25
25
  ```typescript
26
26
  // ./src/example.ts
27
- import {DeepRedact} from 'deep-redact'; // If you're using CommonJS, import with require('deep-redact') instead. Both CommonJS and ESM support named and default imports.
27
+ import {DeepRedact} from '@hackylabs/deep-redact'; // If you're using CommonJS, import with require('@hackylabs/deep-redact') instead. Both CommonJS and ESM support named and default imports.
28
28
 
29
29
  const redaction = new DeepRedact({
30
- replacement: '*',
31
- replaceStringByLength: true,
32
- blacklistedKeys: ['password'],
33
- stringTests: [
34
- /^[\d]{13,16}$/, // payment card number
35
- /^[\d]{3,4}$/ // CVV
36
- ],
37
- });
30
+ blacklistedKeys: ['sensitive', 'password', /name/i],
31
+ serialise: false,
32
+ })
38
33
 
39
34
  const obj = {
40
- password: '<h1><strong>Password</strong></h1>',
41
- cardNumber: '1234567812345678',
42
- cvv: '123',
43
- };
44
-
45
- redaction.redact(obj) // { password: '**********************************', cardNumber: '****************', cvv: '***' }
35
+ keepThis: 'This is fine',
36
+ sensitive: 'This is not fine',
37
+ user: {
38
+ id: 1,
39
+ password: '<h1><strong>Password</strong></h1>',
40
+ firstName: 'John',
41
+ }
42
+ }
43
+
44
+ redaction.redact(obj)
45
+ // {
46
+ // keepThis: 'This is fine',
47
+ // sensitive: '[REDACTED]',
48
+ // user: {
49
+ // id: 1,
50
+ // password: '[REDACTED]',
51
+ // firstName: '[REDACTED]'
52
+ // }
53
+ // }
46
54
  ```
47
55
 
48
56
  ## Configuration
@@ -51,17 +59,17 @@ redaction.redact(obj) // { password: '**********************************', cardN
51
59
 
52
60
  | key | description | type | options | default | required |
53
61
  | --- | --- | --- | --- | --- | --- |
54
- | blacklistedKeys | Deeply compare names of these keys against the keys in your object. | array | Array<string│BlacklistKeyConfig> | [] | N |
62
+ | blacklistedKeys | Deeply compare names of these keys against the keys in your object. | array | Array<string│RegExp│BlacklistKeyConfig> | [] | N |
55
63
  | stringTests | Array of regular expressions to perform against string values, whether that value is a flat string or nested within an object. | array | RegExp[] | [] | N |
56
64
  | fuzzyKeyMatch | Loosely compare key names by checking if the key name of your unredacted object is included anywhere within the name of your blacklisted key. For example, is "pass" (your key) included in "password" (from config). | boolean | | false | N |
57
65
  | caseSensitiveKeyMatch | Loosely compare key names by normalising the strings. This involves removing non-word characters and transforms the string to lowercase. This means you never have to worry having to list duplicate keys in different formats such as snake_case, camelCase, PascalCase or any other case. | boolean | | true | N |
58
66
  | remove | Determines whether or not to remove the key from the object when it is redacted. | boolean | | false | N |
59
67
  | retainStructure | Determines whether or not keep all nested values of a key that is going to be redacted. Circular references are always removed. | boolean | | false | N |
60
- | replacement | When a value is going to be redacted, what would you like to replace it with? | string | | [REDACTED] | N |
68
+ | replacement | When a value is going to be redacted, what would you like to replace it with? | string │ function | | [REDACTED] | N |
61
69
  | replaceStringByLength | When a string value is going to be replaced, optionally replace it by repeating the `replacement` to match the length of the value. For example, if `replaceStringByLength` were set to `true` and `replacement` was set to "x", then redacting "secret" would return "xxxxxx". This is sometimes useful for debugging purposes, although it may be less secure as it could give hints to the original value. | boolean | | false | N |
62
- | types | JS types (values of `typeof` keyword). Only values with a typeof equal to `string`, `number`, `bigint`, `boolean` or `object` may be redacted. The other types are only listed as options to keep TypeScript happy, so you never need to list them. | array | Array<'string'│'number'│'bigint'│'boolean'│'symbol'│'undefined'│'object'│'function'> | ['string'] | N |
63
- | serialise | Determines whether or not to serialise the object after redacting. Typical use cases for this are when you want to send it over the network or save to a file, both of which are common use cases for redacting sensitive information. | boolean | | true | N |
64
- | unsupportedTransformer | When an unsafe value is encountered or a value that cannot be serialised. By default, this function will transform an unsupported value `Unsupported` object. BigInt values are converted a string. Dates are returned using their own `toISOString` method. Regular expressions are returned as objects with their `source` and `flags` values. Errors are converted objects. This is useful when you have a custom class that you would like to redact. For safety reasons, you should always transform a BigInt to avoid JSON.stringify throwing an error. | (value: unknown) => unknown | | DeepRedact.transformUnsupported | N |
70
+ | types | JS types (values of `typeof` keyword). Only values with a typeof equal to `string`, `number`, `bigint`, `boolean`, `symbol`, `object`, or `function` will be redacted. Undefined values will never be redacted, although the type `undefined` is included in this list to keep TypeScript happy. | array | Array<'string'│'number'│'bigint'│'boolean'│'symbol'│'undefined'│'object'│'function'> | ['string'] | N |
71
+ | serialise | Determines whether or not to serialise the object after redacting. Typical use cases for this are when you want to send it over the network or save to a file, both of which are common use cases for redacting sensitive information. | boolean | | false | N |
72
+ | serialize | Alias of `serialise` for International-English users. | boolean | | false | N |
65
73
 
66
74
  ### BlacklistKeyConfig
67
75
 
@@ -74,34 +82,31 @@ redaction.redact(obj) // { password: '**********************************', cardN
74
82
  | retainStructure | boolean | Main options `retainStructure` | N |
75
83
 
76
84
  ### Benchmark
77
- Comparisons are made against JSON.stringify and fast-redact as well as different configurations of deep-redact, using
78
- [this test object](./test/setup/dummyUser.ts). The benchmark is run on a 2021 iMac with an M1 chip with 16GB memory
79
- running Sonoma 14.5.
85
+ Comparisons are made against JSON.stringify and Fast Redact as well as different configurations of Deep Redact, using
86
+ [this test object](./test/setup/dummyUser.ts). Fast Redact was configured to redact the same keys on the same object as
87
+ Deep Redact without using wildcards.
88
+
89
+ The benchmark is run on a 2021 iMac with an M1 chip with 16GB memory running Sonoma 14.5.
80
90
 
81
91
  JSON.stringify is included as a benchmark because it is the fastest way to deeply iterate over an object although it
82
92
  doesn't redact any sensitive information. Fast-redact is included as a benchmark because it's the next fastest redaction
83
- library available. Neither JSON.stringify nor fast-redact offer the same level of configurability as deep-redact.
93
+ library available. Neither JSON.stringify nor Fast Redact offer the same level of configurability as deep-redact.
84
94
 
85
95
  ![Benchmark](./benchmark.png)
86
96
 
87
97
  | scenario | ops / sec | op duration (ms) | margin of error | sample count |
88
98
  | --- | --- | --- | --- | --- |
89
- | JSON.stringify, tiny object | 3878848.93 | 0.0002578084 | 0 | 1939425 |
90
- | DeepRedact, default config, tiny object | 1530332.28 | 0.0006534529 | 0.00001 | 765167 |
91
- | JSON.stringify, large object | 295526.11 | 0.0033837958 | 0.00001 | 147764 |
92
- | fast redact, tiny object | 228053.93 | 0.0043849277 | 0.00002 | 114027 |
93
- | DeepRedact, default config, large object | 92714.28 | 0.0107858256 | 0.00006 | 46358 |
94
- | DeepRedact, remove item, single object | 92349.45 | 0.0108284349 | 0.00005 | 46175 |
95
- | DeepRedact, fuzzy matching, single object | 89414.82 | 0.0111838282 | 0.00007 | 44708 |
96
- | DeepRedact, fuzzy and case insensitive matching, single object | 87852.36 | 0.0113827334 | 0.00006 | 43927 |
97
- | DeepRedact, case insensitive matching, single object | 86797.23 | 0.0115211045 | 0.00006 | 43399 |
98
- | DeepRedact, replace string by length, single object | 84150.95 | 0.011883407 | 0.00006 | 42076 |
99
- | DeepRedact, config per key, single object | 71236.85 | 0.0140376786 | 0.00009 | 35619 |
100
- | DeepRedact, retain structure, single object | 69738.86 | 0.0143392076 | 0.00007 | 34870 |
101
- | fast redact, large object | 19480.95 | 0.0513321865 | 0.00038 | 9741 |
102
- | JSON.stringify, 1000 tiny objects | 16003.16 | 0.0624876526 | 0.00017 | 8002 |
103
- | DeepRedact, default config, 1000 tiny objects | 15827.22 | 0.0631822932 | 0.00043 | 7914 |
104
- | DeepRedact, default config, 1000 large objects | 13705.52 | 0.072963285 | 0.00054 | 6853 |
105
- | fast redact, 1000 tiny objects | 7430.88 | 0.1345735164 | 0.00067 | 3716 |
106
- | JSON.stringify, 1000 large objects | 423.52 | 2.3611880519 | 0.00982 | 212 |
107
- | fast redact, 1000 large objects | 77.32 | 12.9327971026 | 0.25939 | 39 |
99
+ | JSON.stringify, large object | 295500.62 | 0.0033840876 | 0.00002 | 147751 |
100
+ | DeepRedact, remove item, single object | 36272.4 | 0.0275691709 | 0.00016 | 18137 |
101
+ | DeepRedact, custom replacer function, single object | 30314.59 | 0.0329874115 | 0.00028 | 15158 |
102
+ | DeepRedact, default config, large object | 30028.19 | 0.0333020395 | 0.0002 | 15015 |
103
+ | DeepRedact, replace string by length, single object | 28756.9 | 0.0347742688 | 0.00028 | 14379 |
104
+ | DeepRedact, retain structure, single object | 24803.01 | 0.0403176903 | 0.00032 | 12402 |
105
+ | DeepRedact, fuzzy matching, single object | 22243.3 | 0.0449573621 | 0.00038 | 11122 |
106
+ | DeepRedact, config per key, single object | 21603.85 | 0.0462880355 | 0.0013 | 10802 |
107
+ | fast redact, large object | 9529.2 | 0.1049406557 | 0.00064 | 4765 |
108
+ | DeepRedact, case insensitive matching, single object | 6503.72 | 0.1537581959 | 0.00105 | 3252 |
109
+ | DeepRedact, default config, 1000 large objects | 5915.05 | 0.1690602382 | 0.00296 | 2958 |
110
+ | DeepRedact, fuzzy and case insensitive matching, single object | 5591.96 | 0.1788283015 | 0.00184 | 2796 |
111
+ | JSON.stringify, 1000 large objects | 394.41 | 2.5354059248 | 0.01001 | 198 |
112
+ | fast redact, 1000 large objects | 172.23 | 5.8060829174 | 0.06886 | 87 |
package/dist/cjs/index.js CHANGED
@@ -1,129 +1,152 @@
1
1
  "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
2
16
  Object.defineProperty(exports, "__esModule", { value: true });
3
17
  exports.DeepRedact = exports.default = void 0;
4
- const normaliseString = (key) => key.toLowerCase().replace(/\W/g, '');
18
+ const redactorUtils_1 = __importDefault(require("./utils/redactorUtils"));
5
19
  class DeepRedact {
20
+ /**
21
+ * Create a new DeepRedact instance with the provided configuration.
22
+ * The configuration will be merged with the default configuration.
23
+ * `blacklistedKeys` will be normalised to an array inherited from the default configuration as the default values.
24
+ * @param {DeepRedactConfig} config. The configuration for the redaction.
25
+ */
6
26
  constructor(config) {
7
- var _a, _b;
8
- this.circularReference = new WeakSet();
27
+ /**
28
+ * A WeakSet to store circular references during redaction. Reset to null after redaction is complete.
29
+ * @private
30
+ */
31
+ this.circularReference = null;
32
+ /**
33
+ * The configuration for the redaction.
34
+ * @private
35
+ */
9
36
  this.config = {
10
- blacklistedKeys: [],
11
- stringTests: [],
12
- fuzzyKeyMatch: false,
13
- caseSensitiveKeyMatch: true,
14
- retainStructure: false,
15
- remove: false,
16
- replaceStringByLength: false,
17
- replacement: '[REDACTED]',
18
- types: ['string'],
19
- serialise: true,
20
- unsupportedTransformer: DeepRedact.unsupportedTransformer,
37
+ serialise: false,
21
38
  };
22
- this.removeCircular = (value) => {
23
- var _a, _b;
24
- if (!(value instanceof Object))
25
- return value;
26
- if (!((_a = this.circularReference) === null || _a === void 0 ? void 0 : _a.has(value))) {
27
- (_b = this.circularReference) === null || _b === void 0 ? void 0 : _b.add(value);
39
+ /**
40
+ * A transformer for unsupported data types. If `serialise` is false, the value will be returned as is,
41
+ * otherwise it will transform the value into a format that is supported by JSON.stringify.
42
+ *
43
+ * Error, RegExp, and Date instances are technically supported by JSON.stringify,
44
+ * but they returned as empty objects, therefore they are also transformed here.
45
+ * @protected
46
+ * @param {unknown} value The value that is not supported by JSON.stringify.
47
+ * @returns {unknown} The value in a format that is supported by JSON.stringify.
48
+ */
49
+ this.unsupportedTransformer = (value) => {
50
+ if (!this.config.serialise)
28
51
  return value;
52
+ if (typeof value === 'bigint') {
53
+ return {
54
+ __unsupported: {
55
+ type: 'bigint',
56
+ value: value.toString(),
57
+ radix: 10,
58
+ },
59
+ };
29
60
  }
30
- return '__circular__';
31
- };
32
- this.redactString = (value, parentShouldRedact = false) => {
33
- if (!this.config.stringTests.some((test) => test.test(value)) && !parentShouldRedact)
34
- return value;
35
- if (this.config.replaceStringByLength)
36
- return this.config.replacement.repeat(value.length);
37
- return this.config.remove ? undefined : this.config.replacement;
38
- };
39
- this.shouldRedactObjectValue = (key) => {
40
- return this.config.blacklistedKeys.some((redactableKey) => (typeof redactableKey === 'string'
41
- ? key === redactableKey
42
- : DeepRedact.complexShouldRedact(key, redactableKey)));
43
- };
44
- this.deepRedact = (value, parentShouldRedact = false) => {
45
- if (value === undefined || value === null)
46
- return value;
47
- let safeValue = this.removeCircular(value);
48
- safeValue = this.config.unsupportedTransformer(safeValue);
49
- if (!(safeValue instanceof Object)) {
50
- // @ts-expect-error - we already know that safeValue is not a function, symbol, undefined, null, or an object
51
- if (!this.config.types.includes(typeof safeValue))
52
- return safeValue;
53
- if (typeof safeValue === 'string')
54
- return this.redactString(safeValue, parentShouldRedact);
55
- if (!parentShouldRedact)
56
- return safeValue;
57
- return this.config.remove
58
- ? undefined
59
- : this.config.replacement;
61
+ if (value instanceof Error) {
62
+ return {
63
+ __unsupported: {
64
+ type: 'error',
65
+ name: value.name,
66
+ message: value.message,
67
+ stack: value.stack,
68
+ },
69
+ };
60
70
  }
61
- if (parentShouldRedact && (!this.config.retainStructure || this.config.remove)) {
62
- return this.config.remove ? undefined : this.config.replacement;
71
+ if (value instanceof RegExp) {
72
+ return {
73
+ __unsupported: {
74
+ type: 'regexp',
75
+ source: value.source,
76
+ flags: value.flags,
77
+ },
78
+ };
79
+ }
80
+ if (value instanceof Date)
81
+ return value.toISOString();
82
+ return value;
83
+ };
84
+ /**
85
+ * Calls `unsupportedTransformer` on the provided value and rewrites any circular references.
86
+ *
87
+ * Circular references will always be removed to avoid infinite recursion.
88
+ * When a circular reference is found, the value will be replaced with `[[CIRCULAR_REFERENCE: path.to.original.value]]`.
89
+ * @protected
90
+ * @param {unknown} value The value to rewrite.
91
+ * @param {string | undefined} path The path to the value in the object.
92
+ * @returns {unknown} The rewritten value.
93
+ */
94
+ this.rewriteUnsupported = (value, path) => {
95
+ const safeValue = this.unsupportedTransformer(value);
96
+ if (!(safeValue instanceof Object))
97
+ return safeValue;
98
+ if (this.circularReference === null)
99
+ this.circularReference = new WeakSet();
100
+ if (Array.isArray(safeValue)) {
101
+ return safeValue.map((val, index) => {
102
+ var _a, _b;
103
+ const newPath = path ? `${path}.[${index}]` : `[${index}]`;
104
+ if ((_a = this.circularReference) === null || _a === void 0 ? void 0 : _a.has(val))
105
+ return `[[CIRCULAR_REFERENCE: ${newPath}]]`;
106
+ if (val instanceof Object) {
107
+ (_b = this.circularReference) === null || _b === void 0 ? void 0 : _b.add(val);
108
+ return this.rewriteUnsupported(val, newPath);
109
+ }
110
+ return val;
111
+ });
63
112
  }
64
- if (Array.isArray(safeValue))
65
- return safeValue.map((val) => this.deepRedact(val, parentShouldRedact));
66
113
  return Object.fromEntries(Object.entries(safeValue).map(([key, val]) => {
67
- const shouldRedact = parentShouldRedact || this.shouldRedactObjectValue(key);
68
- return [key, this.deepRedact(val, shouldRedact)];
114
+ var _a, _b;
115
+ const newPath = path ? `${path}.${key}` : key;
116
+ if ((_a = this.circularReference) === null || _a === void 0 ? void 0 : _a.has(val))
117
+ return [key, `[[CIRCULAR_REFERENCE: ${newPath}]]`];
118
+ if (val instanceof Object)
119
+ (_b = this.circularReference) === null || _b === void 0 ? void 0 : _b.add(val);
120
+ return [key, this.rewriteUnsupported(val, path ? `${path}.${key}` : key)];
69
121
  }));
70
122
  };
71
- this.redact = (value) => {
72
- this.circularReference = new WeakSet();
73
- const redacted = this.deepRedact(value);
123
+ /**
124
+ * Depending on the value of `serialise`, return the value as a JSON string or as the provided value.
125
+ *
126
+ * Also resets the `circularReference` property to null after redaction is complete.
127
+ * This is to ensure that the WeakSet doesn't cause memory leaks.
128
+ * @private
129
+ * @param value
130
+ */
131
+ this.maybeSerialise = (value) => {
74
132
  this.circularReference = null;
75
- return this.config.serialise ? JSON.stringify(redacted) : redacted;
133
+ return this.config.serialise ? JSON.stringify(value) : value;
76
134
  };
77
- this.config = Object.assign(Object.assign(Object.assign({}, this.config), config), { blacklistedKeys: (_b = (_a = config.blacklistedKeys) === null || _a === void 0 ? void 0 : _a.map((key) => {
78
- if (typeof key === 'string')
79
- return key;
80
- return Object.assign({ fuzzyKeyMatch: this.config.fuzzyKeyMatch, caseSensitiveKeyMatch: this.config.caseSensitiveKeyMatch, retainStructure: this.config.retainStructure, remove: this.config.remove }, key);
81
- })) !== null && _b !== void 0 ? _b : [] });
135
+ /**
136
+ * Redact the provided value. The value will be stripped of any circular references and other unsupported data types, before being redacted according to the configuration and finally serialised if required.
137
+ * @param {unknown} value The value to redact.
138
+ * @returns {unknown} The redacted value.
139
+ */
140
+ this.redact = (value) => {
141
+ return this.maybeSerialise(this.redactorUtils.recurse(this.rewriteUnsupported(value)));
142
+ };
143
+ const { serialise, serialize } = config, rest = __rest(config, ["serialise", "serialize"]);
144
+ this.redactorUtils = new redactorUtils_1.default(rest);
145
+ if (serialise !== undefined)
146
+ this.config.serialise = serialise;
147
+ if (serialize !== undefined)
148
+ this.config.serialise = serialize;
82
149
  }
83
150
  }
84
151
  exports.default = DeepRedact;
85
152
  exports.DeepRedact = DeepRedact;
86
- DeepRedact.unsupportedTransformer = (value) => {
87
- if (typeof value === 'bigint') {
88
- return {
89
- __unsupported: {
90
- type: 'bigint',
91
- value: value.toString(),
92
- radix: 10,
93
- },
94
- };
95
- }
96
- if (value instanceof Error) {
97
- return {
98
- __unsupported: {
99
- type: 'error',
100
- name: value.name,
101
- message: value.message,
102
- stack: value.stack,
103
- },
104
- };
105
- }
106
- if (value instanceof RegExp) {
107
- return {
108
- __unsupported: {
109
- type: 'regexp',
110
- source: value.source,
111
- flags: value.flags,
112
- },
113
- };
114
- }
115
- if (value instanceof Date)
116
- return value.toISOString();
117
- return value;
118
- };
119
- DeepRedact.complexShouldRedact = (key, config) => {
120
- if (config.key instanceof RegExp)
121
- return config.key.test(key);
122
- if (config.fuzzyKeyMatch && config.caseSensitiveKeyMatch)
123
- return key.includes(config.key);
124
- if (config.fuzzyKeyMatch && !config.caseSensitiveKeyMatch)
125
- return normaliseString(key).includes(normaliseString(config.key));
126
- if (!config.fuzzyKeyMatch && config.caseSensitiveKeyMatch)
127
- return key === config.key;
128
- return normaliseString(config.key) === normaliseString(key);
129
- };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const defaultConfig = {
4
+ stringTests: [],
5
+ blacklistedKeys: [],
6
+ blacklistedKeysTransformed: [],
7
+ fuzzyKeyMatch: false,
8
+ caseSensitiveKeyMatch: true,
9
+ retainStructure: false,
10
+ remove: false,
11
+ replaceStringByLength: false,
12
+ replacement: '[REDACTED]',
13
+ types: ['string'],
14
+ };
15
+ class RedactorUtils {
16
+ constructor(customConfig) {
17
+ var _a, _b, _c;
18
+ /**
19
+ * The configuration for the redaction.
20
+ * @private
21
+ */
22
+ this.config = defaultConfig;
23
+ /**
24
+ * Get the configuration for an object key. This will check the key against the transformed blacklisted keys.
25
+ * @private
26
+ * @param {string} key The key of the configuration to get.
27
+ * @returns {Required<BlacklistKeyConfig> | undefined} The configuration for the key.
28
+ */
29
+ this.getBlacklistedKeyConfig = (key) => {
30
+ var _a;
31
+ if (!key)
32
+ return undefined;
33
+ return (_a = this.config.blacklistedKeysTransformed) === null || _a === void 0 ? void 0 : _a.find((redactableKey) => {
34
+ return RedactorUtils.complexKeyMatch(key, redactableKey);
35
+ });
36
+ };
37
+ /**
38
+ * Get the recursion configuration for a key. This will check the key against the transformed blacklisted keys.
39
+ * If the key is found, the configuration for the key will be returned, otherwise undefined.
40
+ * @private
41
+ * @param {string} key The key of the configuration to get.
42
+ * @returns {Required<Pick<BlacklistKeyConfig, 'remove' | 'replacement' | 'retainStructure'>>} The configuration for the key.
43
+ */
44
+ this.getRecursionConfig = (key) => {
45
+ const fallback = {
46
+ remove: this.config.remove,
47
+ replacement: this.config.replacement,
48
+ retainStructure: this.config.retainStructure,
49
+ };
50
+ if (!key)
51
+ return fallback;
52
+ const blacklistedKeyConfig = this.getBlacklistedKeyConfig(key);
53
+ if (!blacklistedKeyConfig)
54
+ return fallback;
55
+ return {
56
+ remove: blacklistedKeyConfig.remove,
57
+ replacement: blacklistedKeyConfig.replacement,
58
+ retainStructure: blacklistedKeyConfig.retainStructure,
59
+ };
60
+ };
61
+ /**
62
+ * Determine if a key should be redacted. This will check the key against the blacklisted keys, using the default configuration.
63
+ * @private
64
+ * @param {string} key The key to check.
65
+ * @returns {boolean} Whether the key should be redacted.
66
+ */
67
+ this.shouldRedactObjectValue = (key) => {
68
+ if (!key)
69
+ return false;
70
+ return this.config.blacklistedKeysTransformed.some((redactableKey) => {
71
+ return RedactorUtils.complexKeyMatch(key, redactableKey);
72
+ });
73
+ };
74
+ /**
75
+ * Redact a string. This will redact the string based on the configuration, redacting the string if it matches a pattern or if the parent key should be redacted.
76
+ * @private
77
+ * @param value
78
+ * @param replacement
79
+ * @param remove
80
+ * @param shouldRedact
81
+ */
82
+ this.redactString = (value, replacement, remove, shouldRedact) => {
83
+ if (!value)
84
+ return value;
85
+ const { stringTests } = this.config;
86
+ if (!shouldRedact && !(stringTests === null || stringTests === void 0 ? void 0 : stringTests.some((pattern) => pattern.test(value))))
87
+ return value;
88
+ if (remove)
89
+ return undefined;
90
+ if (typeof replacement === 'function')
91
+ return replacement(value);
92
+ if (this.config.replaceStringByLength)
93
+ return replacement.repeat(value.length);
94
+ return replacement;
95
+ };
96
+ /**
97
+ * Redact a primitive value. This will redact the value if it is a supported type, not an object or array, otherwise it will return the value unchanged.
98
+ * @private
99
+ * @param {unknown} value The value to redact.
100
+ * @param {Transformer | string} replacement The replacement value for redacted data.
101
+ * @param {boolean} remove Whether the redacted data should be removed.
102
+ * @param {boolean} shouldRedact Whether the value should be redacted based on the parent key.
103
+ * @returns {unknown} The redacted value.
104
+ */
105
+ this.redactPrimitive = (value, replacement, remove, shouldRedact) => {
106
+ if (!this.config.types.includes(typeof value))
107
+ return value;
108
+ if (remove && shouldRedact && typeof value !== 'string')
109
+ return undefined;
110
+ if (typeof value === 'string')
111
+ return this.redactString(value, replacement, remove, shouldRedact);
112
+ if (!shouldRedact)
113
+ return value;
114
+ if (typeof replacement === 'function')
115
+ return replacement(value);
116
+ return replacement;
117
+ };
118
+ /**
119
+ * Redact an array. This will redact each value in the array using the `recurse` method.
120
+ * @private
121
+ * @param {unknown[]} value The array to redact.
122
+ * @returns {unknown[]} The redacted array.
123
+ */
124
+ this.redactArray = (value) => value.map((val) => this.recurse(val));
125
+ /**
126
+ * Redact an object. This will recursively redact the object based on the configuration, redacting the keys and values as required.
127
+ * @param {Object} value The object to redact.
128
+ * @param {string | null} key The key of the object if it is part of another object.
129
+ * @param {boolean} parentShouldRedact Whether the item should be redacted based on the key within the parent object.
130
+ */
131
+ this.redactObject = (value, key, parentShouldRedact) => {
132
+ return Object.fromEntries(Object.entries(value).map(([prop, val]) => {
133
+ const shouldRedact = parentShouldRedact || this.shouldRedactObjectValue(prop);
134
+ if (shouldRedact) {
135
+ const { remove } = this.getRecursionConfig(prop);
136
+ if (remove)
137
+ return [];
138
+ }
139
+ return [prop, this.recurse(val, key !== null && key !== void 0 ? key : prop, shouldRedact)];
140
+ }).filter(([prop]) => prop !== undefined));
141
+ };
142
+ /**
143
+ * Redact a value. If the value is an object or array, the redaction will be performed recursively, otherwise the value will be redacted if it is a supported type using the `replace` method.
144
+ * @private
145
+ * @param {unknown} value The value to redact.
146
+ * @param {string | null} key The key of the value if it is part of an object.
147
+ * @param {boolean} parentShouldRedact Whether the parent object should be redacted.
148
+ * @returns {unknown} The redacted value.
149
+ */
150
+ this.recurse = (value, key, parentShouldRedact) => {
151
+ if (value === null)
152
+ return value;
153
+ const { remove, replacement, retainStructure } = this.getRecursionConfig(key);
154
+ if (!(value instanceof Object))
155
+ return this.redactPrimitive(value, replacement, remove, Boolean(key && parentShouldRedact));
156
+ if (parentShouldRedact) {
157
+ if (!retainStructure) {
158
+ return typeof replacement === 'function'
159
+ ? replacement(value)
160
+ : replacement;
161
+ }
162
+ }
163
+ if (Array.isArray(value))
164
+ return this.redactArray(value);
165
+ return this.redactObject(value, key, parentShouldRedact);
166
+ };
167
+ this.config = Object.assign(Object.assign(Object.assign({}, defaultConfig), customConfig), { blacklistedKeys: (_a = customConfig.blacklistedKeys) !== null && _a !== void 0 ? _a : [], blacklistedKeysTransformed: (_c = (_b = customConfig.blacklistedKeys) === null || _b === void 0 ? void 0 : _b.map((key) => {
168
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
169
+ const isObject = !(typeof key === 'string' || key instanceof RegExp);
170
+ const setKey = isObject ? key.key : key;
171
+ const fallback = {
172
+ fuzzyKeyMatch: (_a = customConfig.fuzzyKeyMatch) !== null && _a !== void 0 ? _a : defaultConfig.fuzzyKeyMatch,
173
+ caseSensitiveKeyMatch: (_b = customConfig.caseSensitiveKeyMatch) !== null && _b !== void 0 ? _b : defaultConfig.caseSensitiveKeyMatch,
174
+ retainStructure: (_c = customConfig.retainStructure) !== null && _c !== void 0 ? _c : defaultConfig.retainStructure,
175
+ replacement: (_d = customConfig.replacement) !== null && _d !== void 0 ? _d : defaultConfig.replacement,
176
+ remove: (_e = customConfig.remove) !== null && _e !== void 0 ? _e : defaultConfig.remove,
177
+ key: setKey,
178
+ };
179
+ if (isObject) {
180
+ return {
181
+ fuzzyKeyMatch: (_f = key.fuzzyKeyMatch) !== null && _f !== void 0 ? _f : fallback.fuzzyKeyMatch,
182
+ caseSensitiveKeyMatch: (_g = key.caseSensitiveKeyMatch) !== null && _g !== void 0 ? _g : fallback.caseSensitiveKeyMatch,
183
+ retainStructure: (_h = key.retainStructure) !== null && _h !== void 0 ? _h : fallback.retainStructure,
184
+ replacement: (_j = key.replacement) !== null && _j !== void 0 ? _j : fallback.replacement,
185
+ remove: (_k = key.remove) !== null && _k !== void 0 ? _k : fallback.remove,
186
+ key: setKey,
187
+ };
188
+ }
189
+ return fallback;
190
+ })) !== null && _c !== void 0 ? _c : [] });
191
+ }
192
+ }
193
+ /**
194
+ * Normalise a string for comparison. This will convert the string to lowercase and remove any non-word characters.
195
+ * @private
196
+ * @param str The string to normalise.
197
+ * @returns {string} The normalised string.
198
+ */
199
+ RedactorUtils.normaliseString = (str) => str.toLowerCase().replace(/\W/g, '');
200
+ /**
201
+ * Determine if a key matches a given blacklistedKeyConfig. This will check the key against the blacklisted keys,
202
+ * using the configuration option for the given key falling back to the default configuration.
203
+ * @private
204
+ * @param {string} key The key to check.
205
+ * @param {BlacklistKeyConfig} blacklistKeyConfig The configuration for the key.
206
+ * @returns {boolean} Whether the key should be redacted.
207
+ */
208
+ RedactorUtils.complexKeyMatch = (key, blacklistKeyConfig) => {
209
+ if (blacklistKeyConfig.key instanceof RegExp)
210
+ return blacklistKeyConfig.key.test(key);
211
+ if (blacklistKeyConfig.fuzzyKeyMatch && blacklistKeyConfig.caseSensitiveKeyMatch)
212
+ return key.includes(blacklistKeyConfig.key);
213
+ if (blacklistKeyConfig.fuzzyKeyMatch && !blacklistKeyConfig.caseSensitiveKeyMatch)
214
+ return RedactorUtils.normaliseString(key).includes(RedactorUtils.normaliseString(blacklistKeyConfig.key));
215
+ if (!blacklistKeyConfig.fuzzyKeyMatch && blacklistKeyConfig.caseSensitiveKeyMatch)
216
+ return key === blacklistKeyConfig.key;
217
+ return RedactorUtils.normaliseString(blacklistKeyConfig.key) === RedactorUtils.normaliseString(key);
218
+ };
219
+ exports.default = RedactorUtils;