@hackylabs/deep-redact 2.2.0 → 3.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/LICENSE +1 -1
- package/README.md +36 -56
- package/dist/cjs/index.js +10 -131
- package/dist/cjs/utils/index.js +429 -0
- package/dist/cjs/utils/standardTransformers/bigint.js +10 -0
- package/dist/cjs/utils/standardTransformers/date.js +9 -0
- package/dist/cjs/utils/standardTransformers/error.js +16 -0
- package/dist/cjs/utils/standardTransformers/index.js +19 -0
- package/dist/cjs/utils/standardTransformers/map.js +9 -0
- package/dist/cjs/utils/standardTransformers/regex.js +15 -0
- package/dist/cjs/utils/standardTransformers/set.js +9 -0
- package/dist/cjs/utils/standardTransformers/url.js +9 -0
- package/dist/esm/index.mjs +9 -128
- package/dist/esm/utils/index.mjs +423 -0
- package/dist/esm/utils/standardTransformers/bigint.js +6 -0
- package/dist/esm/utils/standardTransformers/date.js +5 -0
- package/dist/esm/utils/standardTransformers/error.js +12 -0
- package/dist/esm/utils/standardTransformers/index.js +16 -0
- package/dist/esm/utils/standardTransformers/map.js +5 -0
- package/dist/esm/utils/standardTransformers/regex.js +11 -0
- package/dist/esm/utils/standardTransformers/set.js +5 -0
- package/dist/esm/utils/standardTransformers/url.js +5 -0
- package/dist/types/index.d.ts +3 -38
- package/dist/types/types.d.ts +48 -17
- package/dist/types/utils/index.d.ts +130 -0
- package/dist/types/utils/standardTransformers/bigint.d.ts +2 -0
- package/dist/types/utils/standardTransformers/date.d.ts +2 -0
- package/dist/types/utils/standardTransformers/error.d.ts +2 -0
- package/dist/types/utils/standardTransformers/index.d.ts +2 -0
- package/dist/types/utils/standardTransformers/map.d.ts +2 -0
- package/dist/types/utils/standardTransformers/regex.d.ts +2 -0
- package/dist/types/utils/standardTransformers/set.d.ts +2 -0
- package/dist/types/utils/standardTransformers/url.d.ts +2 -0
- package/package.json +66 -13
- package/dist/cjs/utils/redactorUtils.js +0 -263
- package/dist/esm/utils/redactorUtils.mjs +0 -264
- package/dist/types/utils/redactorUtils.d.ts +0 -91
package/dist/esm/index.mjs
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import RedactorUtils from './utils
|
|
1
|
+
import RedactorUtils from './utils';
|
|
2
2
|
class DeepRedact {
|
|
3
3
|
/**
|
|
4
4
|
* The redactorUtils instance to handle the redaction.
|
|
5
5
|
* @private
|
|
6
6
|
*/
|
|
7
7
|
redactorUtils;
|
|
8
|
-
/**
|
|
9
|
-
* A WeakSet to store circular references during redaction. Reset to null after redaction is complete.
|
|
10
|
-
* @private
|
|
11
|
-
*/
|
|
12
|
-
circularReference = null;
|
|
13
8
|
/**
|
|
14
9
|
* The configuration for the redaction.
|
|
15
10
|
* @private
|
|
@@ -25,134 +20,20 @@ class DeepRedact {
|
|
|
25
20
|
*/
|
|
26
21
|
constructor(config) {
|
|
27
22
|
const { serialise, serialize, ...rest } = config;
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
this.config.serialise =
|
|
31
|
-
|
|
32
|
-
this.config.serialise = serialize;
|
|
23
|
+
const englishSerialise = serialise ?? serialize;
|
|
24
|
+
if (typeof englishSerialise === 'boolean')
|
|
25
|
+
this.config.serialise = englishSerialise;
|
|
26
|
+
this.redactorUtils = new RedactorUtils({ ...rest });
|
|
33
27
|
}
|
|
34
|
-
/**
|
|
35
|
-
* A transformer for unsupported data types. If `serialise` is false, the value will be returned as is,
|
|
36
|
-
* otherwise it will transform the value into a format that is supported by JSON.stringify.
|
|
37
|
-
*
|
|
38
|
-
* Error, RegExp, and Date instances are technically supported by JSON.stringify,
|
|
39
|
-
* but they returned as empty objects, therefore they are also transformed here.
|
|
40
|
-
* @protected
|
|
41
|
-
* @param {unknown} value The value that is not supported by JSON.stringify.
|
|
42
|
-
* @returns {unknown} The value in a format that is supported by JSON.stringify.
|
|
43
|
-
*/
|
|
44
|
-
unsupportedTransformer = (value) => {
|
|
45
|
-
if (!this.config.serialise)
|
|
46
|
-
return value;
|
|
47
|
-
if (typeof value === 'bigint') {
|
|
48
|
-
return {
|
|
49
|
-
__unsupported: {
|
|
50
|
-
type: 'bigint',
|
|
51
|
-
value: value.toString(10),
|
|
52
|
-
radix: 10,
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
if (value instanceof Error) {
|
|
57
|
-
return {
|
|
58
|
-
__unsupported: {
|
|
59
|
-
type: 'error',
|
|
60
|
-
name: value.name,
|
|
61
|
-
message: value.message,
|
|
62
|
-
stack: value.stack,
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
if (value instanceof RegExp) {
|
|
67
|
-
return {
|
|
68
|
-
__unsupported: {
|
|
69
|
-
type: 'regexp',
|
|
70
|
-
source: value.source,
|
|
71
|
-
flags: value.flags,
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
if (value instanceof Set) {
|
|
76
|
-
return {
|
|
77
|
-
__unsupported: {
|
|
78
|
-
type: 'set',
|
|
79
|
-
values: Array.from(value),
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
if (value instanceof Map) {
|
|
84
|
-
return {
|
|
85
|
-
__unsupported: {
|
|
86
|
-
type: 'map',
|
|
87
|
-
entries: Object.fromEntries(value.entries()),
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
if (value instanceof URL)
|
|
92
|
-
return value.toString();
|
|
93
|
-
if (value instanceof Date)
|
|
94
|
-
return value.toISOString();
|
|
95
|
-
return value;
|
|
96
|
-
};
|
|
97
|
-
/**
|
|
98
|
-
* Calls `unsupportedTransformer` on the provided value and rewrites any circular references.
|
|
99
|
-
*
|
|
100
|
-
* Circular references will always be removed to avoid infinite recursion.
|
|
101
|
-
* When a circular reference is found, the value will be replaced with `[[CIRCULAR_REFERENCE: path.to.original.value]]`.
|
|
102
|
-
* @protected
|
|
103
|
-
* @param {unknown} value The value to rewrite.
|
|
104
|
-
* @param {string | undefined} path The path to the value in the object.
|
|
105
|
-
* @returns {unknown} The rewritten value.
|
|
106
|
-
*/
|
|
107
|
-
rewriteUnsupported = (value, path) => {
|
|
108
|
-
const safeValue = this.unsupportedTransformer(value);
|
|
109
|
-
if (!(safeValue instanceof Object))
|
|
110
|
-
return safeValue;
|
|
111
|
-
if (this.circularReference === null)
|
|
112
|
-
this.circularReference = new WeakSet();
|
|
113
|
-
if (Array.isArray(safeValue)) {
|
|
114
|
-
return safeValue.map((val, index) => {
|
|
115
|
-
const newPath = path ? `${path}.[${index}]` : `[${index}]`;
|
|
116
|
-
if (this.circularReference?.has(val))
|
|
117
|
-
return `[[CIRCULAR_REFERENCE: ${newPath}]]`;
|
|
118
|
-
if (val instanceof Object) {
|
|
119
|
-
this.circularReference?.add(val);
|
|
120
|
-
return this.rewriteUnsupported(val, newPath);
|
|
121
|
-
}
|
|
122
|
-
return val;
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
return Object.fromEntries(Object.entries(safeValue).map(([key, val]) => {
|
|
126
|
-
const newPath = path ? `${path}.${key}` : key;
|
|
127
|
-
if (this.circularReference?.has(val))
|
|
128
|
-
return [key, `[[CIRCULAR_REFERENCE: ${newPath}]]`];
|
|
129
|
-
if (val instanceof Object)
|
|
130
|
-
this.circularReference?.add(val);
|
|
131
|
-
return [key, this.rewriteUnsupported(val, path ? `${path}.${key}` : key)];
|
|
132
|
-
}));
|
|
133
|
-
};
|
|
134
|
-
/**
|
|
135
|
-
* Depending on the value of `serialise`, return the value as a JSON string or as the provided value.
|
|
136
|
-
*
|
|
137
|
-
* Also resets the `circularReference` property to null after redaction is complete.
|
|
138
|
-
* This is to ensure that the WeakSet doesn't cause memory leaks.
|
|
139
|
-
* @private
|
|
140
|
-
* @param value
|
|
141
|
-
*/
|
|
142
|
-
maybeSerialise = (value) => {
|
|
143
|
-
this.circularReference = null;
|
|
144
|
-
const result = this.redactorUtils.partialStringRedact(value);
|
|
145
|
-
if (!this.config.serialise)
|
|
146
|
-
return result;
|
|
147
|
-
return typeof result === 'string' ? result : JSON.stringify(result);
|
|
148
|
-
};
|
|
149
28
|
/**
|
|
150
29
|
* 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.
|
|
151
30
|
* @param {unknown} value The value to redact.
|
|
152
31
|
* @returns {unknown} The redacted value.
|
|
32
|
+
* @throws {Error} If the value cannot be serialised to JSON and serialise is true.
|
|
153
33
|
*/
|
|
154
34
|
redact = (value) => {
|
|
155
|
-
|
|
35
|
+
const redacted = this.redactorUtils.traverse(value);
|
|
36
|
+
return this.config.serialise ? JSON.stringify(redacted) : redacted;
|
|
156
37
|
};
|
|
157
38
|
}
|
|
158
|
-
export { DeepRedact as default,
|
|
39
|
+
export { DeepRedact, DeepRedact as default, };
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { standardTransformers } from './standardTransformers';
|
|
2
|
+
const defaultConfig = {
|
|
3
|
+
stringTests: [],
|
|
4
|
+
blacklistedKeys: [],
|
|
5
|
+
fuzzyKeyMatch: false,
|
|
6
|
+
caseSensitiveKeyMatch: true,
|
|
7
|
+
retainStructure: false,
|
|
8
|
+
remove: false,
|
|
9
|
+
replaceStringByLength: false,
|
|
10
|
+
replacement: '[REDACTED]',
|
|
11
|
+
types: ['string'],
|
|
12
|
+
transformers: standardTransformers,
|
|
13
|
+
};
|
|
14
|
+
class RedactorUtils {
|
|
15
|
+
/**
|
|
16
|
+
* The configuration for the redaction.
|
|
17
|
+
* @private
|
|
18
|
+
*/
|
|
19
|
+
config = defaultConfig;
|
|
20
|
+
/**
|
|
21
|
+
* The computed regex pattern generated from sanitised blacklist keys of flat strings
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
computedRegex = null;
|
|
25
|
+
/**
|
|
26
|
+
* Regex to sanitise strings for the computed regex
|
|
27
|
+
* @private
|
|
28
|
+
*/
|
|
29
|
+
sanitiseRegex = /[^a-zA-Z0-9_\-\$]/g;
|
|
30
|
+
/**
|
|
31
|
+
* The transformed blacklist keys of flat regex patterns and complex config objects
|
|
32
|
+
* @private
|
|
33
|
+
*/
|
|
34
|
+
blacklistedKeysTransformed = [];
|
|
35
|
+
constructor(customConfig) {
|
|
36
|
+
this.config = {
|
|
37
|
+
...defaultConfig,
|
|
38
|
+
...customConfig,
|
|
39
|
+
};
|
|
40
|
+
this.blacklistedKeysTransformed = (customConfig.blacklistedKeys ?? []).filter(key => typeof key !== 'string').map((key) => this.createTransformedBlacklistedKey(key, customConfig));
|
|
41
|
+
const stringKeys = (customConfig.blacklistedKeys ?? []).filter(key => typeof key === 'string');
|
|
42
|
+
if (stringKeys.length > 0)
|
|
43
|
+
this.computedRegex = new RegExp(stringKeys.map(this.sanitiseStringForRegex).filter(Boolean).join('|'));
|
|
44
|
+
}
|
|
45
|
+
createTransformedBlacklistedKey = (key, customConfig) => {
|
|
46
|
+
if (key instanceof RegExp) {
|
|
47
|
+
return {
|
|
48
|
+
key,
|
|
49
|
+
fuzzyKeyMatch: customConfig.fuzzyKeyMatch ?? defaultConfig.fuzzyKeyMatch,
|
|
50
|
+
caseSensitiveKeyMatch: customConfig.caseSensitiveKeyMatch ?? defaultConfig.caseSensitiveKeyMatch,
|
|
51
|
+
retainStructure: customConfig.retainStructure ?? defaultConfig.retainStructure,
|
|
52
|
+
replacement: customConfig.replacement ?? defaultConfig.replacement,
|
|
53
|
+
replaceStringByLength: customConfig.replaceStringByLength ?? defaultConfig.replaceStringByLength,
|
|
54
|
+
remove: customConfig.remove ?? defaultConfig.remove,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
fuzzyKeyMatch: key.fuzzyKeyMatch ?? customConfig.fuzzyKeyMatch ?? defaultConfig.fuzzyKeyMatch,
|
|
59
|
+
caseSensitiveKeyMatch: key.caseSensitiveKeyMatch ?? customConfig.caseSensitiveKeyMatch ?? defaultConfig.caseSensitiveKeyMatch,
|
|
60
|
+
retainStructure: key.retainStructure ?? customConfig.retainStructure ?? defaultConfig.retainStructure,
|
|
61
|
+
replacement: key.replacement ?? customConfig.replacement ?? defaultConfig.replacement,
|
|
62
|
+
replaceStringByLength: key.replaceStringByLength ?? customConfig.replaceStringByLength ?? defaultConfig.replaceStringByLength,
|
|
63
|
+
remove: key.remove ?? customConfig.remove ?? defaultConfig.remove,
|
|
64
|
+
key: key.key,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Applies transformers to a value
|
|
69
|
+
* @param value - The value to transform
|
|
70
|
+
* @param key - The key to check
|
|
71
|
+
* @returns The transformed value
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
applyTransformers = (value, key, referenceMap) => {
|
|
75
|
+
if (typeof value === 'string')
|
|
76
|
+
return value;
|
|
77
|
+
let transformed = value;
|
|
78
|
+
for (const transformer of this.config.transformers) {
|
|
79
|
+
transformed = transformer(transformed, key, referenceMap);
|
|
80
|
+
if (transformed !== value)
|
|
81
|
+
return transformed;
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Sanitises a string for the computed regex
|
|
87
|
+
* @param key - The string to sanitise
|
|
88
|
+
* @returns The sanitised string
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
sanitiseStringForRegex = (key) => key.replace(this.sanitiseRegex, '');
|
|
92
|
+
/**
|
|
93
|
+
* Checks if a key should be redacted
|
|
94
|
+
* @param key - The key to check
|
|
95
|
+
* @returns Whether the key should be redacted
|
|
96
|
+
* @private
|
|
97
|
+
*/
|
|
98
|
+
shouldRedactKey = (key) => {
|
|
99
|
+
if (this.computedRegex?.test(this.sanitiseStringForRegex(key)))
|
|
100
|
+
return true;
|
|
101
|
+
return this.blacklistedKeysTransformed.some(config => {
|
|
102
|
+
const pattern = config.key;
|
|
103
|
+
if (pattern instanceof RegExp)
|
|
104
|
+
return pattern.test(key);
|
|
105
|
+
if (!config.fuzzyKeyMatch && !config.caseSensitiveKeyMatch)
|
|
106
|
+
return key.toLowerCase() === pattern.toLowerCase();
|
|
107
|
+
if (config.fuzzyKeyMatch && !config.caseSensitiveKeyMatch)
|
|
108
|
+
return key.toLowerCase().includes(pattern.toLowerCase());
|
|
109
|
+
if (config.fuzzyKeyMatch && config.caseSensitiveKeyMatch)
|
|
110
|
+
return key.includes(pattern);
|
|
111
|
+
if (!config.fuzzyKeyMatch && config.caseSensitiveKeyMatch)
|
|
112
|
+
return key === pattern;
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Checks if a value should be redacted
|
|
117
|
+
* @param value - The value to check
|
|
118
|
+
* @param key - The key to check
|
|
119
|
+
* @returns Whether the value should be redacted
|
|
120
|
+
* @private
|
|
121
|
+
*/
|
|
122
|
+
shouldRedactValue = (value, valueKey) => {
|
|
123
|
+
if (!this.config.types.includes(typeof value))
|
|
124
|
+
return false;
|
|
125
|
+
return this.shouldRedactKey(valueKey);
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Redacts a value based on the key-specific config
|
|
129
|
+
* @param value - The value to redact
|
|
130
|
+
* @param key - The key to check
|
|
131
|
+
* @param redactingParent - Whether the parent is being redacted
|
|
132
|
+
* @returns The redacted value
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
redactValue = (value, redactingParent, keyConfig) => {
|
|
136
|
+
if (!this.config.types.includes(typeof value))
|
|
137
|
+
return { transformed: value, redactingParent };
|
|
138
|
+
const remove = keyConfig?.remove ?? this.config.remove;
|
|
139
|
+
const replacement = keyConfig?.replacement ?? this.config.replacement;
|
|
140
|
+
const replaceStringByLength = keyConfig?.replaceStringByLength ?? this.config.replaceStringByLength;
|
|
141
|
+
const retainStructure = keyConfig?.retainStructure ?? this.config.retainStructure;
|
|
142
|
+
if (retainStructure && typeof value === 'object' && value !== null)
|
|
143
|
+
return { transformed: value, redactingParent: true };
|
|
144
|
+
if (remove)
|
|
145
|
+
return { transformed: undefined, redactingParent };
|
|
146
|
+
if (typeof replacement === 'function')
|
|
147
|
+
return { transformed: replacement(value), redactingParent };
|
|
148
|
+
return {
|
|
149
|
+
redactingParent,
|
|
150
|
+
transformed: (typeof value === 'string' && replaceStringByLength)
|
|
151
|
+
? replacement.toString().repeat(value.length)
|
|
152
|
+
: replacement,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Applies string transformations
|
|
157
|
+
* @param value - The value to transform
|
|
158
|
+
* @param key - The key to check
|
|
159
|
+
* @returns The transformed value
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
applyStringTransformations(value, amRedactingParent, keyConfig) {
|
|
163
|
+
if ((this.config.stringTests ?? []).length === 0)
|
|
164
|
+
return { transformed: value, redactingParent: amRedactingParent };
|
|
165
|
+
for (const test of this.config.stringTests) {
|
|
166
|
+
if (test instanceof RegExp) {
|
|
167
|
+
if (test.test(value)) {
|
|
168
|
+
const { transformed, redactingParent } = this.redactValue(value, amRedactingParent, keyConfig);
|
|
169
|
+
return { transformed: transformed, redactingParent };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
if (test.pattern.test(value)) {
|
|
174
|
+
const transformed = test.replacer(value, test.pattern);
|
|
175
|
+
return { transformed, redactingParent: amRedactingParent };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { transformed: value, redactingParent: amRedactingParent };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Handles primitive values
|
|
183
|
+
* @param value - The value to handle
|
|
184
|
+
* @param key - The key to check
|
|
185
|
+
* @param redactingParent - Whether the parent is being redacted
|
|
186
|
+
* @param keyConfig - The key config
|
|
187
|
+
* @returns The transformed value
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
handlePrimitiveValue(value, valueKey, redactingParent, keyConfig) {
|
|
191
|
+
let transformed = value;
|
|
192
|
+
if (redactingParent) {
|
|
193
|
+
if (valueKey === '_transformer' || !this.config.types.includes(typeof value)) {
|
|
194
|
+
return { transformed: value, redactingParent };
|
|
195
|
+
}
|
|
196
|
+
const { transformed: transformedValue } = this.redactValue(value, redactingParent, keyConfig);
|
|
197
|
+
return { transformed: transformedValue, redactingParent };
|
|
198
|
+
}
|
|
199
|
+
if (keyConfig || this.shouldRedactValue(value, valueKey)) {
|
|
200
|
+
return this.redactValue(value, redactingParent, keyConfig);
|
|
201
|
+
}
|
|
202
|
+
if (typeof value === 'string') {
|
|
203
|
+
return this.applyStringTransformations(value, redactingParent, keyConfig);
|
|
204
|
+
}
|
|
205
|
+
return { transformed, redactingParent };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Handles object values
|
|
209
|
+
* @param value - The value to handle
|
|
210
|
+
* @param key - The key to check
|
|
211
|
+
* @param path - The path to the value
|
|
212
|
+
* @param redactingParent - Whether the parent is being redacted
|
|
213
|
+
* @param referenceMap - The reference map
|
|
214
|
+
* @returns The transformed value and stack
|
|
215
|
+
* @private
|
|
216
|
+
*/
|
|
217
|
+
handleObjectValue(value, key, path, amRedactingParent, referenceMap, keyConfig) {
|
|
218
|
+
const fullPath = path.join('.');
|
|
219
|
+
const shouldRedact = amRedactingParent || Boolean(keyConfig) || this.shouldRedactValue(value, key);
|
|
220
|
+
referenceMap.set(value, fullPath);
|
|
221
|
+
if (shouldRedact && !(keyConfig?.retainStructure ?? this.config.retainStructure)) {
|
|
222
|
+
const { transformed, redactingParent } = this.redactValue(value, amRedactingParent, keyConfig);
|
|
223
|
+
return { transformed, redactingParent, stack: [] };
|
|
224
|
+
}
|
|
225
|
+
return this.handleRetainStructure(value, path, shouldRedact);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Handles object values
|
|
229
|
+
* @param value - The value to handle
|
|
230
|
+
* @param path - The path to the value
|
|
231
|
+
* @param redactingParent - Whether the parent is being redacted
|
|
232
|
+
* @returns The transformed value and stack
|
|
233
|
+
* @private
|
|
234
|
+
*/
|
|
235
|
+
handleRetainStructure(value, path, redactingParent) {
|
|
236
|
+
const newValue = Array.isArray(value) ? [] : {};
|
|
237
|
+
const stack = [];
|
|
238
|
+
if (Array.isArray(value)) {
|
|
239
|
+
for (let i = value.length - 1; i >= 0; i--) {
|
|
240
|
+
stack.push({
|
|
241
|
+
parent: newValue,
|
|
242
|
+
key: i.toString(),
|
|
243
|
+
value: value[i],
|
|
244
|
+
path: [...path, i],
|
|
245
|
+
redactingParent,
|
|
246
|
+
keyConfig: this.findMatchingKeyConfig(i.toString()),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
for (const [propKey, propValue] of Object.entries(value).reverse()) {
|
|
252
|
+
stack.push({
|
|
253
|
+
parent: newValue,
|
|
254
|
+
key: propKey,
|
|
255
|
+
value: propValue,
|
|
256
|
+
path: [...path, propKey],
|
|
257
|
+
redactingParent,
|
|
258
|
+
keyConfig: this.findMatchingKeyConfig(propKey),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { transformed: newValue, redactingParent, stack };
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Finds the matching key config
|
|
266
|
+
* @param key - The key to find
|
|
267
|
+
* @returns The matching key config
|
|
268
|
+
* @private
|
|
269
|
+
*/
|
|
270
|
+
findMatchingKeyConfig(key) {
|
|
271
|
+
if (this.computedRegex?.test(key)) {
|
|
272
|
+
return {
|
|
273
|
+
key,
|
|
274
|
+
fuzzyKeyMatch: this.config.fuzzyKeyMatch,
|
|
275
|
+
caseSensitiveKeyMatch: this.config.caseSensitiveKeyMatch,
|
|
276
|
+
replaceStringByLength: this.config.replaceStringByLength,
|
|
277
|
+
replacement: this.config.replacement,
|
|
278
|
+
retainStructure: this.config.retainStructure,
|
|
279
|
+
remove: this.config.remove,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return this.blacklistedKeysTransformed.find(config => {
|
|
283
|
+
const pattern = config.key;
|
|
284
|
+
if (pattern instanceof RegExp)
|
|
285
|
+
return pattern.test(key);
|
|
286
|
+
if (config.fuzzyKeyMatch) {
|
|
287
|
+
const compareKey = config.caseSensitiveKeyMatch ? key : key.toLowerCase();
|
|
288
|
+
const comparePattern = config.caseSensitiveKeyMatch ? pattern : pattern.toLowerCase();
|
|
289
|
+
return compareKey.includes(comparePattern);
|
|
290
|
+
}
|
|
291
|
+
return config.caseSensitiveKeyMatch ? key === pattern : key.toLowerCase() === pattern.toLowerCase();
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Initialises the traversal
|
|
296
|
+
* @param raw - The raw value to traverse
|
|
297
|
+
* @returns The output and stack
|
|
298
|
+
* @private
|
|
299
|
+
*/
|
|
300
|
+
initialiseTraversal(raw) {
|
|
301
|
+
const output = Array.isArray(raw) ? [] : {};
|
|
302
|
+
const stack = [];
|
|
303
|
+
if (typeof raw === 'object' && raw !== null) {
|
|
304
|
+
if (Array.isArray(raw)) {
|
|
305
|
+
for (let i = raw.length - 1; i >= 0; i--) {
|
|
306
|
+
stack.push({
|
|
307
|
+
parent: output,
|
|
308
|
+
key: i.toString(),
|
|
309
|
+
value: raw[i],
|
|
310
|
+
path: [i],
|
|
311
|
+
redactingParent: false,
|
|
312
|
+
keyConfig: this.findMatchingKeyConfig(i.toString()),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
for (const [propKey, propValue] of Object.entries(raw).reverse()) {
|
|
318
|
+
stack.push({
|
|
319
|
+
parent: output,
|
|
320
|
+
key: propKey,
|
|
321
|
+
value: propValue,
|
|
322
|
+
path: [propKey],
|
|
323
|
+
redactingParent: false,
|
|
324
|
+
keyConfig: this.findMatchingKeyConfig(propKey),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return { output, stack };
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Pre-processes the input to replace circular references with transformer objects
|
|
333
|
+
* @param raw - The raw value to process
|
|
334
|
+
* @returns The processed value with circular references replaced
|
|
335
|
+
* @private
|
|
336
|
+
*/
|
|
337
|
+
replaceCircularReferences(raw) {
|
|
338
|
+
if (typeof raw !== 'object' || raw === null)
|
|
339
|
+
return raw;
|
|
340
|
+
const visiting = new WeakSet();
|
|
341
|
+
const pathMap = new WeakMap();
|
|
342
|
+
const processValue = (value, path) => {
|
|
343
|
+
if (typeof value !== 'object' || value === null)
|
|
344
|
+
return value;
|
|
345
|
+
if (visiting.has(value)) {
|
|
346
|
+
const originalPath = pathMap.get(value) || '';
|
|
347
|
+
return {
|
|
348
|
+
_transformer: 'circular',
|
|
349
|
+
value: originalPath,
|
|
350
|
+
path: path
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
visiting.add(value);
|
|
354
|
+
pathMap.set(value, path);
|
|
355
|
+
let result;
|
|
356
|
+
if (Array.isArray(value)) {
|
|
357
|
+
let hasCircular = false;
|
|
358
|
+
const newArray = value.map((item, index) => {
|
|
359
|
+
const itemPath = path ? `${path}.${index}` : index.toString();
|
|
360
|
+
const processed = processValue(item, itemPath);
|
|
361
|
+
if (processed !== item)
|
|
362
|
+
hasCircular = true;
|
|
363
|
+
return processed;
|
|
364
|
+
});
|
|
365
|
+
result = hasCircular ? newArray : value;
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
let hasCircular = false;
|
|
369
|
+
const newObj = {};
|
|
370
|
+
for (const [key, val] of Object.entries(value)) {
|
|
371
|
+
const valuePath = path ? `${path}.${key}` : key;
|
|
372
|
+
const processed = processValue(val, valuePath);
|
|
373
|
+
newObj[key] = processed;
|
|
374
|
+
if (processed !== val)
|
|
375
|
+
hasCircular = true;
|
|
376
|
+
}
|
|
377
|
+
result = hasCircular ? newObj : value;
|
|
378
|
+
}
|
|
379
|
+
visiting.delete(value);
|
|
380
|
+
return result;
|
|
381
|
+
};
|
|
382
|
+
return processValue(raw, '');
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Traverses the raw value
|
|
386
|
+
* @param raw - The raw value to traverse
|
|
387
|
+
* @returns The transformed value
|
|
388
|
+
*/
|
|
389
|
+
traverse = (raw) => {
|
|
390
|
+
if (typeof raw === 'string') {
|
|
391
|
+
const { transformed } = this.applyStringTransformations(raw, false);
|
|
392
|
+
return transformed;
|
|
393
|
+
}
|
|
394
|
+
if (typeof raw !== 'object' || raw === null)
|
|
395
|
+
return raw;
|
|
396
|
+
const referenceMap = new WeakMap();
|
|
397
|
+
const cleanedInput = this.replaceCircularReferences(raw);
|
|
398
|
+
const { output, stack } = this.initialiseTraversal(cleanedInput);
|
|
399
|
+
if (typeof cleanedInput === 'object' && cleanedInput !== null)
|
|
400
|
+
referenceMap.set(cleanedInput, '');
|
|
401
|
+
while (stack.length > 0) {
|
|
402
|
+
const { parent, key, value, path, redactingParent: amRedactingParent, keyConfig } = stack.pop();
|
|
403
|
+
let transformed = this.applyTransformers(value, key, referenceMap);
|
|
404
|
+
let redactingParent = amRedactingParent;
|
|
405
|
+
if (typeof transformed !== 'object' || transformed === null) {
|
|
406
|
+
const primitiveResult = this.handlePrimitiveValue(transformed, key, amRedactingParent, keyConfig);
|
|
407
|
+
redactingParent = primitiveResult.redactingParent;
|
|
408
|
+
transformed = primitiveResult.transformed;
|
|
409
|
+
if (typeof transformed === 'undefined')
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
const objectResult = this.handleObjectValue(transformed, key, path, redactingParent, referenceMap, keyConfig);
|
|
414
|
+
transformed = objectResult.transformed;
|
|
415
|
+
stack.push(...objectResult.stack);
|
|
416
|
+
}
|
|
417
|
+
if (parent !== null && key !== null)
|
|
418
|
+
parent[key] = transformed;
|
|
419
|
+
}
|
|
420
|
+
return output;
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
export default RedactorUtils;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { _bigint } from "./bigint";
|
|
2
|
+
import { _date } from "./date";
|
|
3
|
+
import { _error } from "./error";
|
|
4
|
+
import { _map } from "./map";
|
|
5
|
+
import { _regex } from "./regex";
|
|
6
|
+
import { _set } from "./set";
|
|
7
|
+
import { _url } from "./url";
|
|
8
|
+
export const standardTransformers = [
|
|
9
|
+
_bigint,
|
|
10
|
+
_url,
|
|
11
|
+
_date,
|
|
12
|
+
_error,
|
|
13
|
+
_map,
|
|
14
|
+
_set,
|
|
15
|
+
_regex,
|
|
16
|
+
];
|