@hackylabs/deep-redact 2.2.0 → 2.2.2
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 +42 -50
- package/dist/cjs/index.js +12 -5
- package/dist/cjs/utils/redactorUtils.js +12 -23
- package/dist/esm/index.mjs +12 -5
- package/dist/esm/utils/redactorUtils.mjs +12 -23
- package/dist/types/index.d.ts +3 -0
- package/dist/types/utils/redactorUtils.d.ts +1 -1
- package/package.json +2 -4
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Benjamin Green (https://bengreen.dev)
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
[](https://badge.fury.io/js/@hackylabs%2Fdeep-redact)
|
|
4
4
|
[](https://github.com/hackylabs/deep-redact/blob/main/LICENSE)
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
from strings is supported, by way of custom regex patterns and replacers.
|
|
6
|
+
Deep Redact is a safe, configurable, zero-dependency tool that redacts sensitive information from strings and objects.
|
|
7
|
+
It is designed to be used in a production environment where sensitive information needs to be redacted from logs, error
|
|
8
|
+
messages, files, and other outputs. Supporting both strings and objects or a mix of both, Deep Redact can be used to
|
|
9
|
+
redact sensitive information from a wide range of data structures. Even partially redacting sensitive information from
|
|
10
|
+
strings is supported, by way of custom regex patterns and replacers.
|
|
12
11
|
|
|
13
12
|
Circular references and other unsupported values are handled gracefully, and the library is designed to be as fast as
|
|
14
13
|
possible while still being easy to use and configure.
|
|
@@ -73,6 +72,30 @@ strRedaction.redact('<email>someone@somewhere.com</email><keepThis>This is fine<
|
|
|
73
72
|
// '<email>[REDACTED]</email><keepThis>This is fine</keepThis><password>[REDACTED]</password>'
|
|
74
73
|
```
|
|
75
74
|
|
|
75
|
+
// Override the `unsupportedTransformer` method to handle unsupported values
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
class CustomRedaction extends DeepRedact {
|
|
79
|
+
constructor(options) {
|
|
80
|
+
super(options)
|
|
81
|
+
this.rewriteUnsupported = (value) => {
|
|
82
|
+
if (value instanceof BigInt) return value.toString()
|
|
83
|
+
|
|
84
|
+
// Add more conditional statements for unsupported value types here (e.g. Error, Date, Map, Set, etc.)
|
|
85
|
+
|
|
86
|
+
// If the value is supported, return it
|
|
87
|
+
return value
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const customRedaction = new CustomRedaction({
|
|
93
|
+
blacklistedKeys: ['sensitive', 'password', /name/i],
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
customRedaction.redact({ a: BigInt(1) })
|
|
97
|
+
```
|
|
98
|
+
|
|
76
99
|
## Configuration
|
|
77
100
|
|
|
78
101
|
### Main Options
|
|
@@ -110,47 +133,16 @@ strRedaction.redact('<email>someone@somewhere.com</email><keepThis>This is fine<
|
|
|
110
133
|
| replacer | A function that will be called with the value of the string that matched the pattern and the pattern itself. This function should return the new (redacted) value to replace the original value. | function | Y |
|
|
111
134
|
|
|
112
135
|
### Benchmark
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
values as gracefully as deep-redact unless a third-party library is used to stringify the object beforehand.
|
|
127
|
-
|
|
128
|
-
Fast-redact is included as a benchmark because it's the next fastest library available specifically for redaction.
|
|
129
|
-
|
|
130
|
-
Neither JSON.stringify, Regex.replace nor Fast Redact offer the same level of configurability as deep-redact. Both Fast
|
|
131
|
-
Redact and Obglob are slower and rely on dependencies.
|
|
132
|
-
|
|
133
|
-

|
|
134
|
-
|
|
135
|
-
| scenario | ops / sec | op duration (ms) | margin of error | sample count |
|
|
136
|
-
| --- | --- | --- | --- | --- |
|
|
137
|
-
| DeepRedact, partial redaction | 178183.03 | 0.0056122067 | 0.00003 | 89092 |
|
|
138
|
-
| JSON.stringify, large object | 161672.63 | 0.0061853388 | 0.00004 | 80837 |
|
|
139
|
-
| DeepRedact, remove item, single object | 25085.95 | 0.039862959 | 0.00033 | 12543 |
|
|
140
|
-
| DeepRedact, default config, large object | 23164.57 | 0.0431693713 | 0.00023 | 11583 |
|
|
141
|
-
| Regex replace, large object | 22828.32 | 0.043805244 | 0.00031 | 11415 |
|
|
142
|
-
| DeepRedact, custom replacer function, single object | 22520.52 | 0.0444039553 | 0.00039 | 11261 |
|
|
143
|
-
| DeepRedact, replace string by length, single object | 22470.94 | 0.0445019191 | 0.00025 | 11236 |
|
|
144
|
-
| DeepRedact, retain structure, single object | 18315.67 | 0.0545980591 | 0.00026 | 9158 |
|
|
145
|
-
| DeepRedact, fuzzy matching, single object | 18077.45 | 0.0553175285 | 0.00035 | 9039 |
|
|
146
|
-
| DeepRedact, config per key, single object | 16055.96 | 0.0622821745 | 0.00031 | 8028 |
|
|
147
|
-
| DeepRedact, default config, 1000 large objects | 8077.4 | 0.1238022 | 0.00187 | 4039 |
|
|
148
|
-
| fast redact, large object | 5918.61 | 0.1689584628 | 0.00151 | 2960 |
|
|
149
|
-
| ObGlob, large object | 5193.29 | 0.1925563273 | 0.00972 | 2597 |
|
|
150
|
-
| DeepRedact, case insensitive matching, single object | 3477.6 | 0.2875549482 | 0.00324 | 1739 |
|
|
151
|
-
| DeepRedact, fuzzy and case insensitive matching, single object | 3437.73 | 0.2908896131 | 0.00239 | 1719 |
|
|
152
|
-
| ObGlob, 1000 large objects | 237.69 | 4.2072005126 | 0.05005 | 119 |
|
|
153
|
-
| JSON.stringify, 1000 large objects | 230.33 | 4.3416907672 | 0.04373 | 116 |
|
|
154
|
-
| DeepRedact, partial redaction large string | 127.18 | 7.8628400156 | 0.4176 | 64 |
|
|
155
|
-
| fast redact, 1000 large objects | 119.33 | 8.3803393 | 0.31943 | 60 |
|
|
156
|
-
| Regex replace, 1000 large objects | 97.12 | 10.2963537755 | 0.29341 | 49 |
|
|
136
|
+
|
|
137
|
+
A benchmark comparing Deep Redact against other libraries appeared in earlier versions of this README. It has been
|
|
138
|
+
withdrawn because of a subtle flaw in how the test data was constructed.
|
|
139
|
+
|
|
140
|
+
The multi-object scenarios built their input with `Array(1000).fill(user)`, which produces an array of 1000 references
|
|
141
|
+
to a *single* shared object rather than 1000 distinct objects. Deep Redact's circular-reference protection correctly
|
|
142
|
+
detects the repeated reference and replaces every occurrence after the first with a circular-reference marker, so it
|
|
143
|
+
effectively redacted one object while the other libraries redacted all 1000. That short-circuiting is correct behaviour
|
|
144
|
+
in its own right, which is part of why the benchmarking mistake was easy to overlook, but it meant the libraries were
|
|
145
|
+
doing very different amounts of work and overstated Deep Redact's throughput by roughly 50-70x in those cases. The
|
|
146
|
+
single-object scenarios were also not strictly like-for-like.
|
|
147
|
+
|
|
148
|
+
Because the figures were not representative, this release makes no performance comparison against other libraries.
|
package/dist/cjs/index.js
CHANGED
|
@@ -47,8 +47,6 @@ class DeepRedact {
|
|
|
47
47
|
* @returns {unknown} The value in a format that is supported by JSON.stringify.
|
|
48
48
|
*/
|
|
49
49
|
this.unsupportedTransformer = (value) => {
|
|
50
|
-
if (!this.config.serialise)
|
|
51
|
-
return value;
|
|
52
50
|
if (typeof value === 'bigint') {
|
|
53
51
|
return {
|
|
54
52
|
__unsupported: {
|
|
@@ -145,18 +143,27 @@ class DeepRedact {
|
|
|
145
143
|
* This is to ensure that the WeakSet doesn't cause memory leaks.
|
|
146
144
|
* @private
|
|
147
145
|
* @param value
|
|
146
|
+
* @returns {unknown} The value as a JSON string or as the provided value.
|
|
147
|
+
* @throws {Error} If the value cannot be serialised.
|
|
148
148
|
*/
|
|
149
149
|
this.maybeSerialise = (value) => {
|
|
150
150
|
this.circularReference = null;
|
|
151
|
-
const result = this.redactorUtils.partialStringRedact(value);
|
|
152
151
|
if (!this.config.serialise)
|
|
153
|
-
return
|
|
154
|
-
|
|
152
|
+
return value;
|
|
153
|
+
if (typeof value === 'string')
|
|
154
|
+
return value;
|
|
155
|
+
try {
|
|
156
|
+
return JSON.stringify(value);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
throw new Error('Failed to serialise value. Did you override the `unsupportedTransformer` method and return a value that is not supported by JSON.stringify?');
|
|
160
|
+
}
|
|
155
161
|
};
|
|
156
162
|
/**
|
|
157
163
|
* 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.
|
|
158
164
|
* @param {unknown} value The value to redact.
|
|
159
165
|
* @returns {unknown} The redacted value.
|
|
166
|
+
* @throws {Error} If the value cannot be serialised.
|
|
160
167
|
*/
|
|
161
168
|
this.redact = (value) => {
|
|
162
169
|
return this.maybeSerialise(this.redactorUtils.recurse(this.rewriteUnsupported(value)));
|
|
@@ -83,36 +83,37 @@ class RedactorUtils {
|
|
|
83
83
|
this.redactString = (value, replacement, remove, shouldRedact) => {
|
|
84
84
|
if (!value || typeof value !== 'string')
|
|
85
85
|
return value;
|
|
86
|
+
const maybePartiallyRedacted = this.partialStringRedact(value);
|
|
86
87
|
const { stringTests } = this.config;
|
|
87
88
|
if (!shouldRedact) {
|
|
88
89
|
const result = stringTests === null || stringTests === void 0 ? void 0 : stringTests.map((test) => {
|
|
89
90
|
if (test instanceof RegExp) {
|
|
90
|
-
if (!test.test(
|
|
91
|
-
return
|
|
91
|
+
if (!test.test(maybePartiallyRedacted))
|
|
92
|
+
return maybePartiallyRedacted;
|
|
92
93
|
if (remove)
|
|
93
94
|
return undefined;
|
|
94
95
|
if (typeof replacement === 'function')
|
|
95
|
-
return replacement(
|
|
96
|
+
return replacement(maybePartiallyRedacted);
|
|
96
97
|
if (this.config.replaceStringByLength)
|
|
97
|
-
return replacement.repeat(
|
|
98
|
+
return replacement.repeat(maybePartiallyRedacted.length);
|
|
98
99
|
return replacement;
|
|
99
100
|
}
|
|
100
|
-
if (remove && test.pattern.test(
|
|
101
|
+
if (remove && test.pattern.test(maybePartiallyRedacted))
|
|
101
102
|
return undefined;
|
|
102
|
-
return test.replacer(
|
|
103
|
+
return test.replacer(maybePartiallyRedacted, test.pattern);
|
|
103
104
|
}).filter(Boolean)[0];
|
|
104
105
|
if (result)
|
|
105
106
|
return result;
|
|
106
107
|
if (remove)
|
|
107
108
|
return undefined;
|
|
108
|
-
return
|
|
109
|
+
return maybePartiallyRedacted;
|
|
109
110
|
}
|
|
110
111
|
if (remove)
|
|
111
112
|
return undefined;
|
|
112
113
|
if (typeof replacement === 'function')
|
|
113
|
-
return replacement(
|
|
114
|
+
return replacement(maybePartiallyRedacted);
|
|
114
115
|
if (this.config.replaceStringByLength)
|
|
115
|
-
return replacement.repeat(
|
|
116
|
+
return replacement.repeat(maybePartiallyRedacted.length);
|
|
116
117
|
return replacement;
|
|
117
118
|
};
|
|
118
119
|
/**
|
|
@@ -165,23 +166,11 @@ class RedactorUtils {
|
|
|
165
166
|
const { partialStringTests } = this.config;
|
|
166
167
|
if (partialStringTests.length === 0)
|
|
167
168
|
return value;
|
|
168
|
-
let result;
|
|
169
|
-
if (typeof value === 'string') {
|
|
170
|
-
result = value;
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
try {
|
|
174
|
-
result = JSON.stringify(value);
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
// It should never reach this point, but if it does, it will throw an error that must not contain sensitive data.
|
|
178
|
-
throw new Error('Failed to stringify value for partialStringRedact. Did you replace the rewriteUnsupported method with something that returns non-serialisable data?');
|
|
179
|
-
}
|
|
180
|
-
}
|
|
169
|
+
let result = value;
|
|
181
170
|
partialStringTests.forEach((test) => {
|
|
182
171
|
result = test.replacer(result, test.pattern);
|
|
183
172
|
});
|
|
184
|
-
return
|
|
173
|
+
return result;
|
|
185
174
|
};
|
|
186
175
|
/**
|
|
187
176
|
* 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.
|
package/dist/esm/index.mjs
CHANGED
|
@@ -42,8 +42,6 @@ class DeepRedact {
|
|
|
42
42
|
* @returns {unknown} The value in a format that is supported by JSON.stringify.
|
|
43
43
|
*/
|
|
44
44
|
unsupportedTransformer = (value) => {
|
|
45
|
-
if (!this.config.serialise)
|
|
46
|
-
return value;
|
|
47
45
|
if (typeof value === 'bigint') {
|
|
48
46
|
return {
|
|
49
47
|
__unsupported: {
|
|
@@ -138,18 +136,27 @@ class DeepRedact {
|
|
|
138
136
|
* This is to ensure that the WeakSet doesn't cause memory leaks.
|
|
139
137
|
* @private
|
|
140
138
|
* @param value
|
|
139
|
+
* @returns {unknown} The value as a JSON string or as the provided value.
|
|
140
|
+
* @throws {Error} If the value cannot be serialised.
|
|
141
141
|
*/
|
|
142
142
|
maybeSerialise = (value) => {
|
|
143
143
|
this.circularReference = null;
|
|
144
|
-
const result = this.redactorUtils.partialStringRedact(value);
|
|
145
144
|
if (!this.config.serialise)
|
|
146
|
-
return
|
|
147
|
-
|
|
145
|
+
return value;
|
|
146
|
+
if (typeof value === 'string')
|
|
147
|
+
return value;
|
|
148
|
+
try {
|
|
149
|
+
return JSON.stringify(value);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
throw new Error('Failed to serialise value. Did you override the `unsupportedTransformer` method and return a value that is not supported by JSON.stringify?');
|
|
153
|
+
}
|
|
148
154
|
};
|
|
149
155
|
/**
|
|
150
156
|
* 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
157
|
* @param {unknown} value The value to redact.
|
|
152
158
|
* @returns {unknown} The redacted value.
|
|
159
|
+
* @throws {Error} If the value cannot be serialised.
|
|
153
160
|
*/
|
|
154
161
|
redact = (value) => {
|
|
155
162
|
return this.maybeSerialise(this.redactorUtils.recurse(this.rewriteUnsupported(value)));
|
|
@@ -135,36 +135,37 @@ class RedactorUtils {
|
|
|
135
135
|
redactString = (value, replacement, remove, shouldRedact) => {
|
|
136
136
|
if (!value || typeof value !== 'string')
|
|
137
137
|
return value;
|
|
138
|
+
const maybePartiallyRedacted = this.partialStringRedact(value);
|
|
138
139
|
const { stringTests } = this.config;
|
|
139
140
|
if (!shouldRedact) {
|
|
140
141
|
const result = stringTests?.map((test) => {
|
|
141
142
|
if (test instanceof RegExp) {
|
|
142
|
-
if (!test.test(
|
|
143
|
-
return
|
|
143
|
+
if (!test.test(maybePartiallyRedacted))
|
|
144
|
+
return maybePartiallyRedacted;
|
|
144
145
|
if (remove)
|
|
145
146
|
return undefined;
|
|
146
147
|
if (typeof replacement === 'function')
|
|
147
|
-
return replacement(
|
|
148
|
+
return replacement(maybePartiallyRedacted);
|
|
148
149
|
if (this.config.replaceStringByLength)
|
|
149
|
-
return replacement.repeat(
|
|
150
|
+
return replacement.repeat(maybePartiallyRedacted.length);
|
|
150
151
|
return replacement;
|
|
151
152
|
}
|
|
152
|
-
if (remove && test.pattern.test(
|
|
153
|
+
if (remove && test.pattern.test(maybePartiallyRedacted))
|
|
153
154
|
return undefined;
|
|
154
|
-
return test.replacer(
|
|
155
|
+
return test.replacer(maybePartiallyRedacted, test.pattern);
|
|
155
156
|
}).filter(Boolean)[0];
|
|
156
157
|
if (result)
|
|
157
158
|
return result;
|
|
158
159
|
if (remove)
|
|
159
160
|
return undefined;
|
|
160
|
-
return
|
|
161
|
+
return maybePartiallyRedacted;
|
|
161
162
|
}
|
|
162
163
|
if (remove)
|
|
163
164
|
return undefined;
|
|
164
165
|
if (typeof replacement === 'function')
|
|
165
|
-
return replacement(
|
|
166
|
+
return replacement(maybePartiallyRedacted);
|
|
166
167
|
if (this.config.replaceStringByLength)
|
|
167
|
-
return replacement.repeat(
|
|
168
|
+
return replacement.repeat(maybePartiallyRedacted.length);
|
|
168
169
|
return replacement;
|
|
169
170
|
};
|
|
170
171
|
/**
|
|
@@ -217,23 +218,11 @@ class RedactorUtils {
|
|
|
217
218
|
const { partialStringTests } = this.config;
|
|
218
219
|
if (partialStringTests.length === 0)
|
|
219
220
|
return value;
|
|
220
|
-
let result;
|
|
221
|
-
if (typeof value === 'string') {
|
|
222
|
-
result = value;
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
try {
|
|
226
|
-
result = JSON.stringify(value);
|
|
227
|
-
}
|
|
228
|
-
catch (error) {
|
|
229
|
-
// It should never reach this point, but if it does, it will throw an error that must not contain sensitive data.
|
|
230
|
-
throw new Error('Failed to stringify value for partialStringRedact. Did you replace the rewriteUnsupported method with something that returns non-serialisable data?');
|
|
231
|
-
}
|
|
232
|
-
}
|
|
221
|
+
let result = value;
|
|
233
222
|
partialStringTests.forEach((test) => {
|
|
234
223
|
result = test.replacer(result, test.pattern);
|
|
235
224
|
});
|
|
236
|
-
return
|
|
225
|
+
return result;
|
|
237
226
|
};
|
|
238
227
|
/**
|
|
239
228
|
* 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.
|
package/dist/types/index.d.ts
CHANGED
|
@@ -51,12 +51,15 @@ declare class DeepRedact {
|
|
|
51
51
|
* This is to ensure that the WeakSet doesn't cause memory leaks.
|
|
52
52
|
* @private
|
|
53
53
|
* @param value
|
|
54
|
+
* @returns {unknown} The value as a JSON string or as the provided value.
|
|
55
|
+
* @throws {Error} If the value cannot be serialised.
|
|
54
56
|
*/
|
|
55
57
|
private maybeSerialise;
|
|
56
58
|
/**
|
|
57
59
|
* 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
60
|
* @param {unknown} value The value to redact.
|
|
59
61
|
* @returns {unknown} The redacted value.
|
|
62
|
+
* @throws {Error} If the value cannot be serialised.
|
|
60
63
|
*/
|
|
61
64
|
redact: (value: unknown) => unknown;
|
|
62
65
|
}
|
|
@@ -77,7 +77,7 @@ declare class RedactorUtils {
|
|
|
77
77
|
* @param {boolean} parentShouldRedact Whether the item should be redacted based on the key within the parent object.
|
|
78
78
|
*/
|
|
79
79
|
private redactObject;
|
|
80
|
-
partialStringRedact: (value:
|
|
80
|
+
partialStringRedact: (value: string) => string;
|
|
81
81
|
/**
|
|
82
82
|
* 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.
|
|
83
83
|
* @private
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hackylabs/deep-redact",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
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",
|
|
@@ -51,11 +51,9 @@
|
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"lint": "eslint",
|
|
54
|
-
"build": "npm run lint && npm run test && npm run
|
|
54
|
+
"build": "npm run lint && npm run test && npm run build:esm && npm run build:cjs && npm run update-readme && npm run update-license",
|
|
55
55
|
"build:esm": "tsc --project tsconfig.esm.json && ./scripts/js-to-mjs.sh",
|
|
56
56
|
"build:cjs": "tsc --project tsconfig.cjs.json",
|
|
57
|
-
"bench": "npx vitest bench --watch=false",
|
|
58
|
-
"bench:dev": "npx vitest bench",
|
|
59
57
|
"test:dev": "npx vitest",
|
|
60
58
|
"test": "npx vitest run",
|
|
61
59
|
"update-readme": "npx ts-node ./scripts/update-readme.ts",
|