@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 +51 -46
- package/dist/cjs/index.js +132 -109
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/utils/redactorUtils.js +219 -0
- package/dist/esm/index.mjs +137 -0
- package/dist/esm/types.mjs +1 -0
- package/dist/esm/utils/redactorUtils.js +219 -0
- package/dist/types/index.d.ts +63 -0
- package/dist/types/types.d.ts +129 -0
- package/dist/types/utils/redactorUtils.d.ts +90 -0
- package/package.json +6 -3
- package/dist/cjs/index.d.ts +0 -34
- package/dist/esm/index.d.ts +0 -34
- package/dist/esm/index.js +0 -125
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import RedactorUtils from './utils/redactorUtils';
|
|
2
|
+
class DeepRedact {
|
|
3
|
+
/**
|
|
4
|
+
* The redactorUtils instance to handle the redaction.
|
|
5
|
+
* @private
|
|
6
|
+
*/
|
|
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
|
+
/**
|
|
14
|
+
* The configuration for the redaction.
|
|
15
|
+
* @private
|
|
16
|
+
*/
|
|
17
|
+
config = {
|
|
18
|
+
serialise: false,
|
|
19
|
+
};
|
|
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
|
+
*/
|
|
26
|
+
constructor(config) {
|
|
27
|
+
const { serialise, serialize, ...rest } = config;
|
|
28
|
+
this.redactorUtils = new RedactorUtils(rest);
|
|
29
|
+
if (serialise !== undefined)
|
|
30
|
+
this.config.serialise = serialise;
|
|
31
|
+
if (serialize !== undefined)
|
|
32
|
+
this.config.serialise = serialize;
|
|
33
|
+
}
|
|
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(),
|
|
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 Date)
|
|
76
|
+
return value.toISOString();
|
|
77
|
+
return value;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Calls `unsupportedTransformer` on the provided value and rewrites any circular references.
|
|
81
|
+
*
|
|
82
|
+
* Circular references will always be removed to avoid infinite recursion.
|
|
83
|
+
* When a circular reference is found, the value will be replaced with `[[CIRCULAR_REFERENCE: path.to.original.value]]`.
|
|
84
|
+
* @protected
|
|
85
|
+
* @param {unknown} value The value to rewrite.
|
|
86
|
+
* @param {string | undefined} path The path to the value in the object.
|
|
87
|
+
* @returns {unknown} The rewritten value.
|
|
88
|
+
*/
|
|
89
|
+
rewriteUnsupported = (value, path) => {
|
|
90
|
+
const safeValue = this.unsupportedTransformer(value);
|
|
91
|
+
if (!(safeValue instanceof Object))
|
|
92
|
+
return safeValue;
|
|
93
|
+
if (this.circularReference === null)
|
|
94
|
+
this.circularReference = new WeakSet();
|
|
95
|
+
if (Array.isArray(safeValue)) {
|
|
96
|
+
return safeValue.map((val, index) => {
|
|
97
|
+
const newPath = path ? `${path}.[${index}]` : `[${index}]`;
|
|
98
|
+
if (this.circularReference?.has(val))
|
|
99
|
+
return `[[CIRCULAR_REFERENCE: ${newPath}]]`;
|
|
100
|
+
if (val instanceof Object) {
|
|
101
|
+
this.circularReference?.add(val);
|
|
102
|
+
return this.rewriteUnsupported(val, newPath);
|
|
103
|
+
}
|
|
104
|
+
return val;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return Object.fromEntries(Object.entries(safeValue).map(([key, val]) => {
|
|
108
|
+
const newPath = path ? `${path}.${key}` : key;
|
|
109
|
+
if (this.circularReference?.has(val))
|
|
110
|
+
return [key, `[[CIRCULAR_REFERENCE: ${newPath}]]`];
|
|
111
|
+
if (val instanceof Object)
|
|
112
|
+
this.circularReference?.add(val);
|
|
113
|
+
return [key, this.rewriteUnsupported(val, path ? `${path}.${key}` : key)];
|
|
114
|
+
}));
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Depending on the value of `serialise`, return the value as a JSON string or as the provided value.
|
|
118
|
+
*
|
|
119
|
+
* Also resets the `circularReference` property to null after redaction is complete.
|
|
120
|
+
* This is to ensure that the WeakSet doesn't cause memory leaks.
|
|
121
|
+
* @private
|
|
122
|
+
* @param value
|
|
123
|
+
*/
|
|
124
|
+
maybeSerialise = (value) => {
|
|
125
|
+
this.circularReference = null;
|
|
126
|
+
return this.config.serialise ? JSON.stringify(value) : value;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* 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.
|
|
130
|
+
* @param {unknown} value The value to redact.
|
|
131
|
+
* @returns {unknown} The redacted value.
|
|
132
|
+
*/
|
|
133
|
+
redact = (value) => {
|
|
134
|
+
return this.maybeSerialise(this.redactorUtils.recurse(this.rewriteUnsupported(value)));
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export { DeepRedact as default, DeepRedact };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
const defaultConfig = {
|
|
2
|
+
stringTests: [],
|
|
3
|
+
blacklistedKeys: [],
|
|
4
|
+
blacklistedKeysTransformed: [],
|
|
5
|
+
fuzzyKeyMatch: false,
|
|
6
|
+
caseSensitiveKeyMatch: true,
|
|
7
|
+
retainStructure: false,
|
|
8
|
+
remove: false,
|
|
9
|
+
replaceStringByLength: false,
|
|
10
|
+
replacement: '[REDACTED]',
|
|
11
|
+
types: ['string'],
|
|
12
|
+
};
|
|
13
|
+
class RedactorUtils {
|
|
14
|
+
/**
|
|
15
|
+
* The configuration for the redaction.
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
config = defaultConfig;
|
|
19
|
+
constructor(customConfig) {
|
|
20
|
+
this.config = {
|
|
21
|
+
...defaultConfig,
|
|
22
|
+
...customConfig,
|
|
23
|
+
blacklistedKeys: customConfig.blacklistedKeys ?? [],
|
|
24
|
+
blacklistedKeysTransformed: customConfig.blacklistedKeys?.map((key) => {
|
|
25
|
+
const isObject = !(typeof key === 'string' || key instanceof RegExp);
|
|
26
|
+
const setKey = isObject ? key.key : key;
|
|
27
|
+
const fallback = {
|
|
28
|
+
fuzzyKeyMatch: customConfig.fuzzyKeyMatch ?? defaultConfig.fuzzyKeyMatch,
|
|
29
|
+
caseSensitiveKeyMatch: customConfig.caseSensitiveKeyMatch ?? defaultConfig.caseSensitiveKeyMatch,
|
|
30
|
+
retainStructure: customConfig.retainStructure ?? defaultConfig.retainStructure,
|
|
31
|
+
replacement: customConfig.replacement ?? defaultConfig.replacement,
|
|
32
|
+
remove: customConfig.remove ?? defaultConfig.remove,
|
|
33
|
+
key: setKey,
|
|
34
|
+
};
|
|
35
|
+
if (isObject) {
|
|
36
|
+
return {
|
|
37
|
+
fuzzyKeyMatch: key.fuzzyKeyMatch ?? fallback.fuzzyKeyMatch,
|
|
38
|
+
caseSensitiveKeyMatch: key.caseSensitiveKeyMatch ?? fallback.caseSensitiveKeyMatch,
|
|
39
|
+
retainStructure: key.retainStructure ?? fallback.retainStructure,
|
|
40
|
+
replacement: key.replacement ?? fallback.replacement,
|
|
41
|
+
remove: key.remove ?? fallback.remove,
|
|
42
|
+
key: setKey,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return fallback;
|
|
46
|
+
}) ?? [],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Normalise a string for comparison. This will convert the string to lowercase and remove any non-word characters.
|
|
51
|
+
* @private
|
|
52
|
+
* @param str The string to normalise.
|
|
53
|
+
* @returns {string} The normalised string.
|
|
54
|
+
*/
|
|
55
|
+
static normaliseString = (str) => str.toLowerCase().replace(/\W/g, '');
|
|
56
|
+
/**
|
|
57
|
+
* Determine if a key matches a given blacklistedKeyConfig. This will check the key against the blacklisted keys,
|
|
58
|
+
* using the configuration option for the given key falling back to the default configuration.
|
|
59
|
+
* @private
|
|
60
|
+
* @param {string} key The key to check.
|
|
61
|
+
* @param {BlacklistKeyConfig} blacklistKeyConfig The configuration for the key.
|
|
62
|
+
* @returns {boolean} Whether the key should be redacted.
|
|
63
|
+
*/
|
|
64
|
+
static complexKeyMatch = (key, blacklistKeyConfig) => {
|
|
65
|
+
if (blacklistKeyConfig.key instanceof RegExp)
|
|
66
|
+
return blacklistKeyConfig.key.test(key);
|
|
67
|
+
if (blacklistKeyConfig.fuzzyKeyMatch && blacklistKeyConfig.caseSensitiveKeyMatch)
|
|
68
|
+
return key.includes(blacklistKeyConfig.key);
|
|
69
|
+
if (blacklistKeyConfig.fuzzyKeyMatch && !blacklistKeyConfig.caseSensitiveKeyMatch)
|
|
70
|
+
return RedactorUtils.normaliseString(key).includes(RedactorUtils.normaliseString(blacklistKeyConfig.key));
|
|
71
|
+
if (!blacklistKeyConfig.fuzzyKeyMatch && blacklistKeyConfig.caseSensitiveKeyMatch)
|
|
72
|
+
return key === blacklistKeyConfig.key;
|
|
73
|
+
return RedactorUtils.normaliseString(blacklistKeyConfig.key) === RedactorUtils.normaliseString(key);
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Get the configuration for an object key. This will check the key against the transformed blacklisted keys.
|
|
77
|
+
* @private
|
|
78
|
+
* @param {string} key The key of the configuration to get.
|
|
79
|
+
* @returns {Required<BlacklistKeyConfig> | undefined} The configuration for the key.
|
|
80
|
+
*/
|
|
81
|
+
getBlacklistedKeyConfig = (key) => {
|
|
82
|
+
if (!key)
|
|
83
|
+
return undefined;
|
|
84
|
+
return this.config.blacklistedKeysTransformed?.find((redactableKey) => {
|
|
85
|
+
return RedactorUtils.complexKeyMatch(key, redactableKey);
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Get the recursion configuration for a key. This will check the key against the transformed blacklisted keys.
|
|
90
|
+
* If the key is found, the configuration for the key will be returned, otherwise undefined.
|
|
91
|
+
* @private
|
|
92
|
+
* @param {string} key The key of the configuration to get.
|
|
93
|
+
* @returns {Required<Pick<BlacklistKeyConfig, 'remove' | 'replacement' | 'retainStructure'>>} The configuration for the key.
|
|
94
|
+
*/
|
|
95
|
+
getRecursionConfig = (key) => {
|
|
96
|
+
const fallback = {
|
|
97
|
+
remove: this.config.remove,
|
|
98
|
+
replacement: this.config.replacement,
|
|
99
|
+
retainStructure: this.config.retainStructure,
|
|
100
|
+
};
|
|
101
|
+
if (!key)
|
|
102
|
+
return fallback;
|
|
103
|
+
const blacklistedKeyConfig = this.getBlacklistedKeyConfig(key);
|
|
104
|
+
if (!blacklistedKeyConfig)
|
|
105
|
+
return fallback;
|
|
106
|
+
return {
|
|
107
|
+
remove: blacklistedKeyConfig.remove,
|
|
108
|
+
replacement: blacklistedKeyConfig.replacement,
|
|
109
|
+
retainStructure: blacklistedKeyConfig.retainStructure,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Determine if a key should be redacted. This will check the key against the blacklisted keys, using the default configuration.
|
|
114
|
+
* @private
|
|
115
|
+
* @param {string} key The key to check.
|
|
116
|
+
* @returns {boolean} Whether the key should be redacted.
|
|
117
|
+
*/
|
|
118
|
+
shouldRedactObjectValue = (key) => {
|
|
119
|
+
if (!key)
|
|
120
|
+
return false;
|
|
121
|
+
return this.config.blacklistedKeysTransformed.some((redactableKey) => {
|
|
122
|
+
return RedactorUtils.complexKeyMatch(key, redactableKey);
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* 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.
|
|
127
|
+
* @private
|
|
128
|
+
* @param value
|
|
129
|
+
* @param replacement
|
|
130
|
+
* @param remove
|
|
131
|
+
* @param shouldRedact
|
|
132
|
+
*/
|
|
133
|
+
redactString = (value, replacement, remove, shouldRedact) => {
|
|
134
|
+
if (!value)
|
|
135
|
+
return value;
|
|
136
|
+
const { stringTests } = this.config;
|
|
137
|
+
if (!shouldRedact && !stringTests?.some((pattern) => pattern.test(value)))
|
|
138
|
+
return value;
|
|
139
|
+
if (remove)
|
|
140
|
+
return undefined;
|
|
141
|
+
if (typeof replacement === 'function')
|
|
142
|
+
return replacement(value);
|
|
143
|
+
if (this.config.replaceStringByLength)
|
|
144
|
+
return replacement.repeat(value.length);
|
|
145
|
+
return replacement;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* 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.
|
|
149
|
+
* @private
|
|
150
|
+
* @param {unknown} value The value to redact.
|
|
151
|
+
* @param {Transformer | string} replacement The replacement value for redacted data.
|
|
152
|
+
* @param {boolean} remove Whether the redacted data should be removed.
|
|
153
|
+
* @param {boolean} shouldRedact Whether the value should be redacted based on the parent key.
|
|
154
|
+
* @returns {unknown} The redacted value.
|
|
155
|
+
*/
|
|
156
|
+
redactPrimitive = (value, replacement, remove, shouldRedact) => {
|
|
157
|
+
if (!this.config.types.includes(typeof value))
|
|
158
|
+
return value;
|
|
159
|
+
if (remove && shouldRedact && typeof value !== 'string')
|
|
160
|
+
return undefined;
|
|
161
|
+
if (typeof value === 'string')
|
|
162
|
+
return this.redactString(value, replacement, remove, shouldRedact);
|
|
163
|
+
if (!shouldRedact)
|
|
164
|
+
return value;
|
|
165
|
+
if (typeof replacement === 'function')
|
|
166
|
+
return replacement(value);
|
|
167
|
+
return replacement;
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Redact an array. This will redact each value in the array using the `recurse` method.
|
|
171
|
+
* @private
|
|
172
|
+
* @param {unknown[]} value The array to redact.
|
|
173
|
+
* @returns {unknown[]} The redacted array.
|
|
174
|
+
*/
|
|
175
|
+
redactArray = (value) => value.map((val) => this.recurse(val));
|
|
176
|
+
/**
|
|
177
|
+
* Redact an object. This will recursively redact the object based on the configuration, redacting the keys and values as required.
|
|
178
|
+
* @param {Object} value The object to redact.
|
|
179
|
+
* @param {string | null} key The key of the object if it is part of another object.
|
|
180
|
+
* @param {boolean} parentShouldRedact Whether the item should be redacted based on the key within the parent object.
|
|
181
|
+
*/
|
|
182
|
+
redactObject = (value, key, parentShouldRedact) => {
|
|
183
|
+
return Object.fromEntries(Object.entries(value).map(([prop, val]) => {
|
|
184
|
+
const shouldRedact = parentShouldRedact || this.shouldRedactObjectValue(prop);
|
|
185
|
+
if (shouldRedact) {
|
|
186
|
+
const { remove } = this.getRecursionConfig(prop);
|
|
187
|
+
if (remove)
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
return [prop, this.recurse(val, key ?? prop, shouldRedact)];
|
|
191
|
+
}).filter(([prop]) => prop !== undefined));
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* 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.
|
|
195
|
+
* @private
|
|
196
|
+
* @param {unknown} value The value to redact.
|
|
197
|
+
* @param {string | null} key The key of the value if it is part of an object.
|
|
198
|
+
* @param {boolean} parentShouldRedact Whether the parent object should be redacted.
|
|
199
|
+
* @returns {unknown} The redacted value.
|
|
200
|
+
*/
|
|
201
|
+
recurse = (value, key, parentShouldRedact) => {
|
|
202
|
+
if (value === null)
|
|
203
|
+
return value;
|
|
204
|
+
const { remove, replacement, retainStructure } = this.getRecursionConfig(key);
|
|
205
|
+
if (!(value instanceof Object))
|
|
206
|
+
return this.redactPrimitive(value, replacement, remove, Boolean(key && parentShouldRedact));
|
|
207
|
+
if (parentShouldRedact) {
|
|
208
|
+
if (!retainStructure) {
|
|
209
|
+
return typeof replacement === 'function'
|
|
210
|
+
? replacement(value)
|
|
211
|
+
: replacement;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (Array.isArray(value))
|
|
215
|
+
return this.redactArray(value);
|
|
216
|
+
return this.redactObject(value, key, parentShouldRedact);
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
export default RedactorUtils;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { DeepRedactConfig } from './types';
|
|
2
|
+
declare class DeepRedact {
|
|
3
|
+
/**
|
|
4
|
+
* The redactorUtils instance to handle the redaction.
|
|
5
|
+
* @private
|
|
6
|
+
*/
|
|
7
|
+
private redactorUtils;
|
|
8
|
+
/**
|
|
9
|
+
* A WeakSet to store circular references during redaction. Reset to null after redaction is complete.
|
|
10
|
+
* @private
|
|
11
|
+
*/
|
|
12
|
+
private circularReference;
|
|
13
|
+
/**
|
|
14
|
+
* The configuration for the redaction.
|
|
15
|
+
* @private
|
|
16
|
+
*/
|
|
17
|
+
private readonly config;
|
|
18
|
+
/**
|
|
19
|
+
* Create a new DeepRedact instance with the provided configuration.
|
|
20
|
+
* The configuration will be merged with the default configuration.
|
|
21
|
+
* `blacklistedKeys` will be normalised to an array inherited from the default configuration as the default values.
|
|
22
|
+
* @param {DeepRedactConfig} config. The configuration for the redaction.
|
|
23
|
+
*/
|
|
24
|
+
constructor(config: DeepRedactConfig);
|
|
25
|
+
/**
|
|
26
|
+
* A transformer for unsupported data types. If `serialise` is false, the value will be returned as is,
|
|
27
|
+
* otherwise it will transform the value into a format that is supported by JSON.stringify.
|
|
28
|
+
*
|
|
29
|
+
* Error, RegExp, and Date instances are technically supported by JSON.stringify,
|
|
30
|
+
* but they returned as empty objects, therefore they are also transformed here.
|
|
31
|
+
* @protected
|
|
32
|
+
* @param {unknown} value The value that is not supported by JSON.stringify.
|
|
33
|
+
* @returns {unknown} The value in a format that is supported by JSON.stringify.
|
|
34
|
+
*/
|
|
35
|
+
protected unsupportedTransformer: (value: unknown) => unknown;
|
|
36
|
+
/**
|
|
37
|
+
* Calls `unsupportedTransformer` on the provided value and rewrites any circular references.
|
|
38
|
+
*
|
|
39
|
+
* Circular references will always be removed to avoid infinite recursion.
|
|
40
|
+
* When a circular reference is found, the value will be replaced with `[[CIRCULAR_REFERENCE: path.to.original.value]]`.
|
|
41
|
+
* @protected
|
|
42
|
+
* @param {unknown} value The value to rewrite.
|
|
43
|
+
* @param {string | undefined} path The path to the value in the object.
|
|
44
|
+
* @returns {unknown} The rewritten value.
|
|
45
|
+
*/
|
|
46
|
+
protected rewriteUnsupported: (value: unknown, path?: string) => unknown;
|
|
47
|
+
/**
|
|
48
|
+
* Depending on the value of `serialise`, return the value as a JSON string or as the provided value.
|
|
49
|
+
*
|
|
50
|
+
* Also resets the `circularReference` property to null after redaction is complete.
|
|
51
|
+
* This is to ensure that the WeakSet doesn't cause memory leaks.
|
|
52
|
+
* @private
|
|
53
|
+
* @param value
|
|
54
|
+
*/
|
|
55
|
+
private maybeSerialise;
|
|
56
|
+
/**
|
|
57
|
+
* 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.
|
|
58
|
+
* @param {unknown} value The value to redact.
|
|
59
|
+
* @returns {unknown} The redacted value.
|
|
60
|
+
*/
|
|
61
|
+
redact: (value: unknown) => unknown;
|
|
62
|
+
}
|
|
63
|
+
export { DeepRedact as default, DeepRedact };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export type Types = 'string' | 'number' | 'bigint' | 'boolean' | 'object' | 'function' | 'symbol' | 'undefined';
|
|
2
|
+
export type Transformer = (value: unknown) => unknown;
|
|
3
|
+
export interface BlacklistKeyConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Perform a fuzzy match on the key. This will match any key that contains the string, rather than a case-sensitive match.
|
|
6
|
+
* @default false
|
|
7
|
+
* @example true // match any key that contains the string 'address', such as 'homeAddress', 'workAddress', 'addressLine1', etc.
|
|
8
|
+
* @example false // match only keys that contain 'address' from start to end.
|
|
9
|
+
*/
|
|
10
|
+
fuzzyKeyMatch?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Perform a case-sensitive match on the key
|
|
13
|
+
* @default true
|
|
14
|
+
* @example false // match any key that contains the string 'address' regardless of upper, lower, snake, camel or any other case.
|
|
15
|
+
* @example true // match only keys that are exactly 'address' in the same case.
|
|
16
|
+
*/
|
|
17
|
+
caseSensitiveKeyMatch?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Retain the structure of the object, but redact the values.
|
|
20
|
+
* @default false
|
|
21
|
+
* @example true // retain the structure of the object, but redact the values. { a: '1' } => becomes { a: '[REDACTED]' }
|
|
22
|
+
* @example false // redact the entire object. { a: '1' } => becomes '[REDACTED]'
|
|
23
|
+
*/
|
|
24
|
+
retainStructure?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Remove the redacted data instead of replacing it with the `replacement` value.
|
|
27
|
+
* @default false // replace the redacted data with the `replacement` value.
|
|
28
|
+
* @example true // remove the redacted data.
|
|
29
|
+
*/
|
|
30
|
+
remove?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Replace string values with a redacted string of the same length, using the `replacement` option. Ignored if `remove` is true, `replacement` is a function, or the value is not a string.
|
|
33
|
+
* @default false
|
|
34
|
+
* @example true // if `replacement` equals `*` then `joe.bloggs@example.com` becomes `**********************`
|
|
35
|
+
* @example false // if `replacement` equals `*` then `joe.bloggs@example.com` becomes `*`
|
|
36
|
+
*/
|
|
37
|
+
replacement?: string | ((value: unknown) => unknown);
|
|
38
|
+
/**
|
|
39
|
+
* The key to redact. Can be a string or a RegExp.
|
|
40
|
+
* @example 'address' // redact any key that is 'address'.
|
|
41
|
+
* @example /^address$/ // redact any key that is exactly 'address'.
|
|
42
|
+
*/
|
|
43
|
+
key: string | RegExp;
|
|
44
|
+
}
|
|
45
|
+
export interface BaseDeepRedactConfig {
|
|
46
|
+
/**
|
|
47
|
+
* Keys that should be redacted. Can be a string, or an object with additional configuration options.
|
|
48
|
+
* @default []
|
|
49
|
+
* @example ['password', 'ssn'] // redact any key that is 'password' or 'ssn'.
|
|
50
|
+
* @example [{ key: 'address', fuzzyKeyMatch: true, caseSensitiveKeyMatch: false }] // redact any key that contains 'address' regardless of case.
|
|
51
|
+
*/
|
|
52
|
+
blacklistedKeys?: Array<string | RegExp | BlacklistKeyConfig>;
|
|
53
|
+
blacklistedKeysTransformed: Array<Required<BlacklistKeyConfig>>;
|
|
54
|
+
/**
|
|
55
|
+
* Redact a string value that matches a test pattern.
|
|
56
|
+
* @default []
|
|
57
|
+
* @example [
|
|
58
|
+
* /^[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}$/, // redact any string that looks like an IP address.
|
|
59
|
+
* ]
|
|
60
|
+
*/
|
|
61
|
+
stringTests?: RegExp[];
|
|
62
|
+
/**
|
|
63
|
+
* Perform a fuzzy match on the key. This will match any key that contains the string, rather than a case-sensitive match.
|
|
64
|
+
* @default false
|
|
65
|
+
* @example true // match any key that contains the string 'address', such as 'homeAddress', 'workAddress', 'addressLine1', etc.
|
|
66
|
+
* @example false // match only keys that contain 'address' from start to end.
|
|
67
|
+
*/
|
|
68
|
+
fuzzyKeyMatch?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Perform a case-sensitive match on the key
|
|
71
|
+
* @default true
|
|
72
|
+
* @example false // match any key that contains the string 'address' regardless of upper, lower, snake, camel or any other case.
|
|
73
|
+
* @example true // match only keys that are exactly 'address' in the same case.
|
|
74
|
+
*/
|
|
75
|
+
caseSensitiveKeyMatch?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Retain the structure of the object, but redact the values.
|
|
78
|
+
* @default false
|
|
79
|
+
* @example true // retain the structure of the object, but redact the values. { a: '1' } => becomes { a: '[REDACTED]' }
|
|
80
|
+
* @example false // redact the entire object. { a: '1' } => becomes '[REDACTED]'
|
|
81
|
+
*/
|
|
82
|
+
retainStructure?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Replace string values with a redacted string of the same length, using the `replacement` option. Ignored if `remove` is true, `replacement` is a function, or the value is not a string.
|
|
85
|
+
* @default false
|
|
86
|
+
* @example true // if `replacement` equals `*` then `joe.bloggs@example.com` becomes `**********************`
|
|
87
|
+
* @example false // if `replacement` equals `*` then `joe.bloggs@example.com` becomes `*`
|
|
88
|
+
*/
|
|
89
|
+
replaceStringByLength?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* The replacement value for redacted data. Can be a string, or a function that takes the original value and returns any value.
|
|
92
|
+
* @default '[REDACTED]'
|
|
93
|
+
* @example (value) => `REDACTED: ${typeof value}` // redact the value with a prefix of 'REDACTED: ' and the type of the value.
|
|
94
|
+
* @example (value) => return typeof value === 'string' ? '*'.repeat(value.length) : '[REDACTED]' // redact the value with a string of the same length.
|
|
95
|
+
* @param value The original value that is being redacted.
|
|
96
|
+
* @returns The redacted value or undefined to remove the value.
|
|
97
|
+
*/
|
|
98
|
+
replacement?: string | Transformer;
|
|
99
|
+
/**
|
|
100
|
+
* Remove the redacted data instead of replacing it with the `replacement` value.
|
|
101
|
+
*/
|
|
102
|
+
remove?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* The types of values that should be redacted. If the value is not one of these types, it will not be redacted.
|
|
105
|
+
* @default ['string']
|
|
106
|
+
* @example ['string', 'number'] // redact only strings and numbers, leave other types unchanged.
|
|
107
|
+
*/
|
|
108
|
+
types?: Types[];
|
|
109
|
+
/**
|
|
110
|
+
* Serialise the redacted data. If true, the redacted data will be returned as a JSON string. If false, it will be returned as an object.
|
|
111
|
+
* @default true
|
|
112
|
+
* @example true // return the redacted data as a JSON string.
|
|
113
|
+
* @example false // return the redacted data as the same type as the original data.
|
|
114
|
+
*/
|
|
115
|
+
serialise?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Alias of `serialise` for International-English users.
|
|
118
|
+
*/
|
|
119
|
+
serialize?: boolean;
|
|
120
|
+
}
|
|
121
|
+
export type DeepRedactConfig = Partial<Omit<BaseDeepRedactConfig, 'blacklistedKeysTransformed' | 'blacklistedKeys' | 'stringTests'>> & ({
|
|
122
|
+
blacklistedKeys: BaseDeepRedactConfig['blacklistedKeys'];
|
|
123
|
+
stringTests: BaseDeepRedactConfig['stringTests'];
|
|
124
|
+
} | {
|
|
125
|
+
blacklistedKeys: BaseDeepRedactConfig['blacklistedKeys'];
|
|
126
|
+
} | {
|
|
127
|
+
stringTests: BaseDeepRedactConfig['stringTests'];
|
|
128
|
+
});
|
|
129
|
+
export type RedactorUtilsConfig = Omit<BaseDeepRedactConfig, 'serialise' | 'serialize'>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { RedactorUtilsConfig } from '../types';
|
|
2
|
+
declare class RedactorUtils {
|
|
3
|
+
/**
|
|
4
|
+
* The configuration for the redaction.
|
|
5
|
+
* @private
|
|
6
|
+
*/
|
|
7
|
+
private readonly config;
|
|
8
|
+
constructor(customConfig: Omit<RedactorUtilsConfig, 'blacklistedKeysTransformed'>);
|
|
9
|
+
/**
|
|
10
|
+
* Normalise a string for comparison. This will convert the string to lowercase and remove any non-word characters.
|
|
11
|
+
* @private
|
|
12
|
+
* @param str The string to normalise.
|
|
13
|
+
* @returns {string} The normalised string.
|
|
14
|
+
*/
|
|
15
|
+
private static normaliseString;
|
|
16
|
+
/**
|
|
17
|
+
* Determine if a key matches a given blacklistedKeyConfig. This will check the key against the blacklisted keys,
|
|
18
|
+
* using the configuration option for the given key falling back to the default configuration.
|
|
19
|
+
* @private
|
|
20
|
+
* @param {string} key The key to check.
|
|
21
|
+
* @param {BlacklistKeyConfig} blacklistKeyConfig The configuration for the key.
|
|
22
|
+
* @returns {boolean} Whether the key should be redacted.
|
|
23
|
+
*/
|
|
24
|
+
private static complexKeyMatch;
|
|
25
|
+
/**
|
|
26
|
+
* Get the configuration for an object key. This will check the key against the transformed blacklisted keys.
|
|
27
|
+
* @private
|
|
28
|
+
* @param {string} key The key of the configuration to get.
|
|
29
|
+
* @returns {Required<BlacklistKeyConfig> | undefined} The configuration for the key.
|
|
30
|
+
*/
|
|
31
|
+
private getBlacklistedKeyConfig;
|
|
32
|
+
/**
|
|
33
|
+
* Get the recursion configuration for a key. This will check the key against the transformed blacklisted keys.
|
|
34
|
+
* If the key is found, the configuration for the key will be returned, otherwise undefined.
|
|
35
|
+
* @private
|
|
36
|
+
* @param {string} key The key of the configuration to get.
|
|
37
|
+
* @returns {Required<Pick<BlacklistKeyConfig, 'remove' | 'replacement' | 'retainStructure'>>} The configuration for the key.
|
|
38
|
+
*/
|
|
39
|
+
private getRecursionConfig;
|
|
40
|
+
/**
|
|
41
|
+
* Determine if a key should be redacted. This will check the key against the blacklisted keys, using the default configuration.
|
|
42
|
+
* @private
|
|
43
|
+
* @param {string} key The key to check.
|
|
44
|
+
* @returns {boolean} Whether the key should be redacted.
|
|
45
|
+
*/
|
|
46
|
+
private shouldRedactObjectValue;
|
|
47
|
+
/**
|
|
48
|
+
* 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.
|
|
49
|
+
* @private
|
|
50
|
+
* @param value
|
|
51
|
+
* @param replacement
|
|
52
|
+
* @param remove
|
|
53
|
+
* @param shouldRedact
|
|
54
|
+
*/
|
|
55
|
+
private redactString;
|
|
56
|
+
/**
|
|
57
|
+
* 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.
|
|
58
|
+
* @private
|
|
59
|
+
* @param {unknown} value The value to redact.
|
|
60
|
+
* @param {Transformer | string} replacement The replacement value for redacted data.
|
|
61
|
+
* @param {boolean} remove Whether the redacted data should be removed.
|
|
62
|
+
* @param {boolean} shouldRedact Whether the value should be redacted based on the parent key.
|
|
63
|
+
* @returns {unknown} The redacted value.
|
|
64
|
+
*/
|
|
65
|
+
private redactPrimitive;
|
|
66
|
+
/**
|
|
67
|
+
* Redact an array. This will redact each value in the array using the `recurse` method.
|
|
68
|
+
* @private
|
|
69
|
+
* @param {unknown[]} value The array to redact.
|
|
70
|
+
* @returns {unknown[]} The redacted array.
|
|
71
|
+
*/
|
|
72
|
+
private redactArray;
|
|
73
|
+
/**
|
|
74
|
+
* Redact an object. This will recursively redact the object based on the configuration, redacting the keys and values as required.
|
|
75
|
+
* @param {Object} value The object to redact.
|
|
76
|
+
* @param {string | null} key The key of the object if it is part of another object.
|
|
77
|
+
* @param {boolean} parentShouldRedact Whether the item should be redacted based on the key within the parent object.
|
|
78
|
+
*/
|
|
79
|
+
private redactObject;
|
|
80
|
+
/**
|
|
81
|
+
* 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.
|
|
82
|
+
* @private
|
|
83
|
+
* @param {unknown} value The value to redact.
|
|
84
|
+
* @param {string | null} key The key of the value if it is part of an object.
|
|
85
|
+
* @param {boolean} parentShouldRedact Whether the parent object should be redacted.
|
|
86
|
+
* @returns {unknown} The redacted value.
|
|
87
|
+
*/
|
|
88
|
+
recurse: (value: unknown, key?: string | null, parentShouldRedact?: boolean) => unknown;
|
|
89
|
+
}
|
|
90
|
+
export default RedactorUtils;
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hackylabs/deep-redact",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A fast, safe and configurable zero-dependency library for redacting strings or deeply redacting arrays and objects.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Benjamin Green (https://bengreen.dev)",
|
|
8
|
+
"types": "./dist/types/index.d.ts",
|
|
9
|
+
"main": "./dist/cjs/index.js",
|
|
10
|
+
"module": "./dist/esm/index.mjs",
|
|
8
11
|
"keywords": [
|
|
9
12
|
"redact",
|
|
10
13
|
"redaction",
|
|
@@ -26,7 +29,7 @@
|
|
|
26
29
|
".": {
|
|
27
30
|
"import": "./dist/esm/index.js",
|
|
28
31
|
"require": "./dist/cjs/index.js",
|
|
29
|
-
"types": "./dist/
|
|
32
|
+
"types": "./dist/types/index.d.ts"
|
|
30
33
|
}
|
|
31
34
|
},
|
|
32
35
|
"files": [
|
|
@@ -39,7 +42,7 @@
|
|
|
39
42
|
"scripts": {
|
|
40
43
|
"lint": "eslint",
|
|
41
44
|
"build": "npm run lint && npm run test && npm run bench && npm run build:esm && npm run build:cjs && npm run update-readme && npm run update-license",
|
|
42
|
-
"build:esm": "tsc --project tsconfig.esm.json",
|
|
45
|
+
"build:esm": "tsc --project tsconfig.esm.json && ./scripts/js-to-mjs.sh",
|
|
43
46
|
"build:cjs": "tsc --project tsconfig.cjs.json",
|
|
44
47
|
"bench": "npx vitest bench --watch=false",
|
|
45
48
|
"bench:dev": "npx vitest bench",
|