n8n-nodes-redactor 5.0.0 → 5.2.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.
@@ -767,7 +767,7 @@ class Redactor {
767
767
  };
768
768
  }
769
769
  async execute() {
770
- var _a, _b, _c, _d, _e, _f, _g;
770
+ var _a, _b, _c, _d, _e, _f, _g, _h;
771
771
  const items = this.getInputData();
772
772
  const allPaired = items.map((_, i) => ({ item: i }));
773
773
  const operation = this.getNodeParameter('operation', 0);
@@ -1041,6 +1041,7 @@ class Redactor {
1041
1041
  keyOrigins: new WeakMap(),
1042
1042
  inputText: (0, engine_1.collectInputText)(items.map((it) => it.json)),
1043
1043
  lockedSpans: new WeakMap(),
1044
+ priorTokens: (0, engine_1.priorTokenMatcher)((_e = vault.getSession(sessionId)) === null || _e === void 0 ? void 0 : _e.entries),
1044
1045
  };
1045
1046
  const allHits = [];
1046
1047
  const returnData = [];
@@ -1091,7 +1092,7 @@ class Redactor {
1091
1092
  if (includeReport && this.getNodeParameter('includeRopa', 0, false)) {
1092
1093
  const d = this.getNodeParameter('ropa', 0, {});
1093
1094
  const list = (v) => String(v !== null && v !== void 0 ? v : '').split(',').map((x) => x.trim()).filter(Boolean);
1094
- report.ropa = (0, ropa_1.generateRopaRecord)(allHits, (0, classification_1.classifyData)(allHits), items.length, enabledPatterns.length, mode, sessionTtl, !!vaultPassphrase, auditLog, { name: String((_e = d.controllerName) !== null && _e !== void 0 ? _e : ''), contactEmail: String((_f = d.controllerContact) !== null && _f !== void 0 ? _f : ''), dpo: d.dpo ? String(d.dpo) : undefined }, list(d.purposes), String((_g = d.legalBasis) !== null && _g !== void 0 ? _g : 'legitimate_interest'), list(d.dataSubjects), list(d.recipients), []);
1095
+ report.ropa = (0, ropa_1.generateRopaRecord)(allHits, (0, classification_1.classifyData)(allHits), items.length, enabledPatterns.length, mode, sessionTtl, !!vaultPassphrase, auditLog, { name: String((_f = d.controllerName) !== null && _f !== void 0 ? _f : ''), contactEmail: String((_g = d.controllerContact) !== null && _g !== void 0 ? _g : ''), dpo: d.dpo ? String(d.dpo) : undefined }, list(d.purposes), String((_h = d.legalBasis) !== null && _h !== void 0 ? _h : 'legitimate_interest'), list(d.dataSubjects), list(d.recipients), []);
1095
1096
  }
1096
1097
  return [returnData, [{ json: report, pairedItem: allPaired }]];
1097
1098
  }
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.collectTokenLike = collectTokenLike;
37
37
  exports.collectInputText = collectInputText;
38
38
  exports.redactValue = redactValue;
39
+ exports.priorTokenMatcher = priorTokenMatcher;
39
40
  exports.restoreValue = restoreValue;
40
41
  exports.enhanceWithPresidio = enhanceWithPresidio;
41
42
  exports.buildReport = buildReport;
@@ -70,7 +71,11 @@ function isAllowListed(value, allowList) {
70
71
  });
71
72
  }
72
73
  function generateToken(label, index) {
73
- return `[${label}_${index}]`;
74
+ const safe = label.toUpperCase().replace(/[^A-Z0-9_]/g, '_');
75
+ return `[${/^[A-Z]/.test(safe) ? safe : `X${safe}`}_${index}]`;
76
+ }
77
+ function setOwn(obj, key, value) {
78
+ Object.defineProperty(obj, key, { value, enumerable: true, writable: true, configurable: true });
74
79
  }
75
80
  const TOKEN_RE = /^\[[A-Z][A-Z0-9_]*_\d+\]$/;
76
81
  function maskValue(value, label) {
@@ -460,7 +465,7 @@ function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, displ
460
465
  if (out === null || out === text)
461
466
  return value;
462
467
  if (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize') {
463
- ctx.lastLocked = [[0, out.length]];
468
+ ctx.lastLocked = numCtx.lastLocked;
464
469
  return out;
465
470
  }
466
471
  const mapped = numericMapping(out, text, ctx, vault, sessionId);
@@ -472,7 +477,7 @@ function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, key
472
477
  const out = redactText(key, ctx, vault, sessionId, keyHits, itemIndex, '');
473
478
  if (out === key)
474
479
  return key;
475
- const clashes = (k) => k === '' || k in taken || sourceKeys.has(k);
480
+ const clashes = (k) => k === '' || Object.prototype.hasOwnProperty.call(taken, k) || sourceKeys.has(k);
476
481
  if (!clashes(out))
477
482
  return out;
478
483
  if (ctx.mode === 'token' || ctx.mode === 'pseudonymize') {
@@ -502,7 +507,7 @@ function redactObject(value, ctx, vault, sessionId, hits, itemIndex, path, displ
502
507
  const childDisplay = display ? `${display}.${outKey}` : outKey;
503
508
  for (const h of keyHits)
504
509
  hits.push({ ...h, field: childDisplay });
505
- obj[outKey] = lockChild(ctx, obj, outKey, () => redactValue(v, ctx, vault, sessionId, hits, itemIndex, fieldPath, siblingKeys, childDisplay));
510
+ setOwn(obj, outKey, lockChild(ctx, obj, outKey, () => redactValue(v, ctx, vault, sessionId, hits, itemIndex, fieldPath, siblingKeys, childDisplay)));
506
511
  }
507
512
  (_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.set(obj, origins);
508
513
  return obj;
@@ -544,35 +549,56 @@ function assertUserRegexInput(text, what) {
544
549
  throw new Error(`${what} cannot scan a value of ${text.length} characters, the limit is ${USER_REGEX_MAX_INPUT}. ` +
545
550
  'Exclude the field with field rules, or remove the regex.');
546
551
  }
547
- function replaceInFree(seg, re, fn, into) {
548
- var _a;
549
- let last = 0;
550
- for (const m of seg.text.matchAll(re)) {
551
- const at = (_a = m.index) !== null && _a !== void 0 ? _a : 0;
552
- if (m[0] === '')
553
- continue;
554
- const replacement = fn(m[0], seg.src + at);
555
- if (replacement === null)
556
- continue;
557
- if (at > last)
558
- into.push({ text: seg.text.slice(last, at), locked: false, src: seg.src + last });
559
- into.push({ text: replacement, locked: true, src: -1 });
560
- last = at + m[0].length;
552
+ class SegmentCursor {
553
+ constructor(segs) {
554
+ this.segs = segs;
555
+ this.next = [];
556
+ this.i = 0;
557
+ this.cur = segs[0];
558
+ }
559
+ freeAt(start, end) {
560
+ while (this.cur && (this.cur.locked || this.cur.src + this.cur.text.length <= start))
561
+ this.take();
562
+ const cur = this.cur;
563
+ return cur && cur.src <= start && end <= cur.src + cur.text.length ? cur : undefined;
564
+ }
565
+ lock(start, end, replacement) {
566
+ const cur = this.cur;
567
+ if (start > cur.src)
568
+ this.next.push({ text: cur.text.slice(0, start - cur.src), locked: false, src: cur.src });
569
+ this.next.push({ text: replacement, locked: true, src: -1 });
570
+ const tail = end - cur.src;
571
+ if (tail < cur.text.length)
572
+ this.cur = { text: cur.text.slice(tail), locked: false, src: end };
573
+ else
574
+ this.cur = this.segs[++this.i];
575
+ }
576
+ finish() {
577
+ while (this.cur)
578
+ this.take();
579
+ return this.next;
580
+ }
581
+ take() {
582
+ this.next.push(this.cur);
583
+ this.cur = this.segs[++this.i];
561
584
  }
562
- if (last < seg.text.length)
563
- into.push({ text: seg.text.slice(last), locked: false, src: seg.src + last });
564
585
  }
565
586
  function replaceFree(pass, re, fn) {
566
- const flags = re.flags.includes('g') ? re.flags : `${re.flags}g`;
567
- const global = new RegExp(re.source, flags);
568
- const next = [];
569
- for (const seg of pass.segs) {
570
- if (seg.locked)
571
- next.push(seg);
572
- else
573
- replaceInFree(seg, global, fn, next);
587
+ const global = new RegExp(re.source, re.flags.includes('g') ? re.flags : `${re.flags}g`);
588
+ const cursor = new SegmentCursor(pass.segs);
589
+ let m;
590
+ while ((m = global.exec(pass.source)) !== null) {
591
+ const start = m.index;
592
+ const end = start + m[0].length;
593
+ if (m[0] === '' || !cursor.freeAt(start, end)) {
594
+ global.lastIndex = start + 1;
595
+ continue;
596
+ }
597
+ const replacement = fn(m[0], start);
598
+ if (replacement !== null)
599
+ cursor.lock(start, end, replacement);
574
600
  }
575
- pass.segs = next;
601
+ pass.segs = cursor.finish();
576
602
  }
577
603
  function redactMatch(pass, match, pattern, detail) {
578
604
  const replacement = applyRedaction(match, pattern, pass.ctx, pass.vault, pass.sessionId);
@@ -621,19 +647,18 @@ function builtInStage(pass) {
621
647
  replaceFree(pass, pattern.regex, (match, offset) => builtInMatch(pass, pattern, match, offset));
622
648
  }
623
649
  }
624
- function freeNames(pass) {
625
- const names = new Set();
626
- for (const seg of pass.segs) {
650
+ function sourceNames(pass) {
651
+ const names = new Set((0, names_1.detectNamesInText)(pass.source).map((nm) => nm.name));
652
+ for (const seg of pass.segs)
627
653
  if (!seg.locked)
628
654
  for (const nm of (0, names_1.detectNamesInText)(seg.text))
629
655
  names.add(nm.name);
630
- }
631
- return names;
656
+ return [...names].sort((x, y) => y.length - x.length);
632
657
  }
633
658
  function nameStage(pass) {
634
659
  if (pass.ctx.skipSemantic)
635
660
  return;
636
- for (const name of freeNames(pass)) {
661
+ for (const name of sourceNames(pass)) {
637
662
  if ((name.includes('[') && name.includes(']')) || isAllowListed(name, pass.ctx.allowList))
638
663
  continue;
639
664
  let replacement;
@@ -689,12 +714,27 @@ function lockChild(ctx, container, key, compute) {
689
714
  ctx.lastLocked = undefined;
690
715
  return out;
691
716
  }
717
+ function priorTokenMatcher(entries) {
718
+ const tokens = Object.keys(entries !== null && entries !== void 0 ? entries : {}).filter((t) => t.length > 0).sort((a, b) => b.length - a.length);
719
+ return tokens.length > 0 ? new RegExp(tokens.map(escapeRe).join('|'), 'g') : undefined;
720
+ }
721
+ function literalStage(pass) {
722
+ const { ctx, vault, sessionId } = pass;
723
+ if (!ctx.priorTokens || (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize'))
724
+ return;
725
+ replaceFree(pass, ctx.priorTokens, (match) => {
726
+ const token = freshToken('LITERAL', vault, sessionId, ctx.reservedTokens);
727
+ vault.addEntry(sessionId, { token, original: match, patternLabel: 'LITERAL', category: 'other', createdAt: new Date().toISOString() });
728
+ return token;
729
+ });
730
+ }
692
731
  function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
693
732
  var _a;
694
733
  const pass = {
695
734
  source: text, segs: [{ text, locked: false, src: 0 }], threshold: (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0,
696
735
  ctx, vault, sessionId, hits, itemIndex, fieldPath,
697
736
  };
737
+ literalStage(pass);
698
738
  denyListStage(pass);
699
739
  builtInStage(pass);
700
740
  nameStage(pass);
@@ -721,7 +761,7 @@ function restoreWith(value, restore, entries) {
721
761
  if (value !== null && typeof value === 'object') {
722
762
  const obj = {};
723
763
  for (const [k, v] of Object.entries(value)) {
724
- obj[restore(k)] = restoreWith(v, restore, entries);
764
+ setOwn(obj, restore(k), restoreWith(v, restore, entries));
725
765
  }
726
766
  return obj;
727
767
  }
@@ -808,7 +848,7 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path,
808
848
  const child = path ? `${path}.${k}` : k;
809
849
  const childPolicy = policyPath ? `${policyPath}.${original}` : original;
810
850
  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[k] = await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy, lock);
851
+ setOwn(out, k, await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy, lock));
812
852
  }
813
853
  return out;
814
854
  }
@@ -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,10 @@ 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 };
96
+ if (body[j] === '\\' && body[j + 1] === 'B')
97
+ return { set: codeSet(66), next: j + 2, code: 66 };
85
98
  if (body[j] === '\\') {
86
99
  const { length, code } = readEscape(body, j);
87
100
  return { set: escapeSet(body, j), next: j + length, code };
@@ -3,9 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkRegex = checkRegex;
4
4
  const regexCharset_1 = require("./regexCharset");
5
5
  const MAX_REGEX_LENGTH = 500;
6
- const newFrame = (zeroWidth = false) => ({
6
+ const newFrame = (zeroWidth = false, inLook = zeroWidth) => ({
7
7
  hasVariableQuantifier: false, hasAlternation: false, run: null, chars: (0, regexCharset_1.emptySet)(), zeroWidth,
8
- branchConsumes: false, allConsume: true, nullable: false,
8
+ branchConsumes: false, allConsume: true, nullable: false, inLook,
9
9
  });
10
10
  function endBranch(frame) {
11
11
  frame.allConsume && (frame.allConsume = frame.branchConsumes);
@@ -48,7 +48,7 @@ function skipClass(src, i) {
48
48
  function groupPrefixLength(src, i) {
49
49
  if (src[i + 1] !== '?')
50
50
  return 1;
51
- const named = /^\(\?<[A-Za-z_$][\w$]*>/.exec(src.slice(i));
51
+ const named = /^\(\?<[^=!>][^>]*>/.exec(src.slice(i));
52
52
  if (named)
53
53
  return named[0].length;
54
54
  const look = /^\(\?(?::|=|!|<=|<!)/.exec(src.slice(i));
@@ -79,7 +79,8 @@ function readToken(src, i, stack) {
79
79
  if (c === '[')
80
80
  return { atomEnd: skipClass(src, i), closed: null };
81
81
  if (c === '(') {
82
- stack.push(newFrame(/^\(\?(?:=|!|<=|<!)/.test(src.slice(i))));
82
+ const look = /^\(\?(?:=|!|<=|<!)/.test(src.slice(i));
83
+ stack.push(newFrame(look, look || stack[stack.length - 1].inLook));
83
84
  return { atomEnd: i, closed: null, next: i + groupPrefixLength(src, i) };
84
85
  }
85
86
  if (c === '|')
@@ -127,8 +128,11 @@ function consumes(q, chars, closed) {
127
128
  return false;
128
129
  return !(closed === null || closed === void 0 ? void 0 : closed.nullable);
129
130
  }
131
+ const IN_LOOK = 'repeats a variable part inside a lookahead or lookbehind, which is not supported';
130
132
  function stepProblem(src, i, step, q, top) {
131
133
  var _a;
134
+ if (top.inLook && isVariable(q))
135
+ return IN_LOOK;
132
136
  const problem = applyQuantifier(q, step.closed, top);
133
137
  if (problem || (q === null || q === void 0 ? void 0 : q.max) === 0)
134
138
  return problem;
@@ -157,7 +161,45 @@ function structuralProblem(src) {
157
161
  }
158
162
  return null;
159
163
  }
164
+ 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])/;
165
+ const CLASS_ESCAPE_RANGE = /\\[dDwWsS]-[^\]]|[^[^\\]-\\[dDwWsS]/;
166
+ const BAD_ESCAPE = 'uses an escape that is not supported (octal, back-reference or unknown letter)';
167
+ const EMPTY_CLASS = 'uses an empty class [] or [^], which is not supported';
168
+ const ESCAPE_RANGE = 'uses a range with \\d, \\w or \\s as an end, which is not supported';
169
+ function classProblem(src, i) {
170
+ if (/^\[\^?\]/.test(src.slice(i)))
171
+ return { end: i, problem: EMPTY_CLASS };
172
+ const end = skipClass(src, i);
173
+ const body = src.slice(i, end);
174
+ for (let j = 1; j < body.length; j++) {
175
+ if (body[j] === '\\' && !KNOWN_ESCAPES.test(body.slice(j + 1)))
176
+ return { end, problem: BAD_ESCAPE };
177
+ if (body[j] === '\\')
178
+ j++;
179
+ }
180
+ return { end, problem: CLASS_ESCAPE_RANGE.test(body) ? ESCAPE_RANGE : null };
181
+ }
182
+ function unsupportedSyntax(src) {
183
+ let i = 0;
184
+ while (i < src.length) {
185
+ if (src[i] === '[') {
186
+ const { end, problem } = classProblem(src, i);
187
+ if (problem)
188
+ return problem;
189
+ i = end;
190
+ }
191
+ else if (src[i] === '\\') {
192
+ if (!KNOWN_ESCAPES.test(src.slice(i + 1)))
193
+ return BAD_ESCAPE;
194
+ i += 2;
195
+ }
196
+ else
197
+ i++;
198
+ }
199
+ return null;
200
+ }
160
201
  function checkRegex(regexStr) {
202
+ var _a;
161
203
  if (typeof regexStr !== 'string' || regexStr.length === 0)
162
204
  return 'is empty';
163
205
  if (regexStr.length > MAX_REGEX_LENGTH)
@@ -168,5 +210,5 @@ function checkRegex(regexStr) {
168
210
  catch {
169
211
  return 'is not a valid regular expression';
170
212
  }
171
- return structuralProblem(regexStr);
213
+ return (_a = unsupportedSyntax(regexStr)) !== null && _a !== void 0 ? _a : structuralProblem(regexStr);
172
214
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-redactor",
3
- "version": "5.0.0",
3
+ "version": "5.2.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",