n8n-nodes-redactor 4.3.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.
@@ -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:
@@ -70,7 +70,11 @@ function isAllowListed(value, allowList) {
70
70
  });
71
71
  }
72
72
  function generateToken(label, index) {
73
- return `[${label}_${index}]`;
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) {
@@ -414,7 +418,7 @@ function getSemanticLabel(fieldKey, siblingKeys) {
414
418
  return null;
415
419
  }
416
420
  function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
417
- var _a, _b;
421
+ var _a;
418
422
  const semantic = getSemanticLabel(path, siblingKeys);
419
423
  if (!semantic || !semantic.label || raw.trim().length === 0)
420
424
  return null;
@@ -424,8 +428,7 @@ function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, displ
424
428
  if (semConf < ((_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0))
425
429
  return null;
426
430
  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]]);
431
+ ctx.lastLocked = replacement ? [[0, replacement.length]] : undefined;
429
432
  hits.push({
430
433
  token: replacement, original: '***',
431
434
  patternName: `semantic_${semantic.label}`,
@@ -444,14 +447,13 @@ function numericMapping(out, text, ctx, vault, sessionId) {
444
447
  existing.numeric = true;
445
448
  return out;
446
449
  }
447
- const token = existing ? freshToken('NUMBER', vault, sessionId, ctx.reservedTokens) : out;
450
+ const token = freshToken('NUMBER', vault, sessionId, ctx.reservedTokens);
448
451
  vault.addEntry(sessionId, {
449
452
  token, original: text, patternLabel: 'NUMBER', category: 'other', createdAt: new Date().toISOString(), numeric: true,
450
453
  });
451
454
  return token;
452
455
  }
453
456
  function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
454
- var _a;
455
457
  const text = String(value);
456
458
  const numCtx = { ...ctx, dedup: false, numeric: true };
457
459
  let out = null;
@@ -461,10 +463,12 @@ function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, displ
461
463
  out = redactText(text, numCtx, vault, sessionId, hits, itemIndex, display);
462
464
  if (out === null || out === text)
463
465
  return value;
464
- if (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize')
466
+ if (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize') {
467
+ ctx.lastLocked = numCtx.lastLocked;
465
468
  return out;
469
+ }
466
470
  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]]);
471
+ ctx.lastLocked = [[0, mapped.length]];
468
472
  return mapped;
469
473
  }
470
474
  function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits) {
@@ -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 in taken || sourceKeys.has(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[outKey] = redactValue(v, ctx, vault, sessionId, hits, itemIndex, fieldPath, siblingKeys, childDisplay);
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;
@@ -525,7 +529,11 @@ function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath
525
529
  return redactNumber(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath, siblingKeys);
526
530
  }
527
531
  if (Array.isArray(value)) {
528
- return value.map((v, i) => redactValue(v, ctx, vault, sessionId, hits, itemIndex, `${currentPath}[${i}]`, undefined, `${displayPath}[${i}]`));
532
+ const out = [];
533
+ value.forEach((v, i) => {
534
+ out.push(lockChild(ctx, out, String(i), () => redactValue(v, ctx, vault, sessionId, hits, itemIndex, `${currentPath}[${i}]`, undefined, `${displayPath}[${i}]`)));
535
+ });
536
+ return out;
529
537
  }
530
538
  if (value !== null && typeof value === 'object') {
531
539
  return redactObject(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath);
@@ -533,49 +541,40 @@ function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath
533
541
  return value;
534
542
  }
535
543
  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;
544
+ const USER_REGEX_MAX_INPUT = 20000;
545
+ function assertUserRegexInput(text, what) {
546
+ if (text.length <= USER_REGEX_MAX_INPUT)
547
+ return;
548
+ throw new Error(`${what} cannot scan a value of ${text.length} characters, the limit is ${USER_REGEX_MAX_INPUT}. ` +
549
+ 'Exclude the field with field rules, or remove the regex.');
548
550
  }
549
- function replaceInFree(seg, re, fn, into) {
550
- var _a;
551
- let last = 0;
552
- for (const m of seg.text.matchAll(re)) {
553
- const at = (_a = m.index) !== null && _a !== void 0 ? _a : 0;
554
- if (m[0] === '')
555
- continue;
556
- const replacement = fn(m[0], seg.src + at);
557
- if (replacement === null)
558
- continue;
559
- if (at > last)
560
- into.push({ text: seg.text.slice(last, at), locked: false, src: seg.src + last });
561
- into.push({ text: replacement, locked: true, src: -1 });
562
- last = at + m[0].length;
563
- }
564
- if (last < seg.text.length)
565
- 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);
566
553
  }
567
- function replaceFree(pass, re, fn, window) {
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
+ }
565
+ function replaceFree(pass, re, fn) {
566
+ var _a;
568
567
  const flags = re.flags.includes('g') ? re.flags : `${re.flags}g`;
569
- const global = new RegExp(re.source, flags);
570
- const next = [];
571
- for (const seg of pass.segs) {
572
- if (seg.locked)
573
- next.push(seg);
574
- else
575
- for (const piece of window ? windows(seg, window) : [seg])
576
- replaceInFree(piece, global, fn, next);
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);
577
577
  }
578
- pass.segs = next;
579
578
  }
580
579
  function redactMatch(pass, match, pattern, detail) {
581
580
  const replacement = applyRedaction(match, pattern, pass.ctx, pass.vault, pass.sessionId);
@@ -590,16 +589,17 @@ function denyListRegex(entry) {
590
589
  function denyListStage(pass) {
591
590
  var _a;
592
591
  for (const entry of (_a = pass.ctx.denyList) !== null && _a !== void 0 ? _a : []) {
592
+ if (entry.type === 'regex')
593
+ assertUserRegexInput(pass.source, `Deny list regex "${entry.value}"`);
593
594
  try {
594
595
  const re = denyListRegex(entry);
595
596
  if (!re)
596
597
  continue;
597
- const window = entry.type === 'regex' ? USER_REGEX_WINDOW : undefined;
598
598
  replaceFree(pass, re, (match) => TOKEN_RE.test(match)
599
599
  ? null
600
600
  : redactMatch(pass, match, { label: 'DENY_LIST', category: 'other' }, {
601
601
  patternName: 'denyList', patternLabel: 'DENY_LIST', category: 'other', confidence: context_1.DENY_LIST_CONFIDENCE,
602
- }), window);
602
+ }));
603
603
  }
604
604
  catch { }
605
605
  }
@@ -623,28 +623,21 @@ function builtInStage(pass) {
623
623
  replaceFree(pass, pattern.regex, (match, offset) => builtInMatch(pass, pattern, match, offset));
624
624
  }
625
625
  }
626
- function freeNames(pass) {
627
- const names = new Map();
628
- 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);
634
- }
635
- 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);
636
629
  }
637
630
  function nameStage(pass) {
638
631
  if (pass.ctx.skipSemantic)
639
632
  return;
640
- for (const [name, start] of freeNames(pass)) {
633
+ for (const name of sourceNames(pass)) {
641
634
  if ((name.includes('[') && name.includes(']')) || isAllowListed(name, pass.ctx.allowList))
642
635
  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
636
  let replacement;
647
- replaceFree(pass, new RegExp(escapeRe(name), 'g'), (match) => {
637
+ replaceFree(pass, new RegExp(escapeRe(name), 'g'), (match, start) => {
638
+ const confidence = (0, context_1.hasContextWords)(pass.source, start, start + match.length, names_1.NAME_CONTEXT_WORDS) ? 0.8 : 0.65;
639
+ if (confidence < pass.threshold)
640
+ return null;
648
641
  replacement !== null && replacement !== void 0 ? replacement : (replacement = redactMatch(pass, match, { label: 'PERSON_NAME', category: 'identity' }, {
649
642
  patternName: 'personNameDict', patternLabel: 'PERSON_NAME', category: 'identity', confidence,
650
643
  }));
@@ -659,20 +652,19 @@ function customStage(pass) {
659
652
  if (!cp.label || !cp.regex || !isSafeRegex(cp.regex))
660
653
  continue;
661
654
  const pattern = { label: cp.label.toUpperCase(), category: cp.category || 'identity' };
655
+ assertUserRegexInput(pass.source, `Custom pattern "${cp.label}"`);
662
656
  try {
663
657
  replaceFree(pass, new RegExp(cp.regex, 'g'), (match) => TOKEN_RE.test(match) || isAllowListed(match, pass.ctx.allowList)
664
658
  ? null
665
659
  : redactMatch(pass, match, pattern, {
666
660
  patternName: `custom_${cp.label}`, patternLabel: pattern.label, category: pattern.category,
667
661
  confidence: context_1.CUSTOM_PATTERN_CONFIDENCE,
668
- }), USER_REGEX_WINDOW);
662
+ }));
669
663
  }
670
664
  catch { }
671
665
  }
672
666
  }
673
- function recordLocked(ctx, itemIndex, fieldPath, segs) {
674
- if (!ctx.lockedSpans || fieldPath === '')
675
- return;
667
+ function recordLocked(ctx, segs) {
676
668
  const spans = [];
677
669
  let at = 0;
678
670
  for (const seg of segs) {
@@ -680,10 +672,20 @@ function recordLocked(ctx, itemIndex, fieldPath, segs) {
680
672
  spans.push([at, at + seg.text.length]);
681
673
  at += seg.text.length;
682
674
  }
683
- if (spans.length > 0)
684
- ctx.lockedSpans.set(lockKey(itemIndex, fieldPath), spans);
675
+ ctx.lastLocked = spans.length > 0 ? spans : undefined;
676
+ }
677
+ function lockChild(ctx, container, key, compute) {
678
+ var _a;
679
+ ctx.lastLocked = undefined;
680
+ const out = compute();
681
+ if (ctx.lastLocked && ctx.lockedSpans && typeof out === 'string') {
682
+ const byKey = (_a = ctx.lockedSpans.get(container)) !== null && _a !== void 0 ? _a : new Map();
683
+ byKey.set(key, ctx.lastLocked);
684
+ ctx.lockedSpans.set(container, byKey);
685
+ }
686
+ ctx.lastLocked = undefined;
687
+ return out;
685
688
  }
686
- const lockKey = (itemIndex, fieldPath) => `${itemIndex}\u0000${fieldPath}`;
687
689
  function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
688
690
  var _a;
689
691
  const pass = {
@@ -694,7 +696,7 @@ function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
694
696
  builtInStage(pass);
695
697
  nameStage(pass);
696
698
  customStage(pass);
697
- recordLocked(ctx, itemIndex, fieldPath, pass.segs);
699
+ recordLocked(ctx, pass.segs);
698
700
  return pass.segs.map((s) => s.text).join('');
699
701
  }
700
702
  function makeRestorer(entries) {
@@ -716,7 +718,7 @@ function restoreWith(value, restore, entries) {
716
718
  if (value !== null && typeof value === 'object') {
717
719
  const obj = {};
718
720
  for (const [k, v] of Object.entries(value)) {
719
- obj[restore(k)] = restoreWith(v, restore, entries);
721
+ setOwn(obj, restore(k), restoreWith(v, restore, entries));
720
722
  }
721
723
  return obj;
722
724
  }
@@ -745,18 +747,17 @@ function resolveOverlaps(spans) {
745
747
  }
746
748
  return kept.sort((a, b) => b.start - a.start);
747
749
  }
748
- function presidioSpans(value, entities, ctx, key) {
749
- var _a, _b, _c;
750
+ function presidioSpans(value, entities, ctx, locked) {
751
+ var _a;
750
752
  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
753
  const free = (s) => !locked.some(([start, end]) => s.start < end && start < s.end);
753
754
  return resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold && free(s)));
754
755
  }
755
- async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path) {
756
+ async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path, locked) {
756
757
  if (value.trim().length === 0 || TOKEN_RE.test(value))
757
758
  return value;
758
759
  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));
760
+ const spans = presidioSpans(value, entities, ctx, locked);
760
761
  let text = value;
761
762
  for (const span of spans) {
762
763
  const matchedText = value.slice(span.start, span.end);
@@ -776,13 +777,13 @@ async function presidioRedactString(value, ctx, vault, sessionId, hits, itemInde
776
777
  }
777
778
  return text;
778
779
  }
779
- async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
780
- var _a, _b;
780
+ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath, locked = []) {
781
+ var _a, _b, _c, _d, _e, _f;
781
782
  if (typeof value === 'string') {
782
783
  if (!shouldProcessField(policyPath, ctx.fieldMode, ctx.fieldRules))
783
784
  return value;
784
785
  try {
785
- return await presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path);
786
+ return await presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path, locked);
786
787
  }
787
788
  catch {
788
789
  return value;
@@ -791,18 +792,20 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path,
791
792
  if (Array.isArray(value)) {
792
793
  const out = [];
793
794
  for (let i = 0; i < value.length; i++) {
794
- out.push(await presidioWalk(value[i], ctx, vault, sessionId, hits, itemIndex, `${path}[${i}]`, `${policyPath}[${i}]`));
795
+ 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));
796
+ out.push(await presidioWalk(value[i], ctx, vault, sessionId, hits, itemIndex, `${path}[${i}]`, `${policyPath}[${i}]`, lock));
795
797
  }
796
798
  return out;
797
799
  }
798
800
  if (value !== null && typeof value === 'object') {
799
- const origins = (_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.get(value);
801
+ const origins = (_c = ctx.keyOrigins) === null || _c === void 0 ? void 0 : _c.get(value);
800
802
  const out = {};
801
803
  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;
804
+ const original = (_d = origins === null || origins === void 0 ? void 0 : origins.get(k)) !== null && _d !== void 0 ? _d : k;
803
805
  const child = path ? `${path}.${k}` : k;
804
806
  const childPolicy = policyPath ? `${policyPath}.${original}` : original;
805
- out[k] = await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy);
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);
808
+ setOwn(out, k, await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy, lock));
806
809
  }
807
810
  return out;
808
811
  }
@@ -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,53 @@ 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);
55
- }
56
- if (/[a-zA-Z0-9]/.test(letter))
57
- return fullSet();
52
+ const HEX = /^[0-9a-fA-F]+$/;
53
+ const CONTROL = { n: 10, r: 13, t: 9, v: 11, f: 12 };
54
+ function readEscape(src, i) {
55
+ var _a, _b;
56
+ const e = (_a = src[i + 1]) !== null && _a !== void 0 ? _a : '';
57
+ const hex = (n) => (HEX.test(src.slice(i + 2, i + 2 + n)) && src.length >= i + 2 + n ? n : 0);
58
+ if (e === 'x' && hex(2))
59
+ return { length: 4, code: parseInt(src.slice(i + 2, i + 4), 16) };
60
+ if (e === 'u' && hex(4))
61
+ return { length: 6, code: parseInt(src.slice(i + 2, i + 6), 16) };
62
+ if (e === 'c' && /[A-Za-z]/.test((_b = src[i + 2]) !== null && _b !== void 0 ? _b : ''))
63
+ return { length: 3, code: src.charCodeAt(i + 2) % 32 };
64
+ if (e === '0')
65
+ return { length: 2, code: 0 };
66
+ const control = CONTROL[e];
67
+ if (control !== undefined)
68
+ return { length: 2, code: control };
69
+ if (/[a-zA-Z0-9]/.test(e))
70
+ return { length: 2, code: null };
71
+ return { length: 2, code: e.charCodeAt(0) };
72
+ }
73
+ function codeSet(code) {
58
74
  const set = (0, exports.emptySet)();
59
- addCode(set, letter.charCodeAt(0));
75
+ addCode(set, code);
60
76
  return set;
61
77
  }
78
+ function escapeSet(src, i) {
79
+ var _a;
80
+ const { code } = readEscape(src, i);
81
+ const set = (0, exports.emptySet)();
82
+ if (code !== null) {
83
+ addCode(set, code);
84
+ return set;
85
+ }
86
+ const letter = (_a = src[i + 1]) !== null && _a !== void 0 ? _a : '';
87
+ const fill = CLASS_ESCAPES[letter.toLowerCase()];
88
+ if (!fill)
89
+ return fullSet();
90
+ fill(set);
91
+ return letter === letter.toLowerCase() ? set : complement(set);
92
+ }
62
93
  function classMember(body, j) {
63
- var _a, _b;
94
+ if (body[j] === '\\' && body[j + 1] === 'b')
95
+ return { set: codeSet(8), next: j + 2, code: 8 };
64
96
  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 };
97
+ const { length, code } = readEscape(body, j);
98
+ return { set: escapeSet(body, j), next: j + length, code };
68
99
  }
69
100
  const set = (0, exports.emptySet)();
70
101
  addCode(set, body.charCodeAt(j));
@@ -90,13 +121,13 @@ function classSet(body) {
90
121
  }
91
122
  return negated ? complement(set) : set;
92
123
  }
93
- function atomCharset(src, start, end) {
94
- var _a, _b;
124
+ function rawCharset(src, start, end) {
125
+ var _a;
95
126
  const c = src[start];
96
127
  if (c === '^' || c === '$')
97
128
  return null;
98
129
  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 : '\\');
130
+ return /[bB]/.test((_a = src[start + 1]) !== null && _a !== void 0 ? _a : '') ? null : escapeSet(src, start);
100
131
  if (c === '[')
101
132
  return classSet(src.slice(start + 1, end - 1));
102
133
  if (c === '.')
@@ -105,3 +136,7 @@ function atomCharset(src, start, end) {
105
136
  addCode(set, src.charCodeAt(start));
106
137
  return set;
107
138
  }
139
+ function atomCharset(src, start, end) {
140
+ const set = rawCharset(src, start, end);
141
+ return set && fold(set);
142
+ }
@@ -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()];
@@ -133,7 +157,45 @@ function structuralProblem(src) {
133
157
  }
134
158
  return null;
135
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
+ }
136
197
  function checkRegex(regexStr) {
198
+ var _a;
137
199
  if (typeof regexStr !== 'string' || regexStr.length === 0)
138
200
  return 'is empty';
139
201
  if (regexStr.length > MAX_REGEX_LENGTH)
@@ -144,5 +206,5 @@ function checkRegex(regexStr) {
144
206
  catch {
145
207
  return 'is not a valid regular expression';
146
208
  }
147
- return structuralProblem(regexStr);
209
+ return (_a = unsupportedSyntax(regexStr)) !== null && _a !== void 0 ? _a : structuralProblem(regexStr);
148
210
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-redactor",
3
- "version": "4.3.0",
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",