n8n-nodes-redactor 4.3.0 → 5.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.
@@ -1040,7 +1040,7 @@ class Redactor {
1040
1040
  reservedTokens: (0, engine_1.collectTokenLike)(items.map((it) => it.json)),
1041
1041
  keyOrigins: new WeakMap(),
1042
1042
  inputText: (0, engine_1.collectInputText)(items.map((it) => it.json)),
1043
- lockedSpans: new Map(),
1043
+ lockedSpans: new WeakMap(),
1044
1044
  };
1045
1045
  const allHits = [];
1046
1046
  const returnData = [];
@@ -57,7 +57,7 @@ function isAllowListed(value, allowList) {
57
57
  case 'contains':
58
58
  return value.includes(entry.value);
59
59
  case 'regex':
60
- if (!isSafeRegex(entry.value))
60
+ if (!isSafeRegex(entry.value) || value.length > USER_REGEX_MAX_INPUT)
61
61
  return false;
62
62
  return new RegExp(entry.value, 'i').test(value);
63
63
  default:
@@ -414,7 +414,7 @@ function getSemanticLabel(fieldKey, siblingKeys) {
414
414
  return null;
415
415
  }
416
416
  function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
417
- var _a, _b;
417
+ var _a;
418
418
  const semantic = getSemanticLabel(path, siblingKeys);
419
419
  if (!semantic || !semantic.label || raw.trim().length === 0)
420
420
  return null;
@@ -424,8 +424,7 @@ function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, displ
424
424
  if (semConf < ((_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0))
425
425
  return null;
426
426
  const replacement = applyRedaction(raw, { label: semantic.label, category: semantic.category }, ctx, vault, sessionId);
427
- if (replacement)
428
- (_b = ctx.lockedSpans) === null || _b === void 0 ? void 0 : _b.set(lockKey(itemIndex, display), [[0, replacement.length]]);
427
+ ctx.lastLocked = replacement ? [[0, replacement.length]] : undefined;
429
428
  hits.push({
430
429
  token: replacement, original: '***',
431
430
  patternName: `semantic_${semantic.label}`,
@@ -444,14 +443,13 @@ function numericMapping(out, text, ctx, vault, sessionId) {
444
443
  existing.numeric = true;
445
444
  return out;
446
445
  }
447
- const token = existing ? freshToken('NUMBER', vault, sessionId, ctx.reservedTokens) : out;
446
+ const token = freshToken('NUMBER', vault, sessionId, ctx.reservedTokens);
448
447
  vault.addEntry(sessionId, {
449
448
  token, original: text, patternLabel: 'NUMBER', category: 'other', createdAt: new Date().toISOString(), numeric: true,
450
449
  });
451
450
  return token;
452
451
  }
453
452
  function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
454
- var _a;
455
453
  const text = String(value);
456
454
  const numCtx = { ...ctx, dedup: false, numeric: true };
457
455
  let out = null;
@@ -461,10 +459,12 @@ function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, displ
461
459
  out = redactText(text, numCtx, vault, sessionId, hits, itemIndex, display);
462
460
  if (out === null || out === text)
463
461
  return value;
464
- if (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize')
462
+ if (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize') {
463
+ ctx.lastLocked = [[0, out.length]];
465
464
  return out;
465
+ }
466
466
  const mapped = numericMapping(out, text, ctx, vault, sessionId);
467
- (_a = ctx.lockedSpans) === null || _a === void 0 ? void 0 : _a.set(lockKey(itemIndex, display), [[0, mapped.length]]);
467
+ ctx.lastLocked = [[0, mapped.length]];
468
468
  return mapped;
469
469
  }
470
470
  function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits) {
@@ -502,7 +502,7 @@ function redactObject(value, ctx, vault, sessionId, hits, itemIndex, path, displ
502
502
  const childDisplay = display ? `${display}.${outKey}` : outKey;
503
503
  for (const h of keyHits)
504
504
  hits.push({ ...h, field: childDisplay });
505
- obj[outKey] = redactValue(v, ctx, vault, sessionId, hits, itemIndex, fieldPath, siblingKeys, childDisplay);
505
+ obj[outKey] = lockChild(ctx, obj, outKey, () => redactValue(v, ctx, vault, sessionId, hits, itemIndex, fieldPath, siblingKeys, childDisplay));
506
506
  }
507
507
  (_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.set(obj, origins);
508
508
  return obj;
@@ -525,7 +525,11 @@ function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath
525
525
  return redactNumber(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath, siblingKeys);
526
526
  }
527
527
  if (Array.isArray(value)) {
528
- return value.map((v, i) => redactValue(v, ctx, vault, sessionId, hits, itemIndex, `${currentPath}[${i}]`, undefined, `${displayPath}[${i}]`));
528
+ const out = [];
529
+ value.forEach((v, i) => {
530
+ out.push(lockChild(ctx, out, String(i), () => redactValue(v, ctx, vault, sessionId, hits, itemIndex, `${currentPath}[${i}]`, undefined, `${displayPath}[${i}]`)));
531
+ });
532
+ return out;
529
533
  }
530
534
  if (value !== null && typeof value === 'object') {
531
535
  return redactObject(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath);
@@ -533,18 +537,12 @@ function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath
533
537
  return value;
534
538
  }
535
539
  const escapeRe = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
536
- const USER_REGEX_WINDOW = 2048;
537
- function windows(seg, size) {
538
- const out = [];
539
- let at = 0;
540
- while (seg.text.length - at > size) {
541
- const cut = seg.text.lastIndexOf(' ', at + size);
542
- const end = cut > at + size / 2 ? cut + 1 : at + size;
543
- out.push({ text: seg.text.slice(at, end), locked: false, src: seg.src + at });
544
- at = end;
545
- }
546
- out.push({ text: seg.text.slice(at), locked: false, src: seg.src + at });
547
- return out;
540
+ const USER_REGEX_MAX_INPUT = 20000;
541
+ function assertUserRegexInput(text, what) {
542
+ if (text.length <= USER_REGEX_MAX_INPUT)
543
+ return;
544
+ throw new Error(`${what} cannot scan a value of ${text.length} characters, the limit is ${USER_REGEX_MAX_INPUT}. ` +
545
+ 'Exclude the field with field rules, or remove the regex.');
548
546
  }
549
547
  function replaceInFree(seg, re, fn, into) {
550
548
  var _a;
@@ -564,7 +562,7 @@ function replaceInFree(seg, re, fn, into) {
564
562
  if (last < seg.text.length)
565
563
  into.push({ text: seg.text.slice(last), locked: false, src: seg.src + last });
566
564
  }
567
- function replaceFree(pass, re, fn, window) {
565
+ function replaceFree(pass, re, fn) {
568
566
  const flags = re.flags.includes('g') ? re.flags : `${re.flags}g`;
569
567
  const global = new RegExp(re.source, flags);
570
568
  const next = [];
@@ -572,8 +570,7 @@ function replaceFree(pass, re, fn, window) {
572
570
  if (seg.locked)
573
571
  next.push(seg);
574
572
  else
575
- for (const piece of window ? windows(seg, window) : [seg])
576
- replaceInFree(piece, global, fn, next);
573
+ replaceInFree(seg, global, fn, next);
577
574
  }
578
575
  pass.segs = next;
579
576
  }
@@ -590,16 +587,17 @@ function denyListRegex(entry) {
590
587
  function denyListStage(pass) {
591
588
  var _a;
592
589
  for (const entry of (_a = pass.ctx.denyList) !== null && _a !== void 0 ? _a : []) {
590
+ if (entry.type === 'regex')
591
+ assertUserRegexInput(pass.source, `Deny list regex "${entry.value}"`);
593
592
  try {
594
593
  const re = denyListRegex(entry);
595
594
  if (!re)
596
595
  continue;
597
- const window = entry.type === 'regex' ? USER_REGEX_WINDOW : undefined;
598
596
  replaceFree(pass, re, (match) => TOKEN_RE.test(match)
599
597
  ? null
600
598
  : redactMatch(pass, match, { label: 'DENY_LIST', category: 'other' }, {
601
599
  patternName: 'denyList', patternLabel: 'DENY_LIST', category: 'other', confidence: context_1.DENY_LIST_CONFIDENCE,
602
- }), window);
600
+ }));
603
601
  }
604
602
  catch { }
605
603
  }
@@ -624,27 +622,25 @@ function builtInStage(pass) {
624
622
  }
625
623
  }
626
624
  function freeNames(pass) {
627
- const names = new Map();
625
+ const names = new Set();
628
626
  for (const seg of pass.segs) {
629
- if (seg.locked)
630
- continue;
631
- for (const nm of (0, names_1.detectNamesInText)(seg.text))
632
- if (!names.has(nm.name))
633
- names.set(nm.name, seg.src + nm.start);
627
+ if (!seg.locked)
628
+ for (const nm of (0, names_1.detectNamesInText)(seg.text))
629
+ names.add(nm.name);
634
630
  }
635
631
  return names;
636
632
  }
637
633
  function nameStage(pass) {
638
634
  if (pass.ctx.skipSemantic)
639
635
  return;
640
- for (const [name, start] of freeNames(pass)) {
636
+ for (const name of freeNames(pass)) {
641
637
  if ((name.includes('[') && name.includes(']')) || isAllowListed(name, pass.ctx.allowList))
642
638
  continue;
643
- const confidence = (0, context_1.hasContextWords)(pass.source, start, start + name.length, names_1.NAME_CONTEXT_WORDS) ? 0.8 : 0.65;
644
- if (confidence < pass.threshold)
645
- continue;
646
639
  let replacement;
647
- replaceFree(pass, new RegExp(escapeRe(name), 'g'), (match) => {
640
+ replaceFree(pass, new RegExp(escapeRe(name), 'g'), (match, start) => {
641
+ const confidence = (0, context_1.hasContextWords)(pass.source, start, start + match.length, names_1.NAME_CONTEXT_WORDS) ? 0.8 : 0.65;
642
+ if (confidence < pass.threshold)
643
+ return null;
648
644
  replacement !== null && replacement !== void 0 ? replacement : (replacement = redactMatch(pass, match, { label: 'PERSON_NAME', category: 'identity' }, {
649
645
  patternName: 'personNameDict', patternLabel: 'PERSON_NAME', category: 'identity', confidence,
650
646
  }));
@@ -659,20 +655,19 @@ function customStage(pass) {
659
655
  if (!cp.label || !cp.regex || !isSafeRegex(cp.regex))
660
656
  continue;
661
657
  const pattern = { label: cp.label.toUpperCase(), category: cp.category || 'identity' };
658
+ assertUserRegexInput(pass.source, `Custom pattern "${cp.label}"`);
662
659
  try {
663
660
  replaceFree(pass, new RegExp(cp.regex, 'g'), (match) => TOKEN_RE.test(match) || isAllowListed(match, pass.ctx.allowList)
664
661
  ? null
665
662
  : redactMatch(pass, match, pattern, {
666
663
  patternName: `custom_${cp.label}`, patternLabel: pattern.label, category: pattern.category,
667
664
  confidence: context_1.CUSTOM_PATTERN_CONFIDENCE,
668
- }), USER_REGEX_WINDOW);
665
+ }));
669
666
  }
670
667
  catch { }
671
668
  }
672
669
  }
673
- function recordLocked(ctx, itemIndex, fieldPath, segs) {
674
- if (!ctx.lockedSpans || fieldPath === '')
675
- return;
670
+ function recordLocked(ctx, segs) {
676
671
  const spans = [];
677
672
  let at = 0;
678
673
  for (const seg of segs) {
@@ -680,10 +675,20 @@ function recordLocked(ctx, itemIndex, fieldPath, segs) {
680
675
  spans.push([at, at + seg.text.length]);
681
676
  at += seg.text.length;
682
677
  }
683
- if (spans.length > 0)
684
- ctx.lockedSpans.set(lockKey(itemIndex, fieldPath), spans);
678
+ ctx.lastLocked = spans.length > 0 ? spans : undefined;
679
+ }
680
+ function lockChild(ctx, container, key, compute) {
681
+ var _a;
682
+ ctx.lastLocked = undefined;
683
+ const out = compute();
684
+ if (ctx.lastLocked && ctx.lockedSpans && typeof out === 'string') {
685
+ const byKey = (_a = ctx.lockedSpans.get(container)) !== null && _a !== void 0 ? _a : new Map();
686
+ byKey.set(key, ctx.lastLocked);
687
+ ctx.lockedSpans.set(container, byKey);
688
+ }
689
+ ctx.lastLocked = undefined;
690
+ return out;
685
691
  }
686
- const lockKey = (itemIndex, fieldPath) => `${itemIndex}\u0000${fieldPath}`;
687
692
  function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
688
693
  var _a;
689
694
  const pass = {
@@ -694,7 +699,7 @@ function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
694
699
  builtInStage(pass);
695
700
  nameStage(pass);
696
701
  customStage(pass);
697
- recordLocked(ctx, itemIndex, fieldPath, pass.segs);
702
+ recordLocked(ctx, pass.segs);
698
703
  return pass.segs.map((s) => s.text).join('');
699
704
  }
700
705
  function makeRestorer(entries) {
@@ -745,18 +750,17 @@ function resolveOverlaps(spans) {
745
750
  }
746
751
  return kept.sort((a, b) => b.start - a.start);
747
752
  }
748
- function presidioSpans(value, entities, ctx, key) {
749
- var _a, _b, _c;
753
+ function presidioSpans(value, entities, ctx, locked) {
754
+ var _a;
750
755
  const threshold = (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0;
751
- const locked = (_c = (_b = ctx.lockedSpans) === null || _b === void 0 ? void 0 : _b.get(key)) !== null && _c !== void 0 ? _c : [];
752
756
  const free = (s) => !locked.some(([start, end]) => s.start < end && start < s.end);
753
757
  return resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold && free(s)));
754
758
  }
755
- async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path) {
759
+ async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path, locked) {
756
760
  if (value.trim().length === 0 || TOKEN_RE.test(value))
757
761
  return value;
758
762
  const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
759
- const spans = presidioSpans(value, entities, ctx, lockKey(itemIndex, path));
763
+ const spans = presidioSpans(value, entities, ctx, locked);
760
764
  let text = value;
761
765
  for (const span of spans) {
762
766
  const matchedText = value.slice(span.start, span.end);
@@ -776,13 +780,13 @@ async function presidioRedactString(value, ctx, vault, sessionId, hits, itemInde
776
780
  }
777
781
  return text;
778
782
  }
779
- async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
780
- var _a, _b;
783
+ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath, locked = []) {
784
+ var _a, _b, _c, _d, _e, _f;
781
785
  if (typeof value === 'string') {
782
786
  if (!shouldProcessField(policyPath, ctx.fieldMode, ctx.fieldRules))
783
787
  return value;
784
788
  try {
785
- return await presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path);
789
+ return await presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path, locked);
786
790
  }
787
791
  catch {
788
792
  return value;
@@ -791,18 +795,20 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path,
791
795
  if (Array.isArray(value)) {
792
796
  const out = [];
793
797
  for (let i = 0; i < value.length; i++) {
794
- out.push(await presidioWalk(value[i], ctx, vault, sessionId, hits, itemIndex, `${path}[${i}]`, `${policyPath}[${i}]`));
798
+ const lock = (_b = (_a = ctx.lockedSpans) === null || _a === void 0 ? void 0 : _a.get(value)) === null || _b === void 0 ? void 0 : _b.get(String(i));
799
+ out.push(await presidioWalk(value[i], ctx, vault, sessionId, hits, itemIndex, `${path}[${i}]`, `${policyPath}[${i}]`, lock));
795
800
  }
796
801
  return out;
797
802
  }
798
803
  if (value !== null && typeof value === 'object') {
799
- const origins = (_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.get(value);
804
+ const origins = (_c = ctx.keyOrigins) === null || _c === void 0 ? void 0 : _c.get(value);
800
805
  const out = {};
801
806
  for (const [k, v] of Object.entries(value)) {
802
- const original = (_b = origins === null || origins === void 0 ? void 0 : origins.get(k)) !== null && _b !== void 0 ? _b : k;
807
+ const original = (_d = origins === null || origins === void 0 ? void 0 : origins.get(k)) !== null && _d !== void 0 ? _d : k;
803
808
  const child = path ? `${path}.${k}` : k;
804
809
  const childPolicy = policyPath ? `${policyPath}.${original}` : original;
805
- out[k] = await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy);
810
+ 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);
806
812
  }
807
813
  return out;
808
814
  }
@@ -3,19 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.emptySet = void 0;
4
4
  exports.union = union;
5
5
  exports.overlaps = overlaps;
6
+ exports.readEscape = readEscape;
6
7
  exports.atomCharset = atomCharset;
7
8
  const emptySet = () => ({ ascii: new Array(128).fill(false), other: false });
8
9
  exports.emptySet = emptySet;
9
10
  const fullSet = () => ({ ascii: new Array(128).fill(true), other: true });
10
11
  function addCode(set, code) {
11
- if (code >= 128) {
12
+ if (code >= 128)
12
13
  set.other = true;
13
- return;
14
- }
15
- set.ascii[code] = true;
16
- const ch = String.fromCharCode(code);
17
- for (const folded of [ch.toLowerCase(), ch.toUpperCase()])
18
- set.ascii[folded.charCodeAt(0)] = true;
14
+ else
15
+ set.ascii[code] = true;
19
16
  }
20
17
  function addRange(set, from, to) {
21
18
  for (let c = from; c <= Math.min(to, 127); c++)
@@ -32,11 +29,18 @@ function overlaps(a, b) {
32
29
  function complement(set) {
33
30
  return { ascii: set.ascii.map((v) => !v), other: true };
34
31
  }
32
+ function fold(set) {
33
+ const ascii = [...set.ascii];
34
+ for (let c = 65; c <= 90; c++)
35
+ ascii[c] = ascii[c + 32] = set.ascii[c] || set.ascii[c + 32];
36
+ return { ascii, other: set.other };
37
+ }
35
38
  const CLASS_ESCAPES = {
36
39
  d: (s) => addRange(s, 48, 57),
37
40
  w: (s) => {
38
41
  addRange(s, 48, 57);
39
42
  addRange(s, 65, 90);
43
+ addRange(s, 97, 122);
40
44
  addCode(s, 95);
41
45
  },
42
46
  s: (s) => {
@@ -45,26 +49,42 @@ const CLASS_ESCAPES = {
45
49
  s.other = true;
46
50
  },
47
51
  };
48
- function escapeSet(letter) {
49
- const lower = letter.toLowerCase();
50
- const fill = CLASS_ESCAPES[lower];
51
- if (fill) {
52
- const set = (0, exports.emptySet)();
53
- fill(set);
54
- return letter === lower ? set : complement(set);
52
+ const HEX = /^[0-9a-fA-F]+$/;
53
+ function readEscape(src, i) {
54
+ var _a, _b;
55
+ const e = (_a = src[i + 1]) !== null && _a !== void 0 ? _a : '';
56
+ const hex = (n) => (HEX.test(src.slice(i + 2, i + 2 + n)) && src.length >= i + 2 + n ? n : 0);
57
+ if (e === 'x' && hex(2))
58
+ return { length: 4, code: parseInt(src.slice(i + 2, i + 4), 16) };
59
+ if (e === 'u' && hex(4))
60
+ return { length: 6, code: parseInt(src.slice(i + 2, i + 6), 16) };
61
+ if (e === 'c' && /[A-Za-z]/.test((_b = src[i + 2]) !== null && _b !== void 0 ? _b : ''))
62
+ return { length: 3, code: src.charCodeAt(i + 2) % 32 };
63
+ if (e === '0')
64
+ return { length: 2, code: 0 };
65
+ if (/[a-zA-Z0-9]/.test(e))
66
+ return { length: 2, code: null };
67
+ return { length: 2, code: e.charCodeAt(0) };
68
+ }
69
+ function escapeSet(src, i) {
70
+ var _a;
71
+ const { code } = readEscape(src, i);
72
+ const set = (0, exports.emptySet)();
73
+ if (code !== null) {
74
+ addCode(set, code);
75
+ return set;
55
76
  }
56
- if (/[a-zA-Z0-9]/.test(letter))
77
+ const letter = (_a = src[i + 1]) !== null && _a !== void 0 ? _a : '';
78
+ const fill = CLASS_ESCAPES[letter.toLowerCase()];
79
+ if (!fill)
57
80
  return fullSet();
58
- const set = (0, exports.emptySet)();
59
- addCode(set, letter.charCodeAt(0));
60
- return set;
81
+ fill(set);
82
+ return letter === letter.toLowerCase() ? set : complement(set);
61
83
  }
62
84
  function classMember(body, j) {
63
- var _a, _b;
64
85
  if (body[j] === '\\') {
65
- const set = escapeSet((_a = body[j + 1]) !== null && _a !== void 0 ? _a : '\\');
66
- const single = /[a-zA-Z0-9]/.test((_b = body[j + 1]) !== null && _b !== void 0 ? _b : '') ? null : body.charCodeAt(j + 1);
67
- return { set, next: j + 2, code: single };
86
+ const { length, code } = readEscape(body, j);
87
+ return { set: escapeSet(body, j), next: j + length, code };
68
88
  }
69
89
  const set = (0, exports.emptySet)();
70
90
  addCode(set, body.charCodeAt(j));
@@ -90,13 +110,13 @@ function classSet(body) {
90
110
  }
91
111
  return negated ? complement(set) : set;
92
112
  }
93
- function atomCharset(src, start, end) {
94
- var _a, _b;
113
+ function rawCharset(src, start, end) {
114
+ var _a;
95
115
  const c = src[start];
96
116
  if (c === '^' || c === '$')
97
117
  return null;
98
118
  if (c === '\\')
99
- return /[bB]/.test((_a = src[start + 1]) !== null && _a !== void 0 ? _a : '') ? null : escapeSet((_b = src[start + 1]) !== null && _b !== void 0 ? _b : '\\');
119
+ return /[bB]/.test((_a = src[start + 1]) !== null && _a !== void 0 ? _a : '') ? null : escapeSet(src, start);
100
120
  if (c === '[')
101
121
  return classSet(src.slice(start + 1, end - 1));
102
122
  if (c === '.')
@@ -105,3 +125,7 @@ function atomCharset(src, start, end) {
105
125
  addCode(set, src.charCodeAt(start));
106
126
  return set;
107
127
  }
128
+ function atomCharset(src, start, end) {
129
+ const set = rawCharset(src, start, end);
130
+ return set && fold(set);
131
+ }
@@ -5,7 +5,13 @@ const regexCharset_1 = require("./regexCharset");
5
5
  const MAX_REGEX_LENGTH = 500;
6
6
  const newFrame = (zeroWidth = false) => ({
7
7
  hasVariableQuantifier: false, hasAlternation: false, run: null, chars: (0, regexCharset_1.emptySet)(), zeroWidth,
8
+ branchConsumes: false, allConsume: true, nullable: false,
8
9
  });
10
+ function endBranch(frame) {
11
+ frame.allConsume && (frame.allConsume = frame.branchConsumes);
12
+ frame.branchConsumes = false;
13
+ frame.nullable = !frame.allConsume;
14
+ }
9
15
  function readBraces(src, i) {
10
16
  const m = /^\{(\d+)(,(\d*))?\}/.exec(src.slice(i));
11
17
  if (!m)
@@ -48,13 +54,27 @@ function groupPrefixLength(src, i) {
48
54
  const look = /^\(\?(?::|=|!|<=|<!)/.exec(src.slice(i));
49
55
  return look ? look[0].length : 1;
50
56
  }
57
+ function alternation(stack, i) {
58
+ const top = stack[stack.length - 1];
59
+ top.hasAlternation = true;
60
+ top.run = null;
61
+ endBranch(top);
62
+ return { atomEnd: i, closed: null, next: i + 1 };
63
+ }
64
+ function closeGroup(c, stack) {
65
+ if (c !== ')' || stack.length <= 1)
66
+ return null;
67
+ const closed = stack.pop();
68
+ endBranch(closed);
69
+ return closed;
70
+ }
51
71
  function readToken(src, i, stack) {
52
72
  var _a;
53
73
  const c = src[i];
54
74
  if (c === '\\') {
55
75
  if (/[1-9k]/.test((_a = src[i + 1]) !== null && _a !== void 0 ? _a : ''))
56
76
  return { atomEnd: i, closed: null, error: 'back-references are not supported' };
57
- return { atomEnd: i + 2, closed: null };
77
+ return { atomEnd: i + (0, regexCharset_1.readEscape)(src, i).length, closed: null };
58
78
  }
59
79
  if (c === '[')
60
80
  return { atomEnd: skipClass(src, i), closed: null };
@@ -62,13 +82,9 @@ function readToken(src, i, stack) {
62
82
  stack.push(newFrame(/^\(\?(?:=|!|<=|<!)/.test(src.slice(i))));
63
83
  return { atomEnd: i, closed: null, next: i + groupPrefixLength(src, i) };
64
84
  }
65
- if (c === '|') {
66
- stack[stack.length - 1].hasAlternation = true;
67
- stack[stack.length - 1].run = null;
68
- return { atomEnd: i, closed: null, next: i + 1 };
69
- }
70
- const closed = c === ')' && stack.length > 1 ? stack.pop() : null;
71
- return { atomEnd: i + 1, closed };
85
+ if (c === '|')
86
+ return alternation(stack, i);
87
+ return { atomEnd: i + 1, closed: closeGroup(c, stack) };
72
88
  }
73
89
  const NESTED = 'a repeated group contains a quantifier or alternation (catastrophic backtracking)';
74
90
  function isRisky(frame) {
@@ -106,13 +122,21 @@ function stepChars(src, i, step) {
106
122
  return (0, regexCharset_1.atomCharset)(src, i, step.atomEnd);
107
123
  return closed.zeroWidth ? null : closed.chars;
108
124
  }
125
+ function consumes(q, chars, closed) {
126
+ if (chars === null || (q !== null && q.min < 1))
127
+ return false;
128
+ return !(closed === null || closed === void 0 ? void 0 : closed.nullable);
129
+ }
109
130
  function stepProblem(src, i, step, q, top) {
110
- var _a, _b;
131
+ var _a;
111
132
  const problem = applyQuantifier(q, step.closed, top);
112
133
  if (problem || (q === null || q === void 0 ? void 0 : q.max) === 0)
113
134
  return problem;
114
- const variable = isVariable(q) || ((_a = step.closed) === null || _a === void 0 ? void 0 : _a.hasVariableQuantifier) === true;
115
- return applySequence(stepChars(src, i, step), variable, ((_b = q === null || q === void 0 ? void 0 : q.min) !== null && _b !== void 0 ? _b : 1) >= 1, top);
135
+ const chars = stepChars(src, i, step);
136
+ const required = consumes(q, chars, step.closed);
137
+ if (required)
138
+ top.branchConsumes = true;
139
+ return applySequence(chars, isVariable(q) || !!((_a = step.closed) === null || _a === void 0 ? void 0 : _a.hasVariableQuantifier), required, top);
116
140
  }
117
141
  function structuralProblem(src) {
118
142
  const stack = [newFrame()];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-redactor",
3
- "version": "4.3.0",
3
+ "version": "5.0.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",