@hackylabs/deep-redact 3.0.5 → 4.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 +221 -106
- package/dist/adapters/console/index.cjs +74 -0
- package/dist/adapters/console/index.d.cts +22 -0
- package/dist/adapters/console/index.d.ts +22 -0
- package/dist/adapters/console/index.js +73 -0
- package/dist/index.cjs +2743 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2741 -55
- package/dist/node-console-sink-BnRUkAAr.cjs +19 -0
- package/dist/node-console-sink-DaQleNZ8.js +14 -0
- package/dist/public-Da0aARA9.d.cts +127 -0
- package/dist/public-Dw4ycNzO.d.ts +127 -0
- package/package.json +66 -106
- package/dist/index.mjs +0 -40
- package/dist/types/index.d.ts +0 -29
- package/dist/types/types.d.ts +0 -224
- package/dist/types/utils/TransformerRegistry.d.ts +0 -52
- package/dist/types/utils/index.d.ts +0 -131
- package/dist/types/utils/standardTransformers/bigint.d.ts +0 -2
- package/dist/types/utils/standardTransformers/date.d.ts +0 -2
- package/dist/types/utils/standardTransformers/error.d.ts +0 -2
- package/dist/types/utils/standardTransformers/index.d.ts +0 -9
- package/dist/types/utils/standardTransformers/map.d.ts +0 -2
- package/dist/types/utils/standardTransformers/regex.d.ts +0 -2
- package/dist/types/utils/standardTransformers/set.d.ts +0 -2
- package/dist/types/utils/standardTransformers/url.d.ts +0 -2
- package/dist/types.js +0 -2
- package/dist/types.mjs +0 -1
- package/dist/utils/TransformerRegistry.js +0 -100
- package/dist/utils/TransformerRegistry.mjs +0 -94
- package/dist/utils/index.js +0 -471
- package/dist/utils/index.mjs +0 -467
- package/dist/utils/standardTransformers/bigint.js +0 -10
- package/dist/utils/standardTransformers/bigint.mjs +0 -6
- package/dist/utils/standardTransformers/date.js +0 -9
- package/dist/utils/standardTransformers/date.mjs +0 -5
- package/dist/utils/standardTransformers/error.js +0 -16
- package/dist/utils/standardTransformers/error.mjs +0 -12
- package/dist/utils/standardTransformers/index.js +0 -38
- package/dist/utils/standardTransformers/index.mjs +0 -35
- package/dist/utils/standardTransformers/map.js +0 -9
- package/dist/utils/standardTransformers/map.mjs +0 -5
- package/dist/utils/standardTransformers/regex.js +0 -15
- package/dist/utils/standardTransformers/regex.mjs +0 -11
- package/dist/utils/standardTransformers/set.js +0 -9
- package/dist/utils/standardTransformers/set.mjs +0 -5
- package/dist/utils/standardTransformers/url.js +0 -9
- package/dist/utils/standardTransformers/url.mjs +0 -5
package/dist/index.js
CHANGED
|
@@ -1,55 +1,2741 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
import { t as getNodeConsoleDiagnosticSink } from "./node-console-sink-DaQleNZ8.js";
|
|
2
|
+
//#region src/core/compiler/compile-diagnostics.ts
|
|
3
|
+
const compileDiagnostics = (diagnostics) => {
|
|
4
|
+
return Object.freeze({
|
|
5
|
+
eventName: "redaction.failure",
|
|
6
|
+
sink: diagnostics?.sink
|
|
7
|
+
});
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/core/compiler/compile-ignored-value-types.ts
|
|
11
|
+
const compileIgnoredValueTypes = (configured) => {
|
|
12
|
+
return Object.freeze({
|
|
13
|
+
bigint: configured?.bigint === true,
|
|
14
|
+
Date: configured?.Date === true,
|
|
15
|
+
Error: configured?.Error === true,
|
|
16
|
+
Map: configured?.Map === true,
|
|
17
|
+
RegExp: configured?.RegExp === true,
|
|
18
|
+
Set: configured?.Set === true,
|
|
19
|
+
URL: configured?.URL === true
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/core/compiler/compile-value-types.ts
|
|
24
|
+
const compileValueTypes = (configured) => {
|
|
25
|
+
const allows = (name) => {
|
|
26
|
+
return configured === void 0 ? name === "string" : configured.includes(name);
|
|
27
|
+
};
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
string: allows("string"),
|
|
30
|
+
number: allows("number"),
|
|
31
|
+
bigint: allows("bigint"),
|
|
32
|
+
boolean: allows("boolean"),
|
|
33
|
+
object: allows("object"),
|
|
34
|
+
function: allows("function"),
|
|
35
|
+
symbol: allows("symbol"),
|
|
36
|
+
undefined: allows("undefined")
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/transformers/built-ins.ts
|
|
41
|
+
const bigintTransformer = (value) => {
|
|
42
|
+
if (typeof value !== "bigint") return value;
|
|
43
|
+
return {
|
|
44
|
+
_transformer: "bigint",
|
|
45
|
+
value: {
|
|
46
|
+
radix: 10,
|
|
47
|
+
number: value.toString(10)
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const dateTransformer = (value) => {
|
|
52
|
+
if (!(value instanceof Date)) return value;
|
|
53
|
+
return {
|
|
54
|
+
_transformer: "date",
|
|
55
|
+
datetime: value.toISOString()
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
const errorTransformer = (value) => {
|
|
59
|
+
if (!(value instanceof Error)) return value;
|
|
60
|
+
return {
|
|
61
|
+
_transformer: "error",
|
|
62
|
+
value: {
|
|
63
|
+
type: value.constructor.name,
|
|
64
|
+
message: value.message,
|
|
65
|
+
stack: value.stack
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
const mapTransformer = (value) => {
|
|
70
|
+
if (!(value instanceof Map)) return value;
|
|
71
|
+
return {
|
|
72
|
+
_transformer: "map",
|
|
73
|
+
value: Object.fromEntries(value.entries())
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
const regexTransformer = (value) => {
|
|
77
|
+
if (!(value instanceof RegExp)) return value;
|
|
78
|
+
return {
|
|
79
|
+
_transformer: "regex",
|
|
80
|
+
value: {
|
|
81
|
+
source: value.source,
|
|
82
|
+
flags: value.flags
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
const setTransformer = (value) => {
|
|
87
|
+
if (!(value instanceof Set)) return value;
|
|
88
|
+
return {
|
|
89
|
+
_transformer: "set",
|
|
90
|
+
value: [...value.values()]
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
const urlTransformer = (value) => {
|
|
94
|
+
if (!(value instanceof URL)) return value;
|
|
95
|
+
return {
|
|
96
|
+
_transformer: "url",
|
|
97
|
+
value: value.href
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/core/compiler/compile-transformers.ts
|
|
102
|
+
const emptyTransformers = Object.freeze([]);
|
|
103
|
+
const mergeTransformers = (configured, builtIns = emptyTransformers) => {
|
|
104
|
+
return Object.freeze([...configured ?? emptyTransformers, ...builtIns]);
|
|
105
|
+
};
|
|
106
|
+
const compileByType = (configured) => {
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
bigint: mergeTransformers(configured?.bigint, Object.freeze([bigintTransformer])),
|
|
109
|
+
object: mergeTransformers(configured?.object)
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
const compileByConstructor = (configured) => {
|
|
113
|
+
const customRegistrations = configured?.custom ?? [];
|
|
114
|
+
return Object.freeze({
|
|
115
|
+
Date: mergeTransformers(configured?.Date, Object.freeze([dateTransformer])),
|
|
116
|
+
Error: mergeTransformers(configured?.Error, Object.freeze([errorTransformer])),
|
|
117
|
+
Map: mergeTransformers(configured?.Map, Object.freeze([mapTransformer])),
|
|
118
|
+
RegExp: mergeTransformers(configured?.RegExp, Object.freeze([regexTransformer])),
|
|
119
|
+
Set: mergeTransformers(configured?.Set, Object.freeze([setTransformer])),
|
|
120
|
+
URL: mergeTransformers(configured?.URL, Object.freeze([urlTransformer])),
|
|
121
|
+
custom: Object.freeze(customRegistrations.map((registration) => {
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
constructor: registration.constructor,
|
|
124
|
+
transformers: mergeTransformers(registration.transformers)
|
|
125
|
+
});
|
|
126
|
+
}))
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
const compileTransformers = (configured) => {
|
|
130
|
+
return Object.freeze({
|
|
131
|
+
byType: compileByType(configured?.byType),
|
|
132
|
+
byConstructor: compileByConstructor(configured?.byConstructor),
|
|
133
|
+
fallback: mergeTransformers(configured?.fallback)
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/core/validation/regex-safety.ts
|
|
138
|
+
const cloneRegExp = (pattern) => {
|
|
139
|
+
return new RegExp(pattern.source, pattern.flags);
|
|
140
|
+
};
|
|
141
|
+
const maxRegexSourceLength = 256;
|
|
142
|
+
const nestedQuantifierRegexPattern = /\((?:\?:|\?=|\?!|\?<=|\?<!|\?<[^>]+>)?(?:\\.|[^()[\]\\]|\[[^\]]*])*(?:[+*]|\{\d+(?:,\d*)?\})(?:\\.|[^()[\]\\]|\[[^\]]*])*\)(?:[+*]|\{\d+(?:,\d*)?\})/;
|
|
143
|
+
const quantifiedGroupRegexPattern = /\((?:\?:|\?=|\?!|\?<=|\?<!|\?<[^>]+>)?((?:\\.|[^()[\]\\]|\[[^\]]*])*)\)(?:[+*]|\{\d+(?:,\d*)?\})/g;
|
|
144
|
+
const isRegExp = (value) => {
|
|
145
|
+
return value instanceof RegExp;
|
|
146
|
+
};
|
|
147
|
+
const splitRegexAlternatives = (source) => {
|
|
148
|
+
const alternatives = [""];
|
|
149
|
+
let inCharacterClass = false;
|
|
150
|
+
let groupDepth = 0;
|
|
151
|
+
let escaped = false;
|
|
152
|
+
for (const character of source) {
|
|
153
|
+
if (escaped) {
|
|
154
|
+
alternatives[alternatives.length - 1] += character;
|
|
155
|
+
escaped = false;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (character === "\\") {
|
|
159
|
+
alternatives[alternatives.length - 1] += character;
|
|
160
|
+
escaped = true;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (character === "[") {
|
|
164
|
+
inCharacterClass = true;
|
|
165
|
+
alternatives[alternatives.length - 1] += character;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (character === "]" && inCharacterClass) {
|
|
169
|
+
inCharacterClass = false;
|
|
170
|
+
alternatives[alternatives.length - 1] += character;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (!inCharacterClass) {
|
|
174
|
+
if (character === "(") {
|
|
175
|
+
groupDepth++;
|
|
176
|
+
alternatives[alternatives.length - 1] += character;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (character === ")") {
|
|
180
|
+
groupDepth--;
|
|
181
|
+
alternatives[alternatives.length - 1] += character;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (character === "|" && groupDepth === 0) {
|
|
185
|
+
alternatives.push("");
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
alternatives[alternatives.length - 1] += character;
|
|
190
|
+
}
|
|
191
|
+
return alternatives;
|
|
192
|
+
};
|
|
193
|
+
const hasPrefixOverlappingAlternatives = (alternatives) => {
|
|
194
|
+
const nonEmptyAlternatives = alternatives.filter((alternative) => alternative.length > 0);
|
|
195
|
+
return nonEmptyAlternatives.some((alternative, index) => {
|
|
196
|
+
return nonEmptyAlternatives.some((otherAlternative, otherIndex) => {
|
|
197
|
+
return index !== otherIndex && otherAlternative.startsWith(alternative);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
const hasUnsafeOverlappingAlternation = (source) => {
|
|
202
|
+
for (const match of source.matchAll(quantifiedGroupRegexPattern)) {
|
|
203
|
+
const alternatives = splitRegexAlternatives(match[1] ?? "");
|
|
204
|
+
if (alternatives.length > 1 && hasPrefixOverlappingAlternatives(alternatives)) return true;
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
};
|
|
208
|
+
const lowercaseInitial = (value) => {
|
|
209
|
+
return `${value.charAt(0).toLowerCase()}${value.slice(1)}`;
|
|
210
|
+
};
|
|
211
|
+
const getUnsupportedRegexMessage = (selector, label, options = {}) => {
|
|
212
|
+
if (selector.sticky) return options.allowGlobal === true ? `${label} must not use sticky flag.` : `${label} must not use global or sticky flags.`;
|
|
213
|
+
if (selector.global && options.allowGlobal !== true) return `${label} must not use global or sticky flags.`;
|
|
214
|
+
if ([...selector.source].length > maxRegexSourceLength) return `${label} source must be at most ${maxRegexSourceLength} characters.`;
|
|
215
|
+
if (nestedQuantifierRegexPattern.test(selector.source)) return `Unsafe ${lowercaseInitial(label)} uses a nested quantified pattern.`;
|
|
216
|
+
if (hasUnsafeOverlappingAlternation(selector.source)) return `Unsafe ${lowercaseInitial(label)} uses an overlapping alternation pattern.`;
|
|
217
|
+
};
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/core/matching/path-parser.ts
|
|
220
|
+
const barePropertyPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
221
|
+
const indexSegmentPattern = /^(0|[1-9]\d*)$/;
|
|
222
|
+
const regexLikeSegmentPattern = /^\/.+\/[A-Za-z]*$/;
|
|
223
|
+
var PathSyntaxError = class extends SyntaxError {
|
|
224
|
+
selector;
|
|
225
|
+
constructor(selector, message) {
|
|
226
|
+
super(message);
|
|
227
|
+
this.name = "PathSyntaxError";
|
|
228
|
+
this.selector = selector;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const createPropertyPathSegment = (value) => {
|
|
232
|
+
return Object.freeze({
|
|
233
|
+
kind: "property",
|
|
234
|
+
value
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
const createIndexPathSegment = (value) => {
|
|
238
|
+
return Object.freeze({
|
|
239
|
+
kind: "index",
|
|
240
|
+
value
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
const createWildcardPathSegment = () => {
|
|
244
|
+
return Object.freeze({ kind: "wildcard" });
|
|
245
|
+
};
|
|
246
|
+
const createRecursiveWildcardPathSegment = () => {
|
|
247
|
+
return Object.freeze({ kind: "recursive-wildcard" });
|
|
248
|
+
};
|
|
249
|
+
const createIgnorePropertyPathSegment = (value) => {
|
|
250
|
+
return Object.freeze({
|
|
251
|
+
kind: "ignore-property",
|
|
252
|
+
value
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
const createIgnoreIndexPathSegment = (value) => {
|
|
256
|
+
return Object.freeze({
|
|
257
|
+
kind: "ignore-index",
|
|
258
|
+
value
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
const cloneRegexMatcher = (matcher) => {
|
|
262
|
+
return new RegExp(matcher.source, matcher.flags);
|
|
263
|
+
};
|
|
264
|
+
const createRegexPathSegment = (matcher) => {
|
|
265
|
+
return Object.freeze({
|
|
266
|
+
kind: "regex",
|
|
267
|
+
matcher: cloneRegexMatcher(matcher)
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
const createIgnoreRegexPathSegment = (matcher) => {
|
|
271
|
+
return Object.freeze({
|
|
272
|
+
kind: "ignore-regex",
|
|
273
|
+
matcher: cloneRegexMatcher(matcher)
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
const renderRawSelector = (selector) => {
|
|
277
|
+
return typeof selector === "string" ? selector : JSON.stringify(selector);
|
|
278
|
+
};
|
|
279
|
+
const createUnsupportedWildcardError = (selector, segment) => {
|
|
280
|
+
const rawSelector = renderRawSelector(selector);
|
|
281
|
+
if (segment === "**") return new PathSyntaxError(rawSelector, "Unsupported recursive wildcard segment \"**\".");
|
|
282
|
+
if (segment === "*") return new PathSyntaxError(rawSelector, "Unsupported wildcard segment \"*\".");
|
|
283
|
+
return new PathSyntaxError(rawSelector, `Unsupported wildcard syntax in segment "${segment}".`);
|
|
284
|
+
};
|
|
285
|
+
const createExactSegment = (selector, value) => {
|
|
286
|
+
if (typeof value === "number") return createIndexPathSegment(value);
|
|
287
|
+
if (value.length === 0) throw new PathSyntaxError(renderRawSelector(selector), "Path selectors must not contain empty segments.");
|
|
288
|
+
if (value.includes("*")) throw createUnsupportedWildcardError(selector, value);
|
|
289
|
+
if (regexLikeSegmentPattern.test(value)) throw new PathSyntaxError(renderRawSelector(selector), `Unsupported regex-like segment "${value}".`);
|
|
290
|
+
if (value.startsWith("!")) throw new PathSyntaxError(renderRawSelector(selector), `Unsupported exclusion syntax in segment "${value}". Ignore selectors are structured-selector only.`);
|
|
291
|
+
if (indexSegmentPattern.test(value)) return createIndexPathSegment(Number(value));
|
|
292
|
+
if (barePropertyPattern.test(value)) return createPropertyPathSegment(value);
|
|
293
|
+
throw new PathSyntaxError(renderRawSelector(selector), `Unsupported exact path segment "${value}". Use bracket-quoted property syntax for literal special-character keys.`);
|
|
294
|
+
};
|
|
295
|
+
const createLiteralStructuredPropertySegment = (selector, value) => {
|
|
296
|
+
if (value.length === 0) throw new PathSyntaxError(renderRawSelector(selector), "Path selectors must not contain empty segments.");
|
|
297
|
+
if (regexLikeSegmentPattern.test(value)) throw new PathSyntaxError(renderRawSelector(selector), `Unsupported regex-like segment "${value}".`);
|
|
298
|
+
return createPropertyPathSegment(value);
|
|
299
|
+
};
|
|
300
|
+
const createLiteralStructuredIndexSegment = (selector, value) => {
|
|
301
|
+
if (!Number.isInteger(value) || value < 0) throw new PathSyntaxError(renderRawSelector(selector), "Structured numeric segments must be non-negative integers.");
|
|
302
|
+
return createIndexPathSegment(value);
|
|
303
|
+
};
|
|
304
|
+
const createLiteralStructuredIgnorePropertySegment = (selector, value) => {
|
|
305
|
+
if (value.length === 0) throw new PathSyntaxError(renderRawSelector(selector), "Path selectors must not contain empty segments.");
|
|
306
|
+
if (regexLikeSegmentPattern.test(value)) throw new PathSyntaxError(renderRawSelector(selector), `Unsupported regex-like segment "${value}".`);
|
|
307
|
+
return createIgnorePropertyPathSegment(value);
|
|
308
|
+
};
|
|
309
|
+
const createLiteralStructuredIgnoreIndexSegment = (selector, value) => {
|
|
310
|
+
if (!Number.isInteger(value) || value < 0) throw new PathSyntaxError(renderRawSelector(selector), "Structured ignore indexes must be non-negative integers.");
|
|
311
|
+
return createIgnoreIndexPathSegment(value);
|
|
312
|
+
};
|
|
313
|
+
const parseQuotedProperty = (selector, rawSelector, startIndex) => {
|
|
314
|
+
const quote = selector[startIndex];
|
|
315
|
+
let index = startIndex + 1;
|
|
316
|
+
let value = "";
|
|
317
|
+
while (index < selector.length) {
|
|
318
|
+
const character = selector[index];
|
|
319
|
+
if (character === "\\") {
|
|
320
|
+
index += 1;
|
|
321
|
+
if (index >= selector.length) throw new PathSyntaxError(rawSelector, "Quoted property selector has an unfinished escape sequence.");
|
|
322
|
+
value += selector[index];
|
|
323
|
+
index += 1;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (character === quote) {
|
|
327
|
+
if (value.length === 0) throw new PathSyntaxError(rawSelector, "Path selectors must not contain empty segments.");
|
|
328
|
+
return {
|
|
329
|
+
nextIndex: index + 1,
|
|
330
|
+
segment: createPropertyPathSegment(value)
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
value += character;
|
|
334
|
+
index += 1;
|
|
335
|
+
}
|
|
336
|
+
throw new PathSyntaxError(rawSelector, "Quoted property selector is not closed.");
|
|
337
|
+
};
|
|
338
|
+
const parseBracketSegment = (selector, rawSelector, startIndex) => {
|
|
339
|
+
const index = startIndex + 1;
|
|
340
|
+
if (index >= selector.length) throw new PathSyntaxError(rawSelector, "Bracket selector is not closed.");
|
|
341
|
+
if (selector[index] === "\"" || selector[index] === "'") {
|
|
342
|
+
const quotedProperty = parseQuotedProperty(selector, rawSelector, index);
|
|
343
|
+
if (selector[quotedProperty.nextIndex] !== "]") throw new PathSyntaxError(rawSelector, "Quoted property selector must be closed with ].");
|
|
344
|
+
return {
|
|
345
|
+
nextIndex: quotedProperty.nextIndex + 1,
|
|
346
|
+
segment: quotedProperty.segment
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const closingIndex = selector.indexOf("]", index);
|
|
350
|
+
if (closingIndex === -1) throw new PathSyntaxError(rawSelector, "Bracket selector is not closed.");
|
|
351
|
+
const value = selector.slice(index, closingIndex);
|
|
352
|
+
if (value.length === 0) throw new PathSyntaxError(rawSelector, "Path selectors must not contain empty segments.");
|
|
353
|
+
if (value.includes("*")) throw createUnsupportedWildcardError(rawSelector, value);
|
|
354
|
+
if (regexLikeSegmentPattern.test(value)) throw new PathSyntaxError(rawSelector, `Unsupported regex-like segment "${value}".`);
|
|
355
|
+
if (!indexSegmentPattern.test(value)) throw new PathSyntaxError(rawSelector, `Unsupported bracket segment "${value}". Use numeric indexes or quoted property selectors only.`);
|
|
356
|
+
return {
|
|
357
|
+
nextIndex: closingIndex + 1,
|
|
358
|
+
segment: createIndexPathSegment(Number(value))
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
const parseBareSegment = (selector, rawSelector, startIndex) => {
|
|
362
|
+
let index = startIndex;
|
|
363
|
+
while (index < selector.length && selector[index] !== "." && selector[index] !== "[") {
|
|
364
|
+
if (selector[index] === "]") throw new PathSyntaxError(rawSelector, "Unexpected ] in path selector.");
|
|
365
|
+
index += 1;
|
|
366
|
+
}
|
|
367
|
+
const value = selector.slice(startIndex, index);
|
|
368
|
+
if (value === "*") return {
|
|
369
|
+
nextIndex: index,
|
|
370
|
+
segment: createWildcardPathSegment()
|
|
371
|
+
};
|
|
372
|
+
if (value === "**") return {
|
|
373
|
+
nextIndex: index,
|
|
374
|
+
segment: createRecursiveWildcardPathSegment()
|
|
375
|
+
};
|
|
376
|
+
return {
|
|
377
|
+
nextIndex: index,
|
|
378
|
+
segment: createExactSegment(rawSelector, value)
|
|
379
|
+
};
|
|
380
|
+
};
|
|
381
|
+
const parseStringPathSelector = (selector) => {
|
|
382
|
+
if (selector.length === 0) throw new PathSyntaxError(selector, "Path selectors must not be empty.");
|
|
383
|
+
const segments = [];
|
|
384
|
+
let index = 0;
|
|
385
|
+
let recursiveWildcardCount = 0;
|
|
386
|
+
while (index < selector.length) {
|
|
387
|
+
if (selector[index] === ".") throw new PathSyntaxError(selector, "Path selectors must not contain empty segments.");
|
|
388
|
+
const parsedSegment = selector[index] === "[" ? parseBracketSegment(selector, selector, index) : parseBareSegment(selector, selector, index);
|
|
389
|
+
segments.push(parsedSegment.segment);
|
|
390
|
+
if (parsedSegment.segment.kind === "recursive-wildcard") {
|
|
391
|
+
recursiveWildcardCount += 1;
|
|
392
|
+
if (recursiveWildcardCount > 1) throw new PathSyntaxError(selector, "Path selectors may contain at most one recursive wildcard segment \"**\".");
|
|
393
|
+
}
|
|
394
|
+
index = parsedSegment.nextIndex;
|
|
395
|
+
while (index < selector.length && selector[index] === "[") {
|
|
396
|
+
const bracketSegment = parseBracketSegment(selector, selector, index);
|
|
397
|
+
segments.push(bracketSegment.segment);
|
|
398
|
+
index = bracketSegment.nextIndex;
|
|
399
|
+
}
|
|
400
|
+
if (index >= selector.length) break;
|
|
401
|
+
if (selector[index] !== ".") throw new PathSyntaxError(selector, `Unexpected character "${selector[index]}" in path selector.`);
|
|
402
|
+
index += 1;
|
|
403
|
+
if (index >= selector.length || selector[index] === "." || selector[index] === "[") throw new PathSyntaxError(selector, "Path selectors must not contain empty segments.");
|
|
404
|
+
}
|
|
405
|
+
return Object.freeze({
|
|
406
|
+
raw: selector,
|
|
407
|
+
segments: Object.freeze(segments)
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
const isIgnorePathSegment = (value) => {
|
|
411
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
412
|
+
const keys = Object.keys(value);
|
|
413
|
+
return keys.length === 1 && keys[0] === "ignore";
|
|
414
|
+
};
|
|
415
|
+
const parseStructuredPathSelector = (selector) => {
|
|
416
|
+
if (selector.length === 0) throw new PathSyntaxError(renderRawSelector(selector), "Path selectors must not be empty.");
|
|
417
|
+
const segments = selector.map((segment) => {
|
|
418
|
+
if (isRegExp(segment)) return createRegexPathSegment(segment);
|
|
419
|
+
if (typeof segment === "string" || typeof segment === "number") return typeof segment === "string" ? createLiteralStructuredPropertySegment(selector, segment) : createLiteralStructuredIndexSegment(selector, segment);
|
|
420
|
+
if (!isIgnorePathSegment(segment)) throw new PathSyntaxError(renderRawSelector(selector), `Unsupported structured selector segment ${JSON.stringify(segment)}.`);
|
|
421
|
+
if (typeof segment.ignore === "string") return createLiteralStructuredIgnorePropertySegment(selector, segment.ignore);
|
|
422
|
+
if (typeof segment.ignore === "number") return createLiteralStructuredIgnoreIndexSegment(selector, segment.ignore);
|
|
423
|
+
if (isRegExp(segment.ignore)) return createIgnoreRegexPathSegment(segment.ignore);
|
|
424
|
+
throw new PathSyntaxError(renderRawSelector(selector), `Unsupported structured selector segment ${JSON.stringify(segment)}.`);
|
|
425
|
+
});
|
|
426
|
+
return Object.freeze({
|
|
427
|
+
raw: selector,
|
|
428
|
+
segments: Object.freeze(segments)
|
|
429
|
+
});
|
|
430
|
+
};
|
|
431
|
+
const isExactPathSegment = (segment) => {
|
|
432
|
+
return segment.kind === "property" || segment.kind === "index";
|
|
433
|
+
};
|
|
434
|
+
const isDynamicPathSegment = (segment) => {
|
|
435
|
+
return !isExactPathSegment(segment);
|
|
436
|
+
};
|
|
437
|
+
const isSingleWildcardSegment = (segment) => {
|
|
438
|
+
return segment.kind === "wildcard";
|
|
439
|
+
};
|
|
440
|
+
const containsOnlySingleWildcardDynamics = (segments) => {
|
|
441
|
+
return segments.every((segment) => isExactPathSegment(segment) || isSingleWildcardSegment(segment));
|
|
442
|
+
};
|
|
443
|
+
const exactSegmentsEqual = (left, right) => {
|
|
444
|
+
if (left.kind === "property" && right.kind === "property") return left.value === right.value;
|
|
445
|
+
if (left.kind === "index" && right.kind === "index") return left.value === right.value;
|
|
446
|
+
return false;
|
|
447
|
+
};
|
|
448
|
+
const occupiesWildcardDepthAsIntermediate = (candidate, wildcardRule) => {
|
|
449
|
+
const wildcardDepth = wildcardRule.findIndex((segment) => isSingleWildcardSegment(segment));
|
|
450
|
+
if (wildcardDepth === -1) return false;
|
|
451
|
+
if (candidate.length < wildcardDepth + 2) return false;
|
|
452
|
+
for (let depth = 0; depth <= wildcardDepth; depth += 1) if (!isExactPathSegment(candidate[depth])) return false;
|
|
453
|
+
for (let depth = 0; depth < wildcardDepth; depth += 1) if (!exactSegmentsEqual(candidate[depth], wildcardRule[depth])) return false;
|
|
454
|
+
return true;
|
|
455
|
+
};
|
|
456
|
+
const hasUnsafeWildcardOverlap = (rules) => {
|
|
457
|
+
return rules.some((wildcardRule) => rules.some((candidate) => occupiesWildcardDepthAsIntermediate(candidate, wildcardRule)));
|
|
458
|
+
};
|
|
459
|
+
const parsePathSelector = (selector) => {
|
|
460
|
+
return typeof selector === "string" ? parseStringPathSelector(selector) : parseStructuredPathSelector(selector);
|
|
461
|
+
};
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/core/matching/key-normaliser.ts
|
|
464
|
+
const canonicaliseKey = (value) => {
|
|
465
|
+
return value.toLowerCase().trim().replaceAll(/[_-]/g, "");
|
|
466
|
+
};
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/core/matching/path-normaliser.ts
|
|
469
|
+
const canonicalBarePropertyPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
470
|
+
const renderPropertySegment = (value, isRoot) => {
|
|
471
|
+
if (canonicalBarePropertyPattern.test(value)) return `${isRoot ? "" : "."}${value}`;
|
|
472
|
+
return `[${JSON.stringify(value)}]`;
|
|
473
|
+
};
|
|
474
|
+
const renderExactPathSegment = (segment, isRoot) => {
|
|
475
|
+
if (segment.kind === "index") return `${isRoot ? "" : "."}${segment.value}`;
|
|
476
|
+
return renderPropertySegment(segment.value, isRoot);
|
|
477
|
+
};
|
|
478
|
+
const renderCanonicalPath = (segments) => {
|
|
479
|
+
return segments.map((segment, index) => renderExactPathSegment(segment, index === 0)).join("");
|
|
480
|
+
};
|
|
481
|
+
const appendCanonicalPathSegment = (parentPath, segment) => {
|
|
482
|
+
return `${parentPath ?? ""}${renderExactPathSegment(segment, parentPath === void 0)}`;
|
|
483
|
+
};
|
|
484
|
+
const renderDynamicPathSegment = (segment, isRoot) => {
|
|
485
|
+
if (segment.kind === "wildcard") return `${isRoot ? "" : "."}*`;
|
|
486
|
+
if (segment.kind === "recursive-wildcard") return `${isRoot ? "" : "."}**`;
|
|
487
|
+
if (segment.kind === "ignore-index") return `${isRoot ? "" : "."}{ignore:${segment.value}}`;
|
|
488
|
+
if (segment.kind === "ignore-property") return `${isRoot ? "" : "."}{ignore:${JSON.stringify(segment.value)}}`;
|
|
489
|
+
if (segment.kind === "regex") return `${isRoot ? "" : "."}{regex:${JSON.stringify({
|
|
490
|
+
source: segment.matcher.source,
|
|
491
|
+
flags: segment.matcher.flags
|
|
492
|
+
})}}`;
|
|
493
|
+
if (segment.kind === "ignore-regex") return `${isRoot ? "" : "."}{ignore-regex:${JSON.stringify({
|
|
494
|
+
source: segment.matcher.source,
|
|
495
|
+
flags: segment.matcher.flags
|
|
496
|
+
})}}`;
|
|
497
|
+
return renderExactPathSegment(segment, isRoot);
|
|
498
|
+
};
|
|
499
|
+
const renderSelectorSignature = (segments) => {
|
|
500
|
+
return segments.map((segment, index) => renderDynamicPathSegment(segment, index === 0)).join("");
|
|
501
|
+
};
|
|
502
|
+
const normaliseParsedPath = (parsedPath) => {
|
|
503
|
+
if (!parsedPath.segments.every((segment) => isExactPathSegment(segment))) throw new TypeError("Dynamic selectors cannot be canonicalised as exact paths.");
|
|
504
|
+
return Object.freeze({
|
|
505
|
+
canonicalPath: renderCanonicalPath(parsedPath.segments),
|
|
506
|
+
segments: parsedPath.segments
|
|
507
|
+
});
|
|
508
|
+
};
|
|
509
|
+
const createTraversalBudget = () => {
|
|
510
|
+
return {
|
|
511
|
+
depth: 0,
|
|
512
|
+
nodesVisited: 0
|
|
513
|
+
};
|
|
514
|
+
};
|
|
515
|
+
const isDepthExceeded = (budget, maxDepth) => {
|
|
516
|
+
return budget.depth > maxDepth;
|
|
517
|
+
};
|
|
518
|
+
const isNodeBudgetExceeded = (budget, maxNodes) => {
|
|
519
|
+
return budget.nodesVisited > maxNodes;
|
|
520
|
+
};
|
|
521
|
+
var BudgetExceededError = class extends Error {
|
|
522
|
+
code = "BUDGET_EXCEEDED";
|
|
523
|
+
constructor(message) {
|
|
524
|
+
super(message);
|
|
525
|
+
this.name = "BudgetExceededError";
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
const createBudgetExceededError = (kind, limit) => {
|
|
529
|
+
return new BudgetExceededError(kind === "depth" ? `Traversal depth limit (${limit}) exceeded.` : `Traversal node budget (${limit}) exceeded.`);
|
|
530
|
+
};
|
|
531
|
+
const isBudgetExceededError = (error) => {
|
|
532
|
+
return error instanceof BudgetExceededError;
|
|
533
|
+
};
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/core/compiler/compile-redactor-plan.ts
|
|
536
|
+
const createLookupTable = () => {
|
|
537
|
+
return Object.create(null);
|
|
538
|
+
};
|
|
539
|
+
const toPublicPathSegment = (segment) => {
|
|
540
|
+
if (segment.kind === "property") return segment.value;
|
|
541
|
+
if (segment.kind === "index") return segment.value;
|
|
542
|
+
if (segment.kind === "wildcard") return Object.freeze({ any: true });
|
|
543
|
+
if (segment.kind === "recursive-wildcard") return Object.freeze({ anyDepth: true });
|
|
544
|
+
if (segment.kind === "ignore-property") return Object.freeze({ ignore: segment.value });
|
|
545
|
+
if (segment.kind === "ignore-index") return Object.freeze({ ignore: segment.value });
|
|
546
|
+
if (segment.kind === "regex") return new RegExp(segment.matcher.source, segment.matcher.flags);
|
|
547
|
+
return Object.freeze({ ignore: new RegExp(segment.matcher.source, segment.matcher.flags) });
|
|
548
|
+
};
|
|
549
|
+
const compileRulePath = (segments) => {
|
|
550
|
+
return Object.freeze(segments.map((segment) => toPublicPathSegment(segment)));
|
|
551
|
+
};
|
|
552
|
+
const createDefaultPolicy = (options) => {
|
|
553
|
+
return Object.freeze({
|
|
554
|
+
censor: options.censor,
|
|
555
|
+
remove: options.remove ?? false,
|
|
556
|
+
replaceStringByLength: options.replaceStringByLength ?? false,
|
|
557
|
+
retainStructure: options.retainStructure ?? false
|
|
558
|
+
});
|
|
559
|
+
};
|
|
560
|
+
const createKeyMatchDefaults = (options) => {
|
|
561
|
+
return Object.freeze({
|
|
562
|
+
caseSensitiveKeyMatch: options.caseSensitiveKeyMatch ?? true,
|
|
563
|
+
fuzzyKeyMatch: options.fuzzyKeyMatch ?? false
|
|
564
|
+
});
|
|
565
|
+
};
|
|
566
|
+
const mergePolicy = (defaults, overrides) => {
|
|
567
|
+
return Object.freeze({
|
|
568
|
+
censor: overrides.censor ?? defaults.censor,
|
|
569
|
+
remove: overrides.remove ?? defaults.remove,
|
|
570
|
+
replaceStringByLength: overrides.replaceStringByLength ?? defaults.replaceStringByLength,
|
|
571
|
+
retainStructure: overrides.retainStructure ?? defaults.retainStructure
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
const isPathRule = (pathEntry) => {
|
|
575
|
+
return typeof pathEntry === "object" && pathEntry !== null && !Array.isArray(pathEntry) && "path" in pathEntry;
|
|
576
|
+
};
|
|
577
|
+
const toPathRule = (pathEntry) => {
|
|
578
|
+
return isPathRule(pathEntry) ? pathEntry : { path: pathEntry };
|
|
579
|
+
};
|
|
580
|
+
const compilePathRule = (pathEntry, defaults) => {
|
|
581
|
+
const parsedPath = parsePathSelector(toPathRule(pathEntry).path);
|
|
582
|
+
const policy = mergePolicy(defaults, isPathRule(pathEntry) ? pathEntry : {});
|
|
583
|
+
const rulePath = compileRulePath(parsedPath.segments);
|
|
584
|
+
if (parsedPath.segments.some((segment) => isDynamicPathSegment(segment))) return Object.freeze({
|
|
585
|
+
signature: renderSelectorSignature(parsedPath.segments),
|
|
586
|
+
policy,
|
|
587
|
+
rulePath,
|
|
588
|
+
segments: parsedPath.segments
|
|
589
|
+
});
|
|
590
|
+
const normalisedPath = normaliseParsedPath(parsedPath);
|
|
591
|
+
return Object.freeze({
|
|
592
|
+
canonicalPath: normalisedPath.canonicalPath,
|
|
593
|
+
policy,
|
|
594
|
+
rulePath,
|
|
595
|
+
segments: normalisedPath.segments
|
|
596
|
+
});
|
|
597
|
+
};
|
|
598
|
+
const compilePathRules = (pathEntries, defaults) => {
|
|
599
|
+
const exactPathRules = createLookupTable();
|
|
600
|
+
const dynamicPathRules = [];
|
|
601
|
+
for (const pathEntry of pathEntries) {
|
|
602
|
+
const compiledRule = compilePathRule(pathEntry, defaults);
|
|
603
|
+
if ("canonicalPath" in compiledRule) {
|
|
604
|
+
exactPathRules[compiledRule.canonicalPath] = compiledRule;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
dynamicPathRules.push(compiledRule);
|
|
608
|
+
}
|
|
609
|
+
return Object.freeze({
|
|
610
|
+
dynamicPathRules: Object.freeze(dynamicPathRules),
|
|
611
|
+
exactPathRules: Object.freeze(exactPathRules)
|
|
612
|
+
});
|
|
613
|
+
};
|
|
614
|
+
const isKeyRule = (keySelector) => {
|
|
615
|
+
return typeof keySelector === "object" && keySelector !== null && !(keySelector instanceof RegExp) && "key" in keySelector;
|
|
616
|
+
};
|
|
617
|
+
const keyRuleHasPolicyOverrides = (rule) => {
|
|
618
|
+
return rule.censor !== void 0 || rule.remove !== void 0 || rule.retainStructure !== void 0 || rule.replaceStringByLength !== void 0;
|
|
619
|
+
};
|
|
620
|
+
const compileKeyRulePolicy = (rule, defaults) => {
|
|
621
|
+
return keyRuleHasPolicyOverrides(rule) ? mergePolicy(defaults, {
|
|
622
|
+
censor: rule.censor,
|
|
623
|
+
remove: rule.remove,
|
|
624
|
+
retainStructure: rule.retainStructure,
|
|
625
|
+
replaceStringByLength: rule.replaceStringByLength
|
|
626
|
+
}) : void 0;
|
|
627
|
+
};
|
|
628
|
+
const toLiteralKeyRule = (configuredKey, overrides, defaults, keyDefaults) => {
|
|
629
|
+
const fuzzyKeyMatch = overrides?.fuzzyKeyMatch ?? keyDefaults.fuzzyKeyMatch;
|
|
630
|
+
const caseSensitiveKeyMatch = overrides?.caseSensitiveKeyMatch ?? keyDefaults.caseSensitiveKeyMatch;
|
|
631
|
+
let matchMode = "exact";
|
|
632
|
+
if (fuzzyKeyMatch) matchMode = caseSensitiveKeyMatch ? "contains" : "canonical-contains";
|
|
633
|
+
else if (!caseSensitiveKeyMatch) matchMode = "canonical-exact";
|
|
634
|
+
return Object.freeze({
|
|
635
|
+
canonicalKey: canonicaliseKey(configuredKey),
|
|
636
|
+
configuredKey,
|
|
637
|
+
matchMode,
|
|
638
|
+
policy: overrides === void 0 ? void 0 : compileKeyRulePolicy(overrides, defaults),
|
|
639
|
+
rulePath: Object.freeze([configuredKey])
|
|
640
|
+
});
|
|
641
|
+
};
|
|
642
|
+
const compileExactKeyRules = (keys, defaults, keyDefaults) => {
|
|
643
|
+
const literalMatchers = [];
|
|
644
|
+
for (const key of keys) if (typeof key === "string") literalMatchers.push(toLiteralKeyRule(key, void 0, defaults, keyDefaults));
|
|
645
|
+
else if (isKeyRule(key) && typeof key.key === "string") literalMatchers.push(toLiteralKeyRule(key.key, key, defaults, keyDefaults));
|
|
646
|
+
return Object.freeze({
|
|
647
|
+
literalMatchers: Object.freeze(literalMatchers),
|
|
648
|
+
policy: defaults,
|
|
649
|
+
requiresCanonicalKey: literalMatchers.some((rule) => rule.matchMode.startsWith("canonical"))
|
|
650
|
+
});
|
|
651
|
+
};
|
|
652
|
+
const compileRegexKeyRules = (keys, defaults) => {
|
|
653
|
+
const matchers = [];
|
|
654
|
+
for (const key of keys) if (key instanceof RegExp) matchers.push(Object.freeze({ matcher: new RegExp(key.source, key.flags) }));
|
|
655
|
+
else if (isKeyRule(key) && key.key instanceof RegExp) matchers.push(Object.freeze({
|
|
656
|
+
matcher: new RegExp(key.key.source, key.key.flags),
|
|
657
|
+
policy: compileKeyRulePolicy(key, defaults)
|
|
658
|
+
}));
|
|
659
|
+
return Object.freeze({
|
|
660
|
+
matchers: Object.freeze(matchers),
|
|
661
|
+
policy: defaults
|
|
662
|
+
});
|
|
663
|
+
};
|
|
664
|
+
const isSubstringRule = (stringTest) => {
|
|
665
|
+
return !(stringTest instanceof RegExp);
|
|
666
|
+
};
|
|
667
|
+
const compileSubstringRules = (stringTests, defaults) => {
|
|
668
|
+
return Object.freeze(stringTests.map((stringTest) => {
|
|
669
|
+
if (isSubstringRule(stringTest)) return Object.freeze({
|
|
670
|
+
kind: "structured-replacer",
|
|
671
|
+
pattern: cloneRegExp(stringTest.pattern),
|
|
672
|
+
replacer: stringTest.replacer
|
|
673
|
+
});
|
|
674
|
+
return Object.freeze({
|
|
675
|
+
kind: "whole-value",
|
|
676
|
+
pattern: cloneRegExp(stringTest),
|
|
677
|
+
policy: defaults
|
|
678
|
+
});
|
|
679
|
+
}));
|
|
680
|
+
};
|
|
681
|
+
const compileRedactorPlan = (options = {}) => {
|
|
682
|
+
const defaults = createDefaultPolicy(options);
|
|
683
|
+
const keyDefaults = createKeyMatchDefaults(options);
|
|
684
|
+
const compiledPathRules = compilePathRules(options.paths ?? [], defaults);
|
|
685
|
+
const exactKeyRules = compileExactKeyRules(options.keys ?? [], defaults, keyDefaults);
|
|
686
|
+
const regexKeyRules = compileRegexKeyRules(options.keys ?? [], defaults);
|
|
687
|
+
const substringRules = compileSubstringRules(options.stringTests ?? [], defaults);
|
|
688
|
+
const everyDynamicRuleIsSingleWildcard = compiledPathRules.dynamicPathRules.every((rule) => containsOnlySingleWildcardDynamics(rule.segments));
|
|
689
|
+
const hasAnyPathRule = Object.keys(compiledPathRules.exactPathRules).length > 0 || compiledPathRules.dynamicPathRules.length > 0;
|
|
690
|
+
const hasUnsafeOverlap = compiledPathRules.dynamicPathRules.length > 0 && hasUnsafeWildcardOverlap([...Object.values(compiledPathRules.exactPathRules).map((rule) => rule.segments), ...compiledPathRules.dynamicPathRules.map((rule) => rule.segments)]);
|
|
691
|
+
const pathDrivenOnly = everyDynamicRuleIsSingleWildcard && hasAnyPathRule && !hasUnsafeOverlap && exactKeyRules.literalMatchers.length === 0 && regexKeyRules.matchers.length === 0 && substringRules.length === 0 && !options.fuzzyKeyMatch && options.caseSensitiveKeyMatch !== false;
|
|
692
|
+
return Object.freeze({
|
|
693
|
+
diagnostics: compileDiagnostics(options.diagnostics),
|
|
694
|
+
defaults,
|
|
695
|
+
dynamicPathRules: compiledPathRules.dynamicPathRules,
|
|
696
|
+
exactKeyRules,
|
|
697
|
+
exactPathRules: compiledPathRules.exactPathRules,
|
|
698
|
+
ignoredValueTypes: compileIgnoredValueTypes(options.ignoredValueTypes),
|
|
699
|
+
pathDrivenOnly,
|
|
700
|
+
maxDepth: options.maxDepth ?? 500,
|
|
701
|
+
maxNodes: options.maxNodes ?? 5e4,
|
|
702
|
+
regexKeyRules,
|
|
703
|
+
serialise: options.serialise,
|
|
704
|
+
substringRules,
|
|
705
|
+
transformers: compileTransformers(options.transformers),
|
|
706
|
+
valueTypes: compileValueTypes(options.types)
|
|
707
|
+
});
|
|
708
|
+
};
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region src/core/replacement/apply-redaction.ts
|
|
711
|
+
const defaultCensor = "[REDACTED]";
|
|
712
|
+
const isRedactableType = (value, valueTypes) => {
|
|
713
|
+
return valueTypes[typeof value];
|
|
714
|
+
};
|
|
715
|
+
const removedValue = Symbol("deep-redact.removed");
|
|
716
|
+
const isRemovedValue = (value) => {
|
|
717
|
+
return value === removedValue;
|
|
718
|
+
};
|
|
719
|
+
const buildSameLengthReplacement = (token, targetLength) => {
|
|
720
|
+
if (targetLength === 0) return "";
|
|
721
|
+
const tokenLength = token.length;
|
|
722
|
+
if (tokenLength === 0) return "";
|
|
723
|
+
const quotient = Math.floor(targetLength / tokenLength);
|
|
724
|
+
const remainder = targetLength % tokenLength;
|
|
725
|
+
return token.repeat(quotient) + token.slice(0, remainder);
|
|
726
|
+
};
|
|
727
|
+
const applyRedaction = (value, policy, context) => {
|
|
728
|
+
if (policy.remove) return removedValue;
|
|
729
|
+
if (typeof policy.censor === "function") return policy.censor.call(void 0, value, context);
|
|
730
|
+
const literalCensor = policy.censor ?? defaultCensor;
|
|
731
|
+
if (policy.replaceStringByLength && typeof value === "string") return buildSameLengthReplacement(literalCensor, value.length);
|
|
732
|
+
return literalCensor;
|
|
733
|
+
};
|
|
734
|
+
//#endregion
|
|
735
|
+
//#region src/transformers/resolve-transformer.ts
|
|
736
|
+
const supportedConstructorMatchers = Object.freeze([
|
|
737
|
+
{
|
|
738
|
+
name: "Date",
|
|
739
|
+
matches: (value) => value instanceof Date
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
name: "Error",
|
|
743
|
+
matches: (value) => value instanceof Error
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
name: "Map",
|
|
747
|
+
matches: (value) => value instanceof Map
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
name: "RegExp",
|
|
751
|
+
matches: (value) => value instanceof RegExp
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
name: "Set",
|
|
755
|
+
matches: (value) => value instanceof Set
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
name: "URL",
|
|
759
|
+
matches: (value) => value instanceof URL
|
|
760
|
+
}
|
|
761
|
+
]);
|
|
762
|
+
const resolveSupportedConstructorName = (value) => {
|
|
763
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
764
|
+
for (const matcher of supportedConstructorMatchers) if (matcher.matches(value)) return matcher.name;
|
|
765
|
+
};
|
|
766
|
+
const resolveSupportedTransformableValueKind = (value) => {
|
|
767
|
+
if (typeof value === "bigint") return "bigint";
|
|
768
|
+
return resolveSupportedConstructorName(value);
|
|
769
|
+
};
|
|
770
|
+
const isSupportedTransformableValue = (value) => {
|
|
771
|
+
return resolveSupportedTransformableValueKind(value) !== void 0;
|
|
772
|
+
};
|
|
773
|
+
const applyFirstChangingTransformer = (value, transformers) => {
|
|
774
|
+
for (const transformer of transformers) {
|
|
775
|
+
const transformed = transformer(value);
|
|
776
|
+
if (transformed !== value) return transformed;
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
const resolveCustomConstructorTransformers = (value, plan) => {
|
|
780
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
781
|
+
for (const registration of plan.byConstructor.custom) if (value instanceof registration.constructor) return registration.transformers;
|
|
782
|
+
};
|
|
783
|
+
const resolveTransformedValue = (value, plan) => {
|
|
784
|
+
const supportedValueKind = resolveSupportedTransformableValueKind(value);
|
|
785
|
+
if (supportedValueKind === "bigint") return applyFirstChangingTransformer(value, [...plan.byType.bigint, ...plan.fallback]);
|
|
786
|
+
if (supportedValueKind === void 0) {
|
|
787
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
788
|
+
const byTypeResult = applyFirstChangingTransformer(value, plan.byType.object);
|
|
789
|
+
if (byTypeResult !== void 0) return byTypeResult;
|
|
790
|
+
return applyFirstChangingTransformer(value, [...resolveCustomConstructorTransformers(value, plan) ?? [], ...plan.fallback]);
|
|
791
|
+
}
|
|
792
|
+
const byTypeResult = applyFirstChangingTransformer(value, plan.byType.object);
|
|
793
|
+
if (byTypeResult !== void 0) return byTypeResult;
|
|
794
|
+
const byConstructorResult = applyFirstChangingTransformer(value, plan.byConstructor[supportedValueKind]);
|
|
795
|
+
if (byConstructorResult !== void 0) return byConstructorResult;
|
|
796
|
+
return applyFirstChangingTransformer(value, [...resolveCustomConstructorTransformers(value, plan) ?? [], ...plan.fallback]);
|
|
797
|
+
};
|
|
798
|
+
//#endregion
|
|
799
|
+
//#region src/core/diagnostics/sanitise-diagnostics.ts
|
|
800
|
+
const resolveObjectValueType = (value) => {
|
|
801
|
+
const constructorName = value.constructor?.name;
|
|
802
|
+
if (typeof constructorName === "string" && constructorName.length > 0 && constructorName !== "Object") return constructorName;
|
|
803
|
+
return "object";
|
|
804
|
+
};
|
|
805
|
+
const resolveDiagnosticValueType = (value, override) => {
|
|
806
|
+
if (override !== void 0) return override;
|
|
807
|
+
const supportedValueKind = resolveSupportedTransformableValueKind(value);
|
|
808
|
+
if (supportedValueKind !== void 0) return supportedValueKind;
|
|
809
|
+
if (Array.isArray(value)) return "array";
|
|
810
|
+
if (value === null) return "null";
|
|
811
|
+
if (typeof value === "object") return resolveObjectValueType(value);
|
|
812
|
+
return typeof value;
|
|
813
|
+
};
|
|
814
|
+
const resolveThrownType = (error) => {
|
|
815
|
+
if (error === null) return "null";
|
|
816
|
+
if (typeof error === "object") {
|
|
817
|
+
const constructorName = error.constructor?.name;
|
|
818
|
+
return typeof constructorName === "string" && constructorName.length > 0 ? constructorName : "object";
|
|
819
|
+
}
|
|
820
|
+
return typeof error;
|
|
821
|
+
};
|
|
822
|
+
const sanitiseDiagnosticDetails = (stage, error) => {
|
|
823
|
+
if (error instanceof Error) return Object.freeze({
|
|
824
|
+
errorName: error.name || "Error",
|
|
825
|
+
stage
|
|
826
|
+
});
|
|
827
|
+
return Object.freeze({
|
|
828
|
+
stage,
|
|
829
|
+
thrownType: resolveThrownType(error)
|
|
830
|
+
});
|
|
831
|
+
};
|
|
832
|
+
//#endregion
|
|
833
|
+
//#region src/core/diagnostics/diagnostic-event.ts
|
|
834
|
+
const unsupportedMessage = "Nested value could not be redacted safely and was replaced with [UNSUPPORTED].";
|
|
835
|
+
const createFallbackDiagnosticDetails = (stage) => {
|
|
836
|
+
return Object.freeze({
|
|
837
|
+
stage,
|
|
838
|
+
thrownType: "unknown"
|
|
839
|
+
});
|
|
840
|
+
};
|
|
841
|
+
const resolveFallbackValueType = (value, override) => {
|
|
842
|
+
if (override !== void 0) return override;
|
|
843
|
+
if (Array.isArray(value)) return "array";
|
|
844
|
+
if (value === null) return "null";
|
|
845
|
+
return typeof value === "object" ? "object" : typeof value;
|
|
846
|
+
};
|
|
847
|
+
const createFailureDiagnosticSnapshot = (input) => {
|
|
848
|
+
let details;
|
|
849
|
+
let valueType;
|
|
850
|
+
try {
|
|
851
|
+
details = sanitiseDiagnosticDetails(input.stage, input.error);
|
|
852
|
+
} catch {
|
|
853
|
+
details = createFallbackDiagnosticDetails(input.stage);
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
valueType = resolveDiagnosticValueType(input.value, input.valueType);
|
|
857
|
+
} catch {
|
|
858
|
+
valueType = resolveFallbackValueType(input.value, input.valueType);
|
|
859
|
+
}
|
|
860
|
+
return Object.freeze({
|
|
861
|
+
details,
|
|
862
|
+
valueType
|
|
863
|
+
});
|
|
864
|
+
};
|
|
865
|
+
const createDiagnosticEvent = (plan, path, snapshot) => {
|
|
866
|
+
return Object.freeze({
|
|
867
|
+
details: snapshot.details,
|
|
868
|
+
event: plan.eventName,
|
|
869
|
+
message: unsupportedMessage,
|
|
870
|
+
path,
|
|
871
|
+
valueType: snapshot.valueType
|
|
872
|
+
});
|
|
873
|
+
};
|
|
874
|
+
//#endregion
|
|
875
|
+
//#region src/core/diagnostics/diagnostics-sink.ts
|
|
876
|
+
const emitDiagnosticEvent = (plan, event) => {
|
|
877
|
+
const sink = plan.sink ?? getNodeConsoleDiagnosticSink();
|
|
878
|
+
if (sink === void 0) return;
|
|
879
|
+
try {
|
|
880
|
+
sink(event);
|
|
881
|
+
} catch {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
//#endregion
|
|
886
|
+
//#region src/core/runtime/redact-value.ts
|
|
887
|
+
const unsupportedValue$1 = "[UNSUPPORTED]";
|
|
888
|
+
const createCircularMarker = (path, value) => ({
|
|
889
|
+
_transformer: "circular",
|
|
890
|
+
path,
|
|
891
|
+
value
|
|
892
|
+
});
|
|
893
|
+
const isPlainObject$1 = (value) => {
|
|
894
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
895
|
+
const prototype = Object.getPrototypeOf(value);
|
|
896
|
+
return prototype === Object.prototype || prototype === null;
|
|
897
|
+
};
|
|
898
|
+
const isTraversableContainer = (value) => {
|
|
899
|
+
return Array.isArray(value) || isPlainObject$1(value);
|
|
900
|
+
};
|
|
901
|
+
const canRetainStructure = (value) => {
|
|
902
|
+
return isTraversableContainer(value) || isSupportedTransformableValue(value);
|
|
903
|
+
};
|
|
904
|
+
const hasLookupValue = (table, key) => {
|
|
905
|
+
return Object.hasOwn(table, key);
|
|
906
|
+
};
|
|
907
|
+
const setObjectEntry = (target, key, value) => {
|
|
908
|
+
if (key === "__proto__") Object.defineProperty(target, key, {
|
|
909
|
+
configurable: true,
|
|
910
|
+
enumerable: true,
|
|
911
|
+
value,
|
|
912
|
+
writable: true
|
|
913
|
+
});
|
|
914
|
+
else target[key] = value;
|
|
915
|
+
};
|
|
916
|
+
const findMatchingRegexKey = (matchers, key) => {
|
|
917
|
+
return matchers.find((rule) => {
|
|
918
|
+
rule.matcher.lastIndex = 0;
|
|
919
|
+
return rule.matcher.test(key);
|
|
920
|
+
});
|
|
921
|
+
};
|
|
922
|
+
const findMatchingLiteralKey = (literalMatchers, requiresCanonicalKey, key) => {
|
|
923
|
+
let canonicalKey;
|
|
924
|
+
for (const rule of literalMatchers) {
|
|
925
|
+
if (rule.matchMode === "exact" && key === rule.configuredKey) return rule;
|
|
926
|
+
if (rule.matchMode === "contains" && key.includes(rule.configuredKey)) return rule;
|
|
927
|
+
if (requiresCanonicalKey && (rule.matchMode === "canonical-exact" || rule.matchMode === "canonical-contains")) {
|
|
928
|
+
canonicalKey ??= canonicaliseKey(key);
|
|
929
|
+
if (rule.matchMode === "canonical-exact" && canonicalKey === rule.canonicalKey) return rule;
|
|
930
|
+
if (rule.matchMode === "canonical-contains" && canonicalKey.includes(rule.canonicalKey)) return rule;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
const renderPathSegmentText = (pathSegment) => {
|
|
935
|
+
return pathSegment.kind === "index" ? String(pathSegment.value) : pathSegment.value;
|
|
936
|
+
};
|
|
937
|
+
const createTraversalState = (cycleRegistry) => {
|
|
938
|
+
return {
|
|
939
|
+
budget: createTraversalBudget(),
|
|
940
|
+
completedIdentities: /* @__PURE__ */ new WeakMap(),
|
|
941
|
+
completedSnapshots: /* @__PURE__ */ new WeakMap(),
|
|
942
|
+
cycleRegistry
|
|
943
|
+
};
|
|
944
|
+
};
|
|
945
|
+
const createTraversalBranchState = () => {
|
|
946
|
+
return { activePaths: /* @__PURE__ */ new WeakMap() };
|
|
947
|
+
};
|
|
948
|
+
const emitFailureDiagnostic = (plan, context, options) => {
|
|
949
|
+
const diagnostic = createFailureDiagnosticSnapshot({
|
|
950
|
+
error: options.error,
|
|
951
|
+
stage: options.stage,
|
|
952
|
+
value: options.value,
|
|
953
|
+
valueType: options.valueType
|
|
954
|
+
});
|
|
955
|
+
emitDiagnosticEvent(plan.diagnostics, createDiagnosticEvent(plan.diagnostics, context.canonicalPath ?? "", diagnostic));
|
|
956
|
+
return diagnostic;
|
|
957
|
+
};
|
|
958
|
+
const throwBudgetExceeded = (plan, context, kind) => {
|
|
959
|
+
const limit = kind === "depth" ? plan.maxDepth : plan.maxNodes;
|
|
960
|
+
emitDiagnosticEvent(plan.diagnostics, Object.freeze({
|
|
961
|
+
details: Object.freeze({
|
|
962
|
+
stage: "traversal",
|
|
963
|
+
kind,
|
|
964
|
+
limit
|
|
965
|
+
}),
|
|
966
|
+
event: "budget.exceeded",
|
|
967
|
+
message: kind === "depth" ? `Traversal depth limit (${limit}) exceeded.` : `Traversal node budget (${limit}) exceeded.`,
|
|
968
|
+
path: context.canonicalPath ?? "",
|
|
969
|
+
valueType: "unknown"
|
|
970
|
+
}));
|
|
971
|
+
throw createBudgetExceededError(kind, limit);
|
|
972
|
+
};
|
|
973
|
+
const createUnsupportedTraversalResult = () => {
|
|
974
|
+
return {
|
|
975
|
+
cacheValue: unsupportedValue$1,
|
|
976
|
+
changed: true,
|
|
977
|
+
pathStable: false,
|
|
978
|
+
value: unsupportedValue$1
|
|
979
|
+
};
|
|
980
|
+
};
|
|
981
|
+
const createFailureTraversalResult = (plan, context, options) => {
|
|
982
|
+
emitFailureDiagnostic(plan, context, options);
|
|
983
|
+
return createUnsupportedTraversalResult();
|
|
984
|
+
};
|
|
985
|
+
const renderRulePathSegment = (segment) => {
|
|
986
|
+
if (typeof segment === "string") return `property:${segment}`;
|
|
987
|
+
if (typeof segment === "number") return `index:${segment}`;
|
|
988
|
+
if (segment instanceof RegExp) return `regex:${segment.source}/${segment.flags}`;
|
|
989
|
+
if ("any" in segment && segment.any === true) return "wildcard:*";
|
|
990
|
+
if ("anyDepth" in segment && segment.anyDepth === true) return "wildcard:**";
|
|
991
|
+
if (!("ignore" in segment)) return "unknown";
|
|
992
|
+
const ignored = segment.ignore;
|
|
993
|
+
if (ignored instanceof RegExp) return `ignore-regex:${ignored.source}/${ignored.flags}`;
|
|
994
|
+
return typeof ignored === "number" ? `ignore-index:${ignored}` : `ignore-property:${ignored}`;
|
|
995
|
+
};
|
|
996
|
+
const buildRuleContextKey = (activePolicy) => {
|
|
997
|
+
if (activePolicy === void 0) return "none";
|
|
998
|
+
return `${activePolicy.source}:${activePolicy.rulePath.map((segment) => renderRulePathSegment(segment)).join("|")}`;
|
|
999
|
+
};
|
|
1000
|
+
const usesPathSensitivePolicy = (activePolicy) => {
|
|
1001
|
+
return activePolicy?.source === "exact-path" || activePolicy?.source === "dynamic-path" || typeof activePolicy?.policy.censor === "function";
|
|
1002
|
+
};
|
|
1003
|
+
const resolveCompletedTraversal = (records, canonicalPath, ruleContextKey, value) => {
|
|
1004
|
+
const reusableRecord = records.find((record) => {
|
|
1005
|
+
return record.ruleContextKey === ruleContextKey && (record.pathStable || record.canonicalPath === canonicalPath);
|
|
1006
|
+
});
|
|
1007
|
+
if (reusableRecord !== void 0) return {
|
|
1008
|
+
cacheValue: reusableRecord.value,
|
|
1009
|
+
changed: reusableRecord.value !== value,
|
|
1010
|
+
pathStable: reusableRecord.pathStable,
|
|
1011
|
+
value: reusableRecord.value
|
|
1012
|
+
};
|
|
1013
|
+
};
|
|
1014
|
+
const storeCompletedTraversal = (state, value, record) => {
|
|
1015
|
+
const existingRecords = state.completedIdentities.get(value);
|
|
1016
|
+
if (existingRecords === void 0) {
|
|
1017
|
+
state.completedIdentities.set(value, [record]);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
existingRecords.push(record);
|
|
1021
|
+
};
|
|
1022
|
+
const storeCompletedSnapshot = (state, value, snapshot) => {
|
|
1023
|
+
state.completedSnapshots.set(value, snapshot);
|
|
1024
|
+
};
|
|
1025
|
+
const withActiveIdentity = (branchState, value, canonicalPath, run) => {
|
|
1026
|
+
branchState.activePaths.set(value, canonicalPath);
|
|
1027
|
+
try {
|
|
1028
|
+
return run();
|
|
1029
|
+
} finally {
|
|
1030
|
+
branchState.activePaths.delete(value);
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
const resolveDirectKeyMatch = (plan, key) => {
|
|
1034
|
+
const matchingLiteralRule = findMatchingLiteralKey(plan.exactKeyRules.literalMatchers, plan.exactKeyRules.requiresCanonicalKey, key);
|
|
1035
|
+
if (matchingLiteralRule !== void 0) return {
|
|
1036
|
+
source: "exact-key",
|
|
1037
|
+
rulePath: matchingLiteralRule.rulePath,
|
|
1038
|
+
policy: matchingLiteralRule.policy
|
|
1039
|
+
};
|
|
1040
|
+
const matchingRegexRule = findMatchingRegexKey(plan.regexKeyRules.matchers, key);
|
|
1041
|
+
if (matchingRegexRule !== void 0) return {
|
|
1042
|
+
source: "regex-key",
|
|
1043
|
+
rulePath: Object.freeze([matchingRegexRule.matcher]),
|
|
1044
|
+
policy: matchingRegexRule.policy
|
|
1045
|
+
};
|
|
1046
|
+
};
|
|
1047
|
+
const resolveExactPathRule = (plan, canonicalPath) => {
|
|
1048
|
+
if (canonicalPath === void 0) return;
|
|
1049
|
+
return hasLookupValue(plan.exactPathRules, canonicalPath) ? plan.exactPathRules[canonicalPath] : void 0;
|
|
1050
|
+
};
|
|
1051
|
+
const matchesSingleSegment = (selectorSegment, pathSegment) => {
|
|
1052
|
+
if (selectorSegment.kind === "wildcard") return true;
|
|
1053
|
+
if (selectorSegment.kind === "recursive-wildcard") return false;
|
|
1054
|
+
if (selectorSegment.kind === "ignore-index") return pathSegment.kind === "index" && pathSegment.value !== selectorSegment.value;
|
|
1055
|
+
if (selectorSegment.kind === "ignore-property") return pathSegment.kind === "property" && pathSegment.value !== selectorSegment.value;
|
|
1056
|
+
if (selectorSegment.kind === "regex") return selectorSegment.matcher.test(renderPathSegmentText(pathSegment));
|
|
1057
|
+
if (selectorSegment.kind === "ignore-regex") return !selectorSegment.matcher.test(renderPathSegmentText(pathSegment));
|
|
1058
|
+
if (selectorSegment.kind === "index") return pathSegment.kind === "index" && pathSegment.value === selectorSegment.value;
|
|
1059
|
+
return pathSegment.kind === "property" && pathSegment.value === selectorSegment.value;
|
|
1060
|
+
};
|
|
1061
|
+
const matchesDynamicRule = (selectorSegments, pathSegments, selectorIndex = 0, pathIndex = 0) => {
|
|
1062
|
+
if (selectorIndex >= selectorSegments.length) return pathIndex === pathSegments.length;
|
|
1063
|
+
const selectorSegment = selectorSegments[selectorIndex];
|
|
1064
|
+
if (selectorSegment.kind === "recursive-wildcard") {
|
|
1065
|
+
for (let nextPathIndex = pathIndex; nextPathIndex <= pathSegments.length; nextPathIndex += 1) if (matchesDynamicRule(selectorSegments, pathSegments, selectorIndex + 1, nextPathIndex)) return true;
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
if (pathIndex >= pathSegments.length) return false;
|
|
1069
|
+
return matchesSingleSegment(selectorSegment, pathSegments[pathIndex]) && matchesDynamicRule(selectorSegments, pathSegments, selectorIndex + 1, pathIndex + 1);
|
|
1070
|
+
};
|
|
1071
|
+
const resolveDynamicPathRule = (plan, pathSegments) => {
|
|
1072
|
+
return plan.dynamicPathRules.find((rule) => matchesDynamicRule(rule.segments, pathSegments));
|
|
1073
|
+
};
|
|
1074
|
+
const selectActivePolicy = (plan, exactPathRule, dynamicPathRule, directKeyMatch, inheritedPolicy) => {
|
|
1075
|
+
if (exactPathRule !== void 0) return {
|
|
1076
|
+
policy: exactPathRule.policy,
|
|
1077
|
+
source: "exact-path",
|
|
1078
|
+
rulePath: exactPathRule.rulePath
|
|
1079
|
+
};
|
|
1080
|
+
if (dynamicPathRule !== void 0) return {
|
|
1081
|
+
policy: dynamicPathRule.policy,
|
|
1082
|
+
source: "dynamic-path",
|
|
1083
|
+
rulePath: dynamicPathRule.rulePath
|
|
1084
|
+
};
|
|
1085
|
+
if (inheritedPolicy?.source === "exact-path" || inheritedPolicy?.source === "dynamic-path" || inheritedPolicy?.source === "exact-key" || inheritedPolicy?.source === "regex-key") return inheritedPolicy;
|
|
1086
|
+
if (directKeyMatch?.source === "exact-key") return {
|
|
1087
|
+
policy: directKeyMatch.policy ?? plan.exactKeyRules.policy,
|
|
1088
|
+
source: "exact-key",
|
|
1089
|
+
rulePath: directKeyMatch.rulePath
|
|
1090
|
+
};
|
|
1091
|
+
if (directKeyMatch?.source === "regex-key") return {
|
|
1092
|
+
policy: directKeyMatch.policy ?? plan.regexKeyRules.policy,
|
|
1093
|
+
source: "regex-key",
|
|
1094
|
+
rulePath: directKeyMatch.rulePath
|
|
1095
|
+
};
|
|
1096
|
+
};
|
|
1097
|
+
const buildFunctionCensorContext = (pathSegments, rulePath, rootInput) => {
|
|
1098
|
+
const matchedPath = Object.freeze(pathSegments.map((seg) => seg.value));
|
|
1099
|
+
const rulePathCopy = Object.freeze([...rulePath]);
|
|
1100
|
+
const terminalKey = matchedPath.length > 0 ? matchedPath.at(-1) : void 0;
|
|
1101
|
+
return terminalKey === void 0 ? {
|
|
1102
|
+
matchedPath,
|
|
1103
|
+
rulePath: rulePathCopy,
|
|
1104
|
+
rootInput
|
|
1105
|
+
} : {
|
|
1106
|
+
matchedPath,
|
|
1107
|
+
rulePath: rulePathCopy,
|
|
1108
|
+
rootInput,
|
|
1109
|
+
terminalKey
|
|
1110
|
+
};
|
|
1111
|
+
};
|
|
1112
|
+
const applyConfiguredRedaction = (value, policy, rulePath, pathStable, plan, context) => {
|
|
1113
|
+
try {
|
|
1114
|
+
const redactedValue = applyRedaction(value, policy, buildFunctionCensorContext(context.pathSegments, rulePath, context.rootInput));
|
|
1115
|
+
return {
|
|
1116
|
+
cacheValue: redactedValue,
|
|
1117
|
+
changed: true,
|
|
1118
|
+
pathStable,
|
|
1119
|
+
value: redactedValue
|
|
1120
|
+
};
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
return createFailureTraversalResult(plan, context, {
|
|
1123
|
+
error,
|
|
1124
|
+
stage: "censor",
|
|
1125
|
+
value
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
const transformNestedNode = (value, plan, context, state, branchState) => {
|
|
1130
|
+
try {
|
|
1131
|
+
return transformNode(value, plan, context, state, branchState);
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
if (isBudgetExceededError(error)) throw error;
|
|
1134
|
+
return createFailureTraversalResult(plan, context, {
|
|
1135
|
+
error,
|
|
1136
|
+
stage: "traversal",
|
|
1137
|
+
value
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
const buildSubstringRulePath = (pattern) => {
|
|
1142
|
+
return Object.freeze([cloneRegExp(pattern)]);
|
|
1143
|
+
};
|
|
1144
|
+
const patternMatchesString = (pattern, value) => {
|
|
1145
|
+
pattern.lastIndex = 0;
|
|
1146
|
+
const matched = pattern.test(value);
|
|
1147
|
+
pattern.lastIndex = 0;
|
|
1148
|
+
return matched;
|
|
1149
|
+
};
|
|
1150
|
+
const applySubstringRule = (value, rule, plan, context) => {
|
|
1151
|
+
if (!patternMatchesString(rule.pattern, value)) return;
|
|
1152
|
+
if (rule.kind === "structured-replacer") try {
|
|
1153
|
+
const replacement = rule.replacer(value, cloneRegExp(rule.pattern));
|
|
1154
|
+
return {
|
|
1155
|
+
cacheValue: replacement,
|
|
1156
|
+
changed: replacement !== value,
|
|
1157
|
+
pathStable: true,
|
|
1158
|
+
value: replacement
|
|
1159
|
+
};
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
return createFailureTraversalResult(plan, context, {
|
|
1162
|
+
error,
|
|
1163
|
+
stage: "substring-replacer",
|
|
1164
|
+
value
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
return applyConfiguredRedaction(value, rule.policy, buildSubstringRulePath(rule.pattern), typeof rule.policy.censor !== "function", plan, context);
|
|
1168
|
+
};
|
|
1169
|
+
const applyRootPrimitiveSubstringMatch = (value, rule, plan, context) => {
|
|
1170
|
+
if (!patternMatchesString(rule.pattern, value)) return;
|
|
1171
|
+
const policy = rule.kind === "whole-value" ? rule.policy : plan.defaults;
|
|
1172
|
+
return applyConfiguredRedaction(value, policy, buildSubstringRulePath(rule.pattern), typeof policy.censor !== "function", plan, context);
|
|
1173
|
+
};
|
|
1174
|
+
const transformSubstringValue = (value, plan, context) => {
|
|
1175
|
+
if (typeof value !== "string") return;
|
|
1176
|
+
const isRootInput = context.pathSegments.length === 0;
|
|
1177
|
+
for (const rule of plan.substringRules) {
|
|
1178
|
+
const result = isRootInput ? applyRootPrimitiveSubstringMatch(value, rule, plan, context) : applySubstringRule(value, rule, plan, context);
|
|
1179
|
+
if (result !== void 0) return result;
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
const transformTrackedIdentity = (identity, plan, context, activePolicy, state, branchState, traverse) => {
|
|
1183
|
+
const canonicalPath = context.canonicalPath ?? "";
|
|
1184
|
+
const originalPath = branchState.activePaths.get(identity);
|
|
1185
|
+
if (originalPath !== void 0) {
|
|
1186
|
+
state.cycleRegistry?.set(identity, originalPath);
|
|
1187
|
+
if (plan.serialise) {
|
|
1188
|
+
const marker = createCircularMarker(canonicalPath, originalPath);
|
|
1189
|
+
return {
|
|
1190
|
+
cacheValue: marker,
|
|
1191
|
+
changed: true,
|
|
1192
|
+
pathStable: false,
|
|
1193
|
+
value: marker
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
return {
|
|
1197
|
+
cacheValue: identity,
|
|
1198
|
+
changed: false,
|
|
1199
|
+
pathStable: true,
|
|
1200
|
+
value: identity
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
const completedRecords = state.completedIdentities.get(identity);
|
|
1204
|
+
const ruleContextKey = activePolicy === void 0 ? "none" : buildRuleContextKey(activePolicy);
|
|
1205
|
+
const completedResult = completedRecords === void 0 ? void 0 : resolveCompletedTraversal(completedRecords, canonicalPath, ruleContextKey, identity);
|
|
1206
|
+
if (completedResult !== void 0) return completedResult;
|
|
1207
|
+
if (completedRecords !== void 0) {
|
|
1208
|
+
const snapshot = state.completedSnapshots.get(identity);
|
|
1209
|
+
if (snapshot !== void 0) return replayCompletedTraversal(identity, snapshot, plan, activePolicy, context, ruleContextKey, state, branchState);
|
|
1210
|
+
}
|
|
1211
|
+
state.budget.depth += 1;
|
|
1212
|
+
try {
|
|
1213
|
+
if (isDepthExceeded(state.budget, plan.maxDepth)) throwBudgetExceeded(plan, context, "depth");
|
|
1214
|
+
return withActiveIdentity(branchState, identity, canonicalPath, () => {
|
|
1215
|
+
const result = traverse();
|
|
1216
|
+
storeCompletedTraversal(state, identity, {
|
|
1217
|
+
canonicalPath,
|
|
1218
|
+
pathStable: result.pathStable,
|
|
1219
|
+
ruleContextKey,
|
|
1220
|
+
value: result.changed ? result.cacheValue : result.value
|
|
1221
|
+
});
|
|
1222
|
+
return result;
|
|
1223
|
+
});
|
|
1224
|
+
} finally {
|
|
1225
|
+
state.budget.depth -= 1;
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
const transformArray = (value, plan, inheritedPolicy, canonicalPath, pathSegments, rootInput, suppressDescendantRedaction, state, branchState) => {
|
|
1229
|
+
const cacheValue = new Array(value.length);
|
|
1230
|
+
const transformedValue = new Array(value.length);
|
|
1231
|
+
const snapshotItems = new Array(value.length);
|
|
1232
|
+
const removedIndexes = [];
|
|
1233
|
+
let changed = false;
|
|
1234
|
+
let pathStable = true;
|
|
1235
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1236
|
+
if (!(index in value)) continue;
|
|
1237
|
+
const pathSegment = createIndexPathSegment(index);
|
|
1238
|
+
const itemPath = appendCanonicalPathSegment(canonicalPath, pathSegment);
|
|
1239
|
+
pathSegments.push(pathSegment);
|
|
1240
|
+
try {
|
|
1241
|
+
const itemContext = {
|
|
1242
|
+
canonicalPath: itemPath,
|
|
1243
|
+
inheritedPolicy,
|
|
1244
|
+
pathSegments,
|
|
1245
|
+
rootInput,
|
|
1246
|
+
suppressDescendantRedaction
|
|
1247
|
+
};
|
|
1248
|
+
let item;
|
|
1249
|
+
try {
|
|
1250
|
+
item = value[index];
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
snapshotItems[index] = {
|
|
1253
|
+
diagnostic: emitFailureDiagnostic(plan, itemContext, {
|
|
1254
|
+
error,
|
|
1255
|
+
stage: "traversal-read",
|
|
1256
|
+
value,
|
|
1257
|
+
valueType: "getter"
|
|
1258
|
+
}),
|
|
1259
|
+
present: true
|
|
1260
|
+
};
|
|
1261
|
+
const failureResult = createUnsupportedTraversalResult();
|
|
1262
|
+
cacheValue[index] = failureResult.cacheValue;
|
|
1263
|
+
transformedValue[index] = failureResult.value;
|
|
1264
|
+
changed = true;
|
|
1265
|
+
pathStable &&= failureResult.pathStable;
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
snapshotItems[index] = {
|
|
1269
|
+
present: true,
|
|
1270
|
+
value: item
|
|
1271
|
+
};
|
|
1272
|
+
const itemResult = transformNestedNode(item, plan, itemContext, state, branchState);
|
|
1273
|
+
pathStable &&= itemResult.pathStable;
|
|
1274
|
+
if (isRemovedValue(itemResult.value)) {
|
|
1275
|
+
changed = true;
|
|
1276
|
+
removedIndexes.push(index);
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
cacheValue[index] = itemResult.cacheValue;
|
|
1280
|
+
transformedValue[index] = itemResult.value;
|
|
1281
|
+
if (!itemResult.changed) continue;
|
|
1282
|
+
changed = true;
|
|
1283
|
+
} finally {
|
|
1284
|
+
pathSegments.pop();
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
storeCompletedSnapshot(state, value, {
|
|
1288
|
+
items: snapshotItems,
|
|
1289
|
+
kind: "array"
|
|
1290
|
+
});
|
|
1291
|
+
const serialiseSnapshotOnly = !changed && Boolean(plan.serialise);
|
|
1292
|
+
if (!changed && !serialiseSnapshotOnly) return {
|
|
1293
|
+
cacheValue,
|
|
1294
|
+
changed: false,
|
|
1295
|
+
pathStable,
|
|
1296
|
+
value
|
|
1297
|
+
};
|
|
1298
|
+
if (removedIndexes.length === 0) return {
|
|
1299
|
+
cacheValue,
|
|
1300
|
+
changed: changed || serialiseSnapshotOnly,
|
|
1301
|
+
pathStable: serialiseSnapshotOnly ? false : pathStable,
|
|
1302
|
+
value: transformedValue
|
|
1303
|
+
};
|
|
1304
|
+
const compactedValue = transformedValue.slice();
|
|
1305
|
+
const compactedCacheValue = cacheValue.slice();
|
|
1306
|
+
let removedCount = 0;
|
|
1307
|
+
for (const removedIndex of removedIndexes) {
|
|
1308
|
+
compactedValue.splice(removedIndex - removedCount, 1);
|
|
1309
|
+
compactedCacheValue.splice(removedIndex - removedCount, 1);
|
|
1310
|
+
removedCount += 1;
|
|
1311
|
+
}
|
|
1312
|
+
return {
|
|
1313
|
+
cacheValue: compactedCacheValue,
|
|
1314
|
+
changed,
|
|
1315
|
+
pathStable,
|
|
1316
|
+
value: compactedValue
|
|
1317
|
+
};
|
|
1318
|
+
};
|
|
1319
|
+
const transformObject = (value, plan, inheritedPolicy, canonicalPath, pathSegments, rootInput, suppressDescendantRedaction, state, branchState) => {
|
|
1320
|
+
const cacheValue = {};
|
|
1321
|
+
const snapshotEntries = [];
|
|
1322
|
+
let changed = false;
|
|
1323
|
+
let pathStable = true;
|
|
1324
|
+
const transformedValue = {};
|
|
1325
|
+
let propertyKeys;
|
|
1326
|
+
try {
|
|
1327
|
+
propertyKeys = Object.keys(value);
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
return createFailureTraversalResult(plan, {
|
|
1330
|
+
canonicalPath,
|
|
1331
|
+
inheritedPolicy,
|
|
1332
|
+
pathSegments,
|
|
1333
|
+
rootInput,
|
|
1334
|
+
suppressDescendantRedaction
|
|
1335
|
+
}, {
|
|
1336
|
+
error,
|
|
1337
|
+
stage: "traversal-read",
|
|
1338
|
+
value
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
for (const key of propertyKeys) {
|
|
1342
|
+
const pathSegment = createPropertyPathSegment(key);
|
|
1343
|
+
const propertyPath = appendCanonicalPathSegment(canonicalPath, pathSegment);
|
|
1344
|
+
pathSegments.push(pathSegment);
|
|
1345
|
+
try {
|
|
1346
|
+
const propertyContext = {
|
|
1347
|
+
canonicalPath: propertyPath,
|
|
1348
|
+
directKeyMatch: plan.exactKeyRules.literalMatchers.length === 0 && plan.regexKeyRules.matchers.length === 0 ? void 0 : resolveDirectKeyMatch(plan, key),
|
|
1349
|
+
inheritedPolicy,
|
|
1350
|
+
pathSegments,
|
|
1351
|
+
rootInput,
|
|
1352
|
+
suppressDescendantRedaction
|
|
1353
|
+
};
|
|
1354
|
+
let propertyValue;
|
|
1355
|
+
try {
|
|
1356
|
+
propertyValue = value[key];
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
const diagnostic = emitFailureDiagnostic(plan, propertyContext, {
|
|
1359
|
+
error,
|
|
1360
|
+
stage: "traversal-read",
|
|
1361
|
+
value,
|
|
1362
|
+
valueType: "getter"
|
|
1363
|
+
});
|
|
1364
|
+
snapshotEntries.push({
|
|
1365
|
+
diagnostic,
|
|
1366
|
+
key
|
|
1367
|
+
});
|
|
1368
|
+
const failureResult = createUnsupportedTraversalResult();
|
|
1369
|
+
setObjectEntry(cacheValue, key, failureResult.cacheValue);
|
|
1370
|
+
setObjectEntry(transformedValue, key, failureResult.value);
|
|
1371
|
+
changed = true;
|
|
1372
|
+
pathStable &&= failureResult.pathStable;
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
snapshotEntries.push({
|
|
1376
|
+
key,
|
|
1377
|
+
value: propertyValue
|
|
1378
|
+
});
|
|
1379
|
+
const propertyResult = transformNestedNode(propertyValue, plan, propertyContext, state, branchState);
|
|
1380
|
+
pathStable &&= propertyResult.pathStable;
|
|
1381
|
+
if (isRemovedValue(propertyResult.value)) {
|
|
1382
|
+
changed = true;
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
setObjectEntry(cacheValue, key, propertyResult.cacheValue);
|
|
1386
|
+
setObjectEntry(transformedValue, key, propertyResult.value);
|
|
1387
|
+
if (!propertyResult.changed) continue;
|
|
1388
|
+
changed = true;
|
|
1389
|
+
} finally {
|
|
1390
|
+
pathSegments.pop();
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
storeCompletedSnapshot(state, value, {
|
|
1394
|
+
entries: snapshotEntries,
|
|
1395
|
+
kind: "object"
|
|
1396
|
+
});
|
|
1397
|
+
const serialiseSnapshotOnly = !changed && Boolean(plan.serialise);
|
|
1398
|
+
return {
|
|
1399
|
+
cacheValue,
|
|
1400
|
+
changed: changed || serialiseSnapshotOnly,
|
|
1401
|
+
pathStable: serialiseSnapshotOnly ? false : pathStable,
|
|
1402
|
+
value: changed || serialiseSnapshotOnly ? transformedValue : value
|
|
1403
|
+
};
|
|
1404
|
+
};
|
|
1405
|
+
const transformCompletedArray = (snapshot, plan, inheritedPolicy, canonicalPath, pathSegments, rootInput, suppressDescendantRedaction, state, branchState) => {
|
|
1406
|
+
const cacheValue = new Array(snapshot.items.length);
|
|
1407
|
+
const transformedValue = new Array(snapshot.items.length);
|
|
1408
|
+
const removedIndexes = [];
|
|
1409
|
+
let pathStable = true;
|
|
1410
|
+
for (let index = 0; index < snapshot.items.length; index += 1) {
|
|
1411
|
+
const itemSnapshot = snapshot.items[index];
|
|
1412
|
+
if (itemSnapshot === void 0 || !itemSnapshot.present) continue;
|
|
1413
|
+
const pathSegment = createIndexPathSegment(index);
|
|
1414
|
+
const itemPath = appendCanonicalPathSegment(canonicalPath, pathSegment);
|
|
1415
|
+
if (itemSnapshot.diagnostic !== void 0) {
|
|
1416
|
+
emitDiagnosticEvent(plan.diagnostics, createDiagnosticEvent(plan.diagnostics, itemPath, itemSnapshot.diagnostic));
|
|
1417
|
+
const failureResult = createUnsupportedTraversalResult();
|
|
1418
|
+
cacheValue[index] = failureResult.cacheValue;
|
|
1419
|
+
transformedValue[index] = failureResult.value;
|
|
1420
|
+
pathStable &&= failureResult.pathStable;
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
pathSegments.push(pathSegment);
|
|
1424
|
+
try {
|
|
1425
|
+
const itemResult = transformNestedNode(itemSnapshot.value, plan, {
|
|
1426
|
+
canonicalPath: itemPath,
|
|
1427
|
+
inheritedPolicy,
|
|
1428
|
+
pathSegments,
|
|
1429
|
+
rootInput,
|
|
1430
|
+
suppressDescendantRedaction
|
|
1431
|
+
}, state, branchState);
|
|
1432
|
+
pathStable &&= itemResult.pathStable;
|
|
1433
|
+
if (isRemovedValue(itemResult.value)) {
|
|
1434
|
+
removedIndexes.push(index);
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
cacheValue[index] = itemResult.cacheValue;
|
|
1438
|
+
transformedValue[index] = itemResult.value;
|
|
1439
|
+
} finally {
|
|
1440
|
+
pathSegments.pop();
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (removedIndexes.length === 0) return {
|
|
1444
|
+
cacheValue,
|
|
1445
|
+
changed: true,
|
|
1446
|
+
pathStable,
|
|
1447
|
+
value: transformedValue
|
|
1448
|
+
};
|
|
1449
|
+
const compactedCacheValue = cacheValue.slice();
|
|
1450
|
+
const compactedValue = transformedValue.slice();
|
|
1451
|
+
let removedCount = 0;
|
|
1452
|
+
for (const removedIndex of removedIndexes) {
|
|
1453
|
+
compactedCacheValue.splice(removedIndex - removedCount, 1);
|
|
1454
|
+
compactedValue.splice(removedIndex - removedCount, 1);
|
|
1455
|
+
removedCount += 1;
|
|
1456
|
+
}
|
|
1457
|
+
return {
|
|
1458
|
+
cacheValue: compactedCacheValue,
|
|
1459
|
+
changed: true,
|
|
1460
|
+
pathStable,
|
|
1461
|
+
value: compactedValue
|
|
1462
|
+
};
|
|
1463
|
+
};
|
|
1464
|
+
const transformCompletedObject = (snapshot, plan, inheritedPolicy, canonicalPath, pathSegments, rootInput, suppressDescendantRedaction, state, branchState) => {
|
|
1465
|
+
const cacheValue = {};
|
|
1466
|
+
const transformedValue = {};
|
|
1467
|
+
let pathStable = true;
|
|
1468
|
+
for (const entry of snapshot.entries) {
|
|
1469
|
+
const pathSegment = createPropertyPathSegment(entry.key);
|
|
1470
|
+
const propertyPath = appendCanonicalPathSegment(canonicalPath, pathSegment);
|
|
1471
|
+
if (entry.diagnostic !== void 0) {
|
|
1472
|
+
emitDiagnosticEvent(plan.diagnostics, createDiagnosticEvent(plan.diagnostics, propertyPath, entry.diagnostic));
|
|
1473
|
+
const failureResult = createUnsupportedTraversalResult();
|
|
1474
|
+
setObjectEntry(cacheValue, entry.key, failureResult.cacheValue);
|
|
1475
|
+
setObjectEntry(transformedValue, entry.key, failureResult.value);
|
|
1476
|
+
pathStable &&= failureResult.pathStable;
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
pathSegments.push(pathSegment);
|
|
1480
|
+
try {
|
|
1481
|
+
const propertyResult = transformNestedNode(entry.value, plan, {
|
|
1482
|
+
canonicalPath: propertyPath,
|
|
1483
|
+
directKeyMatch: plan.exactKeyRules.literalMatchers.length === 0 && plan.regexKeyRules.matchers.length === 0 ? void 0 : resolveDirectKeyMatch(plan, entry.key),
|
|
1484
|
+
inheritedPolicy,
|
|
1485
|
+
pathSegments,
|
|
1486
|
+
rootInput,
|
|
1487
|
+
suppressDescendantRedaction
|
|
1488
|
+
}, state, branchState);
|
|
1489
|
+
pathStable &&= propertyResult.pathStable;
|
|
1490
|
+
if (isRemovedValue(propertyResult.value)) continue;
|
|
1491
|
+
setObjectEntry(cacheValue, entry.key, propertyResult.cacheValue);
|
|
1492
|
+
setObjectEntry(transformedValue, entry.key, propertyResult.value);
|
|
1493
|
+
} finally {
|
|
1494
|
+
pathSegments.pop();
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return {
|
|
1498
|
+
cacheValue,
|
|
1499
|
+
changed: true,
|
|
1500
|
+
pathStable,
|
|
1501
|
+
value: transformedValue
|
|
1502
|
+
};
|
|
1503
|
+
};
|
|
1504
|
+
const replayCompletedTraversal = (value, snapshot, plan, inheritedPolicy, context, ruleContextKey, state, branchState) => {
|
|
1505
|
+
const canonicalPath = context.canonicalPath ?? "";
|
|
1506
|
+
const result = withActiveIdentity(branchState, value, canonicalPath, () => {
|
|
1507
|
+
return snapshot.kind === "array" ? transformCompletedArray(snapshot, plan, inheritedPolicy, context.canonicalPath, context.pathSegments, context.rootInput, context.suppressDescendantRedaction, state, branchState) : transformCompletedObject(snapshot, plan, inheritedPolicy, context.canonicalPath, context.pathSegments, context.rootInput, context.suppressDescendantRedaction, state, branchState);
|
|
1508
|
+
});
|
|
1509
|
+
storeCompletedTraversal(state, value, {
|
|
1510
|
+
canonicalPath,
|
|
1511
|
+
pathStable: result.pathStable,
|
|
1512
|
+
ruleContextKey,
|
|
1513
|
+
value: result.cacheValue
|
|
1514
|
+
});
|
|
1515
|
+
return result;
|
|
1516
|
+
};
|
|
1517
|
+
const transformNode = (value, plan, context, state, branchState) => {
|
|
1518
|
+
state.budget.nodesVisited += 1;
|
|
1519
|
+
if (isNodeBudgetExceeded(state.budget, plan.maxNodes)) throwBudgetExceeded(plan, context, "nodes");
|
|
1520
|
+
let activePolicy = context.suppressDescendantRedaction ? void 0 : selectActivePolicy(plan, resolveExactPathRule(plan, context.canonicalPath), plan.dynamicPathRules.length === 0 ? void 0 : resolveDynamicPathRule(plan, context.pathSegments), context.directKeyMatch, context.inheritedPolicy);
|
|
1521
|
+
if (activePolicy !== void 0 && (!activePolicy.policy.retainStructure || !canRetainStructure(value))) {
|
|
1522
|
+
if (isRedactableType(value, plan.valueTypes)) return applyConfiguredRedaction(value, activePolicy.policy, activePolicy.rulePath, !usesPathSensitivePolicy(activePolicy), plan, context);
|
|
1523
|
+
if (!isTraversableContainer(value)) return {
|
|
1524
|
+
cacheValue: value,
|
|
1525
|
+
changed: false,
|
|
1526
|
+
pathStable: true,
|
|
1527
|
+
value
|
|
1528
|
+
};
|
|
1529
|
+
activePolicy = context.inheritedPolicy;
|
|
1530
|
+
}
|
|
1531
|
+
if (!isTraversableContainer(value)) {
|
|
1532
|
+
const substringResult = context.suppressDescendantRedaction || plan.substringRules.length === 0 || !isRedactableType(value, plan.valueTypes) ? void 0 : transformSubstringValue(value, plan, context);
|
|
1533
|
+
if (substringResult !== void 0) return substringResult;
|
|
1534
|
+
return {
|
|
1535
|
+
cacheValue: value,
|
|
1536
|
+
changed: false,
|
|
1537
|
+
pathStable: true,
|
|
1538
|
+
value
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
const inheritedPolicy = activePolicy;
|
|
1542
|
+
return transformTrackedIdentity(value, plan, context, activePolicy, state, branchState, () => {
|
|
1543
|
+
return Array.isArray(value) ? transformArray(value, plan, inheritedPolicy, context.canonicalPath, context.pathSegments, context.rootInput, context.suppressDescendantRedaction, state, branchState) : transformObject(value, plan, inheritedPolicy, context.canonicalPath, context.pathSegments, context.rootInput, context.suppressDescendantRedaction, state, branchState);
|
|
1544
|
+
});
|
|
1545
|
+
};
|
|
1546
|
+
const redactValue = (value, plan, cycleRegistry) => {
|
|
1547
|
+
const state = createTraversalState(cycleRegistry);
|
|
1548
|
+
const branchState = createTraversalBranchState();
|
|
1549
|
+
const result = transformNode(value, plan, {
|
|
1550
|
+
canonicalPath: void 0,
|
|
1551
|
+
inheritedPolicy: void 0,
|
|
1552
|
+
pathSegments: [],
|
|
1553
|
+
rootInput: value
|
|
1554
|
+
}, state, branchState);
|
|
1555
|
+
return isRemovedValue(result.value) ? void 0 : result.value;
|
|
1556
|
+
};
|
|
1557
|
+
//#endregion
|
|
1558
|
+
//#region src/core/runtime/navigate-exact-paths.ts
|
|
1559
|
+
const unsupportedValue = "[UNSUPPORTED]";
|
|
1560
|
+
const noContext = Object.freeze({
|
|
1561
|
+
matchedPath: Object.freeze([]),
|
|
1562
|
+
rootInput: void 0,
|
|
1563
|
+
rulePath: Object.freeze([])
|
|
1564
|
+
});
|
|
1565
|
+
const shallowCopyContainer = (container) => {
|
|
1566
|
+
if (Array.isArray(container)) {
|
|
1567
|
+
const copy = [];
|
|
1568
|
+
copy.length = container.length;
|
|
1569
|
+
for (let index = 0; index < container.length; index += 1) if (index in container) copy[index] = container[index];
|
|
1570
|
+
return copy;
|
|
1571
|
+
}
|
|
1572
|
+
return { ...container };
|
|
1573
|
+
};
|
|
1574
|
+
const appendMatchedKey = (matchedPath, key) => {
|
|
1575
|
+
return matchedPath === void 0 ? [key] : [...matchedPath, key];
|
|
1576
|
+
};
|
|
1577
|
+
const renderConcreteCanonicalPath = (matchedPath) => {
|
|
1578
|
+
let path;
|
|
1579
|
+
for (const key of matchedPath) path = appendCanonicalPathSegment(path, typeof key === "number" ? {
|
|
1580
|
+
kind: "index",
|
|
1581
|
+
value: key
|
|
1582
|
+
} : {
|
|
1583
|
+
kind: "property",
|
|
1584
|
+
value: key
|
|
1585
|
+
});
|
|
1586
|
+
return path ?? "";
|
|
1587
|
+
};
|
|
1588
|
+
const renderAncestorCopyMatchedPath = (matchedPath) => {
|
|
1589
|
+
if (matchedPath === void 0) return;
|
|
1590
|
+
return JSON.stringify(matchedPath.map((segment) => [typeof segment, segment]));
|
|
1591
|
+
};
|
|
1592
|
+
const getAncestorCopy = (ancestorCopies, source, level, matchedPath) => {
|
|
1593
|
+
const records = ancestorCopies.get(source);
|
|
1594
|
+
if (records === void 0) return;
|
|
1595
|
+
const matchedPathKey = renderAncestorCopyMatchedPath(matchedPath);
|
|
1596
|
+
return records.find((candidate) => {
|
|
1597
|
+
return candidate.level === level && candidate.matchedPathKey === matchedPathKey;
|
|
1598
|
+
})?.copy;
|
|
1599
|
+
};
|
|
1600
|
+
const storeAncestorCopy = (ancestorCopies, source, level, matchedPath, copy) => {
|
|
1601
|
+
const matchedPathKey = renderAncestorCopyMatchedPath(matchedPath);
|
|
1602
|
+
const records = ancestorCopies.get(source);
|
|
1603
|
+
if (records === void 0) {
|
|
1604
|
+
ancestorCopies.set(source, [{
|
|
1605
|
+
copy,
|
|
1606
|
+
level,
|
|
1607
|
+
matchedPathKey
|
|
1608
|
+
}]);
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
if (records.some((candidate) => candidate.level === level && candidate.matchedPathKey === matchedPathKey)) return;
|
|
1612
|
+
records.push({
|
|
1613
|
+
copy,
|
|
1614
|
+
level,
|
|
1615
|
+
matchedPathKey
|
|
1616
|
+
});
|
|
1617
|
+
};
|
|
1618
|
+
const insertRule = (root, segments, rule) => {
|
|
1619
|
+
let level = root;
|
|
1620
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
1621
|
+
const segment = segments[index];
|
|
1622
|
+
let node;
|
|
1623
|
+
switch (segment.kind) {
|
|
1624
|
+
case "index": {
|
|
1625
|
+
const map = level.indexChildren ??= /* @__PURE__ */ new Map();
|
|
1626
|
+
const existing = map.get(segment.value);
|
|
1627
|
+
if (existing === void 0) {
|
|
1628
|
+
node = {};
|
|
1629
|
+
map.set(segment.value, node);
|
|
1630
|
+
} else node = existing;
|
|
1631
|
+
break;
|
|
1632
|
+
}
|
|
1633
|
+
case "wildcard":
|
|
1634
|
+
node = level.wildcardChild ??= {};
|
|
1635
|
+
break;
|
|
1636
|
+
case "property": {
|
|
1637
|
+
const map = level.propertyChildren ??= /* @__PURE__ */ new Map();
|
|
1638
|
+
const existing = map.get(segment.value);
|
|
1639
|
+
if (existing === void 0) {
|
|
1640
|
+
node = {};
|
|
1641
|
+
map.set(segment.value, node);
|
|
1642
|
+
} else node = existing;
|
|
1643
|
+
break;
|
|
1644
|
+
}
|
|
1645
|
+
default: throw new TypeError(`Unsupported path segment kind "${segment.kind}" in rule-driven trie.`);
|
|
1646
|
+
}
|
|
1647
|
+
if (index === segments.length - 1) node.rule = rule;
|
|
1648
|
+
level = node;
|
|
1649
|
+
}
|
|
1650
|
+
};
|
|
1651
|
+
const markWildcardSubtrees = (node) => {
|
|
1652
|
+
let hasWildcard = node.wildcardChild !== void 0 || node.rule?.kind === "wildcard";
|
|
1653
|
+
if (node.wildcardChild !== void 0 && markWildcardSubtrees(node.wildcardChild)) hasWildcard = true;
|
|
1654
|
+
if (node.indexChildren !== void 0) {
|
|
1655
|
+
for (const child of node.indexChildren.values()) if (markWildcardSubtrees(child)) hasWildcard = true;
|
|
1656
|
+
}
|
|
1657
|
+
if (node.propertyChildren !== void 0) {
|
|
1658
|
+
for (const child of node.propertyChildren.values()) if (markWildcardSubtrees(child)) hasWildcard = true;
|
|
1659
|
+
}
|
|
1660
|
+
node.subtreeHasWildcard = hasWildcard;
|
|
1661
|
+
return hasWildcard;
|
|
1662
|
+
};
|
|
1663
|
+
const buildPrefixTree = (plan) => {
|
|
1664
|
+
const root = {};
|
|
1665
|
+
for (const rule of Object.values(plan.exactPathRules)) insertRule(root, rule.segments, {
|
|
1666
|
+
canonicalPath: rule.canonicalPath,
|
|
1667
|
+
kind: "exact",
|
|
1668
|
+
policy: rule.policy,
|
|
1669
|
+
rulePath: rule.rulePath
|
|
1670
|
+
});
|
|
1671
|
+
for (const rule of plan.dynamicPathRules) insertRule(root, rule.segments, {
|
|
1672
|
+
kind: "wildcard",
|
|
1673
|
+
policy: rule.policy,
|
|
1674
|
+
rulePath: rule.rulePath
|
|
1675
|
+
});
|
|
1676
|
+
markWildcardSubtrees(root);
|
|
1677
|
+
return root;
|
|
1678
|
+
};
|
|
1679
|
+
const emitCensorFailure = (plan, canonicalPath, value, error) => {
|
|
1680
|
+
emitDiagnosticEvent(plan.diagnostics, createDiagnosticEvent(plan.diagnostics, canonicalPath, createFailureDiagnosticSnapshot({
|
|
1681
|
+
error,
|
|
1682
|
+
stage: "censor",
|
|
1683
|
+
value
|
|
1684
|
+
})));
|
|
1685
|
+
};
|
|
1686
|
+
const applyTerminalRule = (value, rule, plan, rootInput) => {
|
|
1687
|
+
if (!isRedactableType(value, plan.valueTypes)) return value;
|
|
1688
|
+
try {
|
|
1689
|
+
if (typeof rule.policy.censor === "function") return applyRedaction(value, rule.policy, {
|
|
1690
|
+
matchedPath: rule.rulePath,
|
|
1691
|
+
rootInput,
|
|
1692
|
+
rulePath: rule.rulePath,
|
|
1693
|
+
terminalKey: rule.rulePath.at(-1)
|
|
1694
|
+
});
|
|
1695
|
+
return applyRedaction(value, rule.policy, noContext);
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
emitCensorFailure(plan, rule.canonicalPath, value, error);
|
|
1698
|
+
return unsupportedValue;
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
const applyWildcardTerminalRule = (value, rule, plan, rootInput, matchedPath) => {
|
|
1702
|
+
if (!isRedactableType(value, plan.valueTypes)) return value;
|
|
1703
|
+
try {
|
|
1704
|
+
if (typeof rule.policy.censor === "function") return applyRedaction(value, rule.policy, {
|
|
1705
|
+
matchedPath: Object.freeze([...matchedPath]),
|
|
1706
|
+
rootInput,
|
|
1707
|
+
rulePath: rule.rulePath,
|
|
1708
|
+
terminalKey: matchedPath.at(-1)
|
|
1709
|
+
});
|
|
1710
|
+
return applyRedaction(value, rule.policy, noContext);
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
emitCensorFailure(plan, renderConcreteCanonicalPath(matchedPath), value, error);
|
|
1713
|
+
return unsupportedValue;
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
const collectDescendedKeys = (chain) => {
|
|
1717
|
+
if (chain === void 0) return [];
|
|
1718
|
+
const keys = [];
|
|
1719
|
+
for (let node = chain; node !== void 0; node = node.parent) keys.push(node.key);
|
|
1720
|
+
keys.reverse();
|
|
1721
|
+
return keys;
|
|
1722
|
+
};
|
|
1723
|
+
const materialiseRetainMatchedPath = (inherited, leafKey) => {
|
|
1724
|
+
return [
|
|
1725
|
+
...inherited.basePath,
|
|
1726
|
+
...collectDescendedKeys(inherited.descended),
|
|
1727
|
+
leafKey
|
|
1728
|
+
];
|
|
1729
|
+
};
|
|
1730
|
+
const materialiseRetainCanonical = (inherited, leafSegment) => {
|
|
1731
|
+
let path = inherited.baseCanonical;
|
|
1732
|
+
for (const key of collectDescendedKeys(inherited.descended)) path = appendCanonicalPathSegment(path, buildSegment(typeof key === "number" ? "index" : "property", key));
|
|
1733
|
+
return appendCanonicalPathSegment(path, leafSegment);
|
|
1734
|
+
};
|
|
1735
|
+
const applyInheritedLeaf = (value, inherited, segment, key, plan, rootInput) => {
|
|
1736
|
+
if (!isRedactableType(value, plan.valueTypes)) return value;
|
|
1737
|
+
try {
|
|
1738
|
+
if (typeof inherited.policy.censor === "function") return applyRedaction(value, inherited.policy, {
|
|
1739
|
+
matchedPath: materialiseRetainMatchedPath(inherited, key),
|
|
1740
|
+
rootInput,
|
|
1741
|
+
rulePath: inherited.rulePath,
|
|
1742
|
+
terminalKey: key
|
|
1743
|
+
});
|
|
1744
|
+
return applyRedaction(value, inherited.policy, noContext);
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
emitCensorFailure(plan, materialiseRetainCanonical(inherited, segment), value, error);
|
|
1747
|
+
return unsupportedValue;
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
const enterRetain = (rule) => {
|
|
1751
|
+
return {
|
|
1752
|
+
basePath: rule.rulePath,
|
|
1753
|
+
baseCanonical: rule.canonicalPath,
|
|
1754
|
+
descended: void 0,
|
|
1755
|
+
policy: rule.policy,
|
|
1756
|
+
rulePath: rule.rulePath
|
|
1757
|
+
};
|
|
1758
|
+
};
|
|
1759
|
+
const enterRetainWildcard = (rule, matchedPath) => {
|
|
1760
|
+
return {
|
|
1761
|
+
basePath: matchedPath,
|
|
1762
|
+
baseCanonical: renderConcreteCanonicalPath(matchedPath),
|
|
1763
|
+
descended: void 0,
|
|
1764
|
+
policy: rule.policy,
|
|
1765
|
+
rulePath: rule.rulePath
|
|
1766
|
+
};
|
|
1767
|
+
};
|
|
1768
|
+
const descendRetain = (inherited, key) => {
|
|
1769
|
+
return {
|
|
1770
|
+
basePath: inherited.basePath,
|
|
1771
|
+
baseCanonical: inherited.baseCanonical,
|
|
1772
|
+
descended: {
|
|
1773
|
+
key,
|
|
1774
|
+
parent: inherited.descended
|
|
1775
|
+
},
|
|
1776
|
+
policy: inherited.policy,
|
|
1777
|
+
rulePath: inherited.rulePath
|
|
1778
|
+
};
|
|
1779
|
+
};
|
|
1780
|
+
const buildSegment = (kind, key) => {
|
|
1781
|
+
return kind === "index" ? {
|
|
1782
|
+
kind: "index",
|
|
1783
|
+
value: key
|
|
1784
|
+
} : {
|
|
1785
|
+
kind: "property",
|
|
1786
|
+
value: key
|
|
1787
|
+
};
|
|
1788
|
+
};
|
|
1789
|
+
const enterExactHop = (budget, plan) => {
|
|
1790
|
+
budget.depth += 1;
|
|
1791
|
+
budget.nodesVisited += 1;
|
|
1792
|
+
if (isDepthExceeded(budget, plan.maxDepth)) throw createBudgetExceededError("depth", plan.maxDepth);
|
|
1793
|
+
if (isNodeBudgetExceeded(budget, plan.maxNodes)) throw createBudgetExceededError("nodes", plan.maxNodes);
|
|
1794
|
+
};
|
|
1795
|
+
const leaveExactHop = (budget) => {
|
|
1796
|
+
budget.depth -= 1;
|
|
1797
|
+
};
|
|
1798
|
+
const resolveChildInherited = (node, inherited, key) => {
|
|
1799
|
+
if (node?.rule?.kind === "exact") return enterRetain(node.rule);
|
|
1800
|
+
if (inherited === void 0) return;
|
|
1801
|
+
return descendRetain(inherited, key);
|
|
1802
|
+
};
|
|
1803
|
+
const emptyLevel = {};
|
|
1804
|
+
const delegate = Symbol("deep-redact.rule-driven.delegate");
|
|
1805
|
+
const requiresDelegation = (value) => {
|
|
1806
|
+
return typeof value === "bigint" || value !== null && typeof value === "object";
|
|
1807
|
+
};
|
|
1808
|
+
const redactRetained = (container, level, inherited, plan, rootInput, budget) => {
|
|
1809
|
+
budget.depth += 1;
|
|
1810
|
+
if (isDepthExceeded(budget, plan.maxDepth)) throw createBudgetExceededError("depth", plan.maxDepth);
|
|
1811
|
+
try {
|
|
1812
|
+
if (level.wildcardChild !== void 0) return delegate;
|
|
1813
|
+
let copy;
|
|
1814
|
+
if (Array.isArray(container)) {
|
|
1815
|
+
const items = container;
|
|
1816
|
+
let removedIndices;
|
|
1817
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
1818
|
+
if (!(index in items)) continue;
|
|
1819
|
+
budget.nodesVisited += 1;
|
|
1820
|
+
if (isNodeBudgetExceeded(budget, plan.maxNodes)) throw createBudgetExceededError("nodes", plan.maxNodes);
|
|
1821
|
+
const value = items[index];
|
|
1822
|
+
const node = level.indexChildren?.get(index);
|
|
1823
|
+
if (node?.rule?.kind === "wildcard") return delegate;
|
|
1824
|
+
if (node?.rule !== void 0 && !node.rule.policy.retainStructure) {
|
|
1825
|
+
if (isDescendable(value) && !isRedactableType(value, plan.valueTypes)) return delegate;
|
|
1826
|
+
const redacted = applyTerminalRule(value, node.rule, plan, rootInput);
|
|
1827
|
+
copy ??= shallowCopyContainer(container);
|
|
1828
|
+
if (isRemovedValue(redacted)) (removedIndices ??= []).push(index);
|
|
1829
|
+
else copy[index] = redacted;
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
if (Array.isArray(value) || isPlainObject$1(value)) {
|
|
1833
|
+
const childInherited = resolveChildInherited(node, inherited, index);
|
|
1834
|
+
const child = redactRetained(value, node ?? emptyLevel, childInherited, plan, rootInput, budget);
|
|
1835
|
+
if (child === delegate) return delegate;
|
|
1836
|
+
if (child !== value) (copy ??= shallowCopyContainer(container))[index] = child;
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
if (node?.rule !== void 0 || inherited !== void 0) {
|
|
1840
|
+
if (requiresDelegation(value)) return delegate;
|
|
1841
|
+
if (!isRedactableType(value, plan.valueTypes)) continue;
|
|
1842
|
+
const redacted = node?.rule === void 0 ? applyInheritedLeaf(value, inherited, buildSegment("index", index), index, plan, rootInput) : applyTerminalRule(value, node.rule, plan, rootInput);
|
|
1843
|
+
copy ??= shallowCopyContainer(container);
|
|
1844
|
+
if (isRemovedValue(redacted)) (removedIndices ??= []).push(index);
|
|
1845
|
+
else copy[index] = redacted;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
if (copy === void 0) return container;
|
|
1849
|
+
if (removedIndices !== void 0) {
|
|
1850
|
+
const compacted = copy;
|
|
1851
|
+
let removedCount = 0;
|
|
1852
|
+
for (const removedIndex of removedIndices) {
|
|
1853
|
+
compacted.splice(removedIndex - removedCount, 1);
|
|
1854
|
+
removedCount += 1;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
return copy;
|
|
1858
|
+
}
|
|
1859
|
+
for (const key of Object.keys(container)) {
|
|
1860
|
+
const node = level.propertyChildren?.get(key);
|
|
1861
|
+
budget.nodesVisited += 1;
|
|
1862
|
+
if (isNodeBudgetExceeded(budget, plan.maxNodes)) throw createBudgetExceededError("nodes", plan.maxNodes);
|
|
1863
|
+
const value = container[key];
|
|
1864
|
+
if (node?.rule?.kind === "wildcard") return delegate;
|
|
1865
|
+
if (node?.rule !== void 0 && !node.rule.policy.retainStructure) {
|
|
1866
|
+
if (isDescendable(value) && !isRedactableType(value, plan.valueTypes)) return delegate;
|
|
1867
|
+
const redacted = applyTerminalRule(value, node.rule, plan, rootInput);
|
|
1868
|
+
copy ??= shallowCopyContainer(container);
|
|
1869
|
+
if (isRemovedValue(redacted)) delete copy[key];
|
|
1870
|
+
else setObjectEntry(copy, key, redacted);
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
if (Array.isArray(value) || isPlainObject$1(value)) {
|
|
1874
|
+
const childInherited = resolveChildInherited(node, inherited, key);
|
|
1875
|
+
const child = redactRetained(value, node ?? emptyLevel, childInherited, plan, rootInput, budget);
|
|
1876
|
+
if (child === delegate) return delegate;
|
|
1877
|
+
if (child !== value) setObjectEntry(copy ??= shallowCopyContainer(container), key, child);
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
if (node?.rule !== void 0 || inherited !== void 0) {
|
|
1881
|
+
if (requiresDelegation(value)) return delegate;
|
|
1882
|
+
if (!isRedactableType(value, plan.valueTypes)) continue;
|
|
1883
|
+
const redacted = node?.rule === void 0 ? applyInheritedLeaf(value, inherited, buildSegment("property", key), key, plan, rootInput) : applyTerminalRule(value, node.rule, plan, rootInput);
|
|
1884
|
+
copy ??= shallowCopyContainer(container);
|
|
1885
|
+
if (isRemovedValue(redacted)) delete copy[key];
|
|
1886
|
+
else setObjectEntry(copy, key, redacted);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return copy ?? container;
|
|
1890
|
+
} finally {
|
|
1891
|
+
budget.depth -= 1;
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
const isDescendable = (value) => {
|
|
1895
|
+
return Array.isArray(value) || isPlainObject$1(value);
|
|
1896
|
+
};
|
|
1897
|
+
const resolveRetainTerminal = (value, childNode, plan, rootInput, ancestorCopies, budget) => {
|
|
1898
|
+
if (isDescendable(value)) {
|
|
1899
|
+
const existing = getAncestorCopy(ancestorCopies, value, childNode, void 0);
|
|
1900
|
+
if (existing !== void 0) return existing;
|
|
1901
|
+
const descended = redactRetained(value, childNode, enterRetain(childNode.rule), plan, rootInput, budget);
|
|
1902
|
+
if (descended === delegate) return delegate;
|
|
1903
|
+
if (descended !== value) storeAncestorCopy(ancestorCopies, value, childNode, void 0, descended);
|
|
1904
|
+
return descended;
|
|
1905
|
+
}
|
|
1906
|
+
if (requiresDelegation(value)) return delegate;
|
|
1907
|
+
return applyTerminalRule(value, childNode.rule, plan, rootInput);
|
|
1908
|
+
};
|
|
1909
|
+
const resolveRetainTerminalWildcard = (value, childNode, plan, rootInput, ancestorCopies, budget, matchedPath) => {
|
|
1910
|
+
const rule = childNode.rule;
|
|
1911
|
+
if (isDescendable(value)) {
|
|
1912
|
+
const existing = getAncestorCopy(ancestorCopies, value, childNode, matchedPath);
|
|
1913
|
+
if (existing !== void 0) return existing;
|
|
1914
|
+
const descended = redactRetained(value, childNode, enterRetainWildcard(rule, matchedPath), plan, rootInput, budget);
|
|
1915
|
+
if (descended === delegate) return delegate;
|
|
1916
|
+
if (descended !== value) storeAncestorCopy(ancestorCopies, value, childNode, matchedPath, descended);
|
|
1917
|
+
return descended;
|
|
1918
|
+
}
|
|
1919
|
+
if (requiresDelegation(value)) return delegate;
|
|
1920
|
+
return applyWildcardTerminalRule(value, rule, plan, rootInput, matchedPath);
|
|
1921
|
+
};
|
|
1922
|
+
const navigateNode = (container, level, plan, rootInput, ancestorCopies, compactedArrayCopies, budget, matchedPath) => {
|
|
1923
|
+
let copy = getAncestorCopy(ancestorCopies, container, level, matchedPath);
|
|
1924
|
+
let removedIndices;
|
|
1925
|
+
if (level.indexChildren !== void 0) for (const [index, childNode] of level.indexChildren) {
|
|
1926
|
+
if (!(index in container)) continue;
|
|
1927
|
+
enterExactHop(budget, plan);
|
|
1928
|
+
try {
|
|
1929
|
+
const value = container[index];
|
|
1930
|
+
if (childNode.rule?.kind === "wildcard") {
|
|
1931
|
+
const concreteMatchedPath = appendMatchedKey(matchedPath, index);
|
|
1932
|
+
if (childNode.rule.policy.retainStructure) {
|
|
1933
|
+
const retained = resolveRetainTerminalWildcard(value, childNode, plan, rootInput, ancestorCopies, budget, concreteMatchedPath);
|
|
1934
|
+
if (retained === delegate) return delegate;
|
|
1935
|
+
if (retained !== value) {
|
|
1936
|
+
if (copy === void 0) {
|
|
1937
|
+
copy = shallowCopyContainer(container);
|
|
1938
|
+
storeAncestorCopy(ancestorCopies, container, level, matchedPath, copy);
|
|
1939
|
+
}
|
|
1940
|
+
if (isRemovedValue(retained)) (removedIndices ??= []).push(index);
|
|
1941
|
+
else copy[index] = retained;
|
|
1942
|
+
}
|
|
1943
|
+
} else {
|
|
1944
|
+
if (isDescendable(value) && !isRedactableType(value, plan.valueTypes)) return delegate;
|
|
1945
|
+
const redacted = applyWildcardTerminalRule(value, childNode.rule, plan, rootInput, concreteMatchedPath);
|
|
1946
|
+
if (copy === void 0) {
|
|
1947
|
+
copy = shallowCopyContainer(container);
|
|
1948
|
+
storeAncestorCopy(ancestorCopies, container, level, matchedPath, copy);
|
|
1949
|
+
}
|
|
1950
|
+
if (isRemovedValue(redacted)) (removedIndices ??= []).push(index);
|
|
1951
|
+
else copy[index] = redacted;
|
|
1952
|
+
}
|
|
1953
|
+
continue;
|
|
1954
|
+
}
|
|
1955
|
+
if (childNode.rule !== void 0 && !childNode.rule.policy.retainStructure) {
|
|
1956
|
+
if (isDescendable(value) && !isRedactableType(value, plan.valueTypes)) return delegate;
|
|
1957
|
+
const redacted = applyTerminalRule(value, childNode.rule, plan, rootInput);
|
|
1958
|
+
if (copy === void 0) {
|
|
1959
|
+
copy = shallowCopyContainer(container);
|
|
1960
|
+
storeAncestorCopy(ancestorCopies, container, level, matchedPath, copy);
|
|
1961
|
+
}
|
|
1962
|
+
if (isRemovedValue(redacted)) (removedIndices ??= []).push(index);
|
|
1963
|
+
else copy[index] = redacted;
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
if (childNode.rule !== void 0) {
|
|
1967
|
+
const retained = resolveRetainTerminal(value, childNode, plan, rootInput, ancestorCopies, budget);
|
|
1968
|
+
if (retained === delegate) return delegate;
|
|
1969
|
+
if (retained !== value) {
|
|
1970
|
+
if (copy === void 0) {
|
|
1971
|
+
copy = shallowCopyContainer(container);
|
|
1972
|
+
storeAncestorCopy(ancestorCopies, container, level, matchedPath, copy);
|
|
1973
|
+
}
|
|
1974
|
+
if (isRemovedValue(retained)) (removedIndices ??= []).push(index);
|
|
1975
|
+
else copy[index] = retained;
|
|
1976
|
+
}
|
|
1977
|
+
continue;
|
|
1978
|
+
}
|
|
1979
|
+
if (isDescendable(value)) {
|
|
1980
|
+
const child = navigateNode(value, childNode, plan, rootInput, ancestorCopies, compactedArrayCopies, budget, matchedPath !== void 0 || childNode.subtreeHasWildcard === true ? appendMatchedKey(matchedPath, index) : void 0);
|
|
1981
|
+
if (child === delegate) return delegate;
|
|
1982
|
+
if (child !== value) {
|
|
1983
|
+
if (copy === void 0) {
|
|
1984
|
+
copy = shallowCopyContainer(container);
|
|
1985
|
+
storeAncestorCopy(ancestorCopies, container, level, matchedPath, copy);
|
|
1986
|
+
}
|
|
1987
|
+
copy[index] = child;
|
|
1988
|
+
}
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
if (value !== null && typeof value === "object") return delegate;
|
|
1992
|
+
} finally {
|
|
1993
|
+
leaveExactHop(budget);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
if (level.propertyChildren !== void 0) for (const [key, childNode] of level.propertyChildren) {
|
|
1997
|
+
if (!(key in container)) continue;
|
|
1998
|
+
enterExactHop(budget, plan);
|
|
1999
|
+
try {
|
|
2000
|
+
const value = container[key];
|
|
2001
|
+
if (childNode.rule?.kind === "wildcard") {
|
|
2002
|
+
const concreteMatchedPath = appendMatchedKey(matchedPath, key);
|
|
2003
|
+
if (childNode.rule.policy.retainStructure) {
|
|
2004
|
+
const retained = resolveRetainTerminalWildcard(value, childNode, plan, rootInput, ancestorCopies, budget, concreteMatchedPath);
|
|
2005
|
+
if (retained === delegate) return delegate;
|
|
2006
|
+
if (retained !== value) {
|
|
2007
|
+
if (copy === void 0) {
|
|
2008
|
+
copy = shallowCopyContainer(container);
|
|
2009
|
+
storeAncestorCopy(ancestorCopies, container, level, matchedPath, copy);
|
|
2010
|
+
}
|
|
2011
|
+
if (isRemovedValue(retained)) delete copy[key];
|
|
2012
|
+
else setObjectEntry(copy, key, retained);
|
|
2013
|
+
}
|
|
2014
|
+
} else {
|
|
2015
|
+
if (isDescendable(value) && !isRedactableType(value, plan.valueTypes)) return delegate;
|
|
2016
|
+
const redacted = applyWildcardTerminalRule(value, childNode.rule, plan, rootInput, concreteMatchedPath);
|
|
2017
|
+
if (copy === void 0) {
|
|
2018
|
+
copy = shallowCopyContainer(container);
|
|
2019
|
+
storeAncestorCopy(ancestorCopies, container, level, matchedPath, copy);
|
|
2020
|
+
}
|
|
2021
|
+
if (isRemovedValue(redacted)) delete copy[key];
|
|
2022
|
+
else setObjectEntry(copy, key, redacted);
|
|
2023
|
+
}
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
if (childNode.rule !== void 0 && !childNode.rule.policy.retainStructure) {
|
|
2027
|
+
if (isDescendable(value) && !isRedactableType(value, plan.valueTypes)) return delegate;
|
|
2028
|
+
const redacted = applyTerminalRule(value, childNode.rule, plan, rootInput);
|
|
2029
|
+
if (copy === void 0) {
|
|
2030
|
+
copy = shallowCopyContainer(container);
|
|
2031
|
+
storeAncestorCopy(ancestorCopies, container, level, matchedPath, copy);
|
|
2032
|
+
}
|
|
2033
|
+
if (isRemovedValue(redacted)) delete copy[key];
|
|
2034
|
+
else setObjectEntry(copy, key, redacted);
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
if (childNode.rule !== void 0) {
|
|
2038
|
+
const retained = resolveRetainTerminal(value, childNode, plan, rootInput, ancestorCopies, budget);
|
|
2039
|
+
if (retained === delegate) return delegate;
|
|
2040
|
+
if (retained !== value) {
|
|
2041
|
+
if (copy === void 0) {
|
|
2042
|
+
copy = shallowCopyContainer(container);
|
|
2043
|
+
storeAncestorCopy(ancestorCopies, container, level, matchedPath, copy);
|
|
2044
|
+
}
|
|
2045
|
+
if (isRemovedValue(retained)) delete copy[key];
|
|
2046
|
+
else setObjectEntry(copy, key, retained);
|
|
2047
|
+
}
|
|
2048
|
+
continue;
|
|
2049
|
+
}
|
|
2050
|
+
if (isDescendable(value)) {
|
|
2051
|
+
const child = navigateNode(value, childNode, plan, rootInput, ancestorCopies, compactedArrayCopies, budget, matchedPath !== void 0 || childNode.subtreeHasWildcard === true ? appendMatchedKey(matchedPath, key) : void 0);
|
|
2052
|
+
if (child === delegate) return delegate;
|
|
2053
|
+
if (child !== value) {
|
|
2054
|
+
if (copy === void 0) {
|
|
2055
|
+
copy = shallowCopyContainer(container);
|
|
2056
|
+
storeAncestorCopy(ancestorCopies, container, level, matchedPath, copy);
|
|
2057
|
+
}
|
|
2058
|
+
setObjectEntry(copy, key, child);
|
|
2059
|
+
}
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
if (value !== null && typeof value === "object") return delegate;
|
|
2063
|
+
} finally {
|
|
2064
|
+
leaveExactHop(budget);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
if (level.wildcardChild !== void 0) {
|
|
2068
|
+
const wildcardChild = level.wildcardChild;
|
|
2069
|
+
if (Array.isArray(container)) {
|
|
2070
|
+
const items = container;
|
|
2071
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
2072
|
+
if (!(index in items)) continue;
|
|
2073
|
+
if (level.indexChildren?.has(index)) continue;
|
|
2074
|
+
budget.nodesVisited += 1;
|
|
2075
|
+
if (isNodeBudgetExceeded(budget, plan.maxNodes)) throw createBudgetExceededError("nodes", plan.maxNodes);
|
|
2076
|
+
const value = items[index];
|
|
2077
|
+
const result = applyWildcardEdge(value, wildcardChild, appendMatchedKey(matchedPath, index), plan, rootInput, ancestorCopies, compactedArrayCopies, budget);
|
|
2078
|
+
if (result === delegate) return delegate;
|
|
2079
|
+
if (result !== value) {
|
|
2080
|
+
if (copy === void 0) {
|
|
2081
|
+
copy = shallowCopyContainer(container);
|
|
2082
|
+
storeAncestorCopy(ancestorCopies, container, level, matchedPath, copy);
|
|
2083
|
+
}
|
|
2084
|
+
if (isRemovedValue(result)) (removedIndices ??= []).push(index);
|
|
2085
|
+
else copy[index] = result;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
} else for (const key of Object.keys(container)) {
|
|
2089
|
+
if (level.propertyChildren?.has(key)) continue;
|
|
2090
|
+
budget.nodesVisited += 1;
|
|
2091
|
+
if (isNodeBudgetExceeded(budget, plan.maxNodes)) throw createBudgetExceededError("nodes", plan.maxNodes);
|
|
2092
|
+
const value = container[key];
|
|
2093
|
+
const result = applyWildcardEdge(value, wildcardChild, appendMatchedKey(matchedPath, key), plan, rootInput, ancestorCopies, compactedArrayCopies, budget);
|
|
2094
|
+
if (result === delegate) return delegate;
|
|
2095
|
+
if (result !== value) {
|
|
2096
|
+
if (copy === void 0) {
|
|
2097
|
+
copy = shallowCopyContainer(container);
|
|
2098
|
+
storeAncestorCopy(ancestorCopies, container, level, matchedPath, copy);
|
|
2099
|
+
}
|
|
2100
|
+
if (isRemovedValue(result)) delete copy[key];
|
|
2101
|
+
else setObjectEntry(copy, key, result);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
if (copy === void 0) return container;
|
|
2106
|
+
if (removedIndices !== void 0 && Array.isArray(copy) && !compactedArrayCopies.has(copy)) {
|
|
2107
|
+
const compacted = copy;
|
|
2108
|
+
let removedCount = 0;
|
|
2109
|
+
for (const removedIndex of removedIndices.sort((a, b) => a - b)) {
|
|
2110
|
+
compacted.splice(removedIndex - removedCount, 1);
|
|
2111
|
+
removedCount += 1;
|
|
2112
|
+
}
|
|
2113
|
+
compactedArrayCopies.add(copy);
|
|
2114
|
+
}
|
|
2115
|
+
return copy;
|
|
2116
|
+
};
|
|
2117
|
+
const applyWildcardEdge = (value, wildcardChild, matchedPath, plan, rootInput, ancestorCopies, compactedArrayCopies, budget) => {
|
|
2118
|
+
if (wildcardChild.rule !== void 0) {
|
|
2119
|
+
if (!wildcardChild.rule.policy.retainStructure) {
|
|
2120
|
+
if (isDescendable(value) && !isRedactableType(value, plan.valueTypes)) return delegate;
|
|
2121
|
+
return applyWildcardTerminalRule(value, wildcardChild.rule, plan, rootInput, matchedPath);
|
|
2122
|
+
}
|
|
2123
|
+
return resolveRetainTerminalWildcard(value, wildcardChild, plan, rootInput, ancestorCopies, budget, matchedPath);
|
|
2124
|
+
}
|
|
2125
|
+
if (isDescendable(value)) return navigateNode(value, wildcardChild, plan, rootInput, ancestorCopies, compactedArrayCopies, budget, matchedPath);
|
|
2126
|
+
if (value !== null && typeof value === "object") return delegate;
|
|
2127
|
+
return value;
|
|
2128
|
+
};
|
|
2129
|
+
const buildPathDrivenExecutor = (plan, fallback) => {
|
|
2130
|
+
const root = buildPrefixTree(plan);
|
|
2131
|
+
return function pathDriven(input) {
|
|
2132
|
+
if (input === null || typeof input !== "object") return input;
|
|
2133
|
+
if (!(Array.isArray(input) || isPlainObject$1(input))) return fallback(input);
|
|
2134
|
+
const budget = createTraversalBudget();
|
|
2135
|
+
const compactedArrayCopies = /* @__PURE__ */ new Set();
|
|
2136
|
+
let result;
|
|
2137
|
+
try {
|
|
2138
|
+
result = navigateNode(input, root, plan, input, /* @__PURE__ */ new Map(), compactedArrayCopies, budget, void 0);
|
|
2139
|
+
} catch (error) {
|
|
2140
|
+
if (isBudgetExceededError(error)) throw error;
|
|
2141
|
+
return fallback(input);
|
|
2142
|
+
}
|
|
2143
|
+
return result === delegate ? fallback(input) : result;
|
|
2144
|
+
};
|
|
2145
|
+
};
|
|
2146
|
+
//#endregion
|
|
2147
|
+
//#region src/core/validation/validation-report.ts
|
|
2148
|
+
const formatValidationIssues = (issues) => {
|
|
2149
|
+
return issues.map((issue, index) => `${index + 1}. ${issue.path}: ${issue.message}`).join("\n");
|
|
2150
|
+
};
|
|
2151
|
+
var DeepRedactValidationError = class extends TypeError {
|
|
2152
|
+
issues;
|
|
2153
|
+
constructor(issues) {
|
|
2154
|
+
super(formatValidationIssues(issues));
|
|
2155
|
+
this.name = "DeepRedactValidationError";
|
|
2156
|
+
this.issues = Object.freeze([...issues]);
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
const createValidationReport = (issues) => {
|
|
2160
|
+
return Object.freeze({
|
|
2161
|
+
valid: issues.length === 0,
|
|
2162
|
+
issues: Object.freeze([...issues])
|
|
2163
|
+
});
|
|
2164
|
+
};
|
|
2165
|
+
const assertValidConfig = (report) => {
|
|
2166
|
+
if (!report.valid) throw new DeepRedactValidationError(report.issues);
|
|
2167
|
+
};
|
|
2168
|
+
//#endregion
|
|
2169
|
+
//#region src/core/validation/validate-paths.ts
|
|
2170
|
+
const pushIssue$1 = (issues, path, message) => {
|
|
2171
|
+
issues.push({
|
|
2172
|
+
path,
|
|
2173
|
+
message
|
|
2174
|
+
});
|
|
2175
|
+
};
|
|
2176
|
+
const validateRegexPathSegments = (segments, path, issues) => {
|
|
2177
|
+
let valid = true;
|
|
2178
|
+
for (const segment of segments) {
|
|
2179
|
+
if (segment.kind !== "regex" && segment.kind !== "ignore-regex") continue;
|
|
2180
|
+
const unsupportedRegexMessage = getUnsupportedRegexMessage(segment.matcher, "Regex path segment");
|
|
2181
|
+
if (unsupportedRegexMessage !== void 0) {
|
|
2182
|
+
pushIssue$1(issues, path, unsupportedRegexMessage);
|
|
2183
|
+
valid = false;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
return valid;
|
|
2187
|
+
};
|
|
2188
|
+
const validatePathSelectors = (selectorCandidates, issues) => {
|
|
2189
|
+
const seenCanonicalPaths = /* @__PURE__ */ new Map();
|
|
2190
|
+
const seenDynamicSelectors = /* @__PURE__ */ new Map();
|
|
2191
|
+
for (const selectorCandidate of selectorCandidates) try {
|
|
2192
|
+
const parsedPath = parsePathSelector(selectorCandidate.selector);
|
|
2193
|
+
if (!validateRegexPathSegments(parsedPath.segments, selectorCandidate.configPath, issues)) continue;
|
|
2194
|
+
if (parsedPath.segments.some((segment) => isDynamicPathSegment(segment))) {
|
|
2195
|
+
const signature = renderSelectorSignature(parsedPath.segments);
|
|
2196
|
+
const previousDefinitionPath = seenDynamicSelectors.get(signature);
|
|
2197
|
+
if (previousDefinitionPath !== void 0) {
|
|
2198
|
+
pushIssue$1(issues, selectorCandidate.configPath, `Duplicate dynamic selector "${signature}" already defined at ${previousDefinitionPath}.`);
|
|
2199
|
+
continue;
|
|
2200
|
+
}
|
|
2201
|
+
seenDynamicSelectors.set(signature, selectorCandidate.configPath);
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
const normalisedPath = normaliseParsedPath(parsedPath);
|
|
2205
|
+
const previousDefinitionPath = seenCanonicalPaths.get(normalisedPath.canonicalPath);
|
|
2206
|
+
if (previousDefinitionPath !== void 0) {
|
|
2207
|
+
pushIssue$1(issues, selectorCandidate.configPath, `Duplicate canonical selector "${normalisedPath.canonicalPath}" already defined at ${previousDefinitionPath}.`);
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
seenCanonicalPaths.set(normalisedPath.canonicalPath, selectorCandidate.configPath);
|
|
2211
|
+
} catch (error) {
|
|
2212
|
+
pushIssue$1(issues, selectorCandidate.configPath, error instanceof Error ? error.message : "Invalid path selector.");
|
|
2213
|
+
}
|
|
2214
|
+
};
|
|
2215
|
+
//#endregion
|
|
2216
|
+
//#region src/core/validation/validate-config.ts
|
|
2217
|
+
const rootOptionNames = new Set([
|
|
2218
|
+
"caseSensitiveKeyMatch",
|
|
2219
|
+
"censor",
|
|
2220
|
+
"diagnostics",
|
|
2221
|
+
"fuzzyKeyMatch",
|
|
2222
|
+
"keys",
|
|
2223
|
+
"maxDepth",
|
|
2224
|
+
"maxNodes",
|
|
2225
|
+
"paths",
|
|
2226
|
+
"remove",
|
|
2227
|
+
"replaceStringByLength",
|
|
2228
|
+
"retainStructure",
|
|
2229
|
+
"ignoredValueTypes",
|
|
2230
|
+
"serialise",
|
|
2231
|
+
"stringTests",
|
|
2232
|
+
"transformers",
|
|
2233
|
+
"types"
|
|
2234
|
+
]);
|
|
2235
|
+
const valueTypeNames = new Set([
|
|
2236
|
+
"string",
|
|
2237
|
+
"number",
|
|
2238
|
+
"bigint",
|
|
2239
|
+
"boolean",
|
|
2240
|
+
"object",
|
|
2241
|
+
"function",
|
|
2242
|
+
"symbol",
|
|
2243
|
+
"undefined"
|
|
2244
|
+
]);
|
|
2245
|
+
const pathRuleOptionNames = new Set([
|
|
2246
|
+
"path",
|
|
2247
|
+
"censor",
|
|
2248
|
+
"remove",
|
|
2249
|
+
"replaceStringByLength",
|
|
2250
|
+
"retainStructure"
|
|
2251
|
+
]);
|
|
2252
|
+
const substringRuleOptionNames = new Set(["pattern", "replacer"]);
|
|
2253
|
+
const keyRuleOptionNames = new Set([
|
|
2254
|
+
"caseSensitiveKeyMatch",
|
|
2255
|
+
"censor",
|
|
2256
|
+
"fuzzyKeyMatch",
|
|
2257
|
+
"key",
|
|
2258
|
+
"remove",
|
|
2259
|
+
"replaceStringByLength",
|
|
2260
|
+
"retainStructure"
|
|
2261
|
+
]);
|
|
2262
|
+
const transformerOptionNames = new Set([
|
|
2263
|
+
"byType",
|
|
2264
|
+
"byConstructor",
|
|
2265
|
+
"fallback"
|
|
2266
|
+
]);
|
|
2267
|
+
const transformerByTypeOptionNames = new Set(["bigint", "object"]);
|
|
2268
|
+
const transformerByConstructorOptionNames = new Set([
|
|
2269
|
+
"Date",
|
|
2270
|
+
"Error",
|
|
2271
|
+
"Map",
|
|
2272
|
+
"RegExp",
|
|
2273
|
+
"Set",
|
|
2274
|
+
"URL",
|
|
2275
|
+
"custom"
|
|
2276
|
+
]);
|
|
2277
|
+
const ignoredValueTypeOptionNames = new Set([
|
|
2278
|
+
"bigint",
|
|
2279
|
+
"Date",
|
|
2280
|
+
"Error",
|
|
2281
|
+
"Map",
|
|
2282
|
+
"RegExp",
|
|
2283
|
+
"Set",
|
|
2284
|
+
"URL"
|
|
2285
|
+
]);
|
|
2286
|
+
const diagnosticsOptionNames = new Set(["sink"]);
|
|
2287
|
+
const customConstructorRegistrationOptionNames = new Set(["constructor", "transformers"]);
|
|
2288
|
+
const disallowedCustomConstructors = new Set([
|
|
2289
|
+
Object,
|
|
2290
|
+
Array,
|
|
2291
|
+
Date,
|
|
2292
|
+
Error,
|
|
2293
|
+
Map,
|
|
2294
|
+
RegExp,
|
|
2295
|
+
Set,
|
|
2296
|
+
URL
|
|
2297
|
+
]);
|
|
2298
|
+
const isPlainObject = (value) => {
|
|
2299
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2300
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2301
|
+
return prototype === Object.prototype || prototype === null;
|
|
2302
|
+
};
|
|
2303
|
+
const isPathSelector = (value) => {
|
|
2304
|
+
return typeof value === "string" || Array.isArray(value);
|
|
2305
|
+
};
|
|
2306
|
+
const pushIssue = (issues, path, message) => {
|
|
2307
|
+
issues.push({
|
|
2308
|
+
path,
|
|
2309
|
+
message
|
|
2310
|
+
});
|
|
2311
|
+
};
|
|
2312
|
+
const validateAllowedOptions = (value, allowedOptions, path, issues) => {
|
|
2313
|
+
for (const optionName of Object.keys(value)) if (!allowedOptions.has(optionName)) pushIssue(issues, path, `Unsupported option "${optionName}".`);
|
|
2314
|
+
};
|
|
2315
|
+
const validateBooleanOption = (value, path, optionName, issues) => {
|
|
2316
|
+
if (value !== void 0 && typeof value !== "boolean") pushIssue(issues, `${path}.${optionName}`, `${optionName} must be a boolean.`);
|
|
2317
|
+
};
|
|
2318
|
+
const validatePositiveIntegerOption = (value, path, optionName, issues) => {
|
|
2319
|
+
if (value === void 0) return;
|
|
2320
|
+
if (!Number.isInteger(value) || value < 1) pushIssue(issues, `${path}.${optionName}`, `${optionName} must be a positive integer.`);
|
|
2321
|
+
};
|
|
2322
|
+
const validateCensorOption = (value, path, issues) => {
|
|
2323
|
+
if (value !== void 0 && typeof value !== "string" && typeof value !== "function") pushIssue(issues, `${path}.censor`, "censor must be a string or function.");
|
|
2324
|
+
};
|
|
2325
|
+
const validateSerialiseOption = (value, path, issues) => {
|
|
2326
|
+
if (value !== void 0 && typeof value !== "boolean" && typeof value !== "function") {
|
|
2327
|
+
pushIssue(issues, `${path}.serialise`, "serialise must be a boolean or function.");
|
|
2328
|
+
return false;
|
|
2329
|
+
}
|
|
2330
|
+
return true;
|
|
2331
|
+
};
|
|
2332
|
+
const validateConflictingOptions = (value, path, issues) => {
|
|
2333
|
+
if (value.remove === true && value.censor !== void 0) pushIssue(issues, path, "remove cannot be combined with censor.");
|
|
2334
|
+
if (value.remove === true && value.retainStructure === true) pushIssue(issues, path, "remove cannot be combined with retainStructure.");
|
|
2335
|
+
if (value.remove === true && value.replaceStringByLength === true) pushIssue(issues, path, "remove cannot be combined with replaceStringByLength.");
|
|
2336
|
+
if (value.replaceStringByLength === true && value.censor === "") pushIssue(issues, path, "replaceStringByLength cannot be combined with an empty string censor.");
|
|
2337
|
+
};
|
|
2338
|
+
const regexLikeKeySelectorPattern = /^\/.+\/[A-Za-z]*$/;
|
|
2339
|
+
const getUnsupportedKeySelectorMessage = (selector) => {
|
|
2340
|
+
if (selector.startsWith("!")) return `Unsupported exclusion key selector "${selector}". Ignore selectors must use structured selector objects and are not supported in this configuration format.`;
|
|
2341
|
+
if (selector.includes("**")) return `Unsupported recursive wildcard key selector "${selector}".`;
|
|
2342
|
+
if (selector.includes("*")) return `Unsupported wildcard key selector "${selector}".`;
|
|
2343
|
+
if (regexLikeKeySelectorPattern.test(selector)) return `Unsupported regex-like key selector "${selector}".`;
|
|
2344
|
+
};
|
|
2345
|
+
const getUnsupportedKeyRegexMessage = (selector) => {
|
|
2346
|
+
return getUnsupportedRegexMessage(selector, "Regex key selector");
|
|
2347
|
+
};
|
|
2348
|
+
const validateLiteralKeySelector = (entry, path, issues) => {
|
|
2349
|
+
if (entry.length === 0) {
|
|
2350
|
+
pushIssue(issues, path, "key selectors must not be empty.");
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
const unsupportedSelectorMessage = getUnsupportedKeySelectorMessage(entry);
|
|
2354
|
+
if (unsupportedSelectorMessage !== void 0) pushIssue(issues, path, unsupportedSelectorMessage);
|
|
2355
|
+
};
|
|
2356
|
+
const validateKeyRule = (value, path, defaults, issues) => {
|
|
2357
|
+
validateAllowedOptions(value, keyRuleOptionNames, path, issues);
|
|
2358
|
+
if (isRegExp(value.key)) {
|
|
2359
|
+
const unsupportedRegexMessage = getUnsupportedKeyRegexMessage(value.key);
|
|
2360
|
+
if (unsupportedRegexMessage !== void 0) pushIssue(issues, `${path}.key`, unsupportedRegexMessage);
|
|
2361
|
+
} else if (typeof value.key !== "string") pushIssue(issues, `${path}.key`, "key must be a string or RegExp.");
|
|
2362
|
+
else if (value.key.length === 0) pushIssue(issues, `${path}.key`, "key must not be empty.");
|
|
2363
|
+
else {
|
|
2364
|
+
const unsupportedSelectorMessage = getUnsupportedKeySelectorMessage(value.key);
|
|
2365
|
+
if (unsupportedSelectorMessage !== void 0) pushIssue(issues, `${path}.key`, unsupportedSelectorMessage);
|
|
2366
|
+
}
|
|
2367
|
+
validateBooleanOption(value.fuzzyKeyMatch, path, "fuzzyKeyMatch", issues);
|
|
2368
|
+
validateBooleanOption(value.caseSensitiveKeyMatch, path, "caseSensitiveKeyMatch", issues);
|
|
2369
|
+
validateCensorOption(value.censor, path, issues);
|
|
2370
|
+
validateBooleanOption(value.remove, path, "remove", issues);
|
|
2371
|
+
validateBooleanOption(value.retainStructure, path, "retainStructure", issues);
|
|
2372
|
+
validateBooleanOption(value.replaceStringByLength, path, "replaceStringByLength", issues);
|
|
2373
|
+
const effectiveReplaceStringByLength = typeof value.replaceStringByLength === "boolean" ? value.replaceStringByLength : defaults.replaceStringByLength;
|
|
2374
|
+
validateConflictingOptions({
|
|
2375
|
+
censor: value.censor ?? defaults.censor,
|
|
2376
|
+
remove: value.remove ?? defaults.remove,
|
|
2377
|
+
retainStructure: value.retainStructure ?? defaults.retainStructure,
|
|
2378
|
+
replaceStringByLength: effectiveReplaceStringByLength
|
|
2379
|
+
}, path, issues);
|
|
2380
|
+
};
|
|
2381
|
+
const validateKeys = (value, path, defaults, issues) => {
|
|
2382
|
+
if (value === void 0) return;
|
|
2383
|
+
if (!Array.isArray(value)) {
|
|
2384
|
+
pushIssue(issues, path, "keys must be an array.");
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
for (const [index, entry] of value.entries()) {
|
|
2388
|
+
const entryPath = `${path}[${index}]`;
|
|
2389
|
+
if (isRegExp(entry)) {
|
|
2390
|
+
const unsupportedRegexMessage = getUnsupportedKeyRegexMessage(entry);
|
|
2391
|
+
if (unsupportedRegexMessage !== void 0) pushIssue(issues, entryPath, unsupportedRegexMessage);
|
|
2392
|
+
continue;
|
|
2393
|
+
}
|
|
2394
|
+
if (typeof entry === "string") {
|
|
2395
|
+
validateLiteralKeySelector(entry, entryPath, issues);
|
|
2396
|
+
continue;
|
|
2397
|
+
}
|
|
2398
|
+
if (isPlainObject(entry)) {
|
|
2399
|
+
validateKeyRule(entry, entryPath, defaults, issues);
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
pushIssue(issues, entryPath, "key selectors must be strings or RegExp instances or key-rule objects.");
|
|
2403
|
+
}
|
|
2404
|
+
};
|
|
2405
|
+
const zeroLengthProbeValues = Object.freeze([
|
|
2406
|
+
"",
|
|
2407
|
+
"a",
|
|
2408
|
+
"safe",
|
|
2409
|
+
"secret",
|
|
2410
|
+
"token=secret",
|
|
2411
|
+
"api-key=secret",
|
|
2412
|
+
"prefix-secret-suffix"
|
|
2413
|
+
]);
|
|
2414
|
+
const patternCanMatchZeroLength = (pattern) => {
|
|
2415
|
+
const matcher = new RegExp(pattern.source, pattern.flags);
|
|
2416
|
+
return zeroLengthProbeValues.some((probe) => {
|
|
2417
|
+
matcher.lastIndex = 0;
|
|
2418
|
+
const match = matcher.exec(probe);
|
|
2419
|
+
matcher.lastIndex = 0;
|
|
2420
|
+
return match?.[0] === "";
|
|
2421
|
+
});
|
|
2422
|
+
};
|
|
2423
|
+
const validateSubstringPattern = (pattern, path, issues) => {
|
|
2424
|
+
const unsupportedRegexMessage = getUnsupportedRegexMessage(pattern, "Substring rule pattern", { allowGlobal: true });
|
|
2425
|
+
if (unsupportedRegexMessage !== void 0) pushIssue(issues, path, unsupportedRegexMessage);
|
|
2426
|
+
if (patternCanMatchZeroLength(pattern)) pushIssue(issues, path, "Substring rule pattern must not match zero-length strings.");
|
|
2427
|
+
};
|
|
2428
|
+
const validateStringTests = (value, path, issues) => {
|
|
2429
|
+
if (value === void 0) return;
|
|
2430
|
+
if (!Array.isArray(value)) {
|
|
2431
|
+
pushIssue(issues, path, "stringTests must be an array.");
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
for (const [index, entry] of value.entries()) {
|
|
2435
|
+
const entryPath = `${path}[${index}]`;
|
|
2436
|
+
if (isRegExp(entry)) {
|
|
2437
|
+
validateSubstringPattern(entry, entryPath, issues);
|
|
2438
|
+
continue;
|
|
2439
|
+
}
|
|
2440
|
+
if (!isPlainObject(entry)) {
|
|
2441
|
+
pushIssue(issues, entryPath, "string test entries must be RegExp instances or substring rule objects.");
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
validateAllowedOptions(entry, substringRuleOptionNames, entryPath, issues);
|
|
2445
|
+
if (isRegExp(entry.pattern)) validateSubstringPattern(entry.pattern, `${entryPath}.pattern`, issues);
|
|
2446
|
+
else pushIssue(issues, `${entryPath}.pattern`, "pattern must be a RegExp instance.");
|
|
2447
|
+
if (typeof entry.replacer !== "function") pushIssue(issues, `${entryPath}.replacer`, "replacer must be a function.");
|
|
2448
|
+
}
|
|
2449
|
+
};
|
|
2450
|
+
const validateTransformerEntries = (value, path, issues) => {
|
|
2451
|
+
if (value === void 0) return;
|
|
2452
|
+
if (!Array.isArray(value)) {
|
|
2453
|
+
pushIssue(issues, path, `${path.split(".").at(-1) ?? "transformers"} must be an array.`);
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
for (const [index, entry] of value.entries()) if (typeof entry !== "function") pushIssue(issues, `${path}[${index}]`, "Transformer entries must be functions.");
|
|
2457
|
+
};
|
|
2458
|
+
const isConstructable = (value) => {
|
|
2459
|
+
if (typeof value !== "function") return false;
|
|
2460
|
+
try {
|
|
2461
|
+
Reflect.construct(Object, [], value);
|
|
2462
|
+
return true;
|
|
2463
|
+
} catch {
|
|
2464
|
+
return false;
|
|
2465
|
+
}
|
|
2466
|
+
};
|
|
2467
|
+
const isArraySubclassConstructor = (value) => {
|
|
2468
|
+
const prototype = value.prototype;
|
|
2469
|
+
return typeof prototype === "object" && prototype !== null && Object.prototype.isPrototypeOf.call(Array.prototype, prototype);
|
|
2470
|
+
};
|
|
2471
|
+
const validateCustomConstructorRegistrations = (value, path, issues) => {
|
|
2472
|
+
if (value === void 0) return;
|
|
2473
|
+
if (!Array.isArray(value)) {
|
|
2474
|
+
pushIssue(issues, path, "custom must be an array.");
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
const seenConstructors = /* @__PURE__ */ new Set();
|
|
2478
|
+
for (const [index, registration] of value.entries()) {
|
|
2479
|
+
const registrationPath = `${path}[${index}]`;
|
|
2480
|
+
if (!isPlainObject(registration)) {
|
|
2481
|
+
pushIssue(issues, registrationPath, "Custom constructor registrations must be objects.");
|
|
2482
|
+
continue;
|
|
2483
|
+
}
|
|
2484
|
+
validateAllowedOptions(registration, customConstructorRegistrationOptionNames, registrationPath, issues);
|
|
2485
|
+
const constructor = Object.hasOwn(registration, "constructor") ? registration.constructor : void 0;
|
|
2486
|
+
if (!isConstructable(constructor)) pushIssue(issues, `${registrationPath}.constructor`, "Custom constructor must be constructable.");
|
|
2487
|
+
else if (disallowedCustomConstructors.has(constructor)) pushIssue(issues, `${registrationPath}.constructor`, "Custom constructor must not be Object, Array, or a built-in constructor.");
|
|
2488
|
+
else if (constructor === Function || isArraySubclassConstructor(constructor)) pushIssue(issues, `${registrationPath}.constructor`, "Custom constructor must not be Object, Array, or a built-in constructor; Function and Array subclasses are also unsupported.");
|
|
2489
|
+
else if (seenConstructors.has(constructor)) pushIssue(issues, `${registrationPath}.constructor`, "Custom constructor registrations must not repeat the same constructor.");
|
|
2490
|
+
else seenConstructors.add(constructor);
|
|
2491
|
+
if (Array.isArray(registration.transformers)) validateTransformerEntries(registration.transformers, `${registrationPath}.transformers`, issues);
|
|
2492
|
+
else pushIssue(issues, `${registrationPath}.transformers`, "transformers must be an array.");
|
|
2493
|
+
}
|
|
2494
|
+
};
|
|
2495
|
+
const validateTransformerBuckets = (value, path, allowedOptions, issues) => {
|
|
2496
|
+
if (value === void 0) return;
|
|
2497
|
+
if (!isPlainObject(value)) {
|
|
2498
|
+
pushIssue(issues, path, `${path.split(".").at(-1) ?? "bucket"} must be an object.`);
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
validateAllowedOptions(value, allowedOptions, path, issues);
|
|
2502
|
+
if (allowedOptions.has("custom")) validateCustomConstructorRegistrations(value.custom, `${path}.custom`, issues);
|
|
2503
|
+
for (const [bucketName, entries] of Object.entries(value)) {
|
|
2504
|
+
if (bucketName === "custom") continue;
|
|
2505
|
+
if (!allowedOptions.has(bucketName)) continue;
|
|
2506
|
+
validateTransformerEntries(entries, `${path}.${bucketName}`, issues);
|
|
2507
|
+
}
|
|
2508
|
+
};
|
|
2509
|
+
const validateTransformers = (value, path, issues) => {
|
|
2510
|
+
if (value === void 0) return;
|
|
2511
|
+
if (!isPlainObject(value)) {
|
|
2512
|
+
pushIssue(issues, path, "transformers must be an object.");
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
validateAllowedOptions(value, transformerOptionNames, path, issues);
|
|
2516
|
+
validateTransformerBuckets(value.byType, `${path}.byType`, transformerByTypeOptionNames, issues);
|
|
2517
|
+
validateTransformerBuckets(value.byConstructor, `${path}.byConstructor`, transformerByConstructorOptionNames, issues);
|
|
2518
|
+
validateTransformerEntries(value.fallback, `${path}.fallback`, issues);
|
|
2519
|
+
};
|
|
2520
|
+
const validateIgnoredValueTypes = (value, path, issues) => {
|
|
2521
|
+
if (value === void 0) return;
|
|
2522
|
+
if (!isPlainObject(value)) {
|
|
2523
|
+
pushIssue(issues, path, "ignoredValueTypes must be an object.");
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
validateAllowedOptions(value, ignoredValueTypeOptionNames, path, issues);
|
|
2527
|
+
for (const optionName of Object.keys(value)) {
|
|
2528
|
+
if (!ignoredValueTypeOptionNames.has(optionName)) continue;
|
|
2529
|
+
validateBooleanOption(value[optionName], path, optionName, issues);
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2532
|
+
const validateValueTypes = (value, path, issues) => {
|
|
2533
|
+
if (value === void 0) return;
|
|
2534
|
+
if (!Array.isArray(value)) {
|
|
2535
|
+
pushIssue(issues, path, "types must be an array of value-type names.");
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
for (const [index, entry] of value.entries()) if (typeof entry !== "string" || !valueTypeNames.has(entry)) pushIssue(issues, `${path}[${index}]`, `Unsupported value type name "${String(entry)}". Supported names: ${[...valueTypeNames].join(", ")}.`);
|
|
2539
|
+
};
|
|
2540
|
+
const validateDiagnostics = (value, path, issues) => {
|
|
2541
|
+
if (value === void 0) return;
|
|
2542
|
+
if (!isPlainObject(value)) {
|
|
2543
|
+
pushIssue(issues, path, "diagnostics must be an object.");
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
validateAllowedOptions(value, diagnosticsOptionNames, path, issues);
|
|
2547
|
+
if (value.sink !== void 0 && typeof value.sink !== "function") pushIssue(issues, `${path}.sink`, "sink must be a function.");
|
|
2548
|
+
};
|
|
2549
|
+
const validatePathRule = (value, path, defaults, issues, selectorCandidates) => {
|
|
2550
|
+
if (!isPlainObject(value)) {
|
|
2551
|
+
pushIssue(issues, path, `${path.split(".").at(-1) ?? "entry"} must be a string selector or path-rule object.`);
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
validateAllowedOptions(value, pathRuleOptionNames, path, issues);
|
|
2555
|
+
if (isPathSelector(value.path)) selectorCandidates.push({
|
|
2556
|
+
configPath: `${path}.path`,
|
|
2557
|
+
selector: value.path
|
|
2558
|
+
});
|
|
2559
|
+
else pushIssue(issues, `${path}.path`, "path must be a string or structured selector array.");
|
|
2560
|
+
validateCensorOption(value.censor, path, issues);
|
|
2561
|
+
validateBooleanOption(value.remove, path, "remove", issues);
|
|
2562
|
+
validateBooleanOption(value.retainStructure, path, "retainStructure", issues);
|
|
2563
|
+
validateBooleanOption(value.replaceStringByLength, path, "replaceStringByLength", issues);
|
|
2564
|
+
const effectiveReplaceStringByLength = typeof value.replaceStringByLength === "boolean" ? value.replaceStringByLength : defaults.replaceStringByLength;
|
|
2565
|
+
validateConflictingOptions({
|
|
2566
|
+
censor: value.censor ?? defaults.censor,
|
|
2567
|
+
remove: value.remove ?? defaults.remove,
|
|
2568
|
+
retainStructure: value.retainStructure ?? defaults.retainStructure,
|
|
2569
|
+
replaceStringByLength: effectiveReplaceStringByLength
|
|
2570
|
+
}, path, issues);
|
|
2571
|
+
};
|
|
2572
|
+
const validatePaths = (value, path, defaults, issues, selectorCandidates) => {
|
|
2573
|
+
if (value === void 0) return;
|
|
2574
|
+
if (!Array.isArray(value)) {
|
|
2575
|
+
pushIssue(issues, path, "paths must be an array.");
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
for (const [index, entry] of value.entries()) {
|
|
2579
|
+
const entryPath = `${path}[${index}]`;
|
|
2580
|
+
if (isPathSelector(entry)) {
|
|
2581
|
+
selectorCandidates.push({
|
|
2582
|
+
configPath: entryPath,
|
|
2583
|
+
selector: entry
|
|
2584
|
+
});
|
|
2585
|
+
continue;
|
|
2586
|
+
}
|
|
2587
|
+
if (!isPlainObject(entry)) {
|
|
2588
|
+
pushIssue(issues, entryPath, `${entryPath.split(".").at(-1) ?? "entry"} must be a string selector or path-rule object.`);
|
|
2589
|
+
continue;
|
|
2590
|
+
}
|
|
2591
|
+
validatePathRule(entry, entryPath, defaults, issues, selectorCandidates);
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
const validateConfig = (options) => {
|
|
2595
|
+
const issues = [];
|
|
2596
|
+
const selectorCandidates = [];
|
|
2597
|
+
if (options === void 0) return createValidationReport(issues);
|
|
2598
|
+
if (!isPlainObject(options)) {
|
|
2599
|
+
pushIssue(issues, "options", "options must be an object.");
|
|
2600
|
+
return createValidationReport(issues);
|
|
2601
|
+
}
|
|
2602
|
+
const rootDefaults = {
|
|
2603
|
+
censor: options.censor,
|
|
2604
|
+
remove: options.remove === true,
|
|
2605
|
+
retainStructure: options.retainStructure === true,
|
|
2606
|
+
replaceStringByLength: options.replaceStringByLength === true
|
|
2607
|
+
};
|
|
2608
|
+
validateAllowedOptions(options, rootOptionNames, "options", issues);
|
|
2609
|
+
validateBooleanOption(options.caseSensitiveKeyMatch, "options", "caseSensitiveKeyMatch", issues);
|
|
2610
|
+
validateCensorOption(options.censor, "options", issues);
|
|
2611
|
+
validateDiagnostics(options.diagnostics, "options.diagnostics", issues);
|
|
2612
|
+
validateBooleanOption(options.fuzzyKeyMatch, "options", "fuzzyKeyMatch", issues);
|
|
2613
|
+
validateIgnoredValueTypes(options.ignoredValueTypes, "options.ignoredValueTypes", issues);
|
|
2614
|
+
validateValueTypes(options.types, "options.types", issues);
|
|
2615
|
+
validateKeys(options.keys, "options.keys", rootDefaults, issues);
|
|
2616
|
+
validateStringTests(options.stringTests, "options.stringTests", issues);
|
|
2617
|
+
validateTransformers(options.transformers, "options.transformers", issues);
|
|
2618
|
+
validatePositiveIntegerOption(options.maxDepth, "options", "maxDepth", issues);
|
|
2619
|
+
validatePositiveIntegerOption(options.maxNodes, "options", "maxNodes", issues);
|
|
2620
|
+
validateBooleanOption(options.remove, "options", "remove", issues);
|
|
2621
|
+
validateBooleanOption(options.retainStructure, "options", "retainStructure", issues);
|
|
2622
|
+
validateBooleanOption(options.replaceStringByLength, "options", "replaceStringByLength", issues);
|
|
2623
|
+
validateSerialiseOption(options.serialise, "options", issues);
|
|
2624
|
+
validateConflictingOptions(rootDefaults, "options", issues);
|
|
2625
|
+
validatePaths(options.paths, "options.paths", rootDefaults, issues, selectorCandidates);
|
|
2626
|
+
validatePathSelectors(selectorCandidates, issues);
|
|
2627
|
+
return createValidationReport(issues);
|
|
2628
|
+
};
|
|
2629
|
+
//#endregion
|
|
2630
|
+
//#region src/core/replacement/serialise-output.ts
|
|
2631
|
+
const bareIdentifierPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
2632
|
+
const buildObjectChildPath = (parentPath, key) => {
|
|
2633
|
+
if (!bareIdentifierPattern.test(key)) return `${parentPath ?? ""}["${key.replaceAll("\\", "\\\\").replaceAll("\"", String.raw`\"`)}"]`;
|
|
2634
|
+
return parentPath === void 0 ? key : `${parentPath}.${key}`;
|
|
2635
|
+
};
|
|
2636
|
+
const buildArrayChildPath = (parentPath, index) => {
|
|
2637
|
+
return parentPath === void 0 ? String(index) : `${parentPath}.${index}`;
|
|
2638
|
+
};
|
|
2639
|
+
const isStrictDescendantPath = (ancestor, path) => {
|
|
2640
|
+
if (path === ancestor) return false;
|
|
2641
|
+
if (ancestor === "") return true;
|
|
2642
|
+
return path.startsWith(`${ancestor}.`) || path.startsWith(`${ancestor}[`);
|
|
2643
|
+
};
|
|
2644
|
+
const buildSafeGraph = (value, transformers, seen, identityPaths, currentPath, cycleRegistry) => {
|
|
2645
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === void 0) return value;
|
|
2646
|
+
if (typeof value === "function" || typeof value === "symbol") return "[UNSUPPORTED]";
|
|
2647
|
+
const supportedKind = resolveSupportedTransformableValueKind(value);
|
|
2648
|
+
if (supportedKind !== void 0) {
|
|
2649
|
+
const runtimeIdentity = supportedKind === "bigint" ? void 0 : value;
|
|
2650
|
+
if (runtimeIdentity !== void 0) {
|
|
2651
|
+
if (seen.has(runtimeIdentity)) return {
|
|
2652
|
+
_transformer: "circular",
|
|
2653
|
+
path: currentPath ?? "",
|
|
2654
|
+
value: identityPaths.get(runtimeIdentity) ?? ""
|
|
2655
|
+
};
|
|
2656
|
+
const currentPathStr = currentPath ?? "";
|
|
2657
|
+
identityPaths.set(runtimeIdentity, currentPathStr);
|
|
2658
|
+
seen.add(runtimeIdentity);
|
|
2659
|
+
}
|
|
2660
|
+
try {
|
|
2661
|
+
const transformed = resolveTransformedValue(value, transformers);
|
|
2662
|
+
if (transformed === void 0) return "[UNSUPPORTED]";
|
|
2663
|
+
return buildSafeGraph(transformed, transformers, seen, identityPaths, currentPath, cycleRegistry);
|
|
2664
|
+
} catch {
|
|
2665
|
+
return "[UNSUPPORTED]";
|
|
2666
|
+
} finally {
|
|
2667
|
+
if (runtimeIdentity !== void 0) seen.delete(runtimeIdentity);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
const identity = value;
|
|
2671
|
+
if (seen.has(identity)) return {
|
|
2672
|
+
_transformer: "circular",
|
|
2673
|
+
path: currentPath ?? "",
|
|
2674
|
+
value: identityPaths.get(identity) ?? ""
|
|
2675
|
+
};
|
|
2676
|
+
if (cycleRegistry?.has(identity)) {
|
|
2677
|
+
const registryPath = cycleRegistry.get(identity);
|
|
2678
|
+
if (isStrictDescendantPath(registryPath, currentPath ?? "")) return {
|
|
2679
|
+
_transformer: "circular",
|
|
2680
|
+
path: currentPath ?? "",
|
|
2681
|
+
value: registryPath
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
const currentPathStr = currentPath ?? "";
|
|
2685
|
+
identityPaths.set(identity, currentPathStr);
|
|
2686
|
+
seen.add(identity);
|
|
2687
|
+
try {
|
|
2688
|
+
if (Array.isArray(value)) {
|
|
2689
|
+
const result = [];
|
|
2690
|
+
result.length = value.length;
|
|
2691
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
2692
|
+
if (!(index in value)) continue;
|
|
2693
|
+
result[index] = buildSafeGraph(value[index], transformers, seen, identityPaths, buildArrayChildPath(currentPath, index), cycleRegistry);
|
|
2694
|
+
}
|
|
2695
|
+
return result;
|
|
2696
|
+
}
|
|
2697
|
+
if (isPlainObject$1(value)) {
|
|
2698
|
+
const result = {};
|
|
2699
|
+
for (const key of Object.keys(value)) result[key] = buildSafeGraph(value[key], transformers, seen, identityPaths, buildObjectChildPath(currentPath, key), cycleRegistry);
|
|
2700
|
+
return result;
|
|
2701
|
+
}
|
|
2702
|
+
try {
|
|
2703
|
+
const transformed = resolveTransformedValue(value, transformers);
|
|
2704
|
+
if (transformed !== void 0) return buildSafeGraph(transformed, transformers, seen, identityPaths, currentPath, cycleRegistry);
|
|
2705
|
+
} catch {
|
|
2706
|
+
return "[UNSUPPORTED]";
|
|
2707
|
+
}
|
|
2708
|
+
return "[UNSUPPORTED]";
|
|
2709
|
+
} finally {
|
|
2710
|
+
seen.delete(identity);
|
|
2711
|
+
}
|
|
2712
|
+
};
|
|
2713
|
+
const serialiseOutput = (value, transformers, serialise, cycleRegistry) => {
|
|
2714
|
+
if (!serialise) return value;
|
|
2715
|
+
const safeGraph = value === void 0 ? "[UNSUPPORTED]" : buildSafeGraph(value, transformers, /* @__PURE__ */ new WeakSet(), /* @__PURE__ */ new WeakMap(), void 0, cycleRegistry);
|
|
2716
|
+
if (serialise === true) return JSON.stringify(safeGraph);
|
|
2717
|
+
return serialise(safeGraph);
|
|
2718
|
+
};
|
|
2719
|
+
//#endregion
|
|
2720
|
+
//#region src/core/create-redactor.ts
|
|
2721
|
+
const createCallableRedactor = (plan) => {
|
|
2722
|
+
const generalTraversal = (value, cycleRegistry) => redactValue(value, plan, cycleRegistry);
|
|
2723
|
+
const executor = plan.pathDrivenOnly ? buildPathDrivenExecutor(plan, (v) => generalTraversal(v)) : (v) => generalTraversal(v);
|
|
2724
|
+
return function redact(value) {
|
|
2725
|
+
if (plan.serialise) {
|
|
2726
|
+
const cycleRegistry = /* @__PURE__ */ new WeakMap();
|
|
2727
|
+
return serialiseOutput(generalTraversal(value, cycleRegistry), plan.transformers, plan.serialise, cycleRegistry);
|
|
2728
|
+
}
|
|
2729
|
+
return executor(value);
|
|
2730
|
+
};
|
|
2731
|
+
};
|
|
2732
|
+
const createRedactor$1 = (options) => {
|
|
2733
|
+
assertValidConfig(validateConfig(options));
|
|
2734
|
+
return createCallableRedactor(compileRedactorPlan(options ?? {}));
|
|
2735
|
+
};
|
|
2736
|
+
//#endregion
|
|
2737
|
+
//#region src/index.ts
|
|
2738
|
+
const deepRedact = createRedactor$1;
|
|
2739
|
+
const createRedactor = deepRedact;
|
|
2740
|
+
//#endregion
|
|
2741
|
+
export { createRedactor, deepRedact };
|