n8n-nodes-redactor 5.1.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;
@@ -548,33 +549,56 @@ function assertUserRegexInput(text, what) {
548
549
  throw new Error(`${what} cannot scan a value of ${text.length} characters, the limit is ${USER_REGEX_MAX_INPUT}. ` +
549
550
  'Exclude the field with field rules, or remove the regex.');
550
551
  }
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);
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];
584
+ }
564
585
  }
565
586
  function replaceFree(pass, re, fn) {
566
- var _a;
567
- const flags = re.flags.includes('g') ? re.flags : `${re.flags}g`;
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;
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;
570
592
  const end = start + m[0].length;
571
- const index = m[0] === '' ? -1 : freeSegmentAt(pass.segs, start, end);
572
- if (index < 0)
593
+ if (m[0] === '' || !cursor.freeAt(start, end)) {
594
+ global.lastIndex = start + 1;
573
595
  continue;
596
+ }
574
597
  const replacement = fn(m[0], start);
575
598
  if (replacement !== null)
576
- lockRange(pass.segs, index, start, end, replacement);
599
+ cursor.lock(start, end, replacement);
577
600
  }
601
+ pass.segs = cursor.finish();
578
602
  }
579
603
  function redactMatch(pass, match, pattern, detail) {
580
604
  const replacement = applyRedaction(match, pattern, pass.ctx, pass.vault, pass.sessionId);
@@ -625,6 +649,10 @@ function builtInStage(pass) {
625
649
  }
626
650
  function sourceNames(pass) {
627
651
  const names = new Set((0, names_1.detectNamesInText)(pass.source).map((nm) => nm.name));
652
+ for (const seg of pass.segs)
653
+ if (!seg.locked)
654
+ for (const nm of (0, names_1.detectNamesInText)(seg.text))
655
+ names.add(nm.name);
628
656
  return [...names].sort((x, y) => y.length - x.length);
629
657
  }
630
658
  function nameStage(pass) {
@@ -686,12 +714,27 @@ function lockChild(ctx, container, key, compute) {
686
714
  ctx.lastLocked = undefined;
687
715
  return out;
688
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
+ }
689
731
  function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
690
732
  var _a;
691
733
  const pass = {
692
734
  source: text, segs: [{ text, locked: false, src: 0 }], threshold: (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0,
693
735
  ctx, vault, sessionId, hits, itemIndex, fieldPath,
694
736
  };
737
+ literalStage(pass);
695
738
  denyListStage(pass);
696
739
  builtInStage(pass);
697
740
  nameStage(pass);
@@ -93,6 +93,8 @@ function escapeSet(src, i) {
93
93
  function classMember(body, j) {
94
94
  if (body[j] === '\\' && body[j + 1] === 'b')
95
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 };
96
98
  if (body[j] === '\\') {
97
99
  const { length, code } = readEscape(body, j);
98
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-redactor",
3
- "version": "5.1.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",