n8n-nodes-redactor 4.2.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.
@@ -1039,6 +1039,8 @@ class Redactor {
1039
1039
  scanNumbers: true,
1040
1040
  reservedTokens: (0, engine_1.collectTokenLike)(items.map((it) => it.json)),
1041
1041
  keyOrigins: new WeakMap(),
1042
+ inputText: (0, engine_1.collectInputText)(items.map((it) => it.json)),
1043
+ lockedSpans: new WeakMap(),
1042
1044
  };
1043
1045
  const allHits = [];
1044
1046
  const returnData = [];
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.collectTokenLike = collectTokenLike;
37
+ exports.collectInputText = collectInputText;
37
38
  exports.redactValue = redactValue;
38
39
  exports.restoreValue = restoreValue;
39
40
  exports.enhanceWithPresidio = enhanceWithPresidio;
@@ -56,7 +57,7 @@ function isAllowListed(value, allowList) {
56
57
  case 'contains':
57
58
  return value.includes(entry.value);
58
59
  case 'regex':
59
- if (!isSafeRegex(entry.value))
60
+ if (!isSafeRegex(entry.value) || value.length > USER_REGEX_MAX_INPUT)
60
61
  return false;
61
62
  return new RegExp(entry.value, 'i').test(value);
62
63
  default:
@@ -97,8 +98,8 @@ function maskValue(value, label) {
97
98
  function hashValue(value) {
98
99
  return crypto.createHash('sha256').update(value).digest('hex').slice(0, 12);
99
100
  }
100
- function storeReversible(match, pattern, vault, sessionId, dedup, makeToken) {
101
- if (dedup) {
101
+ function storeReversible(match, pattern, ctx, vault, sessionId, makeToken) {
102
+ if (ctx.dedup) {
102
103
  const existing = vault.findByOriginal(sessionId, match);
103
104
  if (existing && !existing.numeric)
104
105
  return existing.token;
@@ -120,14 +121,14 @@ const IRREVERSIBLE = {
120
121
  blackout: (match) => '\u2588'.repeat(match.length),
121
122
  remove: () => '',
122
123
  };
123
- function applyRedaction(match, pattern, mode, vault, sessionId, dedup, reserved) {
124
- if (mode === 'token') {
125
- return storeReversible(match, pattern, vault, sessionId, dedup, () => freshToken(pattern.label, vault, sessionId, reserved));
124
+ function applyRedaction(match, pattern, ctx, vault, sessionId) {
125
+ if (ctx.mode === 'token') {
126
+ return storeReversible(match, pattern, ctx, vault, sessionId, () => freshToken(pattern.label, vault, sessionId, ctx.reservedTokens));
126
127
  }
127
- if (mode === 'pseudonymize') {
128
- return storeReversible(match, pattern, vault, sessionId, dedup, () => uniquePseudonym(vault, sessionId, match, pattern.label));
128
+ if (ctx.mode === 'pseudonymize') {
129
+ return storeReversible(match, pattern, ctx, vault, sessionId, () => uniquePseudonym(vault, sessionId, match, pattern.label, ctx));
129
130
  }
130
- const irreversible = IRREVERSIBLE[mode];
131
+ const irreversible = IRREVERSIBLE[ctx.mode];
131
132
  return irreversible ? irreversible(match, pattern.label) : match;
132
133
  }
133
134
  function freshToken(label, vault, sessionId, reserved) {
@@ -140,36 +141,46 @@ function freshToken(label, vault, sessionId, reserved) {
140
141
  return token;
141
142
  }
142
143
  const TOKEN_LIKE_RE = /\[[A-Z][A-Z0-9_]*_\d+\]/g;
143
- function collectTokenLike(value, into = new Set()) {
144
- const scan = (text) => {
145
- for (const m of text.matchAll(TOKEN_LIKE_RE))
146
- into.add(m[0]);
147
- };
148
- if (typeof value === 'string')
149
- scan(value);
144
+ function walkText(value, visit) {
145
+ if (typeof value === 'string' || typeof value === 'number')
146
+ visit(String(value));
150
147
  else if (Array.isArray(value))
151
- value.forEach((v) => collectTokenLike(v, into));
148
+ value.forEach((v) => walkText(v, visit));
152
149
  else if (value !== null && typeof value === 'object') {
153
150
  for (const [k, v] of Object.entries(value)) {
154
- scan(k);
155
- collectTokenLike(v, into);
151
+ visit(k);
152
+ walkText(v, visit);
156
153
  }
157
154
  }
155
+ }
156
+ function collectTokenLike(value, into = new Set()) {
157
+ walkText(value, (text) => {
158
+ for (const m of text.matchAll(TOKEN_LIKE_RE))
159
+ into.add(m[0]);
160
+ });
158
161
  return into;
159
162
  }
160
- function uniquePseudonym(vault, sessionId, original, label) {
161
- var _a, _b;
163
+ function collectInputText(value) {
164
+ const parts = [];
165
+ walkText(value, (text) => parts.push(text));
166
+ return parts.join('\u0000');
167
+ }
168
+ function reusable(taken, original, ctx) {
169
+ return !!taken && taken.original === original && !!taken.numeric === !!ctx.numeric;
170
+ }
171
+ function uniquePseudonym(vault, sessionId, original, label, ctx) {
172
+ var _a, _b, _c;
162
173
  const entries = (_b = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {};
163
174
  for (let attempt = 0; attempt < 32; attempt++) {
164
175
  const seed = attempt === 0 ? original : `${original}\u0000${attempt}`;
165
176
  const candidate = (0, pseudonymize_1.generatePseudonym)(sessionId, seed, label);
166
- const taken = entries[candidate];
167
- if (candidate === original)
177
+ if (candidate === original || ((_c = ctx.inputText) === null || _c === void 0 ? void 0 : _c.includes(candidate)))
168
178
  continue;
169
- if (!taken || taken.original === original)
179
+ const taken = entries[candidate];
180
+ if (!taken || reusable(taken, original, ctx))
170
181
  return candidate;
171
182
  }
172
- return generateToken(label, Object.keys(entries).length);
183
+ return freshToken(label, vault, sessionId, ctx.reservedTokens);
173
184
  }
174
185
  function shouldProcessField(fieldPath, fieldMode, fieldRules) {
175
186
  if (fieldMode === 'all')
@@ -412,7 +423,8 @@ function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, displ
412
423
  const semConf = semantic.label === 'IDENTIFIER' ? context_1.AMBIGUOUS_FIELD_CONFIDENCE : context_1.SEMANTIC_CONFIDENCE;
413
424
  if (semConf < ((_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0))
414
425
  return null;
415
- const replacement = applyRedaction(raw, { label: semantic.label, category: semantic.category }, ctx.mode, vault, sessionId, ctx.dedup, ctx.reservedTokens);
426
+ const replacement = applyRedaction(raw, { label: semantic.label, category: semantic.category }, ctx, vault, sessionId);
427
+ ctx.lastLocked = replacement ? [[0, replacement.length]] : undefined;
416
428
  hits.push({
417
429
  token: replacement, original: '***',
418
430
  patternName: `semantic_${semantic.label}`,
@@ -423,10 +435,23 @@ function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, displ
423
435
  });
424
436
  return replacement;
425
437
  }
438
+ function numericMapping(out, text, ctx, vault, sessionId) {
439
+ var _a, _b;
440
+ const entries = (_b = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {};
441
+ const existing = entries[out];
442
+ if ((existing === null || existing === void 0 ? void 0 : existing.original) === text) {
443
+ existing.numeric = true;
444
+ return out;
445
+ }
446
+ const token = freshToken('NUMBER', vault, sessionId, ctx.reservedTokens);
447
+ vault.addEntry(sessionId, {
448
+ token, original: text, patternLabel: 'NUMBER', category: 'other', createdAt: new Date().toISOString(), numeric: true,
449
+ });
450
+ return token;
451
+ }
426
452
  function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
427
- var _a;
428
453
  const text = String(value);
429
- const numCtx = { ...ctx, dedup: false };
454
+ const numCtx = { ...ctx, dedup: false, numeric: true };
430
455
  let out = null;
431
456
  if (!ctx.skipSemantic)
432
457
  out = redactSemantic(text, numCtx, vault, sessionId, hits, itemIndex, path, display, siblingKeys);
@@ -434,10 +459,13 @@ function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, displ
434
459
  out = redactText(text, numCtx, vault, sessionId, hits, itemIndex, display);
435
460
  if (out === null || out === text)
436
461
  return value;
437
- const entry = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries[out];
438
- if (entry && entry.original === text)
439
- entry.numeric = true;
440
- return out;
462
+ if (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize') {
463
+ ctx.lastLocked = [[0, out.length]];
464
+ return out;
465
+ }
466
+ const mapped = numericMapping(out, text, ctx, vault, sessionId);
467
+ ctx.lastLocked = [[0, mapped.length]];
468
+ return mapped;
441
469
  }
442
470
  function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits) {
443
471
  var _a;
@@ -474,7 +502,7 @@ function redactObject(value, ctx, vault, sessionId, hits, itemIndex, path, displ
474
502
  const childDisplay = display ? `${display}.${outKey}` : outKey;
475
503
  for (const h of keyHits)
476
504
  hits.push({ ...h, field: childDisplay });
477
- 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));
478
506
  }
479
507
  (_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.set(obj, origins);
480
508
  return obj;
@@ -497,7 +525,11 @@ function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath
497
525
  return redactNumber(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath, siblingKeys);
498
526
  }
499
527
  if (Array.isArray(value)) {
500
- 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;
501
533
  }
502
534
  if (value !== null && typeof value === 'object') {
503
535
  return redactObject(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath);
@@ -505,38 +537,46 @@ function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath
505
537
  return value;
506
538
  }
507
539
  const escapeRe = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
508
- function segments(text, produced) {
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.');
546
+ }
547
+ function replaceInFree(seg, re, fn, into) {
509
548
  var _a;
510
- if (produced.size === 0)
511
- return [{ text, locked: false }];
512
- const guard = new RegExp([...produced].sort((a, b) => b.length - a.length).map(escapeRe).join('|'), 'g');
513
- const out = [];
514
549
  let last = 0;
515
- for (const m of text.matchAll(guard)) {
550
+ for (const m of seg.text.matchAll(re)) {
516
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;
517
557
  if (at > last)
518
- out.push({ text: text.slice(last, at), locked: false });
519
- out.push({ text: m[0], locked: true });
558
+ into.push({ text: seg.text.slice(last, at), locked: false, src: seg.src + last });
559
+ into.push({ text: replacement, locked: true, src: -1 });
520
560
  last = at + m[0].length;
521
561
  }
522
- if (last < text.length)
523
- out.push({ text: text.slice(last), locked: false });
524
- return out;
525
- }
526
- function replaceOutside(text, produced, re, fn) {
527
- return segments(text, produced)
528
- .map((seg) => (seg.locked ? seg.text : seg.text.replace(new RegExp(re.source, re.flags), fn)))
529
- .join('');
562
+ if (last < seg.text.length)
563
+ into.push({ text: seg.text.slice(last), locked: false, src: seg.src + last });
530
564
  }
531
- function occursOutside(text, produced, needle) {
532
- return segments(text, produced).some((seg) => !seg.locked && seg.text.includes(needle));
565
+ 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);
574
+ }
575
+ pass.segs = next;
533
576
  }
534
577
  function redactMatch(pass, match, pattern, detail) {
535
- const { ctx, vault, sessionId } = pass;
536
- const replacement = applyRedaction(match, pattern, ctx.mode, vault, sessionId, ctx.dedup, ctx.reservedTokens);
578
+ const replacement = applyRedaction(match, pattern, pass.ctx, pass.vault, pass.sessionId);
537
579
  pass.hits.push({ token: replacement, original: '***', field: pass.fieldPath, itemIndex: pass.itemIndex, ...detail });
538
- if (replacement)
539
- pass.produced.add(replacement);
540
580
  return replacement;
541
581
  }
542
582
  function denyListRegex(entry) {
@@ -547,12 +587,14 @@ function denyListRegex(entry) {
547
587
  function denyListStage(pass) {
548
588
  var _a;
549
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}"`);
550
592
  try {
551
593
  const re = denyListRegex(entry);
552
594
  if (!re)
553
595
  continue;
554
- pass.result = replaceOutside(pass.result, pass.produced, re, (match) => TOKEN_RE.test(match)
555
- ? match
596
+ replaceFree(pass, re, (match) => TOKEN_RE.test(match)
597
+ ? null
556
598
  : redactMatch(pass, match, { label: 'DENY_LIST', category: 'other' }, {
557
599
  patternName: 'denyList', patternLabel: 'DENY_LIST', category: 'other', confidence: context_1.DENY_LIST_CONFIDENCE,
558
600
  }));
@@ -560,17 +602,15 @@ function denyListStage(pass) {
560
602
  catch { }
561
603
  }
562
604
  }
563
- function builtInMatch(pass, pattern, match, args) {
605
+ function builtInMatch(pass, pattern, match, offset) {
564
606
  if (TOKEN_RE.test(match) || (match.startsWith('[') && match.endsWith(']')))
565
- return match;
607
+ return null;
566
608
  const validatorPassed = pattern.validate ? pattern.validate(match) : true;
567
609
  if (!validatorPassed || isAllowListed(match, pass.ctx.allowList))
568
- return match;
569
- const offsetArg = args[args.length - 2];
570
- const offset = typeof offsetArg === 'number' ? offsetArg : 0;
610
+ return null;
571
611
  const confidence = (0, context_1.calculateConfidence)(pattern.name, !!pattern.validate, validatorPassed, pass.source, offset, offset + match.length);
572
612
  if (confidence < pass.threshold)
573
- return match;
613
+ return null;
574
614
  return redactMatch(pass, match, pattern, {
575
615
  patternName: pattern.name, patternLabel: pattern.label, category: pattern.category, confidence,
576
616
  });
@@ -578,24 +618,34 @@ function builtInMatch(pass, pattern, match, args) {
578
618
  function builtInStage(pass) {
579
619
  const enabled = (0, patterns_1.getPatternsByNames)(pass.ctx.enabledPatterns);
580
620
  for (const pattern of [...enabled.filter((p) => p.validate), ...enabled.filter((p) => !p.validate)]) {
581
- const re = new RegExp(pattern.regex.source, pattern.regex.flags);
582
- pass.result = replaceOutside(pass.result, pass.produced, re, (match, ...args) => builtInMatch(pass, pattern, match, args));
621
+ replaceFree(pass, pattern.regex, (match, offset) => builtInMatch(pass, pattern, match, offset));
583
622
  }
584
623
  }
624
+ function freeNames(pass) {
625
+ const names = new Set();
626
+ for (const seg of pass.segs) {
627
+ if (!seg.locked)
628
+ for (const nm of (0, names_1.detectNamesInText)(seg.text))
629
+ names.add(nm.name);
630
+ }
631
+ return names;
632
+ }
585
633
  function nameStage(pass) {
586
634
  if (pass.ctx.skipSemantic)
587
635
  return;
588
- for (const nm of (0, names_1.detectNamesInText)(pass.result)) {
589
- if ((nm.name.includes('[') && nm.name.includes(']')) || isAllowListed(nm.name, pass.ctx.allowList))
590
- continue;
591
- const confidence = (0, context_1.hasContextWords)(pass.source, nm.start, nm.end, names_1.NAME_CONTEXT_WORDS) ? 0.8 : 0.65;
592
- if (confidence < pass.threshold || !occursOutside(pass.result, pass.produced, nm.name))
636
+ for (const name of freeNames(pass)) {
637
+ if ((name.includes('[') && name.includes(']')) || isAllowListed(name, pass.ctx.allowList))
593
638
  continue;
594
- const before = new Set(pass.produced);
595
- const replacement = redactMatch(pass, nm.name, { label: 'PERSON_NAME', category: 'identity' }, {
596
- patternName: 'personNameDict', patternLabel: 'PERSON_NAME', category: 'identity', confidence,
639
+ let replacement;
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;
644
+ replacement !== null && replacement !== void 0 ? replacement : (replacement = redactMatch(pass, match, { label: 'PERSON_NAME', category: 'identity' }, {
645
+ patternName: 'personNameDict', patternLabel: 'PERSON_NAME', category: 'identity', confidence,
646
+ }));
647
+ return replacement;
597
648
  });
598
- pass.result = replaceOutside(pass.result, before, new RegExp(escapeRe(nm.name), 'g'), () => replacement);
599
649
  }
600
650
  }
601
651
  function customStage(pass) {
@@ -605,9 +655,10 @@ function customStage(pass) {
605
655
  if (!cp.label || !cp.regex || !isSafeRegex(cp.regex))
606
656
  continue;
607
657
  const pattern = { label: cp.label.toUpperCase(), category: cp.category || 'identity' };
658
+ assertUserRegexInput(pass.source, `Custom pattern "${cp.label}"`);
608
659
  try {
609
- pass.result = replaceOutside(pass.result, pass.produced, new RegExp(cp.regex, 'g'), (match) => TOKEN_RE.test(match) || isAllowListed(match, pass.ctx.allowList)
610
- ? match
660
+ replaceFree(pass, new RegExp(cp.regex, 'g'), (match) => TOKEN_RE.test(match) || isAllowListed(match, pass.ctx.allowList)
661
+ ? null
611
662
  : redactMatch(pass, match, pattern, {
612
663
  patternName: `custom_${cp.label}`, patternLabel: pattern.label, category: pattern.category,
613
664
  confidence: context_1.CUSTOM_PATTERN_CONFIDENCE,
@@ -616,17 +667,40 @@ function customStage(pass) {
616
667
  catch { }
617
668
  }
618
669
  }
670
+ function recordLocked(ctx, segs) {
671
+ const spans = [];
672
+ let at = 0;
673
+ for (const seg of segs) {
674
+ if (seg.locked)
675
+ spans.push([at, at + seg.text.length]);
676
+ at += seg.text.length;
677
+ }
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;
691
+ }
619
692
  function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
620
693
  var _a;
621
694
  const pass = {
622
- source: text, result: text, produced: new Set(), threshold: (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0,
695
+ source: text, segs: [{ text, locked: false, src: 0 }], threshold: (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0,
623
696
  ctx, vault, sessionId, hits, itemIndex, fieldPath,
624
697
  };
625
698
  denyListStage(pass);
626
699
  builtInStage(pass);
627
700
  nameStage(pass);
628
701
  customStage(pass);
629
- return pass.result;
702
+ recordLocked(ctx, pass.segs);
703
+ return pass.segs.map((s) => s.text).join('');
630
704
  }
631
705
  function makeRestorer(entries) {
632
706
  const tokens = Object.keys(entries).filter((t) => t.length > 0).sort((a, b) => b.length - a.length);
@@ -676,13 +750,17 @@ function resolveOverlaps(spans) {
676
750
  }
677
751
  return kept.sort((a, b) => b.start - a.start);
678
752
  }
679
- async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path) {
753
+ function presidioSpans(value, entities, ctx, locked) {
680
754
  var _a;
755
+ const threshold = (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0;
756
+ const free = (s) => !locked.some(([start, end]) => s.start < end && start < s.end);
757
+ return resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold && free(s)));
758
+ }
759
+ async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path, locked) {
681
760
  if (value.trim().length === 0 || TOKEN_RE.test(value))
682
761
  return value;
683
762
  const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
684
- const threshold = (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0;
685
- const spans = resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold));
763
+ const spans = presidioSpans(value, entities, ctx, locked);
686
764
  let text = value;
687
765
  for (const span of spans) {
688
766
  const matchedText = value.slice(span.start, span.end);
@@ -692,7 +770,7 @@ async function presidioRedactString(value, ctx, vault, sessionId, hits, itemInde
692
770
  continue;
693
771
  const label = (0, presidio_1.getRedactorLabel)(span.entityType);
694
772
  const category = (0, presidio_1.getRedactorCategory)(span.entityType);
695
- const replacement = applyRedaction(matchedText, { label, category }, ctx.mode, vault, sessionId, ctx.dedup, ctx.reservedTokens);
773
+ const replacement = applyRedaction(matchedText, { label, category }, ctx, vault, sessionId);
696
774
  text = text.slice(0, span.start) + replacement + text.slice(span.end);
697
775
  hits.push({
698
776
  token: replacement, original: '***',
@@ -702,13 +780,13 @@ async function presidioRedactString(value, ctx, vault, sessionId, hits, itemInde
702
780
  }
703
781
  return text;
704
782
  }
705
- async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
706
- var _a, _b;
783
+ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath, locked = []) {
784
+ var _a, _b, _c, _d, _e, _f;
707
785
  if (typeof value === 'string') {
708
786
  if (!shouldProcessField(policyPath, ctx.fieldMode, ctx.fieldRules))
709
787
  return value;
710
788
  try {
711
- return await presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path);
789
+ return await presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path, locked);
712
790
  }
713
791
  catch {
714
792
  return value;
@@ -717,18 +795,20 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path,
717
795
  if (Array.isArray(value)) {
718
796
  const out = [];
719
797
  for (let i = 0; i < value.length; i++) {
720
- 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));
721
800
  }
722
801
  return out;
723
802
  }
724
803
  if (value !== null && typeof value === 'object') {
725
- 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);
726
805
  const out = {};
727
806
  for (const [k, v] of Object.entries(value)) {
728
- 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;
729
808
  const child = path ? `${path}.${k}` : k;
730
809
  const childPolicy = policyPath ? `${policyPath}.${original}` : original;
731
- 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);
732
812
  }
733
813
  return out;
734
814
  }
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.emptySet = void 0;
4
+ exports.union = union;
5
+ exports.overlaps = overlaps;
6
+ exports.readEscape = readEscape;
7
+ exports.atomCharset = atomCharset;
8
+ const emptySet = () => ({ ascii: new Array(128).fill(false), other: false });
9
+ exports.emptySet = emptySet;
10
+ const fullSet = () => ({ ascii: new Array(128).fill(true), other: true });
11
+ function addCode(set, code) {
12
+ if (code >= 128)
13
+ set.other = true;
14
+ else
15
+ set.ascii[code] = true;
16
+ }
17
+ function addRange(set, from, to) {
18
+ for (let c = from; c <= Math.min(to, 127); c++)
19
+ addCode(set, c);
20
+ if (to >= 128)
21
+ set.other = true;
22
+ }
23
+ function union(a, b) {
24
+ return { ascii: a.ascii.map((v, i) => v || b.ascii[i]), other: a.other || b.other };
25
+ }
26
+ function overlaps(a, b) {
27
+ return (a.other && b.other) || a.ascii.some((v, i) => v && b.ascii[i]);
28
+ }
29
+ function complement(set) {
30
+ return { ascii: set.ascii.map((v) => !v), other: true };
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
+ }
38
+ const CLASS_ESCAPES = {
39
+ d: (s) => addRange(s, 48, 57),
40
+ w: (s) => {
41
+ addRange(s, 48, 57);
42
+ addRange(s, 65, 90);
43
+ addRange(s, 97, 122);
44
+ addCode(s, 95);
45
+ },
46
+ s: (s) => {
47
+ for (const c of [9, 10, 11, 12, 13, 32])
48
+ addCode(s, c);
49
+ s.other = true;
50
+ },
51
+ };
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;
76
+ }
77
+ const letter = (_a = src[i + 1]) !== null && _a !== void 0 ? _a : '';
78
+ const fill = CLASS_ESCAPES[letter.toLowerCase()];
79
+ if (!fill)
80
+ return fullSet();
81
+ fill(set);
82
+ return letter === letter.toLowerCase() ? set : complement(set);
83
+ }
84
+ function classMember(body, j) {
85
+ if (body[j] === '\\') {
86
+ const { length, code } = readEscape(body, j);
87
+ return { set: escapeSet(body, j), next: j + length, code };
88
+ }
89
+ const set = (0, exports.emptySet)();
90
+ addCode(set, body.charCodeAt(j));
91
+ return { set, next: j + 1, code: body.charCodeAt(j) };
92
+ }
93
+ function classSet(body) {
94
+ const negated = body.startsWith('^');
95
+ let set = (0, exports.emptySet)();
96
+ let j = negated ? 1 : 0;
97
+ while (j < body.length) {
98
+ const first = classMember(body, j);
99
+ j = first.next;
100
+ if (body[j] === '-' && j + 1 < body.length && first.code !== null) {
101
+ const last = classMember(body, j + 1);
102
+ if (last.code !== null)
103
+ addRange(set, first.code, last.code);
104
+ else
105
+ set = union(union(set, first.set), last.set);
106
+ j = last.next;
107
+ }
108
+ else
109
+ set = union(set, first.set);
110
+ }
111
+ return negated ? complement(set) : set;
112
+ }
113
+ function rawCharset(src, start, end) {
114
+ var _a;
115
+ const c = src[start];
116
+ if (c === '^' || c === '$')
117
+ return null;
118
+ if (c === '\\')
119
+ return /[bB]/.test((_a = src[start + 1]) !== null && _a !== void 0 ? _a : '') ? null : escapeSet(src, start);
120
+ if (c === '[')
121
+ return classSet(src.slice(start + 1, end - 1));
122
+ if (c === '.')
123
+ return fullSet();
124
+ const set = (0, exports.emptySet)();
125
+ addCode(set, src.charCodeAt(start));
126
+ return set;
127
+ }
128
+ function atomCharset(src, start, end) {
129
+ const set = rawCharset(src, start, end);
130
+ return set && fold(set);
131
+ }
@@ -1,7 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkRegex = checkRegex;
4
+ const regexCharset_1 = require("./regexCharset");
4
5
  const MAX_REGEX_LENGTH = 500;
6
+ const newFrame = (zeroWidth = false) => ({
7
+ hasVariableQuantifier: false, hasAlternation: false, run: null, chars: (0, regexCharset_1.emptySet)(), zeroWidth,
8
+ branchConsumes: false, allConsume: true, nullable: false,
9
+ });
10
+ function endBranch(frame) {
11
+ frame.allConsume && (frame.allConsume = frame.branchConsumes);
12
+ frame.branchConsumes = false;
13
+ frame.nullable = !frame.allConsume;
14
+ }
5
15
  function readBraces(src, i) {
6
16
  const m = /^\{(\d+)(,(\d*))?\}/.exec(src.slice(i));
7
17
  if (!m)
@@ -44,32 +54,43 @@ function groupPrefixLength(src, i) {
44
54
  const look = /^\(\?(?::|=|!|<=|<!)/.exec(src.slice(i));
45
55
  return look ? look[0].length : 1;
46
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
+ }
47
71
  function readToken(src, i, stack) {
48
72
  var _a;
49
73
  const c = src[i];
50
74
  if (c === '\\') {
51
75
  if (/[1-9k]/.test((_a = src[i + 1]) !== null && _a !== void 0 ? _a : ''))
52
76
  return { atomEnd: i, closed: null, error: 'back-references are not supported' };
53
- return { atomEnd: i + 2, closed: null };
77
+ return { atomEnd: i + (0, regexCharset_1.readEscape)(src, i).length, closed: null };
54
78
  }
55
79
  if (c === '[')
56
80
  return { atomEnd: skipClass(src, i), closed: null };
57
81
  if (c === '(') {
58
- stack.push({ hasVariableQuantifier: false, hasAlternation: false });
82
+ stack.push(newFrame(/^\(\?(?:=|!|<=|<!)/.test(src.slice(i))));
59
83
  return { atomEnd: i, closed: null, next: i + groupPrefixLength(src, i) };
60
84
  }
61
- if (c === '|') {
62
- stack[stack.length - 1].hasAlternation = true;
63
- return { atomEnd: i, closed: null, next: i + 1 };
64
- }
65
- const closed = c === ')' && stack.length > 1 ? stack.pop() : null;
66
- return { atomEnd: i + 1, closed };
85
+ if (c === '|')
86
+ return alternation(stack, i);
87
+ return { atomEnd: i + 1, closed: closeGroup(c, stack) };
67
88
  }
68
89
  const NESTED = 'a repeated group contains a quantifier or alternation (catastrophic backtracking)';
69
90
  function isRisky(frame) {
70
91
  return !!frame && (frame.hasVariableQuantifier || frame.hasAlternation);
71
92
  }
72
- const NO_GROUP = { hasVariableQuantifier: false, hasAlternation: false };
93
+ const NO_GROUP = newFrame();
73
94
  const repeats = (q) => q !== null && q.max > 1;
74
95
  const isVariable = (q) => q !== null && q.min !== q.max;
75
96
  function applyQuantifier(q, closed, top) {
@@ -80,8 +101,45 @@ function applyQuantifier(q, closed, top) {
80
101
  top.hasAlternation || (top.hasAlternation = inner.hasAlternation);
81
102
  return null;
82
103
  }
104
+ const SEQUENTIAL = 'two repetitions in a row can match the same characters (catastrophic backtracking)';
105
+ function applySequence(chars, variable, required, top) {
106
+ if (chars === null)
107
+ return null;
108
+ top.chars = (0, regexCharset_1.union)(top.chars, chars);
109
+ const shared = top.run !== null && (0, regexCharset_1.overlaps)(top.run, chars);
110
+ if (variable) {
111
+ if (shared)
112
+ return SEQUENTIAL;
113
+ top.run = top.run ? (0, regexCharset_1.union)(top.run, chars) : chars;
114
+ }
115
+ else if (required && !shared)
116
+ top.run = null;
117
+ return null;
118
+ }
119
+ function stepChars(src, i, step) {
120
+ const closed = step.closed;
121
+ if (!closed)
122
+ return (0, regexCharset_1.atomCharset)(src, i, step.atomEnd);
123
+ return closed.zeroWidth ? null : closed.chars;
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
+ }
130
+ function stepProblem(src, i, step, q, top) {
131
+ var _a;
132
+ const problem = applyQuantifier(q, step.closed, top);
133
+ if (problem || (q === null || q === void 0 ? void 0 : q.max) === 0)
134
+ return problem;
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);
140
+ }
83
141
  function structuralProblem(src) {
84
- const stack = [{ hasVariableQuantifier: false, hasAlternation: false }];
142
+ const stack = [newFrame()];
85
143
  let i = 0;
86
144
  while (i < src.length) {
87
145
  const step = readToken(src, i, stack);
@@ -92,7 +150,7 @@ function structuralProblem(src) {
92
150
  continue;
93
151
  }
94
152
  const q = readQuantifier(src, step.atomEnd);
95
- const problem = applyQuantifier(q, step.closed, stack[stack.length - 1]);
153
+ const problem = stepProblem(src, i, step, q, stack[stack.length - 1]);
96
154
  if (problem)
97
155
  return problem;
98
156
  i = step.atomEnd + (q ? q.length : 0);
@@ -27,12 +27,26 @@ async function resolvePassphrase(fns) {
27
27
  }
28
28
  return cred.passphrase;
29
29
  }
30
+ return legacyPassphrase(fns);
31
+ }
32
+ function legacyPassphrase(fns) {
33
+ let raw;
30
34
  try {
31
- return fns.getNodeParameter('vaultPassphrase', 0, '') || '';
35
+ raw = fns.getNodeParameter('vaultPassphrase', 0, '');
32
36
  }
33
- catch {
37
+ catch (error) {
38
+ throw new n8n_workflow_1.NodeOperationError(fns.getNode(), 'The legacy vault passphrase could not be evaluated, so the vault stays locked.', {
39
+ description: `Nothing was stored. Fix the expression, or move the passphrase into a Redactor Vault credential. Cause: ${error.message}`,
40
+ });
41
+ }
42
+ if (raw === undefined || raw === null || raw === '')
34
43
  return '';
44
+ if (typeof raw !== 'string') {
45
+ throw new n8n_workflow_1.NodeOperationError(fns.getNode(), 'The legacy vault passphrase is not text, so the vault stays locked.', {
46
+ description: 'Nothing was stored. Set the passphrase to a text value, or use a Redactor Vault credential.',
47
+ });
35
48
  }
49
+ return raw;
36
50
  }
37
51
  function workflowNamespace(fns) {
38
52
  try {
@@ -47,7 +61,7 @@ function workflowNamespace(fns) {
47
61
  function assertUserRegexes(fns, customPatterns, lists) {
48
62
  const refuse = (what, problem) => {
49
63
  throw new n8n_workflow_1.NodeOperationError(fns.getNode(), `${what} regex ${problem}`, {
50
- description: 'Rewrite the pattern without nested or alternated repetition, for example [ab]+ instead of (a|b)+.',
64
+ description: 'Rewrite the pattern without nested or alternated repetition, for example [ab]+ instead of (a|b)+, and without two repetitions in a row that can match the same characters, for example \\d+ instead of \\d+\\d+. Put a fixed separator such as - or @ between repeated parts.',
51
65
  });
52
66
  };
53
67
  for (const cp of customPatterns) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-redactor",
3
- "version": "4.2.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",