n8n-nodes-redactor 5.0.0 → 5.1.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.
|
@@ -70,7 +70,11 @@ function isAllowListed(value, allowList) {
|
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
function generateToken(label, index) {
|
|
73
|
-
|
|
73
|
+
const safe = label.toUpperCase().replace(/[^A-Z0-9_]/g, '_');
|
|
74
|
+
return `[${/^[A-Z]/.test(safe) ? safe : `X${safe}`}_${index}]`;
|
|
75
|
+
}
|
|
76
|
+
function setOwn(obj, key, value) {
|
|
77
|
+
Object.defineProperty(obj, key, { value, enumerable: true, writable: true, configurable: true });
|
|
74
78
|
}
|
|
75
79
|
const TOKEN_RE = /^\[[A-Z][A-Z0-9_]*_\d+\]$/;
|
|
76
80
|
function maskValue(value, label) {
|
|
@@ -460,7 +464,7 @@ function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, displ
|
|
|
460
464
|
if (out === null || out === text)
|
|
461
465
|
return value;
|
|
462
466
|
if (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize') {
|
|
463
|
-
ctx.lastLocked =
|
|
467
|
+
ctx.lastLocked = numCtx.lastLocked;
|
|
464
468
|
return out;
|
|
465
469
|
}
|
|
466
470
|
const mapped = numericMapping(out, text, ctx, vault, sessionId);
|
|
@@ -472,7 +476,7 @@ function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, key
|
|
|
472
476
|
const out = redactText(key, ctx, vault, sessionId, keyHits, itemIndex, '');
|
|
473
477
|
if (out === key)
|
|
474
478
|
return key;
|
|
475
|
-
const clashes = (k) => k === '' || k
|
|
479
|
+
const clashes = (k) => k === '' || Object.prototype.hasOwnProperty.call(taken, k) || sourceKeys.has(k);
|
|
476
480
|
if (!clashes(out))
|
|
477
481
|
return out;
|
|
478
482
|
if (ctx.mode === 'token' || ctx.mode === 'pseudonymize') {
|
|
@@ -502,7 +506,7 @@ function redactObject(value, ctx, vault, sessionId, hits, itemIndex, path, displ
|
|
|
502
506
|
const childDisplay = display ? `${display}.${outKey}` : outKey;
|
|
503
507
|
for (const h of keyHits)
|
|
504
508
|
hits.push({ ...h, field: childDisplay });
|
|
505
|
-
obj
|
|
509
|
+
setOwn(obj, outKey, lockChild(ctx, obj, outKey, () => redactValue(v, ctx, vault, sessionId, hits, itemIndex, fieldPath, siblingKeys, childDisplay)));
|
|
506
510
|
}
|
|
507
511
|
(_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.set(obj, origins);
|
|
508
512
|
return obj;
|
|
@@ -544,35 +548,33 @@ function assertUserRegexInput(text, what) {
|
|
|
544
548
|
throw new Error(`${what} cannot scan a value of ${text.length} characters, the limit is ${USER_REGEX_MAX_INPUT}. ` +
|
|
545
549
|
'Exclude the field with field rules, or remove the regex.');
|
|
546
550
|
}
|
|
547
|
-
function
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
last = at + m[0].length;
|
|
561
|
-
}
|
|
562
|
-
if (last < seg.text.length)
|
|
563
|
-
into.push({ text: seg.text.slice(last), locked: false, src: seg.src + last });
|
|
551
|
+
function freeSegmentAt(segs, start, end) {
|
|
552
|
+
return segs.findIndex((seg) => !seg.locked && seg.src <= start && end <= seg.src + seg.text.length);
|
|
553
|
+
}
|
|
554
|
+
function lockRange(segs, index, start, end, replacement) {
|
|
555
|
+
const seg = segs[index];
|
|
556
|
+
const parts = [];
|
|
557
|
+
if (start > seg.src)
|
|
558
|
+
parts.push({ text: seg.text.slice(0, start - seg.src), locked: false, src: seg.src });
|
|
559
|
+
parts.push({ text: replacement, locked: true, src: -1 });
|
|
560
|
+
const tail = end - seg.src;
|
|
561
|
+
if (tail < seg.text.length)
|
|
562
|
+
parts.push({ text: seg.text.slice(tail), locked: false, src: end });
|
|
563
|
+
segs.splice(index, 1, ...parts);
|
|
564
564
|
}
|
|
565
565
|
function replaceFree(pass, re, fn) {
|
|
566
|
+
var _a;
|
|
566
567
|
const flags = re.flags.includes('g') ? re.flags : `${re.flags}g`;
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
568
|
+
for (const m of pass.source.matchAll(new RegExp(re.source, flags))) {
|
|
569
|
+
const start = (_a = m.index) !== null && _a !== void 0 ? _a : 0;
|
|
570
|
+
const end = start + m[0].length;
|
|
571
|
+
const index = m[0] === '' ? -1 : freeSegmentAt(pass.segs, start, end);
|
|
572
|
+
if (index < 0)
|
|
573
|
+
continue;
|
|
574
|
+
const replacement = fn(m[0], start);
|
|
575
|
+
if (replacement !== null)
|
|
576
|
+
lockRange(pass.segs, index, start, end, replacement);
|
|
574
577
|
}
|
|
575
|
-
pass.segs = next;
|
|
576
578
|
}
|
|
577
579
|
function redactMatch(pass, match, pattern, detail) {
|
|
578
580
|
const replacement = applyRedaction(match, pattern, pass.ctx, pass.vault, pass.sessionId);
|
|
@@ -621,19 +623,14 @@ function builtInStage(pass) {
|
|
|
621
623
|
replaceFree(pass, pattern.regex, (match, offset) => builtInMatch(pass, pattern, match, offset));
|
|
622
624
|
}
|
|
623
625
|
}
|
|
624
|
-
function
|
|
625
|
-
const names = new Set();
|
|
626
|
-
|
|
627
|
-
if (!seg.locked)
|
|
628
|
-
for (const nm of (0, names_1.detectNamesInText)(seg.text))
|
|
629
|
-
names.add(nm.name);
|
|
630
|
-
}
|
|
631
|
-
return names;
|
|
626
|
+
function sourceNames(pass) {
|
|
627
|
+
const names = new Set((0, names_1.detectNamesInText)(pass.source).map((nm) => nm.name));
|
|
628
|
+
return [...names].sort((x, y) => y.length - x.length);
|
|
632
629
|
}
|
|
633
630
|
function nameStage(pass) {
|
|
634
631
|
if (pass.ctx.skipSemantic)
|
|
635
632
|
return;
|
|
636
|
-
for (const name of
|
|
633
|
+
for (const name of sourceNames(pass)) {
|
|
637
634
|
if ((name.includes('[') && name.includes(']')) || isAllowListed(name, pass.ctx.allowList))
|
|
638
635
|
continue;
|
|
639
636
|
let replacement;
|
|
@@ -721,7 +718,7 @@ function restoreWith(value, restore, entries) {
|
|
|
721
718
|
if (value !== null && typeof value === 'object') {
|
|
722
719
|
const obj = {};
|
|
723
720
|
for (const [k, v] of Object.entries(value)) {
|
|
724
|
-
obj
|
|
721
|
+
setOwn(obj, restore(k), restoreWith(v, restore, entries));
|
|
725
722
|
}
|
|
726
723
|
return obj;
|
|
727
724
|
}
|
|
@@ -808,7 +805,7 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path,
|
|
|
808
805
|
const child = path ? `${path}.${k}` : k;
|
|
809
806
|
const childPolicy = policyPath ? `${policyPath}.${original}` : original;
|
|
810
807
|
const lock = (_f = (_e = ctx.lockedSpans) === null || _e === void 0 ? void 0 : _e.get(value)) === null || _f === void 0 ? void 0 : _f.get(k);
|
|
811
|
-
out
|
|
808
|
+
setOwn(out, k, await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy, lock));
|
|
812
809
|
}
|
|
813
810
|
return out;
|
|
814
811
|
}
|
|
@@ -50,6 +50,7 @@ const CLASS_ESCAPES = {
|
|
|
50
50
|
},
|
|
51
51
|
};
|
|
52
52
|
const HEX = /^[0-9a-fA-F]+$/;
|
|
53
|
+
const CONTROL = { n: 10, r: 13, t: 9, v: 11, f: 12 };
|
|
53
54
|
function readEscape(src, i) {
|
|
54
55
|
var _a, _b;
|
|
55
56
|
const e = (_a = src[i + 1]) !== null && _a !== void 0 ? _a : '';
|
|
@@ -62,10 +63,18 @@ function readEscape(src, i) {
|
|
|
62
63
|
return { length: 3, code: src.charCodeAt(i + 2) % 32 };
|
|
63
64
|
if (e === '0')
|
|
64
65
|
return { length: 2, code: 0 };
|
|
66
|
+
const control = CONTROL[e];
|
|
67
|
+
if (control !== undefined)
|
|
68
|
+
return { length: 2, code: control };
|
|
65
69
|
if (/[a-zA-Z0-9]/.test(e))
|
|
66
70
|
return { length: 2, code: null };
|
|
67
71
|
return { length: 2, code: e.charCodeAt(0) };
|
|
68
72
|
}
|
|
73
|
+
function codeSet(code) {
|
|
74
|
+
const set = (0, exports.emptySet)();
|
|
75
|
+
addCode(set, code);
|
|
76
|
+
return set;
|
|
77
|
+
}
|
|
69
78
|
function escapeSet(src, i) {
|
|
70
79
|
var _a;
|
|
71
80
|
const { code } = readEscape(src, i);
|
|
@@ -82,6 +91,8 @@ function escapeSet(src, i) {
|
|
|
82
91
|
return letter === letter.toLowerCase() ? set : complement(set);
|
|
83
92
|
}
|
|
84
93
|
function classMember(body, j) {
|
|
94
|
+
if (body[j] === '\\' && body[j + 1] === 'b')
|
|
95
|
+
return { set: codeSet(8), next: j + 2, code: 8 };
|
|
85
96
|
if (body[j] === '\\') {
|
|
86
97
|
const { length, code } = readEscape(body, j);
|
|
87
98
|
return { set: escapeSet(body, j), next: j + length, code };
|
|
@@ -157,7 +157,45 @@ function structuralProblem(src) {
|
|
|
157
157
|
}
|
|
158
158
|
return null;
|
|
159
159
|
}
|
|
160
|
+
const KNOWN_ESCAPES = /^(?:[dDwWsSbBnrtvf]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|c[A-Za-z]|0(?![0-9])|[^A-Za-z0-9])/;
|
|
161
|
+
const CLASS_ESCAPE_RANGE = /\\[dDwWsS]-[^\]]|[^[^\\]-\\[dDwWsS]/;
|
|
162
|
+
const BAD_ESCAPE = 'uses an escape that is not supported (octal, back-reference or unknown letter)';
|
|
163
|
+
const EMPTY_CLASS = 'uses an empty class [] or [^], which is not supported';
|
|
164
|
+
const ESCAPE_RANGE = 'uses a range with \\d, \\w or \\s as an end, which is not supported';
|
|
165
|
+
function classProblem(src, i) {
|
|
166
|
+
if (/^\[\^?\]/.test(src.slice(i)))
|
|
167
|
+
return { end: i, problem: EMPTY_CLASS };
|
|
168
|
+
const end = skipClass(src, i);
|
|
169
|
+
const body = src.slice(i, end);
|
|
170
|
+
for (let j = 1; j < body.length; j++) {
|
|
171
|
+
if (body[j] === '\\' && !KNOWN_ESCAPES.test(body.slice(j + 1)))
|
|
172
|
+
return { end, problem: BAD_ESCAPE };
|
|
173
|
+
if (body[j] === '\\')
|
|
174
|
+
j++;
|
|
175
|
+
}
|
|
176
|
+
return { end, problem: CLASS_ESCAPE_RANGE.test(body) ? ESCAPE_RANGE : null };
|
|
177
|
+
}
|
|
178
|
+
function unsupportedSyntax(src) {
|
|
179
|
+
let i = 0;
|
|
180
|
+
while (i < src.length) {
|
|
181
|
+
if (src[i] === '[') {
|
|
182
|
+
const { end, problem } = classProblem(src, i);
|
|
183
|
+
if (problem)
|
|
184
|
+
return problem;
|
|
185
|
+
i = end;
|
|
186
|
+
}
|
|
187
|
+
else if (src[i] === '\\') {
|
|
188
|
+
if (!KNOWN_ESCAPES.test(src.slice(i + 1)))
|
|
189
|
+
return BAD_ESCAPE;
|
|
190
|
+
i += 2;
|
|
191
|
+
}
|
|
192
|
+
else
|
|
193
|
+
i++;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
160
197
|
function checkRegex(regexStr) {
|
|
198
|
+
var _a;
|
|
161
199
|
if (typeof regexStr !== 'string' || regexStr.length === 0)
|
|
162
200
|
return 'is empty';
|
|
163
201
|
if (regexStr.length > MAX_REGEX_LENGTH)
|
|
@@ -168,5 +206,5 @@ function checkRegex(regexStr) {
|
|
|
168
206
|
catch {
|
|
169
207
|
return 'is not a valid regular expression';
|
|
170
208
|
}
|
|
171
|
-
return structuralProblem(regexStr);
|
|
209
|
+
return (_a = unsupportedSyntax(regexStr)) !== null && _a !== void 0 ? _a : structuralProblem(regexStr);
|
|
172
210
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-redactor",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Redact, mask and anonymize PII before it reaches an LLM, then restore it. 216 patterns for emails, phones, addresses, IBAN, cards, names, SSN, passport and tax IDs. Reversible encrypted vault, GDPR Art.30 ROPA record, prompt-injection check. Runs on n8n Cloud and self-hosted n8n 2.x.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|